├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── PicoToolBox.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── other.xml └── vcs.xml ├── LICENSE.md ├── README.md ├── docs ├── Figure_1.png └── PicoToolBoxdoc.md ├── main.py ├── picoscenes.pyx ├── pyproject.toml ├── rx_by_usrpN210.csi ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.csi 2 | .DS_Store 3 | !rx_by_usrpN210.csi 4 | picoscenes.cpp 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | localtest.py 11 | 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rxs_parsing_core"] 2 | path = rxs_parsing_core 3 | url = https://github.com/wifisensing/RXS-Parsing-Core.git 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/PicoToolBox.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Tian Teng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Picoscenes Python Toolbox 2 | 3 | The official Python toolbox for parsing **.csi** files from PicoScenes. 4 | 5 | ## Highlights 6 | 7 | - Supports Linux, macOS, and Windows 8 | - Easy to use 9 | - Faster parsing than the MATLAB implementation 10 | - High scalability 11 | - The official toolbox for parsing **.csi** files 12 | 13 | ## Preparation on Windows 10 or 11 14 | 15 | 1. Install [TDM-GCC-64](https://jmeubank.github.io/tdm-gcc/) (choose the MinGW-w64-based version, version 10.3+). 16 | 2. Set the compiler to MinGW64 using [mingw64ccompiler](https://github.com/imba-tjd/mingw64ccompiler): 17 | 18 | ```bash 19 | pip install git+https://github.com/imba-tjd/mingw64ccompiler 20 | python -m mingw64ccompiler install_specs # Run once 21 | python -m mingw64ccompiler install # Works with venv 22 | ``` 23 | 24 | ## Installation 25 | 26 | 1. Clone this repository with the `--recursive` option: 27 | 28 | ```bash 29 | git clone https://github.com/Herrtian/PicoscenesToolbox.git --recursive 30 | ``` 31 | 32 | 2. Install Python and dependencies: 33 | 34 | ```bash 35 | sudo apt update 36 | sudo apt install python3 python3-pip python3-venv 37 | ``` 38 | 39 | - If you are a **Chinese** user, you can change the pip source to accelerate the download speed: 40 | 41 | ```bash 42 | pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ 43 | ``` 44 | 45 | Preferrably, create a virtual environment 46 | 47 | ```bash 48 | python3 -m venv .venv 49 | source .venv/bin/activate # or run venv\Scripts\activate on Windows 50 | ``` 51 | 52 | 3. Build the package 53 | 54 | ```bash 55 | pip3 install . # add -e for an editable; so no reinstall is required on changes 56 | ``` 57 | 58 | ## Quick start 59 | 60 | **rx_by_usrpN210.csi** is a sample binary csi file created by **Picoscenes**. 61 | Refer to `main.py` for an example usage on how to parse and plot the collected CSI. 62 | This example shows the first frame of **rx_by_usrpN210.csi** and creates a plot 63 | with x-axis representing **SubcarrierIndex** and y-axis showing **Magnitude**. 64 | 65 | ```bash 66 | python3 main.py 67 | ``` 68 | 69 | On a successful run, you should see an output similar to: 70 | 71 | ![](docs/Figure_1.png) 72 | 73 | ## Documentation 74 | 75 | For more details about **PicoscenesToolbox**, refer to the **./docs** folder or visit the [wiki](). 76 | 77 | ## Issues & Feedback 78 | 79 | If you encounter any issues or have questions about **PicoscenesToolbox**, please submit an **issue** on GitHub. 80 | 81 | Consider giving this project a **star** or **fork** if you find it useful! 82 | 83 | ## References 84 | 85 | - **[PicoScenes](https://ps.zpj.io/)**: A powerful Wi-Fi sensing platform middleware for a wide range of hardware. 86 | - This project was released by [Zhiping Jiang](https://zpj.io/bio/). 87 | - [**csiread**](https://github.com/citysu/csiread): A fast channel state information parser for Intel, Atheros, Nexmon, and ESP32 in Python. 88 | - This project, developed by [citysu](https://github.com/citysu/csiread), served as inspiration for **PicoscenesToolbox**. 89 | 90 | ## License 91 | 92 | This project is licensed under the **MIT License**. If you use this toolbox in your project, citing this repository would be greatly appreciated. 93 | 94 | ``` 95 | Tian Teng. PicoscenesToolbox: An official tool plugin for parsing **.csi** from PicoScenes in Python. (2021). https://github.com/Herrtian/PicoscenesToolbox. 96 | -------------------------------------------------------------------------------- /docs/Figure_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wifisensing/PicoScenes-Python-Toolbox/f676855accedf79e848bf2f1d10ee5c1f649a33a/docs/Figure_1.png -------------------------------------------------------------------------------- /docs/PicoToolBoxdoc.md: -------------------------------------------------------------------------------- 1 | # PicoToolBox 2 | 3 | This docs shows you how the datastructure displays in PicoToolBox. 4 | 5 | You will not know how the python parse CSI data,but you would better know the structure if you want to create your own data analysis. 6 | 7 | ## Picoscenes 8 | 9 | Picoscenes is the well packed frames ,you can use **.raw**[] to get a specific frame. 10 | 11 | Example: 12 | 13 | ```python 14 | i = 0 15 | frames = Picoscenes("rx_by_usrpN210.csi") # return a Picoscenes Object 16 | First_Frame = frames.raw[i] # get the first frame ,you can set i to get different frame 17 | ``` 18 | 19 | 20 | 21 | Now i will show you the more details in Picoscenes raw. 22 | 23 | For all the attributes i will give you a small example. 24 | 25 | ### Frame 26 | 27 | frame is a structure which inlcudes all information and structure . 28 | 29 | you can also use **print(frames.raw[0])** to get the newst infomation if i have not updated the docs. 30 | 31 | frame has the method frame_value_xxx_has_value it means some attributes are **optional** 32 | 33 | For example, if **frame_value.mvmExtraSegment.has_value** == 1 ,then **frame.raw[0].MVMExtra** is a concrete value, or it is **None** 34 | 35 | | Name | Type | Illustration | 36 | | ---------------- | -------------------------- | ------------ | 37 | | StandardHeader | ieee80211_mac_frame_header | | 38 | | RxSBasic | RxSBasicSegment | | 39 | | RxExtraInfo | ExtraInfoSegment | | 40 | | CSI | CSISegment | | 41 | | MVMExtra | MVMExtraSegment | optional | 42 | | PicoScenesHeader | PicoScenesFrameHeader | optional | 43 | | TxExtraInfo | ExtraInfoSegment | optional | 44 | | PilotCSI | CSISegment | optional | 45 | | LegacyCSI | CSISegment | optional | 46 | | BasebandSignals | BasebandSignalSegment | optional | 47 | | PreEQSymbols | PreEQSymbolsSegment | optional | 48 | | MPDU | vector[uint8_t] | | 49 | 50 | 51 | 52 | **note**: **StandardHeader** & **CSI** & **RxSBasic** & **RxExtraInfo** are all the strcutures contained. 53 | 54 | 55 | 56 | ### CSI 57 | 58 | CSI is the most important structure in Picoscenes. 59 | 60 | | Name | Type | Illustration | 61 | | ------------------- | ------------------------------ | -------------- | 62 | | DeviceType | PicoScenesDeviceType | uint16_t | 63 | | PacketFormat | PacketFormatEnum | int8_t | 64 | | CBW | ChannelBandwidthEnum | uint16_t | 65 | | CarrierFreq | uint64_t | | 66 | | SamplingRate | uint64_t | | 67 | | SubcarrierBandwidth | uint32_t | | 68 | | numTones | CSIDimension | uint16_t | 69 | | numTx | CSIDimension | uint8_t | 70 | | numRx | CSIDimension | uint8_t | 71 | | numESS | CSIDimension | uint8_t | 72 | | numCSI | CSIDimension | uint16_t | 73 | | ant_sel | uint8_t | | 74 | | CSI | SignalMatrix[ccomplex[double]] | Cplx structure | 75 | | Mag | SignalMatrix[double] | Cplx structure | 76 | | Phase | SignalMatrix[double] | Cplx structure | 77 | | SubcarrierIndex | vector[int16_t] | Cplx structure | 78 | 79 | **uint** means **unsigned int** 80 | 81 | #### Example 82 | 83 | ```python 84 | i = 0 85 | frames = Picoscenes("rx_by_usrpN210.csi") 86 | numTones = frames.raw[i].get("CSI").get("numTones") # get the first frame numtones 87 | ``` 88 | 89 | 90 | 91 | #### PicoScenesDeviceType (uint16_t) 92 | 93 | # all attributes are uint16_t 94 | QCA9300 95 | IWL5300 96 | IWLMVM 97 | MAC80211Compatible 98 | USRP 99 | VirtualSDR 100 | Unknown 101 | 102 | 103 | 104 | #### PacketFormatEnum(int8_t) 105 | 106 | PacketFormat_NonHT 107 | PacketFormat_HT 108 | PacketFormat_VHT 109 | PacketFormat_HESU 110 | PacketFormat_HEMU 111 | PacketFormat_Unknown 112 | 113 | 114 | 115 | #### ChannelBandwidthEnum(uint16_t) 116 | 117 | CBW_5 118 | CBW_10 119 | CBW_20 120 | CBW_40 121 | CBW_80 122 | CBW_160 123 | 124 | #### SignalMatrix 125 | 126 | | Name | Type | Illustratio | 127 | | ---------- | --------------- | ----------- | 128 | | array | vector[T] | | 129 | | dimensions | vector[int64_t] | | 130 | 131 | 132 | 133 | #### CSIDimension 134 | 135 | uint16_t numTones 136 | uint8_t numTx 137 | uint8_t numRx 138 | uint8_t numESS 139 | uint16_t numCSI 140 | 141 | ### StandardHeader 142 | 143 | | Name | Type | Illustration | 144 | | ------------ | -------------------------- | -------------- | 145 | | ControlField | ieee80211_mac_frame_header | Cplx Structure | 146 | | Addr1 | uint8_t [6] | | 147 | | Addr2 | uint8_t [6] | | 148 | | Addr3 | uint8_t [6] | | 149 | | Fragment | uint16_t | | 150 | | Sequence | uint16_t | | 151 | 152 | #### Example 153 | 154 | ```python 155 | i = 0 # stands for the first frame of csi frames 156 | 157 | frames = Picoscenes("rx_by_usrpN210.csi") 158 | Fragment = frames.raw[i].get("StandardHeader").get("Fragment") 159 | ``` 160 | 161 | 162 | 163 | #### ieee80211_mac_frame_header 164 | 165 | | Name | Type | Illustration | 166 | | ---------- | -------- | ------------ | 167 | | version | uint16_t | | 168 | | type | uint16_t | | 169 | | subtype | uint16_t | | 170 | | toDS | uint16_t | | 171 | | fromDS | uint16_t | | 172 | | moreFrags | uint16_t | | 173 | | retry | uint16_t | | 174 | | power_mgmt | uint16_t | | 175 | | more | uint16_t | | 176 | | protect | uint16_t | | 177 | | order | uint16_t | | 178 | 179 | 180 | 181 | ### RxSBasic 182 | 183 | | Name | Type | Illustration | 184 | | ------------ | -------- | ------------ | 185 | | deviceType | uint16_t | | 186 | | timestamp | uint64_t | | 187 | | centerFreq | int16_t | | 188 | | controlFreq | int16_t | | 189 | | CBW | uint16_t | | 190 | | packetFormat | uint8_t | | 191 | | packetCBW | uint16_t | | 192 | | GI | uint16_t | | 193 | | MCS | uint8_t | | 194 | | numSTS | uint8_t | | 195 | | numESS | uint8_t | | 196 | | numRx | uint8_t | | 197 | | noiseFloor | int8_t | | 198 | | rssi | int8_t | | 199 | | rssi1 | int8_t | | 200 | | rssi2 | int8_t | | 201 | | rssi3 | int8_t | | 202 | 203 | 204 | 205 | #### Example 206 | 207 | ```python 208 | i = 0 # stands for the first frame of csi frames 209 | 210 | frames = Picoscenes("rx_by_usrpN210.csi") 211 | timestamp = frames.raw[i].get("RxSBasic").get("timestamp") 212 | 213 | ``` 214 | 215 | 216 | 217 | ### RxExtraInfo(Optional) 218 | 219 | Extrainfo is also a cplx flexible structure. It contains the **has_xxx** attribute. 220 | 221 | **has_xxx** is a **bint** type, which is 0 or 1. 222 | 223 | For example, if **hasLength** == 1 ,then **RxExtraInfo.Length** is a concrete value, or it is **None** 224 | 225 | In order to **not** trouble you ,i will just list the meaningful Information of the RXExtraInfo. 226 | 227 | 228 | 229 | | Name | Type | Illustration | 230 | | ---------------- | --------------------- | -------------- | 231 | | length | uint16_t | | 232 | | version | uint64_t | | 233 | | macaddr_cur | uint8_t [6] | | 234 | | macaddr_rom | uint8_t [6] | | 235 | | chansel | uint32_t | | 236 | | bmode | uint8_t | | 237 | | evm | int8_t [20] | | 238 | | tx_chainmask | uint8_t | | 239 | | rx_chainmask | uint8_t | | 240 | | txpower | uint8_t | | 241 | | cf | uint64_t | | 242 | | txtsf | uint32_t | | 243 | | last_txtsf | uint32_t | | 244 | | channel_flags | uint16_t | | 245 | | tx_ness | uint8_t | | 246 | | tuning_policy | AtherosCFTuningPolicy | Cplx structure | 247 | | pll_rate | uint16_t | | 248 | | pll_clock_select | uint8_t | | 249 | | pll_refdiv | uint8_t | | 250 | | agc | uint8_t | | 251 | | ant_sel | uint8_t [3] | | 252 | | cfo | int32_t | | 253 | | sfo | int32_t | | 254 | 255 | 256 | 257 | #### Example 258 | 259 | ```python 260 | i = 0 # stands for the first frame of csi frames 261 | 262 | frames = Picoscenes("rx_by_usrpN210.csi") 263 | if frames.raw[i].get("RxExtraInfo").get("has_txpower"): 264 | txpower = frames.raw[i].get("RxExtraInfo").get("txpower") 265 | # make sure the frame has_xxx attribute then assign the variable 266 | 267 | ``` 268 | 269 | 270 | 271 | #### AtherosCFTuningPolicy 272 | 273 | | Name | Type | Illustration | 274 | | ----------------------- | ------- | ------------ | 275 | | CFTuningByDefault | uint8_t | | 276 | | CFTuningByChansel | uint8_t | | 277 | | CFTuningByFastCC | uint8_t | | 278 | | CFTuningByHardwareReset | uint8_t | | 279 | 280 | 281 | 282 | ### MVMExtra(Optional) 283 | 284 | | Name | Type | Illustration | 285 | | ---------- | -------- | ------------ | 286 | | FMTClock | uint32_t | | 287 | | usClock | uint32_t | | 288 | | RateNFlags | uint32_t | | 289 | 290 | 291 | 292 | ### PicoScenesHeader(Optional) 293 | 294 | | Name | Type | Illustration | 295 | | ---------- | -------------------- | ------------ | 296 | | MagicValue | uint32_t | | 297 | | Version | uint32_t | | 298 | | DeviceType | PicoScenesDeviceType | uint16_t | 299 | | FrameType | uint8_t | | 300 | | TaskId | uint16_t | | 301 | | TxId | uint16_t | | 302 | 303 | 304 | 305 | ### TxExtraInfo(Optional) 306 | 307 | Txtrainfo is also a cplx flexible structure. It contains the **has_xxx** attribute. 308 | 309 | **has_xxx** is a **bint** type, which is 0 or 1. 310 | 311 | For example, if **hasLength** == 1 ,then **Txtrainfo .Length** is a concrete value, or it is **None** 312 | 313 | In order to **not** trouble you ,i will just list the meaningful Information of the Txtrainfo . 314 | 315 | | Name | Type | Illustration | 316 | | ---------------- | --------------------- | -------------- | 317 | | length | uint16_t | | 318 | | version | uint64_t | | 319 | | macaddr_cur | uint8_t [6] | | 320 | | macaddr_rom | uint8_t [6] | | 321 | | chansel | uint32_t | | 322 | | bmode | uint8_t | | 323 | | evm | int8_t [20] | | 324 | | tx_chainmask | uint8_t | | 325 | | rx_chainmask | uint8_t | | 326 | | txpower | uint8_t | | 327 | | cf | uint64_t | | 328 | | txtsf | uint32_t | | 329 | | last_txtsf | uint32_t | | 330 | | channel_flags | uint16_t | | 331 | | tx_ness | uint8_t | | 332 | | tuning_policy | AtherosCFTuningPolicy | Cplx structure | 333 | | pll_rate | uint16_t | | 334 | | pll_clock_select | uint8_t | | 335 | | pll_refdiv | uint8_t | | 336 | | agc | uint8_t | | 337 | | ant_sel | uint8_t [3] | | 338 | | cfo | int32_t | | 339 | | sfo | int32_t | | 340 | 341 | 342 | 343 | #### Example 344 | 345 | ```python 346 | i = 0 # stands for the first frame of csi frames 347 | 348 | frames = Picoscenes("rx_by_qca9300.csi") 349 | txpower = frames.raw[i].get("TxExtraInfo").get("txpower") 350 | print(txpower) 351 | 352 | ``` 353 | 354 | 355 | 356 | ### PilotCSI(Optional) 357 | 358 | | Name | Type | Illustration | 359 | | ------------------- | ------------------------------ | -------------- | 360 | | DeviceType | PicoScenesDeviceType | uint16_t | 361 | | PacketFormat | PacketFormatEnum | int8_t | 362 | | CBW | ChannelBandwidthEnum | uint16_t | 363 | | CarrierFreq | uint64_t | | 364 | | SamplingRate | uint64_t | | 365 | | SubcarrierBandwidth | uint32_t | | 366 | | numTones | CSIDimension | uint16_t | 367 | | numTx | CSIDimension | uint8_t | 368 | | numRx | CSIDimension | uint8_t | 369 | | numESS | CSIDimension | uint8_t | 370 | | numCSI | CSIDimension | uint16_t | 371 | | ant_sel | uint8_t | | 372 | | CSI | SignalMatrix[ccomplex[double]] | Cplx structure | 373 | | Mag | SignalMatrix[double] | Cplx structure | 374 | | Phase | SignalMatrix[double] | Cplx structure | 375 | | SubcarrierIndex | vector[int16_t] | Cplx structure | 376 | 377 | 378 | 379 | #### Example 380 | 381 | ```python 382 | i = 0 383 | frames = Picoscenes("rx_by_usrpN210.csi") 384 | frame = frames.raw[i] 385 | if frame.get("PilotCSI"): 386 | numTones = frame.get("PilotCSI").get("numTones") 387 | ``` 388 | 389 | 390 | 391 | 392 | 393 | ### LegacyCSI(Optional) 394 | 395 | | Name | Type | Illustration | 396 | | ------------------- | ------------------------------ | -------------- | 397 | | DeviceType | PicoScenesDeviceType | uint16_t | 398 | | PacketFormat | PacketFormatEnum | int8_t | 399 | | CBW | ChannelBandwidthEnum | uint16_t | 400 | | CarrierFreq | uint64_t | | 401 | | SamplingRate | uint64_t | | 402 | | SubcarrierBandwidth | uint32_t | | 403 | | numTones | CSIDimension | uint16_t | 404 | | numTx | CSIDimension | uint8_t | 405 | | numRx | CSIDimension | uint8_t | 406 | | numESS | CSIDimension | uint8_t | 407 | | numCSI | CSIDimension | uint16_t | 408 | | ant_sel | uint8_t | | 409 | | CSI | SignalMatrix[ccomplex[double]] | Cplx structure | 410 | | Mag | SignalMatrix[double] | Cplx structure | 411 | | Phase | SignalMatrix[double] | Cplx structure | 412 | | SubcarrierIndex | vector[int16_t] | Cplx structure | 413 | 414 | 415 | 416 | #### Example 417 | 418 | ```python 419 | i = 0 420 | frames = Picoscenes("rx_by_usrpN210.csi") 421 | frame = frames.raw[i] 422 | if frame.get("LegacyCSI"): 423 | numTones = frame.get("LegacyCSI").get("numTones") 424 | ``` 425 | 426 | 427 | 428 | ### BasebandSignals(Optional[np.asarray]) 429 | 430 | ```python 431 | i = 0 432 | frames = Picoscenes("rx_by_usrpN210.csi") 433 | frame = frames.raw[i] 434 | BasebandSignals = frame.get("BasebandSignals") 435 | ``` 436 | 437 | 438 | 439 | ### PreEQSymbols(Optional[np.asarray]) 440 | 441 | ```python 442 | i = 0 443 | frames = Picoscenes("rx_by_usrpN210.csi") 444 | frame = frames.raw[i] 445 | BasebandSignals = frame.get("PreEQSymbols") 446 | ``` 447 | 448 | 449 | 450 | ### MPDU(uint8_t) 451 | 452 | ```python 453 | i = 0 454 | frames = Picoscenes("rx_by_usrpN210.csi") 455 | frame = frames.raw[i] # get the first frame numtones 456 | MPDU = frame.get("MPDU") 457 | ``` 458 | 459 | **If you have something dont know , you can check picoscenes.pyx or contact with me. ** 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple usage example 3 | 4 | Processing and plotting CSI collected using Picoscenes with a USRP N210. 5 | """ 6 | from picoscenes import Picoscenes 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | 10 | 11 | frames = Picoscenes("rx_by_usrpN210.csi") 12 | frame = frames.raw[0] # Get the first frame in the list 13 | 14 | num_tones = frame.get("CSI").get("numTones") 15 | subcarrier_indices = np.array(frame.get("CSI").get("SubcarrierIndex")) 16 | magnitude = np.array(frame.get("CSI").get("Mag"))[:num_tones] 17 | 18 | plt.title("Magnitude Demo") 19 | plt.xlabel("subcarrier index") 20 | plt.ylabel("CSI Magnitude") 21 | plt.plot(subcarrier_indices, magnitude) 22 | plt.show() 23 | -------------------------------------------------------------------------------- /picoscenes.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | import struct 3 | 4 | from libc.stdio cimport (fopen, fread, fclose, fseek, ftell, printf, FILE, 5 | SEEK_END, SEEK_SET, SEEK_CUR) 6 | from libcpp.memory cimport shared_ptr 7 | from libc.stdint cimport (uint8_t, uint16_t, uint32_t, uint64_t, 8 | int8_t, int16_t, int32_t, int64_t) 9 | from libc.stdlib cimport malloc, realloc, free, exit 10 | from libcpp.string cimport string 11 | from libcpp.vector cimport vector 12 | from libcpp.complex cimport complex as ccomplex 13 | from cython.operator cimport dereference as deref 14 | 15 | import numpy as np 16 | import sys 17 | 18 | cdef extern from "" namespace "std" nogil: 19 | cdef cppclass optional[T]: 20 | ctypedef T value_type 21 | optional() except + 22 | optional(optional &) except + 23 | bint operator ==(optional &, optional &) 24 | bint operator !=(optional &, optional &) 25 | bint operator <(optional &, optional &) 26 | bint operator >(optional &, optional &) 27 | bint operator <=(optional &, optional &) 28 | bint operator >=(optional &, optional &) 29 | bint has_value() const 30 | T& value() 31 | T& operator *() 32 | void swap(optional &) 33 | void reset() 34 | 35 | cdef extern from "rxs_parsing_core/ModularPicoScenesFrame.hxx": 36 | # ModularPicoScenesFrame.hxx 37 | cdef packed struct ieee80211_mac_frame_header_frame_control_field: 38 | uint16_t version 39 | uint16_t type 40 | uint16_t subtype 41 | uint16_t toDS 42 | uint16_t fromDS 43 | uint16_t moreFrags 44 | uint16_t retry 45 | uint16_t power_mgmt 46 | uint16_t more 47 | uint16_t protect 48 | uint16_t order 49 | 50 | # ModularPicoScenesFrame.hxx 51 | cdef packed struct ieee80211_mac_frame_header: 52 | ieee80211_mac_frame_header_frame_control_field fc 53 | # uint16_t dur 54 | uint8_t addr1[6] 55 | uint8_t addr2[6] 56 | uint8_t addr3[6] 57 | uint16_t frag 58 | uint16_t seq 59 | 60 | # RxSBasicSegment.hxx 61 | cdef packed struct RxSBasic: 62 | uint16_t deviceType 63 | uint64_t tstamp 64 | uint64_t systemTime 65 | int16_t centerFreq 66 | int16_t controlFreq 67 | uint16_t cbw 68 | uint8_t packetFormat 69 | uint16_t pkt_cbw 70 | uint16_t guardInterval 71 | uint8_t mcs 72 | uint8_t numSTS 73 | uint8_t numESS 74 | uint8_t numRx 75 | # uint8_t numUser 76 | # uint8_t userIndex 77 | int8_t noiseFloor 78 | int8_t rssi 79 | int8_t rssi_ctl0 80 | int8_t rssi_ctl1 81 | int8_t rssi_ctl2 82 | int8_t rssi_ctl3 83 | int8_t rssi_ctl4 84 | int8_t rssi_ctl5 85 | int8_t rssi_ctl6 86 | int8_t rssi_ctl7 87 | 88 | # RxSBasicSegment.hxx 89 | cdef cppclass RxSBasicSegment: 90 | const RxSBasic & getBasic() const 91 | 92 | # RXSExtraInfo.hxx 93 | cdef enum AtherosCFTuningPolicy: # uint8_t 94 | CFTuningByChansel 95 | CFTuningByFastCC 96 | CFTuningByHardwareReset 97 | CFTuningByDefault 98 | 99 | # RXSExtraInfo.hxx 100 | cdef struct ExtraInfo: 101 | # uint32_t featureCode 102 | bint hasLength 103 | bint hasVersion 104 | bint hasMacAddr_cur 105 | bint hasMacAddr_rom 106 | bint hasChansel 107 | bint hasBMode 108 | bint hasEVM 109 | bint hasTxChainMask 110 | bint hasRxChainMask 111 | bint hasTxpower 112 | bint hasCF 113 | bint hasTxTSF 114 | bint hasLastHWTxTSF 115 | bint hasChannelFlags 116 | bint hasTxNess 117 | bint hasTuningPolicy 118 | bint hasPLLRate 119 | bint hasPLLRefDiv 120 | bint hasPLLClkSel 121 | bint hasAGC 122 | bint hasAntennaSelection 123 | bint hasSamplingRate 124 | bint hasCFO 125 | bint hasSFO 126 | # bint hasPreciseTxTiming 127 | uint16_t length 128 | uint64_t version 129 | uint8_t macaddr_rom[6] 130 | uint8_t macaddr_cur[6] 131 | uint32_t chansel 132 | uint8_t bmode 133 | int8_t evm[20] 134 | uint8_t txChainMask 135 | uint8_t rxChainMask 136 | uint8_t txpower 137 | uint64_t cf 138 | uint32_t txTSF 139 | uint32_t lastHwTxTSF 140 | uint16_t channelFlags 141 | uint8_t tx_ness 142 | AtherosCFTuningPolicy tuningPolicy 143 | uint16_t pll_rate 144 | uint8_t pll_refdiv 145 | uint8_t pll_clock_select 146 | uint8_t agc 147 | uint8_t ant_sel[3] 148 | uint64_t samplingRate 149 | int32_t cfo 150 | int32_t sfo 151 | # double preciseTxTiming 152 | 153 | # ExtraInfoSegment.hxx 154 | cdef cppclass ExtraInfoSegment: 155 | const ExtraInfo & getExtraInfo() const 156 | 157 | # PicoScenesCommons.hxx 158 | cdef enum PicoScenesDeviceType: # uint16_t 159 | QCA9300 160 | IWL5300 161 | IWLMVM_AX200 162 | IWLMVM_AX210 163 | MAC80211Compatible 164 | USRP 165 | VirtualSDR 166 | Unknown 167 | 168 | # PicoScenesCommons.hxx 169 | cdef enum PacketFormatEnum: # int8_t 170 | PacketFormat_NonHT 171 | PacketFormat_HT 172 | PacketFormat_VHT 173 | PacketFormat_HESU 174 | PacketFormat_HEMU 175 | PacketFormat_Unknown 176 | 177 | # PicoScenesCommons.hxx 178 | cdef enum ChannelBandwidthEnum: # uint16_t 179 | CBW_5 180 | CBW_10 181 | CBW_20 182 | CBW_40 183 | CBW_80 184 | CBW_160 185 | 186 | # CSISegment.hxx 187 | cdef cppclass CSIDimension: 188 | uint16_t numTones 189 | uint8_t numTx 190 | uint8_t numRx 191 | uint8_t numESS 192 | uint16_t numCSI 193 | 194 | # SignalMatrix.hxx 195 | cdef cppclass SignalMatrix[T]: 196 | vector[T] array 197 | vector[int64_t] dimensions 198 | 199 | # CSISegment.hxx 200 | cdef cppclass CSI: 201 | PicoScenesDeviceType deviceType 202 | uint8_t firmwareVersion 203 | PacketFormatEnum packetFormat 204 | ChannelBandwidthEnum cbw 205 | uint64_t carrierFreq 206 | uint64_t samplingRate 207 | uint32_t subcarrierBandwidth 208 | CSIDimension dimensions 209 | uint8_t antSel 210 | # int16_t subcarrierOffset 211 | vector[int16_t] subcarrierIndices 212 | SignalMatrix[ccomplex[float]] CSIArray 213 | SignalMatrix[float] magnitudeArray 214 | SignalMatrix[float] phaseArray 215 | 216 | # CSISegment.hxx 217 | cdef cppclass CSISegment: 218 | const shared_ptr[CSI] & getCSI() const 219 | 220 | # DPASRequestSegment.hxx 221 | # cdef packed struct DPASRequest: 222 | # uint8_t requestMode 223 | # uint16_t batchId 224 | # uint16_t batchLength 225 | # uint16_t sequenceId 226 | # uint16_t intervalTime 227 | # uint16_t intervalStep 228 | # PicoScenesDeviceType deviceType 229 | # uint16_t deviceSubtype 230 | # uint64_t carrierFrequency 231 | # uint32_t samplingFrequency 232 | 233 | # cdef cppclass DPASRequestSegment: 234 | # DPASRequest getRequest() const 235 | 236 | # MVMExtraSegment.hxx 237 | cdef cppclass IntelMVMParsedCSIHeader: 238 | # uint32_t ftmClock 239 | # uint32_t muClock 240 | # uint32_t rate_n_flags 241 | uint32_t value56[9]; 242 | uint32_t rateNflag; 243 | uint32_t value96[44]; 244 | 245 | # MVMExtraSegment.hxx 246 | cdef cppclass IntelMVMExtrta: 247 | IntelMVMParsedCSIHeader parsedHeader 248 | 249 | # MVMExtraSegment.hxx 250 | cdef cppclass MVMExtraSegment: 251 | const IntelMVMExtrta & getMvmExtra() const 252 | 253 | # ModularPicoScenesFrame.hxx 254 | cdef packed struct PicoScenesFrameHeader: 255 | uint32_t magicValue 256 | uint32_t version 257 | PicoScenesDeviceType deviceType 258 | # uint8_t numSegments 259 | uint8_t frameType 260 | uint16_t taskId 261 | uint16_t txId 262 | 263 | # BasebandSignalSegment.hxx 264 | cdef cppclass BasebandSignalSegment: 265 | const SignalMatrix[ccomplex[float]] & getSignals() const 266 | 267 | # PreEQSymbolsSegment.hxx 268 | cdef cppclass PreEQSymbolsSegment: 269 | const SignalMatrix[ccomplex[float]] & getPreEqSymbols() const 270 | # ModularPicoScenesFrame.hxx 271 | cdef cppclass ModularPicoScenesRxFrame: 272 | ieee80211_mac_frame_header standardHeader 273 | shared_ptr[RxSBasicSegment] rxSBasicSegment 274 | shared_ptr[ExtraInfoSegment] rxExtraInfoSegment 275 | shared_ptr[CSISegment] csiSegment 276 | shared_ptr[MVMExtraSegment] mvmExtraSegment 277 | shared_ptr[PicoScenesFrameHeader] PicoScenesHeader 278 | shared_ptr[ExtraInfoSegment] txExtraInfoSegment 279 | # optional[DPASRequestSegment] dpasRequestSegment 280 | # optional[CSISegment] pilotCSISegment 281 | shared_ptr[CSISegment] legacyCSISegment 282 | shared_ptr[BasebandSignalSegment] basebandSignalSegment 283 | #optional[PreEQSymbolsSegment] preEQSymbolsSegment 284 | vector[vector[uint8_t]] mpdus 285 | 286 | @ staticmethod 287 | optional[ModularPicoScenesRxFrame] fromBuffer(const uint8_t *buffer, uint32_t bufferLength, bint interpolateCSI) 288 | string toString() const 289 | 290 | 291 | cdef class Picoscenes: 292 | cdef readonly str file 293 | cdef readonly int count 294 | cdef public list raw 295 | cdef bint if_report 296 | 297 | def __cinit__(self, file, if_report=True, *argv, **kw): 298 | self.file = file 299 | self.if_report = if_report 300 | self.raw = list() 301 | self.read() 302 | 303 | def __init__(self, file, if_report=True): 304 | pass 305 | 306 | cpdef read(self): 307 | self.seek(self.file, 0, 0) 308 | 309 | cpdef seek(self, file, long pos, long num): 310 | cdef FILE *f 311 | 312 | tempfile = file.encode(encoding="utf-8") 313 | cdef char *datafile = tempfile 314 | 315 | f = fopen(datafile, "rb") 316 | if f is NULL: 317 | printf("Open failed!\n") 318 | sys.exit(-1) 319 | 320 | fseek(f, 0, SEEK_END) 321 | cdef long lens = ftell(f) 322 | fseek(f, pos, SEEK_SET) 323 | 324 | cdef int count = 0 325 | cdef int l, i 326 | cdef uint32_t field_len 327 | cdef uint32_t buf_size = 1024 328 | cdef unsigned char *buf 329 | cdef optional[ModularPicoScenesRxFrame] frame 330 | 331 | if num == 0: 332 | num = lens 333 | if self.raw: 334 | self.raw.clear() 335 | 336 | buf = malloc(buf_size * sizeof(unsigned char)) # CHECK NULL 337 | while pos < (lens - 4): 338 | l = fread(buf, sizeof(unsigned char), 4, f) 339 | if l < 4: 340 | break 341 | field_len = cu32l(buf[0], buf[1], buf[2], buf[3]) + 4 342 | fseek(f, -4, SEEK_CUR) 343 | if buf_size < field_len: 344 | buf = realloc(buf, field_len) # CHECK NULL 345 | buf_size = field_len 346 | l = fread(buf, sizeof(unsigned char), field_len, f) 347 | 348 | # rxs_parsing_core 349 | frame = ModularPicoScenesRxFrame.fromBuffer(buf, field_len, True) 350 | self.raw.append(parse(&frame)) 351 | 352 | pos += field_len 353 | count += 1 354 | if count >= num: 355 | break 356 | free(buf) 357 | fclose(f) 358 | self.count = count 359 | 360 | cpdef pmsg(self, unsigned char *data): 361 | # This method hasn't been ready 362 | frame = ModularPicoScenesRxFrame.fromBuffer(data, len(data), True) 363 | if self.raw: 364 | self.raw.pop(0) 365 | self.raw.append(parse(&frame)) 366 | if self.if_report and self.raw[0]: 367 | print(frame.value().toString()) 368 | self.count = 1 369 | return 0xf300 # status code 370 | 371 | cdef inline uint32_t cu32l(uint8_t a, uint8_t b, uint8_t c, uint8_t d): 372 | return a | (b << 8) | (c << 16) | (d << 24) 373 | 374 | 375 | cdef parse_ieee80211_mac_frame_header(const ieee80211_mac_frame_header *m): 376 | cdef int i 377 | return { 378 | "ControlField": { 379 | 'Version': m.fc.version, 380 | 'Type': m.fc.type, 381 | 'SubType': m.fc.subtype, 382 | 'ToDS': m.fc.toDS, 383 | 'FromDS': m.fc.fromDS, 384 | 'MoreFrags': m.fc.moreFrags, 385 | 'Retry': m.fc.retry, 386 | 'PowerManagement': m.fc.power_mgmt, 387 | 'More': m.fc.more, 388 | 'Protected': m.fc.protect, 389 | 'Order': m.fc.order, 390 | }, 391 | "Addr1": [m.addr1[i] for i in range(6)], 392 | "Addr2": [m.addr2[i] for i in range(6)], 393 | "Addr3": [m.addr3[i] for i in range(6)], 394 | "Fragment": m.frag, 395 | "Sequence": m.seq, 396 | } 397 | 398 | cdef parse_RxSBasic(const RxSBasic *m): 399 | return { 400 | "deviceType": m.deviceType, 401 | "timestamp": m.tstamp, 402 | "systemns": m.systemTime, 403 | "centerFreq": m.centerFreq, 404 | "controlFreq": m.controlFreq, 405 | "CBW": m.cbw, 406 | "packetFormat": m.packetFormat, 407 | "packetCBW": m.pkt_cbw, 408 | "GI": m.guardInterval, 409 | "MCS": m.mcs, 410 | "numSTS": m.numSTS, 411 | "numESS": m.numESS, 412 | "numRx": m.numRx, 413 | "noiseFloor": m.noiseFloor, 414 | "rssi": m.rssi, 415 | "rssi1": m.rssi_ctl0, 416 | "rssi2": m.rssi_ctl1, 417 | "rssi3": m.rssi_ctl2, 418 | "rssi4": m.rssi_ctl3, 419 | "rssi5": m.rssi_ctl4, 420 | "rssi6": m.rssi_ctl5, 421 | "rssi7": m.rssi_ctl6, 422 | "rssi8": m.rssi_ctl7, 423 | } 424 | 425 | cdef parse_ExtraInfo(const ExtraInfo *m): 426 | cdef int i 427 | result = { 428 | "hasLength": m.hasLength, 429 | "hasVersion": m.hasVersion, 430 | "hasMacAddr_cur": m.hasMacAddr_cur, 431 | "hasMacAddr_rom": m.hasMacAddr_rom, 432 | "hasChansel": m.hasChansel, 433 | "hasBMode": m.hasBMode, 434 | "hasEVM": m.hasEVM, 435 | "hasTxChainMask": m.hasTxChainMask, 436 | "hasRxChainMask": m.hasRxChainMask, 437 | "hasTxpower": m.hasTxpower, 438 | "hasCF": m.hasCF, 439 | "hasTxTSF": m.hasTxTSF, 440 | "hasLastHwTxTSF": m.hasLastHWTxTSF, 441 | "hasChannelFlags": m.hasChannelFlags, 442 | "hasTxNess": m.hasTxNess, 443 | "hasTuningPolicy": m.hasTuningPolicy, 444 | "hasPLLRate": m.hasPLLRate, 445 | "hasPLLClkSel": m.hasPLLClkSel, 446 | "hasPLLRefDiv": m.hasPLLRefDiv, 447 | "hasAGC": m.hasAGC, 448 | "hasAntennaSelection": m.hasAntennaSelection, 449 | "hasSamplingRate": m.hasSamplingRate, 450 | "hasCFO": m.hasCFO, 451 | "hasSFO": m.hasSFO, 452 | } 453 | if m.hasLength: 454 | result["length"] = m.length 455 | if m.hasVersion: 456 | result["version"] = m.version 457 | if m.hasMacAddr_cur: 458 | result["macaddr_cur"] = [m.macaddr_cur[i] for i in range(6)] 459 | if m.hasMacAddr_rom: 460 | result["macaddr_rom"] = [m.macaddr_rom[i] for i in range(6)] 461 | if m.hasChansel: 462 | result["chansel"] = m.chansel 463 | if m.hasBMode: 464 | result["bmode"] = m.bmode 465 | if m.hasEVM: 466 | result["evm"] = [m.evm[i] for i in range(18)] 467 | if m.hasTxChainMask: 468 | result["tx_chainmask"] = m.txChainMask 469 | if m.hasRxChainMask: 470 | result["rx_chainmask"] = m.rxChainMask 471 | if m.hasTxpower: 472 | result["txpower"] = m.txpower 473 | if m.hasCF: 474 | result["cf"] = m.cf 475 | if m.hasTxTSF: 476 | result["txtsf"] = m.txTSF 477 | if m.hasLastHWTxTSF: 478 | result["last_txtsf"] = m.lastHwTxTSF 479 | if m.hasChannelFlags: 480 | result["channel_flags"] = m.channelFlags 481 | if m.hasTxNess: 482 | result["tx_ness"] = m.tx_ness 483 | if m.hasTuningPolicy: 484 | result["tuning_policy"] = m.tuningPolicy 485 | if m.hasPLLRate: 486 | result["pll_rate"] = m.pll_rate 487 | if m.hasPLLClkSel: 488 | result["pll_clock_select"] = m.pll_clock_select 489 | if m.hasPLLRefDiv: 490 | result["pll_refdiv"] = m.pll_refdiv 491 | if m.hasAGC: 492 | result["agc"] = m.agc 493 | if m.hasAntennaSelection: 494 | result["ant_sel"] = [m.ant_sel[i] for i in range(3)] 495 | if m.hasSamplingRate: 496 | result["sf"] = m.samplingRate 497 | if m.hasCFO: 498 | result["cfo"] = m.cfo 499 | if m.hasSFO: 500 | result["sfo"] = m.sfo 501 | return result 502 | 503 | cdef parse_CSI(const CSI *m): 504 | return { 505 | "DeviceType": m.deviceType, 506 | "PacketFormat": m.packetFormat, 507 | "FirmwareVersion": m.firmwareVersion, 508 | "CBW": m.cbw, 509 | "CarrierFreq": m.carrierFreq, 510 | "SamplingRate": m.samplingRate, 511 | "SubcarrierBandwidth": m.subcarrierBandwidth, 512 | "numTones": m.dimensions.numTones, 513 | "numTx": m.dimensions.numTx, 514 | "numRx": m.dimensions.numRx, 515 | "numESS": m.dimensions.numESS, 516 | "numCSI": m.dimensions.numCSI, 517 | "ant_sel": m.dimensions.numESS, 518 | "CSI": m.CSIArray.array, 519 | "Mag": m.magnitudeArray.array, 520 | "Phase": m.phaseArray.array, 521 | "SubcarrierIndex": m.subcarrierIndices, 522 | } 523 | 524 | cdef parse_IntelMVMParsedCSIHeader(const IntelMVMParsedCSIHeader *m): 525 | result = { 526 | "value56":[m.value56[i] for i in range(9)], 527 | "rateNflag": m.rateNflag, 528 | "value96": [m.value96[i] for i in range(44)] , 529 | } 530 | return result 531 | 532 | # cdef parse_DpasRequestSegment(const DPASRequest m): 533 | # return { 534 | # "requestMode": m.requestMode, 535 | # "batchId": m.batchId, 536 | # "batchLength": m.batchLength, 537 | # "sequenceId": m.sequenceId, 538 | # "intervalTime": m.intervalTime, 539 | # "intervalStep": m.intervalStep, 540 | # "deviceType": m.deviceType, 541 | # "deviceSubtype": m.deviceSubtype, 542 | # "carrierFrequency": m.carrierFrequency, 543 | # "samplingFrequency": m.samplingFrequency, 544 | # } 545 | 546 | cdef parse_PicoScenesFrameHeader(const PicoScenesFrameHeader *m): 547 | return { 548 | "MagicValue": m.magicValue, 549 | "Version": m.version, 550 | "DeviceType": m.deviceType, 551 | "FrameType": m.frameType, 552 | "TaskId": m.taskId, 553 | "TxId": m.txId, 554 | } 555 | 556 | cdef parse_SignalMatrix(const SignalMatrix[ccomplex[float]] *m): 557 | return np.asarray(m.array).reshape(m.dimensions) 558 | 559 | cdef parse(optional[ModularPicoScenesRxFrame] *frame): 560 | cdef const SignalMatrix[ccomplex[float]] *sig_matrix_ptr = NULL # 在这里声明 561 | data = {} 562 | cdef ModularPicoScenesRxFrame frame_value 563 | if frame.has_value(): 564 | frame_value = frame.value() 565 | data = { 566 | "StandardHeader": parse_ieee80211_mac_frame_header(&frame_value.standardHeader), 567 | "RxSBasic": parse_RxSBasic(&(deref(frame_value.rxSBasicSegment).getBasic())), 568 | "RxExtraInfo": parse_ExtraInfo(&(deref(frame_value.rxExtraInfoSegment).getExtraInfo())), 569 | "CSI": parse_CSI(&(deref(deref(frame_value.csiSegment).getCSI()))), 570 | } 571 | if frame_value.mvmExtraSegment: 572 | data["MVMExtra"] = parse_IntelMVMParsedCSIHeader( 573 | &(deref(frame_value.mvmExtraSegment).getMvmExtra().parsedHeader)) 574 | if frame_value.PicoScenesHeader: 575 | data["PicoScenesHeader"] = parse_PicoScenesFrameHeader(&(deref(frame_value.PicoScenesHeader))) 576 | if frame_value.txExtraInfoSegment: 577 | data["TxExtraInfo"] = parse_ExtraInfo(&(deref(frame_value.txExtraInfoSegment).getExtraInfo())) 578 | # if frame_value.pilotCSISegment: 579 | # data["PilotCSI"] = parse_CSI(&(deref(deref(frame_value.pilotCSISegment).getCSI()))) 580 | if frame_value.legacyCSISegment: 581 | data["LegacyCSI"] = parse_CSI(&(deref(deref(frame_value.legacyCSISegment).getCSI()))) 582 | # if frame_value.basebandSignalSegment: 583 | # #data["BasebandSignals"] = parse_SignalMatrix(&(deref(frame_value.basebandSignalSegment).getFloat32SignalMatrix())) 584 | # data["BasebandSignals"] = parse_SignalMatrix(&(deref(frame_value.basebandSignalSegment).getSignals())) 585 | 586 | if frame_value.basebandSignalSegment: 587 | sig_matrix_ptr = &deref(frame_value.basebandSignalSegment).getSignals() 588 | data["BasebandSignals"] = parse_SignalMatrix(sig_matrix_ptr) 589 | 590 | # if frame_value.preEQSymbolsSegment: 591 | # data["PreEQSymbols"] = parse_SignalMatrix(&(deref(frame_value.preEQSymbolsSegment).getPreEqSymbols())) 592 | # if frame_value.dpasRequestSegment: 593 | # data["DpasRequestSegment"] = parse_DpasRequestSegment( 594 | # deref(frame_value.dpasRequestSegment).getRequest()) 595 | 596 | data["MPDUS"] = frame_value.mpdus 597 | 598 | # print(data) 599 | return data 600 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=68", "wheel", "Cython>=3.0.10", "numpy>=1.26"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "picoscenes" 7 | version = "0.1.0" 8 | description = "Cython bindings for the PicoScenes Wi-Fi measurement parser" 9 | readme = "README.md" 10 | requires-python = ">=3.8" 11 | authors = [{name = "Tian Teng"}] 12 | dependencies = ["numpy>=1.21.3"] 13 | 14 | [project.optional-dependencies] 15 | demo = [ 16 | "matplotlib" 17 | ] 18 | -------------------------------------------------------------------------------- /rx_by_usrpN210.csi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wifisensing/PicoScenes-Python-Toolbox/f676855accedf79e848bf2f1d10ee5c1f649a33a/rx_by_usrpN210.csi -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from pathlib import Path 5 | from typing import List 6 | 7 | import numpy 8 | from Cython.Build.Dependencies import cythonize 9 | from setuptools import find_packages, setup 10 | from setuptools.command.build_ext import build_ext 11 | from setuptools.extension import Extension 12 | 13 | 14 | def find_files(root: Path, ext: str) -> List[str]: 15 | """ 16 | Search for files of given extension recursively within root 17 | """ 18 | return [str(file) for file in Path(root).glob("*" + ext)] 19 | 20 | 21 | class Build(build_ext): 22 | def build_extensions(self): 23 | if self.compiler.compiler_type in ["unix", "mingw32"]: 24 | for e in self.extensions: 25 | if e.name == "picoscenes": 26 | e.extra_compile_args = ["-std=c++2a", "-Wno-attributes", "-O3"] 27 | if self.compiler.compiler_type in ["msvc"]: 28 | for e in self.extensions: 29 | if e.name == "picoscenes": 30 | e.extra_compile_args = ["/std:c++latest"] 31 | super(Build, self).build_extensions() 32 | 33 | 34 | pico_root = Path.cwd() / "rxs_parsing_core" 35 | pico_generated = pico_root / "preprocess" / "generated" 36 | pico_include = pico_root / "preprocess" 37 | pico_source = find_files(pico_root, ".cxx") + find_files(pico_generated, ".cpp") 38 | pico_extension = Extension( 39 | "picoscenes", 40 | ["./picoscenes.pyx"] + pico_source, 41 | include_dirs=[numpy.get_include(), str(pico_include)], 42 | define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")], 43 | ) 44 | 45 | 46 | if not pico_root.is_dir(): 47 | raise RuntimeError( 48 | f"Parsing core submodule {pico_root} is not a directory; " 49 | + "Did you initialize the submodule?" 50 | ) 51 | 52 | EXTENSIONS = [pico_extension] 53 | 54 | 55 | setup( 56 | name="picoscenes", 57 | version="0.1.0", 58 | packages=find_packages(), 59 | install_requires=["numpy"], 60 | python_requires=">=3", 61 | ext_modules=cythonize( 62 | EXTENSIONS, compiler_directives={"language_level": 3, "binding": False} 63 | ), 64 | cmdclass={"build_ext": Build}, 65 | ) 66 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from picoscenes import Picoscenes 6 | 7 | 8 | class TestConsistency(unittest.TestCase): 9 | def setUp(self): 10 | self.frames = Picoscenes("rx_by_usrpN210.csi") 11 | self.frame = self.frames.raw[0] # Get the first frame in the list 12 | self.subcarrier_indices = np.array(self.frame.get("CSI").get("SubcarrierIndex")) 13 | self.n_tones = self.frame.get("CSI").get("numTones") 14 | self.n_rx_antennas = self.frame.get("RxSBasic").get("numRx") 15 | self.n_streams = self.frame.get("RxSBasic").get("numSTS") 16 | 17 | def test_number_of_frames(self): 18 | # Test file contains 74 frames 19 | n_frames = 74 20 | self.assertEqual(len(self.frames.raw), n_frames) 21 | 22 | def test_subcarrier_indices(self): 23 | self.assertTrue(np.array_equal(self.subcarrier_indices, np.arange(-26, 27))) 24 | 25 | def test_magnitude(self): 26 | # Check shape of magnitude is correctly read 27 | mag = np.array(self.frame.get("CSI").get("Mag")) 28 | self.assertEqual(len(mag), self.n_tones) 29 | 30 | def test_csi(self): 31 | csi = np.array(self.frame.get("CSI").get("CSI")) 32 | self.assertEqual(len(csi), self.n_tones * self.n_streams * self.n_rx_antennas) 33 | 34 | # Just test whether this doesn't crash 35 | csi = csi.reshape((self.n_tones, self.n_streams, self.n_rx_antennas), order="F") 36 | 37 | 38 | if __name__ == "__main__": 39 | unittest.main() 40 | --------------------------------------------------------------------------------