Implement new visualization front-end

This commit is contained in:
Christos Falas 2025-02-22 15:13:08 +00:00
parent 298146e8b0
commit 964e379dbf
No known key found for this signature in database
31 changed files with 5195 additions and 990 deletions

View File

@ -4,9 +4,7 @@ version = "0.1.0"
description = "Use Wi-Fi Channel State Information to locate human presence" description = "Use Wi-Fi Channel State Information to locate human presence"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"flask",
"numpy>=2.0.0", "numpy>=2.0.0",
"flask-sock",
"pytest", "pytest",
"matplotlib", "matplotlib",
"scipy", "scipy",
@ -53,4 +51,5 @@ packages = ["where_fi"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"grpcio-tools>=1.70.0", "grpcio-tools>=1.70.0",
"protoletariat>=3.3.9",
] ]

114
uv.lock
View File

@ -1,4 +1,5 @@
version = 1 version = 1
revision = 1
requires-python = ">=3.11" requires-python = ">=3.11"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.12'", "python_full_version >= '3.12'",
@ -14,15 +15,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
] ]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.8" version = "8.1.8"
@ -113,35 +105,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 },
] ]
[[package]]
name = "flask"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 },
]
[[package]]
name = "flask-sock"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flask" },
{ name = "simple-websocket" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/8f/c6ab717dc90f4e46d1430335cd4ab13e3629410bb760c0ead6de476760fb/flask-sock-0.7.0.tar.gz", hash = "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d", size = 4334 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/98/107728ce3f430b5481eb426ccc5e1f7c8ab0bd01eaf231c62a8d528ff721/flask_sock-0.7.0-py3-none-any.whl", hash = "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a", size = 3982 },
]
[[package]] [[package]]
name = "fonttools" name = "fonttools"
version = "4.55.6" version = "4.55.6"
@ -259,15 +222,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/8a/92aba852bbe2ddf3e44c354b4162b3cf350b810523ffb2d0e5937bd3f249/grpcio_tools-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:04bf30c0eb2741defe3ab6e0a6102b022d69cfd39d68fab9b954993ceca8d346", size = 1118147 }, { url = "https://files.pythonhosted.org/packages/0f/8a/92aba852bbe2ddf3e44c354b4162b3cf350b810523ffb2d0e5937bd3f249/grpcio_tools-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:04bf30c0eb2741defe3ab6e0a6102b022d69cfd39d68fab9b954993ceca8d346", size = 1118147 },
] ]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]] [[package]]
name = "h5py" name = "h5py"
version = "3.12.1" version = "3.12.1"
@ -303,15 +257,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
] ]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.5" version = "3.1.5"
@ -771,6 +716,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 },
] ]
[[package]]
name = "protoletariat"
version = "3.3.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/cd/f8c0b9d113a1d551b7a014339c4da85c71c2ba9ac889855eb45744d89459/protoletariat-3.3.9.tar.gz", hash = "sha256:db209b7058a0b700c5855b79c4151216205a93298b052c227774cba59f213831", size = 20678 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/59/bf79f403c358d1dd4a9cfa2bbfea8734bfa1a1181aecf3a8945ab6f6e7fc/protoletariat-3.3.9-py3-none-any.whl", hash = "sha256:a876a600691005c7d229a149f9639c994a087c4a66bc054717f243f0f353ae66", size = 20718 },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.6" version = "2.10.6"
@ -1003,18 +961,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
] ]
[[package]]
name = "simple-websocket"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842 },
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.17.0"
@ -1110,25 +1056,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
] ]
[[package]]
name = "werkzeug"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
]
[[package]] [[package]]
name = "where-fi" name = "where-fi"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "flask" },
{ name = "flask-sock" },
{ name = "grpcio" }, { name = "grpcio" },
{ name = "h5py" }, { name = "h5py" },
{ name = "matplotlib" }, { name = "matplotlib" },
@ -1145,12 +1077,11 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "grpcio-tools" }, { name = "grpcio-tools" },
{ name = "protoletariat" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "flask" },
{ name = "flask-sock" },
{ name = "grpcio", specifier = ">=1.70.0" }, { name = "grpcio", specifier = ">=1.70.0" },
{ name = "h5py", specifier = ">=3.12.1" }, { name = "h5py", specifier = ">=3.12.1" },
{ name = "matplotlib" }, { name = "matplotlib" },
@ -1165,16 +1096,7 @@ requires-dist = [
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [{ name = "grpcio-tools", specifier = ">=1.70.0" }] dev = [
{ name = "grpcio-tools", specifier = ">=1.70.0" },
[[package]] { name = "protoletariat", specifier = ">=3.3.9" },
name = "wsproto"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 },
] ]

