├── .gitignore ├── CsiExplorer.py ├── CsiPlotter.py ├── CsiSample.py ├── PacketTypes.py ├── README.md ├── app.py ├── files ├── open-f.pcap └── open.pcap ├── gfx ├── browser.png └── terminal.png └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | pytestdebug.log 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | doc/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | pythonenv* 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # profiling data 139 | .prof 140 | -------------------------------------------------------------------------------- /CsiExplorer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from CsiSample import Sample 4 | 5 | 6 | class Explorer(): 7 | def __init__(self, filepath, bandwidth=80): 8 | self.filepath = filepath 9 | self.bandwidth = bandwidth 10 | self.samples = [] 11 | 12 | self.__read() 13 | 14 | def __read_header(self, pcapfile): 15 | self.h_magic = pcapfile.read(4) 16 | 17 | self.h_vmajor = int.from_bytes(# uint16: Major version number 18 | pcapfile.read(2), 19 | byteorder = 'little', 20 | signed = False 21 | ) 22 | 23 | self.h_vminor = int.from_bytes(# uint16: Minor version number 24 | pcapfile.read(2), 25 | byteorder = 'little', 26 | signed = False 27 | ) 28 | 29 | self.h_thiszone = int.from_bytes(# int32: GMT to Local correction 30 | pcapfile.read(4), 31 | byteorder = 'little', 32 | signed = True 33 | ) 34 | 35 | self.h_sigfigs = int.from_bytes(# uint32: Accuracy of Time Stamp 36 | pcapfile.read(4), 37 | byteorder = 'little', 38 | signed = False 39 | ) 40 | 41 | self.h_snaplen = int.from_bytes(# uint32: Max length of packets 42 | pcapfile.read(4), 43 | byteorder = 'little', 44 | signed = False 45 | ) 46 | 47 | self.h_network = int.from_bytes(# uint32: Data link type 48 | pcapfile.read(4), 49 | byteorder = 'little', 50 | signed = False 51 | ) 52 | 53 | 54 | def __read_frame(self, pcapfile): 55 | csi = Sample(self.bandwidth) 56 | 57 | # ----------***** Pcap Frame Header *****---------- 58 | ts_sec = int.from_bytes(# uint32: Timestamp Seconds 59 | pcapfile.read(4), 60 | byteorder = 'little', 61 | signed = False 62 | ) 63 | 64 | ts_usec = int.from_bytes(# uint32: Timestamp micro Seconds 65 | pcapfile.read(4), 66 | byteorder = 'little', 67 | signed = False 68 | ) 69 | 70 | len_incl = int.from_bytes(#uint32: number of octets in file 71 | pcapfile.read(4), 72 | byteorder = 'little', 73 | signed = False 74 | ) 75 | 76 | len_orig = int.from_bytes(#uint32: Actual number of octets 77 | pcapfile.read(4), 78 | byteorder = 'little', 79 | signed = False 80 | ) 81 | 82 | csi.set_ts(ts_sec, ts_usec) 83 | csi.set_len(len_incl, len_orig) 84 | 85 | # ----------***** Protocol Headers *****---------- 86 | pcapfile.seek(14, os.SEEK_CUR) # Skip Ethernet Header 87 | pcapfile.seek(20, os.SEEK_CUR) # Skip IPv4 header 88 | pcapfile.seek(8, os.SEEK_CUR) # Skip UDP header 89 | 90 | # ----------***** CSI Header *****---------- 91 | magic = pcapfile.read(4) 92 | mac_1 = pcapfile.read(6) 93 | mac_2 = pcapfile.read(6) 94 | mac_3 = pcapfile.read(6) 95 | 96 | # fc = int.from_bytes(#uint16: FC 97 | # pcapfile.read(2), 98 | # byteorder = 'big', 99 | # signed = False 100 | # ) 101 | 102 | fc = pcapfile.read(2) 103 | 104 | sc = int.from_bytes(#uint16: SC 105 | pcapfile.read(2), 106 | byteorder = 'little', 107 | signed = False 108 | ) 109 | 110 | rssi = int.from_bytes(#int8: RSSI 111 | pcapfile.read(1), 112 | byteorder='big', 113 | signed=True 114 | ) 115 | 116 | pcapfile.read(1) # Skip 1 padding byte after RSSI 117 | 118 | # pcapfile.seek(10, os.SEEK_CUR) # Skip Reserved bytes 119 | resr = pcapfile.read(10) 120 | 121 | css = pcapfile.read(2) 122 | chanspec = pcapfile.read(2) 123 | chipvers = pcapfile.read(2) 124 | 125 | csi.set_magic(magic) 126 | csi.set_mac(mac_1, mac_2, mac_3) 127 | csi.set_fcsc(fc, sc) 128 | csi.set_rssi(rssi) 129 | csi.set_resr(resr) 130 | csi.set_conf(css, chanspec, chipvers) 131 | 132 | # ----------***** CSI Data *****---------- 133 | len_csi = int(self.bandwidth * 3.2 * 4) 134 | raw_csi = np.frombuffer( 135 | pcapfile.read(len_incl - 86)[:len_csi], 136 | dtype = np.int16, 137 | count = int(len_csi/2) 138 | ) 139 | 140 | csi_converted = np.abs( 141 | np.fft.fftshift(raw_csi[::2] + 1.j * raw_csi[1::2]) 142 | ) 143 | 144 | csi.set_csi(csi_converted) 145 | return csi 146 | 147 | def __read(self): 148 | with open(self.filepath, 'rb') as pcapfile: 149 | filesize = os.stat(self.filepath).st_size 150 | self.__read_header(pcapfile) 151 | 152 | npackets = 0 153 | while pcapfile.tell() < filesize: 154 | self.samples.append(self.__read_frame(pcapfile)) 155 | npackets += 1 156 | 157 | def get_sample(self, sample_index): 158 | return self.samples[sample_index] 159 | 160 | def get_max_index(self): 161 | return len(self.samples) - 1 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /CsiPlotter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import matplotlib.animation as animation 4 | 5 | class Plotter(): 6 | def __init__(self, data, bandwidth=80): 7 | self.bandwidth = bandwidth 8 | self.data = data 9 | 10 | nfft = int(bandwidth * 3.2) 11 | self.x = np.arange(-1 * nfft/2, nfft/2) 12 | 13 | fig, ax = plt.subplots() 14 | 15 | plt.ion() 16 | plt.show() 17 | 18 | def update_data(self, data): 19 | 20 | plt.cla() 21 | plt.plot(self.x, data) 22 | plt.draw() 23 | plt.pause(0.001) -------------------------------------------------------------------------------- /CsiSample.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import PacketTypes 3 | 4 | class Sample(): 5 | def __init__(self, bandwidth): 6 | self.bandwidth = bandwidth 7 | 8 | 9 | def set_ts(self, ts_s, ts_u): 10 | self.ts = ts_s + ts_u*10**-6 11 | 12 | def set_len(self, len_incl, len_orig): 13 | self.len_incl = len_incl 14 | self.len_orig = len_orig 15 | 16 | def set_magic(self, magic): 17 | self.magic = magic 18 | 19 | def set_mac(self, mac_1, mac_2, mac_3): 20 | self.mac_1 = mac_1 21 | self.mac_2 = mac_2 22 | self.mac_3 = mac_3 23 | 24 | def set_fcsc(self, fc, sc): 25 | 26 | fn = sc % 16 # Fragment number 27 | sc = int((sc - fn)/16) 28 | 29 | self.fc = fc 30 | self.sc = sc 31 | self.fn = fn 32 | 33 | maintype = (fc[0] & 0x0f) 34 | subtype = ((fc[0] >> 4) & 0x0f) 35 | 36 | self.type = PacketTypes.TypeMap[maintype]['Type'] 37 | self.subtype = PacketTypes.TypeMap[maintype][subtype] 38 | 39 | 40 | def set_rssi(self, rssi): 41 | self.rssi = rssi 42 | 43 | def set_resr(self, resr): 44 | self.resr = resr 45 | 46 | 47 | def set_conf(self, css, chanspec, chipvers): 48 | self.css = css 49 | self.chanspec = chanspec 50 | self.chipvers = chipvers 51 | 52 | def set_csi(self, csi): 53 | # csi should be a numpy array 54 | self.csi = csi 55 | 56 | def get_csi(self, remove_null=False, remove_pilot=False, max_value=0): 57 | 58 | nullsubcarriers = np.array([x+128 for x in [-128, -127, -126, -125, -124, -123, -1, 0, 1, 123, 124, 125, 126, 127]]) 59 | pilotsubcarriers = np.array([x+128 for x in [-103, -75, -39, -11, 11, 39, 75, 103]]) 60 | 61 | new_csi = np.copy(self.csi) 62 | 63 | if(remove_null): 64 | new_csi[nullsubcarriers] = 0 65 | if(remove_pilot): 66 | new_csi[pilotsubcarriers] = 0 67 | if(max_value > 0): 68 | new_csi[new_csi > max_value] = 0 69 | 70 | return new_csi 71 | 72 | 73 | def __str__(self): 74 | 75 | def macify(mac): 76 | mac = mac.hex() 77 | mac = [mac[0:2], mac[2:4], mac[4:6], mac[6:8], mac[8:10], mac[10:12]] 78 | return ':'.join(mac) 79 | 80 | return ''' 81 | Type: %s.%s 82 | FC: 0x%s 83 | SC: %d.%d 84 | RSSI: %d 85 | Mac_1: %s 86 | Mac_2: %s 87 | Mac_3: %s 88 | Len_incl: %d bytes 89 | Len_orig: %d bytes 90 | Bandwidth: %d MHz 91 | Timestamp: %f s 92 | Magic: %s 93 | RESR: %s 94 | ''' % ( 95 | self.type, 96 | self.subtype, 97 | self.fc.hex(), 98 | self.sc, 99 | self.fn, 100 | self.rssi, 101 | macify(self.mac_1), 102 | macify(self.mac_2), 103 | macify(self.mac_3), 104 | self.len_incl, 105 | self.len_orig, 106 | self.bandwidth, 107 | self.ts, 108 | self.magic.hex(), 109 | str(self.resr) 110 | ) -------------------------------------------------------------------------------- /PacketTypes.py: -------------------------------------------------------------------------------- 1 | Management = { 2 | 'Type': 'Management', 3 | 0b0000: 'Association Request', 4 | 0b0001: 'Association Response', 5 | 0b0010: 'Reassociation Request', 6 | 0b0011: 'Reassociation Response', 7 | 0b0100: 'Probe Request', 8 | 0b0101: 'Probe Response', 9 | 0b0110: 'Timing Advertisement', 10 | 0b0111: 'Reserved', 11 | 0b1000: 'Beacon', 12 | 0b1001: 'ATIM', 13 | 0b1010: 'Disassociation', 14 | 0b1011: 'Authentication', 15 | 0b1100: 'Deauthentication', 16 | 0b1101: 'Action', 17 | 0b1110: 'Action No Ack (NACK)', 18 | 0b1111: 'Reserved' 19 | } 20 | 21 | Control = { 22 | 'Type': 'Control', 23 | 0b0000: 'Reserved', 24 | 0b0001: 'Reserved', 25 | 0b0010: 'Trigger', 26 | 0b0011: 'TACK', 27 | 0b0100: 'Beamforming Report Poll', 28 | 0b0101: 'VHT/HE NDP Announcement', 29 | 0b0110: 'Control Frame Extension', 30 | 0b0111: 'Control Wrapper', 31 | 0b1000: 'Block Ack Request (BAR)', 32 | 0b1001: 'Block Ack (BA)', 33 | 0b1010: 'PS-Poll', 34 | 0b1011: 'RTS', 35 | 0b1100: 'CTS', 36 | 0b1101: 'ACK', 37 | 0b1110: 'CF-End', 38 | 0b1111: 'CF-End + CF-ACK' 39 | } 40 | 41 | Data = { 42 | 'Type': 'Data', 43 | 0b0000: 'Data', 44 | 0b0001: 'Data + CF-ACK', 45 | 0b0010: 'Data + CF-Poll', 46 | 0b0011: 'Data + CF-ACK + CF-Poll', 47 | 0b0100: 'Null (no data)', 48 | 0b0101: 'CF-ACK (no data)', 49 | 0b0110: 'CF-Poll (no data)', 50 | 0b0111: 'CF-ACK + CF-Poll (no data)', 51 | 0b1000: 'QoS Data', 52 | 0b1001: 'QoS Data + CF-ACK', 53 | 0b1010: 'QoS Data + CF-Poll', 54 | 0b1011: 'QoS Data + CF-ACK + CF-Poll', 55 | 0b1100: 'QoS Null (no data)', 56 | 0b1101: 'Reserved', 57 | 0b1110: 'QoS CF-Poll (no data)', 58 | 0b1111: 'QoS CF-ACK + CF-Poll (no data)' 59 | } 60 | 61 | Extension = { 62 | 'Type': 'Extension', 63 | 0b0000: 'DMG Beacon', 64 | 0b0001: 'S1G Beacon', 65 | 0b0010: 'Reserved', 66 | 0b0011: 'Reserved', 67 | 0b0100: 'Reserved', 68 | 0b0101: 'Reserved', 69 | 0b0110: 'Reserved', 70 | 0b0111: 'Reserved', 71 | 0b1000: 'Reserved', 72 | 0b1001: 'Reserved', 73 | 0b1010: 'Reserved', 74 | 0b1011: 'Reserved', 75 | 0b1100: 'Reserved', 76 | 0b1101: 'Reserved', 77 | 0b1110: 'Reserved', 78 | 0b1111: 'Reserved' 79 | } 80 | 81 | TypeMap = { 82 | 0b0000: Management, 83 | 0b0100: Control, 84 | 0b1000: Data, 85 | 0b1100: Extension 86 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | CSI Explorer is deprecated. 4 | 5 | I made a new version, https://github.com/nexmonster/nexmon_csi/tree/feature/python/utils/python which is faster, and works with the 5.4.51 branch. 6 | 7 | 8 | # CSI Explorer 9 | 10 | This is an app to explore CSI samples collected via Nexmon_CSI. 11 | Currently it works only with my [pi-5.4.51-plus](https://github.com/zeroby0/nexmon_csi/tree/pi-5.4.51-plus) or [pi-4.19.97-plus](https://github.com/zeroby0/nexmon_csi/tree/pi-4.19.97-plus) branches. 12 | I'm planning to add support for the default Nexmon_csi pcap files and other devices in the near future. 13 | 14 | There are two ways you can explore CSI samples with: Terminal and Browser. You will need to install [streamlit](https://www.streamlit.io/) to use the browser version. The plot is redrawn on the same matplotlib window to keep it convinient. 15 | 16 | #### Browser 17 | ![Broswer CSI explore](./gfx/browser.png) 18 | 19 | #### Terminal 20 | ![Terminal CSI explore](./gfx/terminal.png) 21 | 22 | ## Installation and Usage 23 | - `pip install numpy matplotlib` for Terminal. 24 | - `pip install numpy streamlit` for Browser. 25 | 26 | Add your PCAP files to the `files` folder. 27 | 28 | - `python main.py` to run the Terminal explorer. 29 | - `streamlit run app.py` to run in the Broswer. 30 | 31 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import time 2 | from os import listdir 3 | from os.path import isfile, join 4 | 5 | import matplotlib.pyplot as plt 6 | import streamlit as st 7 | import numpy as np 8 | import pandas as pd 9 | 10 | from CsiExplorer import Explorer 11 | from CsiPlotter import Plotter 12 | 13 | st.title('CSI Explorer') 14 | 15 | pcapDir = 'files/' 16 | 17 | bandwidth = st.sidebar.selectbox( 18 | 'Select Bandwidth', 19 | [20, 40, 80], 20 | index=2 21 | ) 22 | 23 | pcapfilename = st.sidebar.selectbox( 24 | 'Select file to explore', 25 | [f for f in listdir(pcapDir) if isfile(join(pcapDir, f))] 26 | ) 27 | 28 | remove_null = st.sidebar.checkbox( 29 | 'Remove Null subcarriers', 30 | value=True 31 | ) 32 | 33 | remove_pilot = st.sidebar.checkbox( 34 | 'Remove Pilot subcarriers', 35 | value=True 36 | ) 37 | 38 | show_animation = st.sidebar.checkbox( 39 | 'Play animation', 40 | value=True 41 | ) 42 | 43 | 44 | explorer = Explorer(pcapDir + pcapfilename) 45 | 46 | if show_animation: 47 | fig, ax = plt.subplots() 48 | 49 | nfft = int(bandwidth * 3.2) 50 | x = np.arange(-1 * nfft/2, nfft/2) 51 | 52 | ax.set_ylim(0, 4000) 53 | plt.xlabel("Sub carrier index") 54 | plt.ylabel("Amplitude") 55 | 56 | line, = ax.plot(x, explorer.get_sample(0).get_csi(remove_null, remove_pilot)) 57 | el_plot = st.pyplot(plt) 58 | 59 | el_status = st.markdown('### Showing sample 0') 60 | el_summary = st.text('Summary') 61 | 62 | 63 | 64 | def init(): # give a clean slate to start 65 | line.set_ydata([np.nan] * len(x)) 66 | 67 | def animate(i): # update the y values (every 1000ms) 68 | sample = explorer.get_sample(i) 69 | line.set_ydata(sample.get_csi(remove_null, remove_pilot)) 70 | 71 | el_plot.pyplot(plt) 72 | el_status.markdown('### Showing sample %d' % (i)) 73 | el_summary.text(sample) 74 | 75 | init() 76 | 77 | for i in range(explorer.get_max_index() + 1): 78 | animate(i) 79 | time.sleep(0.05) 80 | 81 | else: 82 | samplenumber = st.sidebar.slider( 83 | label='Select CSI sample to explore', 84 | min_value=0, 85 | max_value=explorer.get_max_index(), 86 | value=0 87 | ) 88 | 89 | 90 | sample = explorer.get_sample(int(samplenumber)) 91 | st.line_chart(sample.get_csi(remove_null, remove_pilot)) 92 | 93 | el_status = st.markdown('### Showing sample %d' % (samplenumber)) 94 | el_summary = st.text(sample) 95 | -------------------------------------------------------------------------------- /files/open-f.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroby0/csi-explorer/9299eed9a686a6abe86db709ba9930e12c459142/files/open-f.pcap -------------------------------------------------------------------------------- /files/open.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroby0/csi-explorer/9299eed9a686a6abe86db709ba9930e12c459142/files/open.pcap -------------------------------------------------------------------------------- /gfx/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroby0/csi-explorer/9299eed9a686a6abe86db709ba9930e12c459142/gfx/browser.png -------------------------------------------------------------------------------- /gfx/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeroby0/csi-explorer/9299eed9a686a6abe86db709ba9930e12c459142/gfx/terminal.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import time 2 | from CsiExplorer import Explorer 3 | from CsiPlotter import Plotter 4 | 5 | filename = input('filename: ') 6 | 7 | explorer = Explorer(f'./files/{filename}') 8 | plotter = Plotter(explorer.get_sample(0).csi) 9 | 10 | def display(index): 11 | sample = explorer.get_sample(index) 12 | print(sample) 13 | plotter.update_data(sample.get_csi(True, True)) 14 | 15 | while True: 16 | index = input('Which sample would you like to explore? ') 17 | 18 | if '-' in index: 19 | start = int(index.split('-')[0]) 20 | end = int(index.split('-')[1]) 21 | for i in range(start, end+1): 22 | display(i) 23 | time.sleep(0.1) 24 | else: 25 | display(int(index)) 26 | 27 | --------------------------------------------------------------------------------