From 841da23c47e27ede33b461f7ded9e6958ed221f6 Mon Sep 17 00:00:00 2001 From: Christos Falas Date: Mon, 25 Nov 2024 12:15:35 +0000 Subject: [PATCH] Add basic preprocessing and visualisation --- .gitignore | 176 ++++++++++++++++ Pipfile | 14 ++ Pipfile.lock | 232 ++++++++++++++++++++++ src/csi.py | 119 +++++++++++ src/main.py | 59 ++++++ src/preprocess.py | 41 ++++ src/visualise/__init__.py | 53 +++++ src/visualise/static/plot.js | 42 ++++ src/visualise/templates/preprocessed.html | 15 ++ 9 files changed, 751 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 src/csi.py create mode 100644 src/main.py create mode 100644 src/preprocess.py create mode 100644 src/visualise/__init__.py create mode 100644 src/visualise/static/plot.js create mode 100644 src/visualise/templates/preprocessed.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad4a1f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..99685e6 --- /dev/null +++ b/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = "*" +numpy = "*" +flask-sock = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..ead4cdb --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,232 @@ +{ + "_meta": { + "hash": { + "sha256": "7a239bffead5df2e0a422f7b84d0ee970f85f9d94ab29b1d56d004f67144422c" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "blinker": { + "hashes": [ + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" + ], + "markers": "python_version >= '3.9'", + "version": "==1.9.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "flask": { + "hashes": [ + "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", + "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==3.1.0" + }, + "flask-sock": { + "hashes": [ + "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a", + "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "markers": "python_version >= '3.7'", + "version": "==0.14.0" + }, + "itsdangerous": { + "hashes": [ + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.0" + }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "markupsafe": { + "hashes": [ + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "numpy": { + "hashes": [ + "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", + "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", + "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", + "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", + "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", + "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", + "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", + "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", + "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", + "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", + "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", + "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", + "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", + "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", + "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", + "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", + "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", + "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", + "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", + "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", + "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", + "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", + "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", + "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", + "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", + "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", + "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", + "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", + "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", + "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", + "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", + "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", + "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", + "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", + "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", + "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", + "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", + "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", + "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", + "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", + "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", + "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", + "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", + "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", + "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", + "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", + "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", + "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", + "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", + "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", + "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", + "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", + "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", + "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", + "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==2.1.3" + }, + "simple-websocket": { + "hashes": [ + "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", + "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.0" + }, + "werkzeug": { + "hashes": [ + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" + ], + "markers": "python_version >= '3.9'", + "version": "==3.1.3" + }, + "wsproto": { + "hashes": [ + "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", + "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==1.2.0" + } + }, + "develop": {} +} diff --git a/src/csi.py b/src/csi.py new file mode 100644 index 0000000..7386528 --- /dev/null +++ b/src/csi.py @@ -0,0 +1,119 @@ +import struct +import numpy as np + + +RATE_MCS_MOD_TYPE_POS = 8 +RATE_MCS_MOD_TYPE_MSK = 0x7 << RATE_MCS_MOD_TYPE_POS +RATE_MCS_CCK_MSK = 0 << RATE_MCS_MOD_TYPE_POS +RATE_MCS_LEGACY_OFDM_MSK = 1 << RATE_MCS_MOD_TYPE_POS +RATE_MCS_HT_MSK = 2 << RATE_MCS_MOD_TYPE_POS +RATE_MCS_VHT_MSK = 3 << RATE_MCS_MOD_TYPE_POS +RATE_MCS_HE_MSK = 4 << RATE_MCS_MOD_TYPE_POS +RATE_MCS_EHT_MSK = 5 << RATE_MCS_MOD_TYPE_POS +RATE_MCS_CHAN_WIDTH_POS = 11 +RATE_MCS_CHAN_WIDTH_MSK = 0x7 << RATE_MCS_CHAN_WIDTH_POS +RATE_MCS_CHAN_WIDTH_20_VAL = 0 +RATE_MCS_CHAN_WIDTH_20 = RATE_MCS_CHAN_WIDTH_20_VAL << RATE_MCS_CHAN_WIDTH_POS +RATE_MCS_CHAN_WIDTH_40_VAL = 1 +RATE_MCS_CHAN_WIDTH_40 = RATE_MCS_CHAN_WIDTH_40_VAL << RATE_MCS_CHAN_WIDTH_POS +RATE_MCS_CHAN_WIDTH_80_VAL = 2 +RATE_MCS_CHAN_WIDTH_80 = RATE_MCS_CHAN_WIDTH_80_VAL << RATE_MCS_CHAN_WIDTH_POS +RATE_MCS_CHAN_WIDTH_160_VAL = 3 +RATE_MCS_CHAN_WIDTH_160 = RATE_MCS_CHAN_WIDTH_160_VAL << RATE_MCS_CHAN_WIDTH_POS +RATE_MCS_CHAN_WIDTH_320_VAL = 4 +RATE_MCS_CHAN_WIDTH_320 = RATE_MCS_CHAN_WIDTH_320_VAL << RATE_MCS_CHAN_WIDTH_POS +RATE_HT_MCS_CODE_MSK = 7 +RATE_MCS_ANT_A_POS = 14 +RATE_MCS_ANT_A_MSK = 1 << RATE_MCS_ANT_A_POS +RATE_MCS_ANT_B_POS = 15 +RATE_MCS_ANT_B_MSK = 1 << RATE_MCS_ANT_B_POS +RATE_MCS_LDPC_POS = 16 +RATE_MCS_LDPC_MSK = 1 << RATE_MCS_LDPC_POS +RATE_MCS_SS_POS = 16 +RATE_MCS_SS_MSK = 1 << RATE_MCS_SS_POS +RATE_MCS_BEAMF_POS = 16 +RATE_MCS_BEAMF_MSK = 1 << RATE_MCS_BEAMF_POS + + +class CSIHeader: + def __init__(self, data: bytes): + self.csi_size = struct.unpack("I", data[0:4])[0] + self.ftm_clock = struct.unpack("I", data[8:12])[0] + self.num_rx = data[46] + self.num_tx = data[47] + self.num_subcarriers = struct.unpack("I", data[52:56])[0] + self.rssi1 = struct.unpack("I", data[60:64])[0] + self.rssi2 = struct.unpack("I", data[64:68])[0] + self.source_mac = struct.unpack("BBBBBB", data[68:74]) + self.source_mac_string = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack( + "BBBBBB", data[68:74] + ) + self.rate_flags = struct.unpack("I", data[92:96])[0] + + rate_format = self.rate_flags & RATE_MCS_MOD_TYPE_MSK + if rate_format == RATE_MCS_CCK_MSK: + rate_format = "CCK" + elif rate_format == RATE_MCS_LEGACY_OFDM_MSK: + rate_format = "LEGACY_OFDM" + elif rate_format == RATE_MCS_VHT_MSK: + rate_format = "VHT" + elif rate_format == RATE_MCS_HT_MSK: + rate_format = "HT" + elif rate_format == RATE_MCS_HE_MSK: + rate_format = "HE" + elif rate_format == RATE_MCS_EHT_MSK: + rate_format = "EHT" + else: + rate_format = "unknown" + self.rate_format = rate_format + + channel_width = self.rate_flags & RATE_MCS_CHAN_WIDTH_MSK + if channel_width == RATE_MCS_CHAN_WIDTH_20: + channel_width = "20" + elif channel_width == RATE_MCS_CHAN_WIDTH_40: + channel_width = "40" + elif channel_width == RATE_MCS_CHAN_WIDTH_80: + channel_width = "80" + elif channel_width == RATE_MCS_CHAN_WIDTH_160: + channel_width = "160" + elif channel_width == RATE_MCS_CHAN_WIDTH_320: + channel_width = "320" + else: + channel_width = "unknown" + self.channel_width = channel_width + + self.mcs = self.rate_flags & RATE_HT_MCS_CODE_MSK + self.antenna_a = True if self.rate_flags & RATE_MCS_ANT_A_MSK else False + self.antenna_b = True if self.rate_flags & RATE_MCS_ANT_B_MSK else False + self.ldpc = True if self.rate_flags & RATE_MCS_LDPC_MSK else False + self.ss = 2 if self.rate_flags & RATE_MCS_SS_MSK else 1 + self.beamforming = True if self.rate_flags & RATE_MCS_BEAMF_MSK else False + + +class CSI: + @staticmethod + def parseCsiData(data: bytes, header: CSIHeader): + csi_matrix = np.zeros( + ( + header.num_subcarriers, + header.num_rx, + header.num_tx, + ), + dtype=complex, + ) + pos = 0 + for j in range(header.num_rx): + for k in range(header.num_tx): + for n in range(header.num_subcarriers): + real = struct.unpack("h", data[pos : pos + 2])[0] + imag = struct.unpack("h", data[pos + 2 : pos + 4])[0] + pos += 4 + csi_matrix[n, j, k] = complex(real, imag) + + return csi_matrix + + def __init__(self, data: bytes): + self.header = CSIHeader(data[:272]) + self.matrix = self.parseCsiData( + data[272 : 272 + self.header.csi_size], self.header + ) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..1738c55 --- /dev/null +++ b/src/main.py @@ -0,0 +1,59 @@ +import logging +import socket +from csi import CSI +from preprocess import Preprocessor +import visualise +import threading +import struct + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", +) + + +class FeitServer: + def __init__( + self, + ip: str, + frequency: int = 5260, + channel_width: int = 80, + frame_format: str = "VHT", + ): + self.ip = ip + self.port = 8008 + self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.server.connect((self.ip, self.port)) + self.start_string = ( + f"feitcsi --frequency {frequency} " + f"--channel-width {channel_width} --format {frame_format} " + f"--mode measure" + ) + self.server.send(b"stop\n") + self.server.send(self.start_string.encode()) + + def listen(self, callback): + while True: + data = self.server.recv(65535) + try: + csidata = CSI(data) + callback(csidata) + except struct.error: + logging.error("Failed to parse CSI data") + + +def process_data(data): + logging.debug("Got CSI frame") + processed = preprocess.preprocess(data) + visualise.add_data(processed) + + +if __name__ == "__main__": + server = FeitServer("10.0.12.62") + preprocess = Preprocessor() + + # Start webapp in background thread + webapp = threading.Thread(target=visualise.start) + webapp.start() + + server.listen(process_data) diff --git a/src/preprocess.py b/src/preprocess.py new file mode 100644 index 0000000..c7807ca --- /dev/null +++ b/src/preprocess.py @@ -0,0 +1,41 @@ +import numpy as np +from csi import CSI +import logging +from queue import Queue + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +class Preprocessor: + NUM_SHORT_TERM_ENTRIES = 10 + LONG_TERM_ALPHA = 0.1 + + def __init__(self): + self.prev_entries = Queue(maxsize=100) + self.short_term_avg = np.zeros((1,), dtype=complex) + self.long_term_avg = np.zeros((1,), dtype=complex) + + def preprocess(self, csi: CSI): + h = csi.matrix + h_hat = np.multiply(h, h.conj() / abs(h.conj())) + + # Assume that all csi matrices will have the same shape + if self.short_term_avg.shape != h_hat.shape: + self.short_term_avg = np.zeros(h_hat.shape, dtype=complex) + self.long_term_avg = np.zeros(h_hat.shape, dtype=complex) + self.prev_entries = Queue(maxsize=100) + + while self.prev_entries.full(): + old_value = self.prev_entries.get() + self.short_term_avg -= old_value / self.NUM_SHORT_TERM_ENTRIES + self.prev_entries.put(h_hat) + self.short_term_avg += h_hat / self.NUM_SHORT_TERM_ENTRIES + self.long_term_avg = ( + self.long_term_avg * (1 - self.LONG_TERM_ALPHA) + + self.short_term_avg * self.LONG_TERM_ALPHA + ) + + # Remove static components + current_measurement = self.short_term_avg - self.long_term_avg + return current_measurement diff --git a/src/visualise/__init__.py b/src/visualise/__init__.py new file mode 100644 index 0000000..d7443a8 --- /dev/null +++ b/src/visualise/__init__.py @@ -0,0 +1,53 @@ +from flask import Flask, render_template, request +from flask_sock import Sock +import numpy as np +import numpy.typing as npt +import json + +app = Flask(__name__) +sock = Sock(app) + + +data: npt.NDArray[np.complex128] = np.array([], dtype=complex) + + +@app.route("/preprocessed") +def preprocessed(): + return render_template("preprocessed.html") + + +subscribers = {} + + +@sock.route("/data") +def get_data(sock): + while True: + msg = sock.receive() + subscribers[sock] = list(map(int, msg.split())) + + +def add_data(new_data): + 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 = [] + for subscriber in subscribers: + try: + subcarrier, rx, tx = subscribers[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 subscribers[subscriber] + + +def start(): + app.run(debug=True, use_reloader=False) diff --git a/src/visualise/static/plot.js b/src/visualise/static/plot.js new file mode 100644 index 0000000..4040704 --- /dev/null +++ b/src/visualise/static/plot.js @@ -0,0 +1,42 @@ +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", // Line chart + 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], + 100, + ); +}); + +function changeSubcarrier() { + const subcarrier = document.getElementById("subcarrier").value; + socket.send(subcarrier + " 0 0"); +} diff --git a/src/visualise/templates/preprocessed.html b/src/visualise/templates/preprocessed.html new file mode 100644 index 0000000..3f9d2b6 --- /dev/null +++ b/src/visualise/templates/preprocessed.html @@ -0,0 +1,15 @@ + + + + + + + +
+

Preprocessing

+
+ + +
+ +