View File

@ -1,15 +1,16 @@
import logging import logging
import multiprocessing as mp import multiprocessing as mp
from typing import Any
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
import torch import torch
import typer import typer
from .. import visualise
from ..config import config from ..config import config
from ..processing.aoa import AoA from ..processing.aoa import AoA
from ..processing.preprocess import Preprocessor from ..processing.preprocess import Preprocessor
from ..visualise import server as visualise
from . import file, globals from . import file, globals
app = typer.Typer(callback=globals.main) app = typer.Typer(callback=globals.main)
@ -40,23 +41,24 @@ def heatmap() -> None:
aoa = AoA() aoa = AoA()
# Start webapp in background process # Start webapp in background process
manager = mp.Manager() webapp_queue: "mp.Queue[visualise.VisualiserData]" = mp.Queue(config.sample_rate)
webapp_queue: "mp.Queue[npt.NDArray[np.float32]]" = manager.Queue(
config.sample_rate
)
webapp = mp.Process(target=visualise.start, args=(webapp_queue,)) webapp = mp.Process(target=visualise.start, args=(webapp_queue,))
webapp.start() webapp.start()
def visualise_data(data: npt.NDArray[Any], dtype: visualise.DataType) -> None:
if not webapp_queue.full():
webapp_queue.put(visualise.VisualiserData(data, dtype))
def callback(antenna_data: npt.NDArray[np.complex64]) -> None: def callback(antenna_data: npt.NDArray[np.complex64]) -> None:
logger.info(f"Got final CSI data with shape {antenna_data.shape}") logger.info(f"Got final CSI data with shape {antenna_data.shape}")
visualise_data(antenna_data, visualise.DataType.RAW_CSI)
processed = preprocessor.preprocess(antenna_data) processed = preprocessor.preprocess(antenna_data)
visualise_data(processed, visualise.DataType.PROCESSED_CSI)
logger.info(f"Processed CSI data with shape {processed.shape}") logger.info(f"Processed CSI data with shape {processed.shape}")
processed_tensor = torch.tensor(processed, device=device) processed_tensor = torch.tensor(processed, device=device)
aoa.update(processed_tensor) aoa.update(processed_tensor)
heatmap = aoa.heatmap()
if not webapp_queue.full(): visualise_data(heatmap, visualise.DataType.HEATMAP)
heatmap = aoa.heatmap()
webapp_queue.put(heatmap)
globals.csi_producer(csi_callback=callback) globals.csi_producer(csi_callback=callback)
logger.info("Finished processing CSI data") logger.info("Finished processing CSI data")

2
where_fi/visualise/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
server/generated
frontend/src/grpc

View File

