Improve visualisation system #12
@ -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
114
uv.lock
@ -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 },
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
if not webapp_queue.full():
|
|
||||||
heatmap = aoa.heatmap()
|
heatmap = aoa.heatmap()
|
||||||
webapp_queue.put(heatmap)
|
visualise_data(heatmap, visualise.DataType.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
2
where_fi/visualise/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
server/generated
|
||||||
|
frontend/src/grpc
|
||||||
9
where_fi/visualise/Makefile
Normal file
9
where_fi/visualise/Makefile
Normal 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/
|
||||||
@ -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")
|
|
||||||
66
where_fi/visualise/envoy.yaml
Normal file
66
where_fi/visualise/envoy.yaml
Normal 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
|
||||||
9
where_fi/visualise/frontend/.editorconfig
Normal file
9
where_fi/visualise/frontend/.editorconfig
Normal 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
|
||||||
1
where_fi/visualise/frontend/.gitattributes
vendored
Normal file
1
where_fi/visualise/frontend/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
34
where_fi/visualise/frontend/.gitignore
vendored
34
where_fi/visualise/frontend/.gitignore
vendored
@ -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
|
||||||
|
|||||||
7
where_fi/visualise/frontend/.prettierrc.json
Normal file
7
where_fi/visualise/frontend/.prettierrc.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
@ -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
1
where_fi/visualise/frontend/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
24
where_fi/visualise/frontend/eslint.config.ts
Normal file
24
where_fi/visualise/frontend/eslint.config.ts
Normal 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,
|
||||||
|
)
|
||||||
12
where_fi/visualise/frontend/index.html
Normal file
12
where_fi/visualise/frontend/index.html
Normal 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>
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { connect } from "./connect";
|
|
||||||
|
|
||||||
connect("http://localhost:8080");
|
|
||||||
5197
where_fi/visualise/frontend/package-lock.json
generated
5197
where_fi/visualise/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
|
||||||
}
|
|
||||||
42
where_fi/visualise/frontend/src/App.vue
Normal file
42
where_fi/visualise/frontend/src/App.vue
Normal 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>
|
||||||
86
where_fi/visualise/frontend/src/assets/main.css
Normal file
86
where_fi/visualise/frontend/src/assets/main.css
Normal 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;
|
||||||
|
}
|
||||||
92
where_fi/visualise/frontend/src/components/Figure.vue
Normal file
92
where_fi/visualise/frontend/src/components/Figure.vue
Normal 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>
|
||||||
83
where_fi/visualise/frontend/src/components/PlotList.vue
Normal file
83
where_fi/visualise/frontend/src/components/PlotList.vue
Normal 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>
|
||||||
1
where_fi/visualise/frontend/src/connect.ts
Normal file
1
where_fi/visualise/frontend/src/connect.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const host = 'http://localhost:8080'
|
||||||
24
where_fi/visualise/frontend/src/main.ts
Normal file
24
where_fi/visualise/frontend/src/main.ts
Normal 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')
|
||||||
16
where_fi/visualise/frontend/tsconfig.app.json
Normal file
16
where_fi/visualise/frontend/tsconfig.app.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
19
where_fi/visualise/frontend/tsconfig.node.json
Normal file
19
where_fi/visualise/frontend/tsconfig.node.json
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
15
where_fi/visualise/frontend/vite.config.ts
Normal file
15
where_fi/visualise/frontend/vite.config.ts
Normal 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)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@ -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'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
Loading…
Reference in New Issue
Block a user