@ -0,0 +1,9 @@
all: server/generated frontend/src/grpc
server/generated: protos/*.proto
mkdir -p server/generated
find protos/ -type f -name "*.proto" | xargs uv run python -m grpc_tools.protoc -Iprotos --python_out=server/generated --pyi_out=server/generated --grpc_python_out=server/generated
find protos/ -type f -name "*.proto" | xargs uv run protol --create-package --in-place --python-out server/generated/ protoc --proto-path=protos/
frontend/src/grpc: protos/*.proto
find protos/ -name "*.proto" | xargs npm exec --prefix frontend/ protoc --ts_out frontend/src/grpc -I protos/

View File

@ -1,134 +0,0 @@
import io
import logging
import multiprocessing as mp
from typing import Generator
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
from flask import Flask, Response, render_template
from flask_sock import Sock
from simple_websocket import Server
from ..config import config
from ..processing.aoa import AoA
matplotlib.use("agg")
app = Flask(__name__)
sock = Sock(app)
aoa_queue = None
logger = logging.getLogger(__name__)
data: npt.NDArray[np.complex128] = np.array([], dtype=complex)
aoa: AoA = AoA()
@app.route("/preprocessed")
def preprocessed() -> str:
return render_template("preprocessed.html")
Subscriber = Server
subscriber_settings: dict[Subscriber, tuple[int, int, int]] = {}
@sock.route("/data")
def get_data(sock: Subscriber) -> None:
while True:
msg = sock.receive()
if len(msg.split()) != 3:
break
subcarrier, rx, tx = map(int, msg.split())
subscriber_settings[sock] = (subcarrier, rx, tx)
def add_data(
raw_data: npt.NDArray[np.complex128], new_data: npt.NDArray[np.complex128]
) -> None:
if config.VISUALISE_RAW:
magn = np.abs(raw_data)
phase = np.angle(raw_data)
new_mag = np.abs(new_data)
new_phase = np.angle(new_data)
fig, axs = plt.subplots(2, 2)
axs[0, 0].plot(magn[:, 0, 0], c="b")
axs[0, 0].plot(magn[:, 1, 0], c="orange")
axs[1, 0].plot(new_mag[:, 0, 0], c="b")
axs[1, 0].plot(new_mag[:, 1, 0], c="orange")
axs[0, 1].plot(phase[:, 0, 0], c="b")
axs[0, 1].plot(phase[:, 1, 0], c="orange")
axs[1, 1].plot(new_phase[:, 0, 0], c="b")
axs[1, 1].plot(new_phase[:, 1, 0], c="orange")
fig.savefig("/tmp/plot.png")
plt.close(fig)
global data
if data.size == 0:
data = np.expand_dims(new_data, axis=0)
else:
data = np.concat([data, np.expand_dims(new_data, axis=0)], axis=0)
# Only keep latest 100 entries
if data.shape[0] > 100:
data = data[-100:]
to_remove: list[Subscriber] = []
for subscriber in subscriber_settings:
try:
subcarrier, rx, tx = subscriber_settings[subscriber]
subscriber.send(new_data.real[subcarrier, rx, tx])
except Exception as e:
to_remove.append(subscriber)
print(e)
for subscriber in to_remove:
del subscriber_settings[subscriber]
def plot_heatmap(heatmap: npt.NDArray[np.float32]) -> io.BytesIO:
logger.info(f"Making heatmap with aoa of {aoa.timestamp}")
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.9, 0.9]) # , polar=True)
r = np.linspace(
0, config.music.heatmap.tof_max, config.music.heatmap.tof_resolution
)
theta = np.linspace(0, np.pi, config.music.heatmap.theta_resolution) # Angle values
mesh = ax.pcolormesh(theta, r, heatmap, edgecolors="face", vmin=0, vmax=50)
fig.colorbar(mesh, ax=ax)
buf = io.BytesIO()
fig.savefig(buf, format="jpeg")
plt.close(fig)
buf.seek(0)
return buf
def gather_aoa() -> Generator[bytes, None, None]:
assert aoa_queue is not None
while True:
logger.debug("Receiving from aoa pipe")
heatmap = aoa_queue.get()
buf = plot_heatmap(heatmap)
yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + buf.read() + b"\r\n")
buf.close()
@app.route("/aoa_tof")
def aoa_tof() -> Response:
return Response(gather_aoa(), mimetype="multipart/x-mixed-replace; boundary=frame")
def start(conn: "mp.Queue[npt.NDArray[np.float32]]") -> None:
global app, aoa_queue
aoa_queue = conn
app.run(debug=True, use_reloader=False, host="0.0.0.0")
logger.info("Visualisation server shut down")

View File

@ -0,0 +1,66 @@
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: echo_service
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: echo_service
connect_timeout: 0.25s
type: logical_dns
# HTTP/2 support
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
lb_policy: round_robin
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 50051

View File

@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -1,4 +1,30 @@
node_modules/ # Logs
**/*_pb.d.ts logs
**/*_pb.js *.log
**/*Pb.ts npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

View File

@ -1,23 +0,0 @@
import { FigureRequest, FigureUpdate } from './figure_pb.js';
import { FigureServiceClient } from './FigureServiceClientPb';
export function connect(host: string) {
var figureService = new FigureServiceClient(host);
var request = new FigureRequest();
var stream = figureService.getFigure(request);
stream.on('data', function(response: FigureUpdate) {
if (response.hasFigure()) {
console.log("Got new figure");
}
else {
console.log("Got figure update");
}
});
stream.on('status', function(status: any) {
console.log("Status:", status);
});
stream.on('end', function() {
console.log("End");
});
}

1
where_fi/visualise/frontend/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,24 @@
import pluginVue from 'eslint-plugin-vue'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
skipFormatting,
)

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Where-Fi Visualizer</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,3 +0,0 @@
import { connect } from "./connect";
connect("http://localhost:8080");

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,46 @@
{ {
"name": "frontend", "name": "visualise",
"version": "1.0.0", "version": "0.0.0",
"description": "Frontend for where-fi", "private": true,
"license": "ISC", "type": "module",
"author": "",
"type": "commonjs",
"main": "index.ts",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix",
"format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47",
"@protobuf-ts/grpcweb-transport": "^2.9.4",
"buffer": "^6.0.3",
"google-protobuf": "^3.21.4", "google-protobuf": "^3.21.4",
"grpc-web": "^1.5.0" "grpc-web": "^1.5.0",
"plotly.js-dist": "^3.0.1",
"plotly.js-dist-min": "^3.0.1",
"vue": "^3.5.13",
"vuetify": "^3.7.12"
}, },
"devDependencies": { "devDependencies": {
"@types/google-protobuf": "^3.15.12", "@protobuf-ts/plugin": "^2.9.4",
"ts-loader": "^9.5.2", "@tsconfig/node22": "^22.0.0",
"typescript": "^5.7.3" "@types/node": "^22.13.1",
"@types/plotly.js": "^2.35.2",
"@types/plotly.js-dist-min": "^2.3.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.3.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.18.0",
"eslint-plugin-vue": "^9.32.0",
"jiti": "^2.4.2",
"npm-run-all2": "^7.0.2",
"prettier": "^3.4.2",
"typescript": "~5.7.3",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.1",
"vue-tsc": "^2.2.0"
} }
} }

View File

@ -1,42 +0,0 @@
const plotElement = document.getElementById("plot");
const layout = {
title: "Real-Time Complex Data Visualization",
xaxis: { title: "Time (ms)" },
yaxis: { title: "Value" },
};
Plotly.newPlot(
plotElement,
[
{
x: [],
y: [],
type: "scatter",
mode: "lines+markers", // Line + markers
},
],
layout,
);
const socket = new WebSocket("ws://" + location.host + "/data");
socket.onopen = function () {
console.log("Connected to the server");
socket.send("0 0 0");
};
socket.addEventListener("message", function (msg) {
Plotly.extendTraces(
plotElement,
{
x: [[msg.timeStamp]],
y: [[msg.data]],
},
[0],
300,
);
});
function changeSubcarrier() {
const subcarrier = document.getElementById("subcarrier").value;
socket.send(subcarrier + " 0 0");
}

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
import { ref } from 'vue'
import PlotList from './components/PlotList.vue'
import Figure from './components/Figure.vue'
import { Figure as FigureType } from './grpc/figure'
var plots = ref<FigureType[]>([])
var paused = ref(false)
function addPlot(fig: FigureType) {
console.log('Plot added', fig)
plots.value.push(fig)
console.log(plots)
}
function removePlot(fig: FigureType) {
console.log('Plot removed', fig)
plots.value = plots.value.filter((x) => x.uuid !== fig.uuid)
console.log(plots)
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$props: {
onClick?: (e: MouseEvent) => void
}
}
}
</script>
<template>
<v-app>
<PlotList @figure-selected="addPlot" @figure-deselected="removePlot" />
<v-main>
<v-container fluid>
<Figure v-for="plot in plots" :key="plot.uuid" :figure="plot" :paused="paused" />
</v-container>
<v-fab location="right bottom" color="secondary" @click="paused = !paused" icon app>
<v-icon :icon="paused ? 'mdi-play' : 'mdi-pause'"></v-icon>
</v-fab>
</v-main>
</v-app>
</template>

View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,92 @@
<script setup lang="ts">
import * as Plotly from 'plotly.js-dist-min'
import { Figure, FigureData } from '../grpc/figure'
import { FigureServiceClient } from '../grpc/figure.client'
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport'
import { host } from '../connect'
import { onMounted, ref } from 'vue'
import { onBeforeUnmount } from 'vue'
import { computed } from 'vue'
const { figure, paused = false } = defineProps<{ figure: Figure; paused?: boolean }>()
const cancel = ref<boolean>(false)
const data = ref<FigureData | null>(null)
const plotData = computed(() => {
if (data.value) {
switch (data.value.figure.oneofKind) {
case 'line':
return data.value.figure.line.lines.map((line) => ({
x: line.x.length == 0 ? undefined : line.x,
y: line.y,
type: 'scatter' as const,
name: line.label,
color: line.color ? line.color : undefined,
}))
case 'heatmap':
return []
/* data.value.figure.heatmap.lines.map((line) => {
return {
x: line.x,
y: line.y,
type: 'scatter',
name: line.label,
color: line.color,
}
})*/
}
}
return []
})
const layout = {
title: { text: figure.title },
xaxis: { title: { text: figure.xLabel } },
yaxis: { title: { text: figure.yLabel } },
}
function updateGraph() {
console.log('Updating graph with', plotData.value)
Plotly.newPlot(figure.uuid, plotData.value, layout, { responsive: true })
}
onMounted(async () => {
console.log('Mounted')
updateGraph()
const transport = new GrpcWebFetchTransport({ baseUrl: host })
const figureService = new FigureServiceClient(transport)
const stream = figureService.getFigureUpdate({ uuid: figure.uuid })
for await (const response of stream.responses) {
if (cancel.value) {
console.log('Stopping stream...')
break
}
console.log(`Got new value for ${figure.uuid}:`, response)
if (!paused) {
data.value = response
updateGraph()
}
}
})
onBeforeUnmount(() => {
console.log('Component is about to unmount, stopping stream...')
cancel.value = true
})
</script>
<template>
<v-row class="w-100">
<v-col cols="12">
<v-card class="w-100">
<v-card-title>{{ figure.title }}</v-card-title>
<v-card-text>
<div :id="figure.uuid" class="w-100" />
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>

View File

@ -0,0 +1,83 @@
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { Figure } from '../grpc/figure'
import { FigureServiceClient } from '../grpc/figure.client'
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport'
import { host } from '../connect'
const plots = ref<Figure[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
var emit = (_: any, ...args: any[]) => {
console.log('No emit function set', args)
}
export default defineComponent({
emits: ['figure-selected', 'figure-deselected'],
data() {
return {
selected: [],
loading: loading,
plots: plots,
error,
}
},
setup(_, ctx) {
console.log('Setup')
emit = ctx.emit
},
async mounted() {
const transport = new GrpcWebFetchTransport({ baseUrl: host })
const figureService = new FigureServiceClient(transport)
const stream = figureService.getFigure({})
for await (const response of stream.responses) {
console.log('Got new figure', response)
plots.value.push(response)
}
console.log('Done fetching figures')
loading.value = false
},
watch: {
selected(newVal: string[], oldVal: string[]) {
const added = newVal.filter((x) => !oldVal.includes(x))
const removed = oldVal.filter((x) => !newVal.includes(x))
for (const uuid of added) {
console.log('Selected', uuid)
const plot = plots.value.find((x) => x.uuid === uuid)
emit('figure-selected', plot)
}
for (const uuid of removed) {
console.log('Deselected', uuid)
const plot = plots.value.find((x) => x.uuid === uuid)
emit('figure-deselected', plot)
}
},
},
})
</script>
<template>
<v-navigation-drawer permanent width="250">
<v-list>
<v-list-item title="Available figures" subtitle="Choose some of the figures below" />
</v-list>
<v-divider />
<v-list v-model:selected="selected" select-strategy="leaf" nav>
<v-list-item v-for="plot in plots" :key="plot.uuid" :value="plot.uuid">
<v-list-item-title>{{ plot.title }}</v-list-item-title>
<v-list-item-subtitle class="text-high-emphasis"
>{{ plot.xLabel }} w/ {{ plot.yLabel }}</v-list-item-subtitle
>
</v-list-item>
<v-skeleton-loader
type="list-item-two-line"
v-for="n in 3"
:key="n"
v-if="loading"
></v-skeleton-loader>
</v-list>
</v-navigation-drawer>
</template>
<style scoped></style>

View File

@ -0,0 +1 @@
export const host = 'http://localhost:8080'

View File

@ -0,0 +1,24 @@
import App from './App.vue'
import { createApp } from 'vue'
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi'
import '@mdi/font/css/materialdesignicons.css'
const vuetify = createVuetify({
components,
directives,
theme: { defaultTheme: 'dark' },
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi,
},
},
})
createApp(App).use(vuetify).mount('#app')

View File

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
},
"vueCompilerOptions": {
"fallthroughAttributes": true,
"strictTemplates": true
}
}

View File

@ -1,12 +1,11 @@
{ {
"compilerOptions": { "files": [],
"outDir": "./dist/", "references": [
"noImplicitAny": true, {
"module": "es6", "path": "./tsconfig.node.json"
"target": "es5", },
"lib": ["es6", "dom"], {
"jsx": "react", "path": "./tsconfig.app.json"
"allowJs": true, }
"moduleResolution": "node" ]
}
} }

View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@ -0,0 +1,15 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})

View File

@ -1,21 +0,0 @@
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script>
</head>
<body>
<div class="container">
<h1>Preprocessing</h1>
<div id="plot"></div>
<script src="{{url_for('static', filename='plot.js')}}"></script>
<input type="number" id="subcarrier" class="form-control" placeholder="Choose subcarrier" onchange="changeSubcarrier()" value="0">
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>