Compare commits
No commits in common. "main" and "python311" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -174,5 +174,3 @@ poetry.toml
|
|||||||
pyrightconfig.json
|
pyrightconfig.json
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
|
||||||
config.yaml
|
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
FROM ghcr.io/astral-sh/uv:latest
|
|
||||||
|
|
||||||
ADD pyproject.toml uv.lock /app
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
RUN uv sync --frozen
|
|
||||||
|
|
||||||
CMD ["uv", "run", "python", "-m", "where_fi", "heatmap"]
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
receive_hosts:
|
|
||||||
- ["10.0.12.90", 8008]
|
|
||||||
- ["10.0.12.91", 8008]
|
|
||||||
|
|
||||||
transmit_host: ["10.0.12.64", 8008]
|
|
||||||
check_hosts: True
|
|
||||||
antennas:
|
|
||||||
order:
|
|
||||||
- [['10.0.12.91', 8008], 0]
|
|
||||||
- [['10.0.12.91', 8008], 1]
|
|
||||||
- [['10.0.12.90', 8008], 0]
|
|
||||||
- [['10.0.12.90', 8008], 1]
|
|
||||||
|
|
||||||
# The spacing between antennas in the linear antenna array in meters
|
|
||||||
spacing: 0.0285
|
|
||||||
|
|
||||||
collection_sample_rate: 20 # Hz
|
|
||||||
processing_sample_rate: 2 # Hz
|
|
||||||
|
|
||||||
# The central Wi-Fi channel to be used for data collection
|
|
||||||
# Should match one of the channels in the standard
|
|
||||||
# https://en.wikipedia.org/wiki/List_of_WLAN_channels
|
|
||||||
central_freq: 2442 # MHz
|
|
||||||
|
|
||||||
# The channel width in MHz. The options are 20, 40, 80, and 160 MHz
|
|
||||||
channel_width: 20
|
|
||||||
|
|
||||||
# frame_format: Can be HT, VHT, HE for the frame format used by
|
|
||||||
# 802.11n, 802.11ac, 802.11ax respectively
|
|
||||||
frame_format: VHT
|
|
||||||
|
|
||||||
preprocessing:
|
|
||||||
subcarrier_step: 1 # Skip every xth subcarrier to improve performance
|
|
||||||
|
|
||||||
# The steps to be applied to the samples before denoising, in this order
|
|
||||||
steps:
|
|
||||||
- fill_pilots
|
|
||||||
- skip_subcarriers
|
|
||||||
- remove_agc
|
|
||||||
- remove_sfo
|
|
||||||
|
|
||||||
# Type of denoising to apply after preprocessing, when accessing
|
|
||||||
# the sample for the main application processing
|
|
||||||
denoising: median
|
|
||||||
|
|
||||||
# The duration of the window to use for the denoising step (in seconds)
|
|
||||||
denoising_window: 1
|
|
||||||
|
|
||||||
music:
|
|
||||||
# The threshold to use to split the noise and signal subspaces
|
|
||||||
eigval_threshold: 100
|
|
||||||
|
|
||||||
# The resolution of the heatmap generated
|
|
||||||
heatmap:
|
|
||||||
theta_resolution: 100
|
|
||||||
tof_resolution: 100
|
|
||||||
tof_max: 2e-8
|
|
||||||
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
services:
|
|
||||||
envoy:
|
|
||||||
image: envoyproxy/envoy
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- ./where_fi/visualise/envoy.yaml:/etc/envoy/envoy.yaml
|
|
||||||
backend:
|
|
||||||
build: .
|
|
||||||
volumes:
|
|
||||||
- ./where_fi:/app/where_fi
|
|
||||||
- ./config.yaml:/app/config
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
for i in {1..80}; do
|
|
||||||
not_running=$(iperf3 -c 10.0.0.2 -d -t 10 -V | grep -Po '[0-9.]*(?= Mbits/sec)' | tail -n 1)
|
|
||||||
echo $not_running >> speedtest_not_running.log
|
|
||||||
echo "not running $not_running"
|
|
||||||
ssh cfalas@10.0.0.2 "ssh root@10.0.12.64 'ping 192.168.55.3 -c 1100 -i 0.01'" > /dev/null &
|
|
||||||
sleep 1s;
|
|
||||||
running=$(iperf3 -c 10.0.0.2 -d -t 10 -V | grep -Po '[0-9.]*(?= Mbits/sec)' | tail -n 1)
|
|
||||||
echo $running >> speedtest_running.log
|
|
||||||
echo "running $running"
|
|
||||||
sleep 1s;
|
|
||||||
done
|
|
||||||
|
|
||||||
wait
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
from itertools import product
|
|
||||||
from queue import Queue
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from where_fi.application import CSIApplication
|
|
||||||
from where_fi.collection import CSIMatrix
|
|
||||||
from where_fi.collection.ingest import RealtimeCSIProducer
|
|
||||||
from where_fi.config import config
|
|
||||||
from where_fi.visualise import server as visualise
|
|
||||||
|
|
||||||
app = CSIApplication(visualise_raw=True)
|
|
||||||
visualise.figures.all_figures["median"] = visualise.figures.RandomVariable(
|
|
||||||
"Median Phase"
|
|
||||||
)
|
|
||||||
visualise.figures.all_figures["median_magn"] = visualise.figures.RandomVariable(
|
|
||||||
"Median Magnitude"
|
|
||||||
)
|
|
||||||
visualise.figures.all_figures["denoised"] = visualise.figures.PerAntennaFigure(
|
|
||||||
[
|
|
||||||
visualise.figures.SimpleLineChart("Denoised CSI Phase", "Subcarrier", "Phase"),
|
|
||||||
visualise.figures.SimpleLineChart(
|
|
||||||
"Denoised CSI Amplitude", "Subcarrier", "Amplitude"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[np.angle, np.abs],
|
|
||||||
)
|
|
||||||
measurements: list[np.complex64] = []
|
|
||||||
|
|
||||||
|
|
||||||
subcarriers = [0, 1]
|
|
||||||
rx_antenna = [0, 1]
|
|
||||||
tx_antenna = [0]
|
|
||||||
|
|
||||||
subcarrier_phase: dict[tuple[int, int, int], Queue[float]] = {
|
|
||||||
x: Queue(config.collection_sample_rate)
|
|
||||||
for x in product(subcarriers, rx_antenna, tx_antenna)
|
|
||||||
}
|
|
||||||
subcarrier_magn: dict[tuple[int, int, int], Queue[float]] = {
|
|
||||||
x: Queue(config.collection_sample_rate)
|
|
||||||
for x in product(subcarriers, rx_antenna, tx_antenna)
|
|
||||||
}
|
|
||||||
|
|
||||||
proc_phase: dict[tuple[int, int, int], Queue[float]] = {
|
|
||||||
x: Queue(config.collection_sample_rate)
|
|
||||||
for x in product(subcarriers, rx_antenna, tx_antenna)
|
|
||||||
}
|
|
||||||
|
|
||||||
proc_magn: dict[tuple[int, int, int], Queue[float]] = {
|
|
||||||
x: Queue(config.collection_sample_rate)
|
|
||||||
for x in product(subcarriers, rx_antenna, tx_antenna)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_sample
|
|
||||||
def _(sample: CSIMatrix) -> None:
|
|
||||||
for i in product(subcarriers, rx_antenna, tx_antenna):
|
|
||||||
phase = cast(float, np.angle(sample[i]))
|
|
||||||
magn = cast(float, np.abs(sample[i]))
|
|
||||||
if subcarrier_phase[i].full():
|
|
||||||
subcarrier_phase[i].get()
|
|
||||||
subcarrier_phase[i].put(phase)
|
|
||||||
if subcarrier_magn[i].full():
|
|
||||||
subcarrier_magn[i].get()
|
|
||||||
subcarrier_magn[i].put(magn)
|
|
||||||
|
|
||||||
|
|
||||||
cnt = 0
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_process
|
|
||||||
def _(proc: CSIMatrix) -> None:
|
|
||||||
global cnt
|
|
||||||
global proc_phase
|
|
||||||
|
|
||||||
for i in product(subcarriers, rx_antenna, tx_antenna):
|
|
||||||
phase = cast(float, np.angle(proc[i]))
|
|
||||||
magn = cast(float, np.abs(proc[i]))
|
|
||||||
if proc_phase[i].full():
|
|
||||||
proc_phase[i].get()
|
|
||||||
proc_phase[i].put(phase)
|
|
||||||
if proc_magn[i].full():
|
|
||||||
proc_magn[i].get()
|
|
||||||
proc_magn[i].put(magn)
|
|
||||||
print(sum(proc_phase[0, 0, 0].queue))
|
|
||||||
app.visualise_data(
|
|
||||||
np.array([x.queue for x in proc_phase.values()]),
|
|
||||||
"median",
|
|
||||||
)
|
|
||||||
app.visualise_data(
|
|
||||||
np.array([x.queue for x in proc_magn.values()]),
|
|
||||||
"median_magn",
|
|
||||||
)
|
|
||||||
app.visualise_data(
|
|
||||||
np.array([x.queue for x in subcarrier_phase.values()]),
|
|
||||||
visualise.figures.Figure.PHASE_ANALYSIS,
|
|
||||||
)
|
|
||||||
app.visualise_data(
|
|
||||||
np.array([x.queue for x in subcarrier_magn.values()]),
|
|
||||||
visualise.figures.Figure.MAGN_ANALYSIS,
|
|
||||||
)
|
|
||||||
app.visualise_data(proc, "denoised")
|
|
||||||
cnt += 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Starting app")
|
|
||||||
app.set_producer(RealtimeCSIProducer())
|
|
||||||
app.start()
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
"""Motion Detection example
|
|
||||||
This example shows how to use the CSI framework to connect to a FeitCSI host and
|
|
||||||
detect changes in the environment
|
|
||||||
"""
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from where_fi.application import CSIApplication
|
|
||||||
from where_fi.collection.ingest import RealtimeCSIProducer
|
|
||||||
from where_fi.config import config
|
|
||||||
from where_fi.visualise import server as visualise
|
|
||||||
|
|
||||||
# Connect to a FeitCSI host
|
|
||||||
app = CSIApplication(visualise_raw=True)
|
|
||||||
|
|
||||||
# Register visualisations
|
|
||||||
app.register_figure(
|
|
||||||
"magn-diff",
|
|
||||||
visualise.figures.PerAntennaFigure(
|
|
||||||
[
|
|
||||||
visualise.figures.SimpleLineChart(
|
|
||||||
"Magnitude diff", "Subcarrier", "Magnitude"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[lambda x: x],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
app.register_figure(
|
|
||||||
"phase-diff",
|
|
||||||
visualise.figures.PerAntennaFigure(
|
|
||||||
[
|
|
||||||
visualise.figures.SimpleLineChart("Phase diff", "Subcarrier", "Phase"),
|
|
||||||
],
|
|
||||||
[lambda x: x],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
MAGN_THRESHOLD = 10
|
|
||||||
PHASE_THRESHOLD = 1
|
|
||||||
QUEUE_SIZE = 200
|
|
||||||
|
|
||||||
# Stores historical data for each receiving antenna, for each subcarrier
|
|
||||||
historical = np.zeros(
|
|
||||||
(QUEUE_SIZE, config.subcarriers, config.antennas.count, 1), dtype=np.complex64
|
|
||||||
)
|
|
||||||
empty = np.load("data/empty.npy")
|
|
||||||
|
|
||||||
sample_position = 0
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_process
|
|
||||||
def _(sample: npt.NDArray[np.complex64]) -> None:
|
|
||||||
"""
|
|
||||||
Process the CSI data and detect changes in the environment.
|
|
||||||
|
|
||||||
This function is called by the framework at a fixed interval, with the latest CSI
|
|
||||||
sample received.
|
|
||||||
|
|
||||||
It is used to detect changes in the environment caused by motion, by comparing each
|
|
||||||
entry in the matrix with a moving average
|
|
||||||
"""
|
|
||||||
global sample_position
|
|
||||||
|
|
||||||
historical[sample_position] = sample
|
|
||||||
sample_position = (sample_position + 1) % QUEUE_SIZE
|
|
||||||
|
|
||||||
mean = np.mean(historical, axis=0)
|
|
||||||
magn_diff = np.abs(mean - sample)
|
|
||||||
phase_diff = np.abs(np.angle(mean) - np.angle(sample))
|
|
||||||
phase_diff = np.min(np.array([phase_diff, np.pi - phase_diff]), axis=0)
|
|
||||||
|
|
||||||
app.visualise_data(magn_diff, "magn-diff")
|
|
||||||
app.visualise_data(phase_diff, "phase-diff")
|
|
||||||
|
|
||||||
if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any():
|
|
||||||
print("Motion detected!")
|
|
||||||
else:
|
|
||||||
print("No motion detected!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Start the application
|
|
||||||
print("Starting app")
|
|
||||||
app.set_producer(RealtimeCSIProducer())
|
|
||||||
app.start()
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
"""Motion Detection example
|
|
||||||
This example shows how to use the CSI framework to connect to a FeitCSI host and
|
|
||||||
detect changes in the environment
|
|
||||||
"""
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from where_fi.application import CSIApplication
|
|
||||||
from where_fi.collection.ingest import RealtimeCSIProducer
|
|
||||||
from where_fi.config import config
|
|
||||||
from where_fi.visualise import server as visualise
|
|
||||||
|
|
||||||
# Connect to a FeitCSI host
|
|
||||||
app = CSIApplication(visualise_raw=True)
|
|
||||||
|
|
||||||
# Register visualisations
|
|
||||||
app.register_figure(
|
|
||||||
"magn-diff",
|
|
||||||
visualise.figures.PerAntennaFigure(
|
|
||||||
[
|
|
||||||
visualise.figures.SimpleLineChart(
|
|
||||||
"Magnitude diff", "Subcarrier", "Magnitude"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[lambda x: x],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
app.register_figure(
|
|
||||||
"phase-diff",
|
|
||||||
visualise.figures.PerAntennaFigure(
|
|
||||||
[
|
|
||||||
visualise.figures.SimpleLineChart("Phase diff", "Subcarrier", "Phase"),
|
|
||||||
],
|
|
||||||
[lambda x: x],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
MAGN_THRESHOLD = 0.2
|
|
||||||
PHASE_THRESHOLD = 0.02
|
|
||||||
QUEUE_SIZE = 20
|
|
||||||
|
|
||||||
# Stores historical data for each receiving antenna, for each subcarrier
|
|
||||||
historical = np.zeros(
|
|
||||||
(QUEUE_SIZE, config.subcarriers, config.antennas.count, 1), dtype=np.complex64
|
|
||||||
)
|
|
||||||
sample_position = 0
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantBinarySensor:
|
|
||||||
"""
|
|
||||||
Represents a binary sensor in Home Assistant.
|
|
||||||
|
|
||||||
Uses the HTTP API [1] to update the state of the sensor.
|
|
||||||
|
|
||||||
[1] - https://www.home-assistant.io/integrations/http/#binary-sensor
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, id: str, name: str) -> None:
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
BASE_URL = os.getenv("HOME_ASSISTANT_URL")
|
|
||||||
API_KEY = os.getenv("HOME_ASSISTANT_API_KEY")
|
|
||||||
|
|
||||||
self.url = f"{BASE_URL}/api/states/binary_sensor.{self.id}"
|
|
||||||
self.headers = {"Authorization": f"Bearer {API_KEY}"}
|
|
||||||
|
|
||||||
self.state = False
|
|
||||||
|
|
||||||
def update(self, state: bool) -> None:
|
|
||||||
if self.state == state:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.state = state
|
|
||||||
data = {
|
|
||||||
"state": "on" if state else "off",
|
|
||||||
"attributes": {"friendly_name": self.name, "device_class": "motion"},
|
|
||||||
}
|
|
||||||
print(f"Updating sensor {self.name} to {data}")
|
|
||||||
requests.post(self.url, json=data, headers=self.headers)
|
|
||||||
|
|
||||||
|
|
||||||
sensor = HomeAssistantBinarySensor("motion_detector", "Room Motion Detector")
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_process
|
|
||||||
def _(sample: npt.NDArray[np.complex64]) -> None:
|
|
||||||
"""
|
|
||||||
Process the CSI data and detect changes in the environment.
|
|
||||||
|
|
||||||
This function is called by the framework at a fixed interval, with the latest CSI
|
|
||||||
sample received.
|
|
||||||
|
|
||||||
It is used to detect changes in the environment caused by motion, by comparing each
|
|
||||||
entry in the matrix with a moving average
|
|
||||||
"""
|
|
||||||
global sample_position
|
|
||||||
|
|
||||||
historical[sample_position] = sample
|
|
||||||
sample_position = (sample_position + 1) % QUEUE_SIZE
|
|
||||||
|
|
||||||
mean = np.mean(historical, axis=0)
|
|
||||||
magn_diff = np.abs(mean - sample)
|
|
||||||
phase_diff = np.abs(np.angle(mean) - np.angle(sample))
|
|
||||||
|
|
||||||
app.visualise_data(magn_diff, "magn-diff")
|
|
||||||
app.visualise_data(phase_diff, "phase-diff")
|
|
||||||
|
|
||||||
if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any():
|
|
||||||
print("Motion detected!")
|
|
||||||
sensor.update(True)
|
|
||||||
else:
|
|
||||||
print("No motion detected!")
|
|
||||||
sensor.update(False)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Start the application
|
|
||||||
print("Starting app")
|
|
||||||
app.set_producer(RealtimeCSIProducer())
|
|
||||||
app.start()
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from where_fi.application import CSIApplication
|
|
||||||
from where_fi.collection import CSIMatrix
|
|
||||||
from where_fi.collection.ingest import RealtimeCSIProducer
|
|
||||||
from where_fi.config import config
|
|
||||||
from where_fi.processing.aoa import AoA
|
|
||||||
from where_fi.visualise import server as visualise
|
|
||||||
|
|
||||||
app = CSIApplication(visualise_raw=True)
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
T = 500
|
|
||||||
|
|
||||||
N_sub1 = config.antennas.count // 2
|
|
||||||
N_sub2 = config.subcarriers // 2
|
|
||||||
L2 = config.subcarriers - N_sub2 + 1
|
|
||||||
L1 = config.antennas.count - N_sub1 + 1
|
|
||||||
N_sensors = config.subcarriers * config.antennas.count
|
|
||||||
historical = torch.zeros(T, N_sensors, N_sensors, dtype=torch.complex64)
|
|
||||||
|
|
||||||
cnt = 0
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_sample
|
|
||||||
def _(sample: CSIMatrix) -> None:
|
|
||||||
global cnt
|
|
||||||
sample_before_resize = sample[:, :, 0].T
|
|
||||||
sample_after = sample_before_resize.reshape(-1, 1)
|
|
||||||
sample_tensor = torch.tensor(sample_after)
|
|
||||||
historical[cnt] = sample_tensor @ torch.conj(sample_tensor).T
|
|
||||||
print(
|
|
||||||
f"{sample.shape} => {sample_before_resize.shape} => {sample_after.shape} "
|
|
||||||
f"=> {historical[cnt].shape}"
|
|
||||||
)
|
|
||||||
cnt = (cnt + 1) % T
|
|
||||||
|
|
||||||
|
|
||||||
def get_steering(theta: float, tau: float) -> torch.Tensor:
|
|
||||||
"""
|
|
||||||
Calculate the alpha value for the given angle and time delay.
|
|
||||||
"""
|
|
||||||
sub, ant = np.indices((N_sub1, N_sub2))
|
|
||||||
alpha = np.exp(
|
|
||||||
-1j
|
|
||||||
* (
|
|
||||||
2 * np.pi * (sub * config.delta_f * tau)
|
|
||||||
+ 2
|
|
||||||
* np.pi
|
|
||||||
* (
|
|
||||||
ant
|
|
||||||
* config.antennas.spacing
|
|
||||||
* np.sin(theta)
|
|
||||||
* 299_792_458
|
|
||||||
/ (config.central_freq_hz + (sub - 28) * config.delta_f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
alpha = sub + 1j * ant
|
|
||||||
alpha = alpha.reshape(-1, 1)
|
|
||||||
return torch.tensor(alpha, dtype=torch.complex64)
|
|
||||||
|
|
||||||
|
|
||||||
aoa = AoA()
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_process
|
|
||||||
def _(_: CSIMatrix) -> None:
|
|
||||||
R: torch.Tensor = torch.mean(historical, axis=0)
|
|
||||||
# Rss = torch.zeros(N_sub1 * N_sub2, N_sub1 * N_sub2, dtype=torch.complex64)
|
|
||||||
# for i in range(L1):
|
|
||||||
# for j in range(L2):
|
|
||||||
# Rss += R[i : i + N_sub1 * N_sub2, j : j + N_sub1 * N_sub2]
|
|
||||||
# Rss /= L1 * L2
|
|
||||||
Rss = R
|
|
||||||
|
|
||||||
aoa.historical_autocorr = torch.unsqueeze(Rss, 0)
|
|
||||||
aoa.heatmap(app.visualise_data)
|
|
||||||
|
|
||||||
# eigvals, eigvecs = torch.linalg.eig(Rss)
|
|
||||||
# app.visualise_data(eigvals.numpy(), visualise.figures.Figure.MUSIC_EIGENVALUES)
|
|
||||||
# E_n = eigvecs[:, torch.abs(eigvals) < config.music.eigval_threshold]
|
|
||||||
# E_n_H = torch.conj(E_n.T)
|
|
||||||
|
|
||||||
# heatmap = np.zeros(
|
|
||||||
# (config.music.heatmap.tof_resolution, config.music.heatmap.theta_resolution)
|
|
||||||
# )
|
|
||||||
# for i_theta, theta in enumerate(
|
|
||||||
# np.linspace(0, np.pi, config.music.heatmap.theta_resolution)
|
|
||||||
# ):
|
|
||||||
# for i_tau, tau in enumerate(
|
|
||||||
# np.linspace(
|
|
||||||
# 0, config.music.heatmap.tof_max, config.music.heatmap.tof_resolution
|
|
||||||
# )
|
|
||||||
# ):
|
|
||||||
# steering = get_steering(theta, tau)
|
|
||||||
# steering_h = torch.conj(steering.T)
|
|
||||||
# c = 1 / (steering_h @ E_n @ E_n_H @ steering)
|
|
||||||
# heatmap[i_tau, i_theta] = torch.abs(c)
|
|
||||||
# print(steering)
|
|
||||||
# app.visualise_data(heatmap, visualise.figures.Figure.AOA_HEATMAP)
|
|
||||||
# eigvals, eigvecs = torch.linalg.eig(Rss)
|
|
||||||
# app.visualise_data(eigvals.numpy(), visualise.figures.Figure.MUSIC_EIGENVALUES)
|
|
||||||
# E_n = eigvecs[:, torch.abs(eigvals) < config.music.eigval_threshold]
|
|
||||||
|
|
||||||
# print(E_n)
|
|
||||||
# c: torch.Tensor = 1 / (steering_h @ E_n @ E_n_H @ steering)
|
|
||||||
# return torch.abs(c)[:, 0, 0]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
producer = RealtimeCSIProducer()
|
|
||||||
app.set_producer(producer)
|
|
||||||
app.start()
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 640 KiB |
@ -1,24 +0,0 @@
|
|||||||
\relax
|
|
||||||
\providecommand\hyper@newdestlabel[2]{}
|
|
||||||
\providecommand\HyField@AuxAddToFields[1]{}
|
|
||||||
\providecommand\HyField@AuxAddToCoFields[2]{}
|
|
||||||
\abx@aux@refcontext{nty/global//global/global}
|
|
||||||
\abx@aux@cite{0}{feitcsi:project}
|
|
||||||
\abx@aux@segm{0}{0}{feitcsi:project}
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {1}Summary}{1}{section.1}\protected@file@percent }
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {2}Multi-NIC setup}{1}{section.2}\protected@file@percent }
|
|
||||||
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Network/hardware setup for multi-NIC CSI collection}}{2}{figure.1}\protected@file@percent }
|
|
||||||
\newlabel{fig:multi-nic}{{1}{2}{Network/hardware setup for multi-NIC CSI collection}{figure.1}{}}
|
|
||||||
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Custom antenna mount}}{2}{figure.2}\protected@file@percent }
|
|
||||||
\newlabel{fig:antenna-mount}{{2}{2}{Custom antenna mount}{figure.2}{}}
|
|
||||||
\abx@aux@cite{0}{Hsu2024}
|
|
||||||
\abx@aux@segm{0}{0}{Hsu2024}
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {3}Data Ingestion}{3}{section.3}\protected@file@percent }
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {4}Data Preprocessing}{3}{section.4}\protected@file@percent }
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {5}Angle of Arrival Estimation}{3}{section.5}\protected@file@percent }
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {6}Dataset Collection}{3}{section.6}\protected@file@percent }
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {7}Difficulties}{4}{section.7}\protected@file@percent }
|
|
||||||
\@writefile{toc}{\contentsline {section}{\numberline {8}Next Steps}{4}{section.8}\protected@file@percent }
|
|
||||||
\abx@aux@read@bbl@mdfivesum{nohash}
|
|
||||||
\abx@aux@read@bblrerun
|
|
||||||
\gdef \@abspage@last{4}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
% $ biblatex auxiliary file $
|
|
||||||
% $ biblatex bbl format version 3.2 $
|
|
||||||
% Do not modify the above lines!
|
|
||||||
%
|
|
||||||
% This is an auxiliary file used by the 'biblatex' package.
|
|
||||||
% This file may safely be deleted. It will be recreated by
|
|
||||||
% biber as required.
|
|
||||||
%
|
|
||||||
\begingroup
|
|
||||||
\makeatletter
|
|
||||||
\@ifundefined{ver@biblatex.sty}
|
|
||||||
{\@latex@error
|
|
||||||
{Missing 'biblatex' package}
|
|
||||||
{The bibliography requires the 'biblatex' package.}
|
|
||||||
\aftergroup\endinput}
|
|
||||||
{}
|
|
||||||
\endgroup
|
|
||||||
|
|
||||||
|
|
||||||
\refsection{0}
|
|
||||||
\datalist[entry]{nty/global//global/global}
|
|
||||||
\entry{Hsu2024}{inproceedings}{}
|
|
||||||
\name{author}{2}{}{%
|
|
||||||
{{hash=d5c2fb414951d03e3ec15a764a03592c}{%
|
|
||||||
family={Hsu},
|
|
||||||
familyi={H\bibinitperiod},
|
|
||||||
given={Ting-Wei},
|
|
||||||
giveni={T\bibinithyphendelim W\bibinitperiod}}}%
|
|
||||||
{{hash=1957109c91b54744e81c1e75d0d1e719}{%
|
|
||||||
family={Hsieh},
|
|
||||||
familyi={H\bibinitperiod},
|
|
||||||
given={Hung-Yun},
|
|
||||||
giveni={H\bibinithyphendelim Y\bibinitperiod}}}%
|
|
||||||
}
|
|
||||||
\strng{namehash}{cf444c64db6e8a2ac5a3d0f08cea6519}
|
|
||||||
\strng{fullhash}{cf444c64db6e8a2ac5a3d0f08cea6519}
|
|
||||||
\strng{bibnamehash}{cf444c64db6e8a2ac5a3d0f08cea6519}
|
|
||||||
\strng{authorbibnamehash}{cf444c64db6e8a2ac5a3d0f08cea6519}
|
|
||||||
\strng{authornamehash}{cf444c64db6e8a2ac5a3d0f08cea6519}
|
|
||||||
\strng{authorfullhash}{cf444c64db6e8a2ac5a3d0f08cea6519}
|
|
||||||
\field{sortinit}{H}
|
|
||||||
\field{sortinithash}{23a3aa7c24e56cfa16945d55545109b5}
|
|
||||||
\field{labelnamesource}{author}
|
|
||||||
\field{labeltitlesource}{title}
|
|
||||||
\field{booktitle}{ICC 2024 - IEEE International Conference on Communications}
|
|
||||||
\field{title}{Robust Multi-User Pose Estimation Based on Spatial and Temporal Features from WiFi CSI}
|
|
||||||
\field{year}{2024}
|
|
||||||
\field{pages}{1600\bibrangedash 1605}
|
|
||||||
\range{pages}{6}
|
|
||||||
\verb{doi}
|
|
||||||
\verb 10.1109/ICC51166.2024.10623053
|
|
||||||
\endverb
|
|
||||||
\keyw{Heating systems;Doppler shift;Time-frequency analysis;Pose estimation;Feature extraction;Robustness;Frequency estimation}
|
|
||||||
\endentry
|
|
||||||
\entry{feitcsi:project}{online}{}
|
|
||||||
\name{author}{3}{}{%
|
|
||||||
{{hash=f7ddbbc0fd5053139c0492b03c89ce6b}{%
|
|
||||||
family={Hutar},
|
|
||||||
familyi={H\bibinitperiod},
|
|
||||||
given={Miroslav},
|
|
||||||
giveni={M\bibinitperiod}}}%
|
|
||||||
{{hash=64e0fb04b292022aee82ce3ae6ec84f9}{%
|
|
||||||
family={Brida},
|
|
||||||
familyi={B\bibinitperiod},
|
|
||||||
given={Peter},
|
|
||||||
giveni={P\bibinitperiod}}}%
|
|
||||||
{{hash=c94ebc034cee4b9c81a21d40bf16345e}{%
|
|
||||||
family={Machaj},
|
|
||||||
familyi={M\bibinitperiod},
|
|
||||||
given={Juraj},
|
|
||||||
giveni={J\bibinitperiod}}}%
|
|
||||||
}
|
|
||||||
\strng{namehash}{6e810c4ddea2920152d10133a21f0f88}
|
|
||||||
\strng{fullhash}{6e810c4ddea2920152d10133a21f0f88}
|
|
||||||
\strng{bibnamehash}{6e810c4ddea2920152d10133a21f0f88}
|
|
||||||
\strng{authorbibnamehash}{6e810c4ddea2920152d10133a21f0f88}
|
|
||||||
\strng{authornamehash}{6e810c4ddea2920152d10133a21f0f88}
|
|
||||||
\strng{authorfullhash}{6e810c4ddea2920152d10133a21f0f88}
|
|
||||||
\field{sortinit}{H}
|
|
||||||
\field{sortinithash}{23a3aa7c24e56cfa16945d55545109b5}
|
|
||||||
\field{labelnamesource}{author}
|
|
||||||
\field{labeltitlesource}{title}
|
|
||||||
\field{title}{FeitCSI, the 802.11 CSI tool}
|
|
||||||
\field{year}{2023}
|
|
||||||
\verb{urlraw}
|
|
||||||
\verb https://feitcsi.kuskosoft.com
|
|
||||||
\endverb
|
|
||||||
\verb{url}
|
|
||||||
\verb https://feitcsi.kuskosoft.com
|
|
||||||
\endverb
|
|
||||||
\endentry
|
|
||||||
\enddatalist
|
|
||||||
\endrefsection
|
|
||||||
\endinput
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,15 +0,0 @@
|
|||||||
[0] Config.pm:307> INFO - This is Biber 2.19
|
|
||||||
[0] Config.pm:310> INFO - Logfile is 'progress.blg'
|
|
||||||
[34] biber:340> INFO - === Fri Feb 7, 2025, 00:59:00
|
|
||||||
[40] Biber.pm:419> INFO - Reading 'progress.bcf'
|
|
||||||
[67] Biber.pm:979> INFO - Found 2 citekeys in bib section 0
|
|
||||||
[74] Biber.pm:4419> INFO - Processing section 0
|
|
||||||
[78] Biber.pm:4610> INFO - Looking for bibtex file 'refs.bib' for section 0
|
|
||||||
[78] bibtex.pm:1713> INFO - LaTeX decoding ...
|
|
||||||
[80] bibtex.pm:1519> INFO - Found BibTeX data source 'refs.bib'
|
|
||||||
[96] UCollate.pm:68> INFO - Overriding locale 'en-US' defaults 'normalization = NFD' with 'normalization = prenormalized'
|
|
||||||
[96] UCollate.pm:68> INFO - Overriding locale 'en-US' defaults 'variable = shifted' with 'variable = non-ignorable'
|
|
||||||
[96] Biber.pm:4239> INFO - Sorting list 'nty/global//global/global' of type 'entry' with template 'nty' and locale 'en-US'
|
|
||||||
[96] Biber.pm:4245> INFO - No sort tailoring available for locale 'en-US'
|
|
||||||
[99] bbl.pm:660> INFO - Writing 'progress.bbl' with encoding 'UTF-8'
|
|
||||||
[99] bbl.pm:763> INFO - Output to progress.bbl
|
|
||||||
@ -1,238 +0,0 @@
|
|||||||
# Fdb version 4
|
|
||||||
["biber progress"] 1738890288.3654 "progress.bcf" "progress.bbl" "progress" 1738890288.36679 2
|
|
||||||
"progress.bcf" 1738890288.29345 107827 959b91c72f79a8e2ef56af97216763cc "lualatex"
|
|
||||||
"refs.bib" 1738889590.55706 686 b716c9640c4b4ad02a2cce84c5facbaa ""
|
|
||||||
(generated)
|
|
||||||
"progress.bbl"
|
|
||||||
"progress.blg"
|
|
||||||
(rewritten before read)
|
|
||||||
["lualatex"] 1738890287.21984 "progress.tex" "progress.pdf" "progress" 1738890288.36683 0
|
|
||||||
"/home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman10-bold.luc" 1737304252.15225 128397 4fa287d2565a98a75146f9efc01cf4d6 ""
|
|
||||||
"/home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman10-italic.luc" 1737304255.01897 136299 b8c58bb5f6e5457acdea1271317b68b7 ""
|
|
||||||
"/home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman10-regular.luc" 1737304251.85225 127314 32f9e61637e26814006c325175759c95 ""
|
|
||||||
"/home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman12-bold.luc" 1737304254.82563 128286 d012e848f630e37591e3db2d7055274f ""
|
|
||||||
"/home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman12-regular.luc" 1737304254.75896 127657 6bad1bc2543754cae9ba9c6746b71bd5 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/opentype/public/lm/lmroman10-bold.otf" 1736268207 111240 0af0b64d6d3df41bead3f9de314afbd4 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/opentype/public/lm/lmroman10-italic.otf" 1736268207 118828 4d461c73423fe2666dad2ff0dfc3ca68 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/opentype/public/lm/lmroman10-regular.otf" 1736268207 111536 ae9d1b331000d544f47e5223081b7b54 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/opentype/public/lm/lmroman12-bold.otf" 1736268207 110496 b9c8767d4cc3bf3f4b21f676bf89aa78 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmex10.tfm" 1736268207 992 662f679a0b3d2d53c1b94050fdaa3f50 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm" 1736268207 1528 abec98dbc43e172678c11b3b9031252a ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi5.tfm" 1736268207 1508 3b32edd0d68f6498a5a375e78f9edc5e ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi7.tfm" 1736268207 1528 e2423ae06dc7dee599cceb79d1c9dc32 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmr10.tfm" 1736268207 1296 45809c5a464d5f32c8f98ba97c1bb47f ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmr5.tfm" 1736268207 1220 ad296dff3c8796c18053ab7b9f86ad7c ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmr7.tfm" 1736268207 1300 53d07721103816e093902637bc167021 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm" 1736268207 1124 6c73e740cf17375f03eec0ee63599741 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy5.tfm" 1736268207 1112 14d5d5f6bd3c949edecb5b872f295553 ""
|
|
||||||
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy7.tfm" 1736268207 1120 2b3f9b25605010c69bc328bea6ac000f ""
|
|
||||||
"/usr/share/texmf-dist/tex/context/base/mkii/supp-pdf.mkii" 1736268207 71627 94eb9990bed73c364d7f53f960cc8c5b ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/bigintcalc/bigintcalc.sty" 1736268207 40635 c40361e206be584d448876bba8a64a3b ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/bitset/bitset.sty" 1736268207 33961 6b5c75130e435b2bfdb9f480a09a39f9 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/gettitlestring/gettitlestring.sty" 1736268207 8371 9d55b8bd010bc717624922fb3477d92e ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/iftex/iftex.sty" 1736268207 7237 bdd120a32c8fdb4b433cf9ca2e7cd98a ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/infwarerr/infwarerr.sty" 1736268207 8356 7bbb2c2373aa810be568c29e333da8ed ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/intcalc/intcalc.sty" 1736268207 31769 002a487f55041f8e805cfbf6385ffd97 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/kvdefinekeys/kvdefinekeys.sty" 1736268207 5412 d5a2436094cd7be85769db90f29250a6 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/ltxcmds/ltxcmds.sty" 1736268207 17865 1a9bd36b4f98178fa551aca822290953 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pdfescape/pdfescape.sty" 1736268207 19007 15924f7228aca6c6d184b115f4baa231 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.lua" 1736268207 9447 5e9f52f1871707a5d27dea360afbe4cb ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.sty" 1736268207 20089 80423eac55aa175305d35b49e04fe23b ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex" 1736268207 1016 1c2b89187d12a2768764b83b4945667c ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.code.tex" 1736268207 43820 1fef971b75380574ab35a0d37fd92608 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal.code.tex" 1736268207 19324 f4e4c6403dd0f1605fd20ed22fa79dea ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicstate.code.tex" 1736268207 6038 ccb406740cc3f03bbfb58ad504fe8c27 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.code.tex" 1736268207 6911 f6d4cf5a3fef5cc879d668b810e82868 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.code.tex" 1736268207 4883 42daaf41e27c3735286e23e48d2d7af9 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.code.tex" 1736268207 2544 8c06d2a7f0f469616ac9e13db6d2f842 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconstruct.code.tex" 1736268207 44195 5e390c414de027626ca5e2df888fa68d ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathprocessing.code.tex" 1736268207 17311 2ef6b2e29e2fc6a2fc8d6d652176e257 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage.code.tex" 1736268207 21302 788a79944eb22192a4929e46963a3067 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns.code.tex" 1736268207 9691 3d42d89522f4650c2f3dc616ca2b925e ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.code.tex" 1736268207 33335 dd1fa4814d4e51f18be97d88bf0da60c ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.code.tex" 1736268207 2965 4c2b1f4e0826925746439038172e5d6f ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code.tex" 1736268207 5196 2cc249e0ee7e03da5f5f6589257b1e5b ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.code.tex" 1736268207 20821 7579108c1e9363e61a0b1584778804aa ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.code.tex" 1736268207 35249 abd4adf948f960299a4b3d27c5dddf46 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransformations.code.tex" 1736268207 22012 81b34a0aa8fa1a6158cc6220b00e4f10 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransparency.code.tex" 1736268207 8893 e851de2175338fdf7c17f3e091d94618 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarycalc.code.tex" 1736268207 15929 463535aa2c4268fead6674a75c0e8266 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryfit.code.tex" 1736268207 3626 2d87dc681257fa32d07a8b3934b10f88 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.arrows.code.tex" 1736268207 410 048d1174dabde96757a5387b8f23d968 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.callouts.code.tex" 1736268207 1201 8bd51e254d3ecf0cd2f21edd9ab6f1bb ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.code.tex" 1736268207 494 8de62576191924285b021f4fc4292e16 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.geometric.code.tex" 1736268207 339 be0fe46d92a80e3385dd6a83511a46f2 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.misc.code.tex" 1736268207 329 ba6d5440f8c16779c2384e0614158266 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.multipart.code.tex" 1736268207 923 c7a223b32ffdeb1c839d97935eee61ff ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.symbols.code.tex" 1736268207 475 4b4056fe07caa0603fede9a162fe666d ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarytopaths.code.tex" 1736268207 11518 738408f795261b70ce8dd47459171309 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex" 1736268207 186782 af500404a9edec4d362912fe762ded92 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryfpu.code.tex" 1736268207 85938 8e4ba97c5906e1c0d158aea81fe29af7 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryintersections.code.tex" 1736268207 44571 38ac24c171fb8fa1a13adc8ce7eb94c5 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.code.tex" 1736268207 32995 ac577023e12c0e4bd8aa420b2e852d1a ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.arrows.code.tex" 1736268207 91587 d9b31a3e308b08833e4528a7b4484b4a ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.callouts.code.tex" 1736268207 33336 427c354e28a4802ffd781da22ae9f383 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.geometric.code.tex" 1736268207 161011 76ab54df0aa1a9d3b27a94864771d38d ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.misc.code.tex" 1736268207 46249 d1f322c52d26cf506b4988f31902cd5d ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.multipart.code.tex" 1736268207 62281 aff261ef10ba6cbe8e3c872a38c05a61 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.symbols.code.tex" 1736268207 90521 9d46d4504c2ffed28ff5ef3c43d15f21 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfint.code.tex" 1736268207 3063 8c415c68a0f3394e45cfeca0b65f6ee6 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex" 1736268207 949 cea70942e7b7eddabfb3186befada2e6 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex" 1736268207 13270 2e54f2ce7622437bf37e013d399743e3 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex" 1736268207 104717 9b2393fbf004a0ce7fa688dbce423848 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base.code.tex" 1736268207 10165 cec5fa73d49da442e56efc2d605ef154 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic.code.tex" 1736268207 28178 41c17713108e0795aac6fef3d275fbca ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code.tex" 1736268207 9649 85779d3d8d573bfd2cd4137ba8202e60 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.comparison.code.tex" 1736268207 3865 ac538ab80c5cf82b345016e474786549 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integerarithmetics.code.tex" 1736268207 3177 27d85c44fbfe09ff3b2cf2879e3ea434 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc.code.tex" 1736268207 11024 0179538121bc2dba172013a3ef89519f ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.random.code.tex" 1736268207 7890 0a86dbf4edfd88d022e0d889ec78cc03 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round.code.tex" 1736268207 3379 781797a101f647bab82741a99944a229 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigonometric.code.tex" 1736268207 92405 f515f31275db273f97b9d8f52e1b0736 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex" 1736268207 37466 97b0a1ba732e306a1a2034f5a73e239f ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex" 1736268207 8471 c2883569d03f69e8e1cabfef4999cfd7 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.code.tex" 1736268207 21211 1e73ec76bd73964d84197cc3d2685b01 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code.tex" 1736268207 16121 346f9013d34804439f7436ff6786cef7 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.code.tex" 1736268207 44792 271e2e1934f34c759f4dedb1e14a5015 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/pgf.revision.tex" 1736268207 114 e6d443369d0673933b38834bf99e422d ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg" 1736268207 926 2963ea0dcf6cc6c0a770b69ec46a477b ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-pdf.def" 1736268207 5542 32f75a31ea6c3a7e1148cd6d5e93dbb7 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-luatex.def" 1736268207 13255 83878f3f820beccc0dd1c2683dabc65e ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex" 1736268207 61351 bc5f86e0355834391e736e97a61abced ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex" 1736268207 1896 b8e0ca0ac371d74c0ca05583f6313c91 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex" 1736268207 7778 53c8b5623d80238f6a20aa1df1868e63 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex" 1736268207 24033 d8893a1ec4d1bfa101b172754743d340 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex" 1736268207 39784 414c54e866ebab4b801e2ad81d9b21d8 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeyslibraryfiltered.code.tex" 1736268207 37433 940bc6d409f1ffd298adfdcaf125dd86 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex" 1736268207 4385 510565c2f07998c8a0e14f0ec07ff23c ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.tex" 1736268207 29239 22e8c7516012992a49873eff0d868fed ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def" 1736268207 6950 8524a062d82b7afdc4a88a57cb377784 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/stringenc/stringenc.sty" 1736268207 21514 b7557edcee22835ef6b03ede1802dad4 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/unicode-data/UnicodeData.txt" 1736268207 1914200 c94d3d92f1c66e50f750fe84b8055939 ""
|
|
||||||
"/usr/share/texmf-dist/tex/generic/uniquecounter/uniquecounter.sty" 1736268207 7008 f92eaa0a3872ed622bbf538217cd2ab7 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/auxhook/auxhook.sty" 1736268207 3935 57aa3c3e203a5c2effb4d2bd2efbc323 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/base/article.cls" 1736268207 20144 147463a6a579f4597269ef9565205cfe ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/base/atbegshi-ltx.sty" 1736268207 3045 273c666a54e60b9f730964f431a56c1b ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/base/atveryend-ltx.sty" 1736268207 2462 6bc53756156dbd71c1ad550d30a3b93f ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/base/ifthen.sty" 1736268207 5319 2b738d02ce36ada6dcdd9534940db0ee ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/base/ltluatex.lua" 1736268207 24543 44e225ecc61706b060f0189a96e1cc1d ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/base/size10.clo" 1736268207 8448 dbc0dbf4156c0bb9ba01a1c685d3bad0 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/base/ts1cmr.fd" 1736268207 2430 fce77d7e103eb98b6c4d486ae1d466c0 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/bbx/numeric.bbx" 1736268207 1818 9ed166ac0a9204a8ebe450ca09db5dde ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/bbx/standard.bbx" 1736268207 25680 409c3f3d570418bc545e8065bebd0688 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/biblatex.cfg" 1736268207 69 249fa6df04d948e51b6d5c67bea30c42 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/biblatex.def" 1736268207 92527 8f6b3a677f74ea525477a813f33c4e65 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/biblatex.sty" 1736268207 528517 7eed285c714f532e12ae48b360c080f8 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/blx-case-expl3.sty" 1736268207 8433 72f8188742e7214b7068f345cd0287ac ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/blx-compat.def" 1736268207 13919 5426dbe90e723f089052b4e908b56ef9 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/blx-dm.def" 1736268207 32455 8d3e554836db11aab80a8e11be62e1b1 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/blx-unicode.def" 1736268207 3786 1f89d14780f0ad89ab94652b37f4e9b8 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/cbx/numeric.cbx" 1736268207 4629 cda468e8a0b1cfa0f61872e171037a4b ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/biblatex/lbx/english.lbx" 1736268207 39965 48ce9ce3350aba9457f1020b1deba5cf ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/elocalloc/elocalloc.sty" 1736268207 1428 7d469063535b93044f827bfdb1b0a130 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/environ/environ.sty" 1736268207 4378 f429f0da968c278653359293040a8f52 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty" 1736268207 13886 d1306dcf79a944f6988e688c1785f9ce ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/etoolbox/etoolbox.sty" 1736268207 46845 3b58f70c6e861a13d927bff09d35ecbc ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/forest/forest.sty" 1736268207 350382 5acb55040bcf8080692df2e32715e5ea ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics-cfg/color.cfg" 1736268207 1213 620bba36b25224fa9b7e1ccb4ecb76fd ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics-cfg/graphics.cfg" 1736268207 1224 978390e9c2234eab29404bc21b268d1e ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics-def/luatex.def" 1736268207 19478 ff26a264ed286e649e9023efac2574b2 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics/graphics.sty" 1736268207 18387 8f900a490197ebaf93c02ae9476d4b09 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics/graphicx.sty" 1736268207 8010 a8d949cbdbc5c983593827c9eec252e1 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics/keyval.sty" 1736268207 2671 7e67d78d9b88c845599a85b2d41f2e39 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics/mathcolor.ltx" 1736268207 2885 9c645d672ae17285bba324998918efd8 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/graphics/trig.sty" 1736268207 4023 293ea1c16429fc0c4cf605f4da1791a9 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/hycolor/hycolor.sty" 1736268207 17914 4c28a13fc3d975e6e81c9bea1d697276 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/hyperref/hluatex.def" 1736268207 51074 bb8178880b603a1e9b88baf1805e8c5a ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/hyperref/hyperref.sty" 1736268207 220920 fd3cbb5f1a2bc9b8f451b8b7d8171264 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/hyperref/nameref.sty" 1736268207 11026 182c63f139a71afd30a28e5f1ed2cd1c ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/hyperref/pd1enc.def" 1736268207 14249 e67cb186717b7ab18d14a4875e7e98b5 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/hyperref/puenc.def" 1736268207 117112 05831178ece2cad4d9629dcf65099b11 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/inlinedef/inlinedef.sty" 1736268207 10102 e5ccd67aecac6cb4bf5de2b491ef79b5 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/kvoptions/kvoptions.sty" 1736268207 22555 6d8e155cfef6d82c3d5c742fea7c992e ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/kvsetkeys/kvsetkeys.sty" 1736268207 13815 760b0c02f691ea230f5359c4e1de23a7 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/l3backend/l3backend-luatex.def" 1736268207 30427 925bbc87643c4ebec807605b70ff6deb ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/l3backend/l3backend-luatex.lua" 1736268207 3839 b2e3045c3d6386724a7a4977b109956f ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/l3kernel/expl3.lua" 1736268207 16940 41c3e6d9ad450f4dc67cddec0e70ecf4 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/l3kernel/expl3.sty" 1736268207 6565 10e89ed128ccd59431746bbdd82129fc ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/l3packages/xparse/xparse.sty" 1736268207 9327 11bedad2ac38f92e405a38ed18489a03 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg" 1736268207 678 4792914a8f45be57bb98413425e4c7af ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/logreq/logreq.def" 1736268207 1620 fb1c32b818f2058eca187e5c41dfae77 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/logreq/logreq.sty" 1736268207 6187 b27afc771af565d3a9ff1ca7d16d0d46 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty" 1736268207 1090 bae35ef70b3168089ef166db3e66f5b2 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty" 1736268207 373 00b204b1d7d095b892ad31a7494b0373 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty" 1736268207 21013 f4ff83d25bb56552493b030f27c075ae ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty" 1736268207 989 c49c8ae06d96f8b15869da7428047b1e ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty" 1736268207 339 c2e180022e3afdb99c7d0ea5ce469b7d ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/math/pgfmath.sty" 1736268207 306 c56a323ca5bf9242f54474ced10fca71 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty" 1736268207 443 8c872229db56122037e86bcda49e14f3 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/utilities/pgffor.sty" 1736268207 348 ee405e64380c11319f0e249fed57e6c5 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty" 1736268207 274 5ae372b7df79135d240456a1c6f2cf9a ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty" 1736268207 325 f9f16d12354225b7dd52a3321f085955 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/pgfopts/pgfopts.sty" 1736268207 5540 d5c60cf09c59da351aa4023ed084e4eb ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/refcount/refcount.sty" 1736268207 9878 9e94e8fa600d95f9c7731bb21dfb67a4 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/rerunfilecheck/rerunfilecheck.sty" 1736268207 9714 ba3194bd52c8499b3f1e3eb91d409670 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/tex-ini-files/lualatexquotejobname.lua" 1736268207 1021 ae37ae5e20605f170274bb33ea1a0e3d ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/titling/titling.sty" 1736268207 7358 95ac619994bd30d405a74f3eca431c84 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/trimspaces/trimspaces.sty" 1736268207 1380 971a51b00a14503ddf754cab24c3f209 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/url/url.sty" 1736268207 12796 8edb7d69a20b857904dd0ea757c14ec9 ""
|
|
||||||
"/usr/share/texmf-dist/tex/latex/xcolor/xcolor.sty" 1736268207 55487 80a65caedd3722f4c20a14a69e785d8f ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lua-uni-algos/lua-uni-case.lua" 1736268207 1921 020c660fdc1d4c83ffebb059fead7b51 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lua-uni-algos/lua-uni-normalize.lua" 1736268207 21269 eb4154856f0afe9e8d886dbf6922dcc6 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lua-uni-algos/lua-uni-parse.lua" 1736268207 2115 596f0e8384e97c26c78a8e88c65a7843 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lualibs/lualibs-basic-merged.lua" 1736268207 131460 7015f38db4e78c4821318144c6980394 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lualibs/lualibs-basic.lua" 1736268207 2685 2fd4fa3426f4dda85135669993283e9b ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lualibs/lualibs-compat.lua" 1736268207 603 398583cb619d20952d67edcedae41608 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lualibs/lualibs-extended-merged.lua" 1736268207 140360 1ec312f1a70c43b37e53af03b4bd7323 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lualibs/lualibs-extended.lua" 1736268207 5000 964f0ac2a95b6856d92913f68e0689a7 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/lualibs/lualibs.lua" 1736268207 3780 3cfe6b013a0ab68c61b751da491afaac ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/fontloader-2023-12-28.lua" 1736268207 885747 f33a8946b4d6a005e76276706db83059 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/fontloader-basics-gen.lua" 1736268207 12788 578c251c496bed783075709e94c2be26 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-arabic.lua" 1736268207 5364 49a6ddd634bdb37bacb08c17d1550f50 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-auxiliary.lua" 1736268207 34239 6dbe1a2a3f36cf57845e54c1927ccfee ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-bcp47.lua" 1736268207 3674 ad13e5d6b82aff53a36384fd239c5d6c ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-case.lua" 1736268207 16116 3d55feb55802d40830807a7459cb345a ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-colors.lua" 1736268207 18372 cda35651d19ec5ae24d18f3b0e1cfeec ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-configuration.lua" 1736268207 34054 0abab568ba9c0082c9ad3e1b75897d49 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-database.lua" 1736268207 135377 0e2f26fead21ffdb36f36a5090220f8d ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-embolden.lua" 1736268207 1004 6b15c8ae9e49cfb4a5e319aadf1ef3ac ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-fallback.lua" 1736268207 4047 3b1569703b22fabe9661f2188fa0dbd3 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-features.lua" 1736268207 35386 756bc857a0d63bf1a6abbd1406e85844 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-define.lua" 1736268207 19482 f1319af7f3837fbda6db470b77627c71 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-plug.lua" 1736268207 42681 201bbdd109fa1f0b02068e59f2f5384d ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-var-cff2.lua" 1736268207 14126 969cfafc9b19f07aa3e945dd2d3bd6ca ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-var-t2-writer.lua" 1736268207 4050 f57f854058ea50d565be9558f3ee2647 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-var-ttf.lua" 1736268207 20523 f40bb0362e93dec1cb185536de4be47d ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-init.lua" 1736268207 19746 5808f9c69b6a266fef8b923aefaecdc3 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-letterspace.lua" 1736268207 20268 45fe7d691be75f6ef7f0c058c40113d9 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-loaders.lua" 1736268207 10733 80907f5cf87e2d5f946936a1ef08b6a8 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-log.lua" 1736268207 11323 b0cd5d77653fae19eceb1c1f195d9b77 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-main.lua" 1736268207 275 b19c9cc34cf1d676c39f872cfb41aef6 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-multiscript.lua" 1736268207 15068 1415dc53c2ad56f6dc892212090d7c03 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-notdef.lua" 1736268207 12258 920854a37e695604457cb1fcad35a814 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-parsers.lua" 1736268207 30436 73f67f95af947fe741c931e6f60b84cb ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-realpath.lua" 1736268207 4692 f3b5a8bf6d49e81bc8637547ad67334d ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-resolvers.lua" 1736268207 11209 0f87b7c297120189ddbd31f6ee324a9f ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-scripts.lua" 1736268207 2506 2026f40b86c3e93eba401524424e71d9 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-status.lua" 1736268207 6317 d7e4e397c1c9279d55136ae8a79c077e ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-suppress.lua" 1736268207 2582 1cbd162cbdc589adec03aa82e2ba0048 ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-szss.lua" 1736268207 6309 bdf184e24947478dc89307adf866a3ca ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-tounicode.lua" 1736268207 7528 245271ce518c136c96f22fee6cb810ab ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-unicode.lua" 1736268207 7917 cebc6454abe34f2fe1c1248f8b2453da ""
|
|
||||||
"/usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload.lua" 1736268207 13565 50482dad4c221a79dbdbb982122baf8c ""
|
|
||||||
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1736329123.12051 5357452 9c63374643a5082f15b788fe1e348f57 ""
|
|
||||||
"/var/lib/texmf/web2c/luahbtex/lualatex.fmt" 1736329099 12260934 4441f94dd3f43b779629a3bc79355f00 ""
|
|
||||||
"images/antenna_mount.png" 1738840144.9769 654857 ec62560db93f2318ba8918fa02f9b186 ""
|
|
||||||
"progress.aux" 1738890288.29345 1753 374bf98cb56ed7ec81c2992c620f6852 "lualatex"
|
|
||||||
"progress.bbl" 0 -1 0 "biber progress"
|
|
||||||
"progress.out" 1738890288.29345 973 dae26985e86cfaf58f64b40b92ed794c "lualatex"
|
|
||||||
"progress.run.xml" 1738890288.29345 2362 e7fd92e99d11b5a90a062c3752e062da "lualatex"
|
|
||||||
"progress.tex" 1738890285.49012 9209 ce48269f8048b7468882ac06a82b3c0f ""
|
|
||||||
(generated)
|
|
||||||
"progress.aux"
|
|
||||||
"progress.bcf"
|
|
||||||
"progress.log"
|
|
||||||
"progress.out"
|
|
||||||
"progress.pdf"
|
|
||||||
"progress.run.xml"
|
|
||||||
(rewritten before read)
|
|
||||||
@ -1,249 +0,0 @@
|
|||||||
PWD /home/cfalas/software/dissertation/progress_report
|
|
||||||
INPUT /var/lib/texmf/web2c/luahbtex/lualatex.fmt
|
|
||||||
INPUT ./progress.tex
|
|
||||||
OUTPUT progress.log
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/tex-ini-files/lualatexquotejobname.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/base/ltluatex.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-main.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-init.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lualibs/lualibs.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lualibs/lualibs-basic.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lualibs/lualibs-basic-merged.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lualibs/lualibs-compat.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lualibs/lualibs-extended.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lualibs/lualibs-extended-merged.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-log.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/fontloader-basics-gen.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-parsers.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-configuration.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-status.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/fontloader-2023-12-28.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-fallback.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-multiscript.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-scripts.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-loaders.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-database.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-unicode.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lua-uni-algos/lua-uni-case.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lua-uni-algos/lua-uni-parse.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-realpath.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-colors.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-resolvers.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-features.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/lua-uni-algos/lua-uni-normalize.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-arabic.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-define.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-var-cff2.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-var-t2-writer.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-var-ttf.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-harf-plug.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-letterspace.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-embolden.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-notdef.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-suppress.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-szss.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-auxiliary.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-tounicode.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-case.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/luatex/luaotfload/luaotfload-bcp47.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/l3kernel/expl3.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/base/article.cls
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/base/size10.clo
|
|
||||||
INPUT /home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman10-regular.luc
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/titling/titling.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics/graphicx.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics/keyval.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics/graphics.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics/trig.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics-def/luatex.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/forest/forest.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/pgf.revision.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/pgf.revision.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeyslibraryfiltered.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-luatex.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-luatex.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-pdf.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/xcolor/xcolor.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics-cfg/color.cfg
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/graphics/mathcolor.ltx
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigonometric.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.random.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.comparison.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integerarithmetics.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfint.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconstruct.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicstate.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransformations.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathprocessing.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransparency.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgf/math/pgfmath.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarytopaths.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarytopaths.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.geometric.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.geometric.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.geometric.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.geometric.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.misc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.misc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.misc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.misc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.symbols.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.symbols.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.symbols.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.symbols.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.arrows.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.arrows.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.arrows.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.arrows.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.callouts.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.callouts.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.callouts.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.callouts.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.multipart.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryshapes.multipart.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.multipart.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/shapes/pgflibraryshapes.multipart.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryfit.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibraryfit.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarycalc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarycalc.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryfpu.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pgf/libraries/pgflibraryfpu.code.tex
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/pgfopts/pgfopts.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/etoolbox/etoolbox.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/elocalloc/elocalloc.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/environ/environ.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/trimspaces/trimspaces.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/l3packages/xparse/xparse.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/l3kernel/expl3.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/l3backend/l3backend-luatex.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/l3backend/l3backend-luatex.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/inlinedef/inlinedef.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/hyperref/hyperref.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/iftex/iftex.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/kvsetkeys/kvsetkeys.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/kvdefinekeys/kvdefinekeys.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pdfescape/pdfescape.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/ltxcmds/ltxcmds.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/infwarerr/infwarerr.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.lua
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/hycolor/hycolor.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/auxhook/auxhook.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/hyperref/nameref.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/refcount/refcount.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/gettitlestring/gettitlestring.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/kvoptions/kvoptions.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/hyperref/pd1enc.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/intcalc/intcalc.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/hyperref/puenc.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/url/url.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/bitset/bitset.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/bigintcalc/bigintcalc.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/base/atbegshi-ltx.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/hyperref/hluatex.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/stringenc/stringenc.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/base/atveryend-ltx.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/rerunfilecheck/rerunfilecheck.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/uniquecounter/uniquecounter.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/biblatex.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/logreq/logreq.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/logreq/logreq.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/base/ifthen.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/blx-dm.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/blx-unicode.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/generic/unicode-data/UnicodeData.txt
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/blx-compat.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/biblatex.def
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/bbx/numeric.bbx
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/bbx/standard.bbx
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/cbx/numeric.cbx
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/biblatex.cfg
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/blx-case-expl3.sty
|
|
||||||
INPUT ./progress.aux
|
|
||||||
OUTPUT progress.aux
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/base/ts1cmr.fd
|
|
||||||
INPUT /usr/share/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
|
|
||||||
INPUT ./progress.out
|
|
||||||
INPUT ./progress.out
|
|
||||||
OUTPUT progress.out
|
|
||||||
OUTPUT progress.pdf
|
|
||||||
INPUT /usr/share/texmf-dist/tex/latex/biblatex/lbx/english.lbx
|
|
||||||
OUTPUT progress.bcf
|
|
||||||
INPUT /home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman10-bold.luc
|
|
||||||
INPUT /home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman10-italic.luc
|
|
||||||
INPUT /home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman12-regular.luc
|
|
||||||
INPUT /home/cfalas/.cache/texlive/texmf-var/luatex-cache/generic/fonts/otl/lmroman12-bold.luc
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmr10.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmr7.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmr5.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmmi7.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmmi5.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmsy7.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmsy5.tfm
|
|
||||||
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmex10.tfm
|
|
||||||
INPUT /var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map
|
|
||||||
INPUT ./images/antenna_mount.png
|
|
||||||
INPUT ./images/antenna_mount.png
|
|
||||||
INPUT ./progress.aux
|
|
||||||
INPUT ./progress.run.xml
|
|
||||||
OUTPUT progress.run.xml
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
\BOOKMARK [1][-]{section.1}{\376\377\000S\000u\000m\000m\000a\000r\000y}{}% 1
|
|
||||||
\BOOKMARK [1][-]{section.2}{\376\377\000M\000u\000l\000t\000i\000-\000N\000I\000C\000\040\000s\000e\000t\000u\000p}{}% 2
|
|
||||||
\BOOKMARK [1][-]{section.3}{\376\377\000D\000a\000t\000a\000\040\000I\000n\000g\000e\000s\000t\000i\000o\000n}{}% 3
|
|
||||||
\BOOKMARK [1][-]{section.4}{\376\377\000D\000a\000t\000a\000\040\000P\000r\000e\000p\000r\000o\000c\000e\000s\000s\000i\000n\000g}{}% 4
|
|
||||||
\BOOKMARK [1][-]{section.5}{\376\377\000A\000n\000g\000l\000e\000\040\000o\000f\000\040\000A\000r\000r\000i\000v\000a\000l\000\040\000E\000s\000t\000i\000m\000a\000t\000i\000o\000n}{}% 5
|
|
||||||
\BOOKMARK [1][-]{section.6}{\376\377\000D\000a\000t\000a\000s\000e\000t\000\040\000C\000o\000l\000l\000e\000c\000t\000i\000o\000n}{}% 6
|
|
||||||
\BOOKMARK [1][-]{section.7}{\376\377\000D\000i\000f\000f\000i\000c\000u\000l\000t\000i\000e\000s}{}% 7
|
|
||||||
\BOOKMARK [1][-]{section.8}{\376\377\000N\000e\000x\000t\000\040\000S\000t\000e\000p\000s}{}% 8
|
|
||||||
Binary file not shown.
@ -1,86 +0,0 @@
|
|||||||
<?xml version="1.0" standalone="yes"?>
|
|
||||||
<!-- logreq request file -->
|
|
||||||
<!-- logreq version 1.0 / dtd version 1.0 -->
|
|
||||||
<!-- Do not edit this file! -->
|
|
||||||
<!DOCTYPE requests [
|
|
||||||
<!ELEMENT requests (internal | external)*>
|
|
||||||
<!ELEMENT internal (generic, (provides | requires)*)>
|
|
||||||
<!ELEMENT external (generic, cmdline?, input?, output?, (provides | requires)*)>
|
|
||||||
<!ELEMENT cmdline (binary, (option | infile | outfile)*)>
|
|
||||||
<!ELEMENT input (file)+>
|
|
||||||
<!ELEMENT output (file)+>
|
|
||||||
<!ELEMENT provides (file)+>
|
|
||||||
<!ELEMENT requires (file)+>
|
|
||||||
<!ELEMENT generic (#PCDATA)>
|
|
||||||
<!ELEMENT binary (#PCDATA)>
|
|
||||||
<!ELEMENT option (#PCDATA)>
|
|
||||||
<!ELEMENT infile (#PCDATA)>
|
|
||||||
<!ELEMENT outfile (#PCDATA)>
|
|
||||||
<!ELEMENT file (#PCDATA)>
|
|
||||||
<!ATTLIST requests
|
|
||||||
version CDATA #REQUIRED
|
|
||||||
>
|
|
||||||
<!ATTLIST internal
|
|
||||||
package CDATA #REQUIRED
|
|
||||||
priority (9) #REQUIRED
|
|
||||||
active (0 | 1) #REQUIRED
|
|
||||||
>
|
|
||||||
<!ATTLIST external
|
|
||||||
package CDATA #REQUIRED
|
|
||||||
priority (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8) #REQUIRED
|
|
||||||
active (0 | 1) #REQUIRED
|
|
||||||
>
|
|
||||||
<!ATTLIST provides
|
|
||||||
type (static | dynamic | editable) #REQUIRED
|
|
||||||
>
|
|
||||||
<!ATTLIST requires
|
|
||||||
type (static | dynamic | editable) #REQUIRED
|
|
||||||
>
|
|
||||||
<!ATTLIST file
|
|
||||||
type CDATA #IMPLIED
|
|
||||||
>
|
|
||||||
]>
|
|
||||||
<requests version="1.0">
|
|
||||||
<internal package="biblatex" priority="9" active="1">
|
|
||||||
<generic>latex</generic>
|
|
||||||
<provides type="dynamic">
|
|
||||||
<file>progress.bcf</file>
|
|
||||||
</provides>
|
|
||||||
<requires type="dynamic">
|
|
||||||
<file>progress.bbl</file>
|
|
||||||
</requires>
|
|
||||||
<requires type="static">
|
|
||||||
<file>blx-dm.def</file>
|
|
||||||
<file>blx-unicode.def</file>
|
|
||||||
<file>blx-compat.def</file>
|
|
||||||
<file>biblatex.def</file>
|
|
||||||
<file>standard.bbx</file>
|
|
||||||
<file>numeric.bbx</file>
|
|
||||||
<file>numeric.cbx</file>
|
|
||||||
<file>biblatex.cfg</file>
|
|
||||||
<file>english.lbx</file>
|
|
||||||
</requires>
|
|
||||||
</internal>
|
|
||||||
<external package="biblatex" priority="5" active="1">
|
|
||||||
<generic>biber</generic>
|
|
||||||
<cmdline>
|
|
||||||
<binary>biber</binary>
|
|
||||||
<infile>progress</infile>
|
|
||||||
</cmdline>
|
|
||||||
<input>
|
|
||||||
<file>progress.bcf</file>
|
|
||||||
</input>
|
|
||||||
<output>
|
|
||||||
<file>progress.bbl</file>
|
|
||||||
</output>
|
|
||||||
<provides type="dynamic">
|
|
||||||
<file>progress.bbl</file>
|
|
||||||
</provides>
|
|
||||||
<requires type="dynamic">
|
|
||||||
<file>progress.bcf</file>
|
|
||||||
</requires>
|
|
||||||
<requires type="editable">
|
|
||||||
<file>refs.bib</file>
|
|
||||||
</requires>
|
|
||||||
</external>
|
|
||||||
</requests>
|
|
||||||
Binary file not shown.
@ -1,132 +0,0 @@
|
|||||||
\documentclass{article}
|
|
||||||
\usepackage{titling}
|
|
||||||
\usepackage{graphicx} % Required for inserting images
|
|
||||||
\usepackage{forest}
|
|
||||||
\usepackage{hyperref}
|
|
||||||
\usepackage[backend=biber]{biblatex}
|
|
||||||
|
|
||||||
\addbibresource{refs.bib}
|
|
||||||
\begin{document}
|
|
||||||
|
|
||||||
% \maketitle
|
|
||||||
|
|
||||||
|
|
||||||
\noindent\textbf{Progress Report:} \textit{Human Presence Detection using Wi-Fi Channel State Information}
|
|
||||||
|
|
||||||
\noindent\textbf{Author:} Christos Falas (\href{mailto:cf575@cam.ac.uk}{cf575@cam.ac.uk})
|
|
||||||
|
|
||||||
\noindent\textbf{Supervisor:} Markus Kuhn
|
|
||||||
|
|
||||||
\noindent\textbf{Project Checkers:} Pietro Lio (pl219) and Jeremy Yallop (jdy22)
|
|
||||||
|
|
||||||
\noindent\textbf{Date:} \today
|
|
||||||
|
|
||||||
\vspace{1cm}
|
|
||||||
|
|
||||||
\section{Summary}
|
|
||||||
|
|
||||||
Overall, I believe that the project is going well, and a lot of progress has been made. However, due to easier logistics, I decided to re-arrange the work plan slightly, so that some tasks that I wasn't planning on doing until a bit later are already done, aand others that were supposed to be done by now are not.
|
|
||||||
|
|
||||||
In the next sections, I try to explain these decisions, as well s give some context on what I've done, and what is left to be done.
|
|
||||||
|
|
||||||
Due to this change in work plan, it is hard to estimate whether my project is behind or ahead of schedule. I would imagine I am roughtly on track (since the extra work I've done is slightly more than the work I skipped). In order to not have this problem, at the end I devised a new work plan.
|
|
||||||
|
|
||||||
\section{Multi-NIC setup}
|
|
||||||
|
|
||||||
Per my initial plan, I was supposed to currently only be working using a single Wi-Fi NIC on the receiving end. However, this only allows me to use CSI data from 2 antennas, which is quite limiting (and would lead to an under-constrained solution in most cases). In order to combat this, I re-arranged my work plan, and moved the work to set up the multi-NIC system to an earlier stage.
|
|
||||||
|
|
||||||
In order to easily work with multiple NICs, I had to use multiple computers, since I was limited by the number of PCIe slots on each computer (this would not be a limit on commercial access point hardware). Along with my main desktop computer, I am using two identical small-form-factor machines, each of which have 2 PCIe ports.
|
|
||||||
|
|
||||||
Per my original proposal, I am using FeitCSI \cite{feitcsi:project} to control the Wi-Fi NICs over the network. However, FeitCSI does not currently support multiple NICs on the same machine. Therefore, in order to use both PCIe slots on the machine for NICs, I had to set up different virtual machines, so that each VM would only detect a single NIC. For ease of management, I configured the VMs to boot from the network, off of a single disk image, such that I only have to make changes in one place. The overall setup is as shown in Figure \ref{fig:multi-nic}.
|
|
||||||
|
|
||||||
While the setup is not realistic for a commercial deployment, it makes working with multiple NICs much easier, is a rough approximation of the desired setup that allows me to collect the same data. This collection process should be a lot easier with newer Wi-Fi 7 hardware.
|
|
||||||
|
|
||||||
\begin{figure}[h]
|
|
||||||
\begin{forest}
|
|
||||||
for tree={draw, align=center, l sep=1cm}
|
|
||||||
[Network Switch, s sep=1cm
|
|
||||||
[Main Machine
|
|
||||||
[Hypervisor
|
|
||||||
[TFTP \& NFS Server, name=tftp, yshift=0.8cm]
|
|
||||||
[Development Machine, name=dev] % Assigning a name to reference later
|
|
||||||
[FeitCSI, name=feit0 [NIC 0, edge=green]]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
[Machine 1, tikz={\node [draw, inner sep=2mm, fit=()(!111)(!lll), rounded corners, dashed]{};}
|
|
||||||
[Hypervisor, name=hyper1
|
|
||||||
[FeitCSI, name=feit11 [NIC 1, name=nic1, edge=green]]
|
|
||||||
[FeitCSI, name=feit12 [NIC 2, edge=green]]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
[Machine 2, tikz={\node [draw, inner sep=2mm, fit=()(!111)(!lll), rounded corners, dashed]{};}
|
|
||||||
[Hypervisor, name=hyper2
|
|
||||||
[FeitCSI, name=feit21 [NIC 3, edge=green]]
|
|
||||||
[FeitCSI, name=feit22 [NIC 4, edge=green]]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
\draw[blue, bend left] (tftp.north) to[out=20, in=160] (feit21.north);
|
|
||||||
\draw[blue, bend left] (tftp.north) to[out=20, in=160] (feit22.north);
|
|
||||||
\draw[blue, bend left] (tftp.north) to[out=20, in=160] (feit11.north);
|
|
||||||
\draw[blue, bend left] (tftp.north) to[out=20, in=160] (feit12.north);
|
|
||||||
\draw[blue, bend left] (tftp.north) to[out=20, in=160] (feit0.north);
|
|
||||||
\end{forest}
|
|
||||||
\caption{Network/hardware setup for multi-NIC CSI collection}
|
|
||||||
\label{fig:multi-nic}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
To ensure consistency in the spacing between antennas, I built a simple custom mount for the 8 antennas (2 per NIC) that I am using, out of a tin can. This mount is shown in Figure \ref{fig:antenna-mount}.
|
|
||||||
|
|
||||||
\begin{figure}[h]
|
|
||||||
\includegraphics[scale=0.5]{images/antenna_mount.png}
|
|
||||||
\centering
|
|
||||||
\caption{Custom antenna mount}
|
|
||||||
\label{fig:antenna-mount}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
\section{Data Ingestion}
|
|
||||||
|
|
||||||
I have implemented a simple data ingestion pipeline, which connects to each of the FeitCSI instances over the network, and collects the CSI data from each of the NICs in realtime (at around 100Hz). The data is then merged using the correct antenna order, and then streamed to downstream consumers (which could save the data to a file for later processing, or process it in realtime).
|
|
||||||
|
|
||||||
Additionally, I implemented a simple visualisation framework which streams data to the browser in realtime, which allows me to see the data as it is being collected and processed. This is useful for debugging, and for understanding the data better.
|
|
||||||
|
|
||||||
\section{Data Preprocessing}
|
|
||||||
|
|
||||||
Data preprocessing was one of my first milestones per my original work plan. Even though I completed it initially in schedule, I believe the implementation is not optimal. Some of the math I used from the original paper I am replicating \cite{Hsu2024} is not very clear, which makes the preprocessing method used not as effective. Therefore, I am currently working on implementing a few different methods for data preprocessing, to see what works best.
|
|
||||||
|
|
||||||
\section{Angle of Arrival Estimation}
|
|
||||||
|
|
||||||
One of the main techniques I was planning to use was angle of arrival estimation, using the MUSIC algorithm. I have implemented this per the original work plan.
|
|
||||||
|
|
||||||
This allows me to estimate a 2-dimensional probability density function, estimating the response of the signal at a specific angle and time of flight. In theory, plotting a heatmap of this on a polar plane should give a rough indication of where a person is in the room.
|
|
||||||
|
|
||||||
I am currently in the process of evaluating the results from this, to see how well it performs under different conditions.
|
|
||||||
|
|
||||||
\section{Dataset Collection}
|
|
||||||
|
|
||||||
Over the weekend 1st-2nd February, I collected data using multiple different configurations, with both the empty room, with one or more people walking around the room (after going through the necessary ethics review). Using this data instead of the realtime data allows for more repeatable experiments, which will allow me to improve my method more effectively, and hence was done earlier than scheduled in the original work plan.
|
|
||||||
|
|
||||||
\section{Difficulties}
|
|
||||||
|
|
||||||
One of the main problems that I had which caused me to lose a lot of time was some hardware issues. The CPU in the main computer I was using had a hardware issue, causing my program to crash within a few seconds of running (but did not show any significant problems with other software running). This was quite difficult to diagnose, since I immediately assumed that this was a bug in my code. After a lot of debugging, running the program on different machines and environments, I realised that this is a hardware issue.
|
|
||||||
|
|
||||||
Even after diagnosing this, I had to wait for a long time (~3 weeks) for Intel to send me a replacement CPU, which caused me to not be able to do as much work over the Christmas vacation as I would have liked.
|
|
||||||
|
|
||||||
\section{Next Steps}
|
|
||||||
|
|
||||||
Since there have been many changes to the original work plan, causing me to both be ahead and behind simultaneously, here is a new work plan which I am planning to follow until the end of my project:
|
|
||||||
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Evaluate different data preprocessing methods, and see which one works best and when. This will lead to improved accuracy in the heatmaps, so that the hotspots are more obvious. I aim to complete this by 21st February.
|
|
||||||
\item Automatically detect peaks/hotspots in the heatmap, to get an automatic counter for the number of people in the room (and evaluate prediction accuracy against the collected dataset). This was originally planned for January, but I believe it can now be done by 7th March.
|
|
||||||
\item Train a neural network to detect the exact position and poses of the people in the scene. I already have everything I need for this, so I believe it can be done by 21st March. This will be done simultaneously with writing a draft of the dissertation.
|
|
||||||
\item Evaluate whether using MIMO (Multiple Input, Multiple Output) will improve the accuracy of the system, and/or allow us to use less receiving antenna. This will also be done simultaneously with writing the dissertation, and should be completed by 4th April.
|
|
||||||
\item Finalise dissertation. This should give sufficient amount of time for editing, as well as for any unexpected issues that may arise, or for any other extensions I would like to add. This will likely be continuous until the submission deadline.
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
|
|
||||||
\printbibliography
|
|
||||||
|
|
||||||
|
|
||||||
\end{document}
|
|
||||||
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
@INPROCEEDINGS{Hsu2024,
|
|
||||||
author={Hsu, Ting-Wei and Hsieh, Hung-Yun},
|
|
||||||
booktitle={ICC 2024 - IEEE International Conference on Communications},
|
|
||||||
title={Robust Multi-User Pose Estimation Based on Spatial and Temporal Features from WiFi CSI},
|
|
||||||
year={2024},
|
|
||||||
volume={},
|
|
||||||
number={},
|
|
||||||
pages={1600-1605},
|
|
||||||
keywords={Heating systems;Doppler shift;Time-frequency analysis;Pose estimation;Feature extraction;Robustness;Frequency estimation},
|
|
||||||
doi={10.1109/ICC51166.2024.10623053}}
|
|
||||||
|
|
||||||
|
|
||||||
@electronic{feitcsi:project,
|
|
||||||
author = {Hutar, Miroslav and Brida, Peter and Machaj, Juraj},
|
|
||||||
title = {FeitCSI, the 802.11 CSI tool},
|
|
||||||
url = {https://feitcsi.kuskosoft.com},
|
|
||||||
year = {2023}
|
|
||||||
}
|
|
||||||
@ -4,28 +4,16 @@ version = "0.1.0"
|
|||||||
description = "Use Wi-Fi Channel State Information to locate human presence"
|
description = "Use Wi-Fi Channel State Information to locate human presence"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"numpy>=2.0.0",
|
"flask",
|
||||||
|
"numpy",
|
||||||
|
"flask-sock",
|
||||||
"pytest",
|
"pytest",
|
||||||
"matplotlib",
|
"matplotlib",
|
||||||
"scipy",
|
"scipy",
|
||||||
"scipy-stubs",
|
"scipy-stubs",
|
||||||
"typer",
|
"typer"
|
||||||
"h5py>=3.12.1",
|
|
||||||
"pyyaml>=6.0.2",
|
|
||||||
"pydantic>=2.10.6",
|
|
||||||
"torch",
|
|
||||||
"grpcio>=1.70.0",
|
|
||||||
"requests>=2.32.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools", "wheel", "grpcio-tools"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
where-fi = "where_fi.cli:cli"
|
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
@ -42,19 +30,3 @@ reportMissingTypeStubs = "warning"
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
python_files = "*.py"
|
python_files = "*.py"
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
package = true
|
|
||||||
|
|
||||||
[tool.setuptools]
|
|
||||||
packages = ["where_fi"]
|
|
||||||
include-package-data = true
|
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"grpc-stubs>=1.53.0.5",
|
|
||||||
"grpcio-tools>=1.70.0",
|
|
||||||
"matplotlib-stubs>=0.1.0",
|
|
||||||
"protoletariat>=3.3.9",
|
|
||||||
"types-requests>=2.32.0.20250328",
|
|
||||||
]
|
|
||||||
|
|||||||
@ -2,10 +2,11 @@ import logging
|
|||||||
|
|
||||||
from . import cli
|
from . import cli
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s %(name)-50s %(levelname)-8s %(message)s",
|
format="%(asctime)s %(name)-40s %(levelname)-8s %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli.cli()
|
cli.app()
|
||||||
48
src/cli.py
Normal file
48
src/cli.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import logging
|
||||||
|
import multiprocessing as mp
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from . import config, visualise
|
||||||
|
from .collection import ingest
|
||||||
|
from .processing.aoa import AoA
|
||||||
|
from .processing.preprocess import Preprocessor
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def antennas() -> None:
|
||||||
|
"""Utility to help determine the order in which antennas are plugged in
|
||||||
|
|
||||||
|
Once the script is running, unplug and replug antennas from left to right, to get
|
||||||
|
the correct order. Every time an antenna is unplugged and replugged, the script will
|
||||||
|
print the antenna identifier. When you are done, press Ctrl+C to stop the script and
|
||||||
|
get the final order.
|
||||||
|
"""
|
||||||
|
from .utils import antenna_order
|
||||||
|
|
||||||
|
antenna_order.main()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def heatmap() -> None:
|
||||||
|
preprocessor = Preprocessor()
|
||||||
|
aoa = AoA()
|
||||||
|
webapp_queue: "mp.Queue[AoA]" = mp.Queue(config.SAMPLE_RATE)
|
||||||
|
# Start webapp in background process
|
||||||
|
webapp = mp.Process(target=visualise.start, args=(webapp_queue,))
|
||||||
|
webapp.start()
|
||||||
|
|
||||||
|
def callback(antenna_data: npt.NDArray[np.complex128]) -> None:
|
||||||
|
logger.info(f"Got final CSI data with shape {antenna_data.shape}")
|
||||||
|
processed = preprocessor.preprocess(antenna_data)
|
||||||
|
# visualise.add_data(all_data, processed)
|
||||||
|
aoa.update(processed)
|
||||||
|
if not webapp_queue.full():
|
||||||
|
webapp_queue.put(aoa)
|
||||||
|
|
||||||
|
ingest.start_processing(callback)
|
||||||
@ -43,8 +43,8 @@ class CSIHeader:
|
|||||||
self.num_rx = data[46]
|
self.num_rx = data[46]
|
||||||
self.num_tx = data[47]
|
self.num_tx = data[47]
|
||||||
self.num_subcarriers = struct.unpack("I", data[52:56])[0]
|
self.num_subcarriers = struct.unpack("I", data[52:56])[0]
|
||||||
self.rssi1: int = struct.unpack("I", data[60:64])[0]
|
self.rssi1 = struct.unpack("I", data[60:64])[0]
|
||||||
self.rssi2: int = struct.unpack("I", data[64:68])[0]
|
self.rssi2 = struct.unpack("I", data[64:68])[0]
|
||||||
self.source_mac = struct.unpack("BBBBBB", data[68:74])
|
self.source_mac = struct.unpack("BBBBBB", data[68:74])
|
||||||
self.source_mac_string = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack(
|
self.source_mac_string = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack(
|
||||||
"BBBBBB", data[68:74]
|
"BBBBBB", data[68:74]
|
||||||
@ -93,14 +93,14 @@ class CSIHeader:
|
|||||||
|
|
||||||
class CSI:
|
class CSI:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parseCsiData(data: bytes, header: CSIHeader) -> npt.NDArray[np.complex64]:
|
def parseCsiData(data: bytes, header: CSIHeader) -> npt.NDArray[np.complex128]:
|
||||||
csi_matrix: npt.NDArray[np.complex64] = np.zeros(
|
csi_matrix: npt.NDArray[np.complex128] = np.zeros(
|
||||||
(
|
(
|
||||||
header.num_subcarriers,
|
header.num_subcarriers,
|
||||||
header.num_rx,
|
header.num_rx,
|
||||||
header.num_tx,
|
header.num_tx,
|
||||||
),
|
),
|
||||||
dtype=np.complex64,
|
dtype=np.complex128,
|
||||||
)
|
)
|
||||||
pos = 0
|
pos = 0
|
||||||
for j in range(header.num_rx):
|
for j in range(header.num_rx):
|
||||||
213
src/collection/ingest.py
Normal file
213
src/collection/ingest.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import logging
|
||||||
|
import multiprocessing as mp
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Callable, NamedTuple, NoReturn
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
from .. import config
|
||||||
|
from .csi_frame import CSI
|
||||||
|
|
||||||
|
Host = tuple[str, int]
|
||||||
|
CSICallback = Callable[[npt.NDArray[np.complex128]], None]
|
||||||
|
|
||||||
|
|
||||||
|
class FeitHost:
|
||||||
|
def __init__(self, host: Host, command: str) -> None:
|
||||||
|
self.command = command
|
||||||
|
self.host = host
|
||||||
|
self.logger = logging.getLogger(
|
||||||
|
f"{__name__}.{self.__class__.__name__}-{self.host[0]}"
|
||||||
|
)
|
||||||
|
self.checker = threading.Thread(target=self.check_continuous)
|
||||||
|
self.checker.start()
|
||||||
|
|
||||||
|
def check_connection(self) -> bool:
|
||||||
|
feitcsi_status = subprocess.run(
|
||||||
|
f"ssh root@{self.host[0]} pgrep feitcsi",
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
return feitcsi_status.returncode == 0
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self.server.connect(self.host)
|
||||||
|
self.server.send(b"stop\n")
|
||||||
|
self.server.send(self.command.encode())
|
||||||
|
self.logger.info(f"Connected to {self.host}")
|
||||||
|
|
||||||
|
def check_continuous(self) -> NoReturn:
|
||||||
|
"""
|
||||||
|
Repeatedly check if the FeitCSI service is running
|
||||||
|
|
||||||
|
Because FeitCSI is using TCP, we will get no information if the service stops,
|
||||||
|
or the computer is not reachable. In order to make debugging easier, this checks
|
||||||
|
and logs continuously if the service is running.
|
||||||
|
"""
|
||||||
|
last_status = False
|
||||||
|
while True:
|
||||||
|
if not self.check_connection():
|
||||||
|
self.logger.error(f"FeitCSI is not running on {self.host[0]}")
|
||||||
|
last_status = False
|
||||||
|
else:
|
||||||
|
if not last_status:
|
||||||
|
self.connect()
|
||||||
|
last_status = True
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
class FeitTransmitter(FeitHost):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
command = (
|
||||||
|
f"feitcsi --frequency {config.CENTRAL_FREQUENCY_MHZ} "
|
||||||
|
f"--channel-width {config.CHANNEL_WIDTH} "
|
||||||
|
f"--format {config.FRAME_FORMAT} "
|
||||||
|
f"--mode inject -s 1 --verbose "
|
||||||
|
f"--inject-delay {1_000_000 // config.SAMPLE_RATE}"
|
||||||
|
)
|
||||||
|
super().__init__(config.INJECT_HOST, command)
|
||||||
|
|
||||||
|
|
||||||
|
class FeitReceiver(FeitHost):
|
||||||
|
def __init__(self, host: Host) -> None:
|
||||||
|
command = (
|
||||||
|
f"feitcsi --frequency {config.CENTRAL_FREQUENCY_MHZ} "
|
||||||
|
f"--channel-width {config.CHANNEL_WIDTH} "
|
||||||
|
f"--format {config.FRAME_FORMAT} "
|
||||||
|
f"--mode measure"
|
||||||
|
)
|
||||||
|
super().__init__(host, command)
|
||||||
|
|
||||||
|
def listen(self, queue: "mp.Queue[CSI]") -> NoReturn:
|
||||||
|
prev_time = datetime.now()
|
||||||
|
self.logger.info("Listening for CSI data")
|
||||||
|
while True:
|
||||||
|
while not hasattr(self, "server"):
|
||||||
|
time.sleep(0.1)
|
||||||
|
# This is the max size of a UDP packet. The size of the actual CSI
|
||||||
|
# packet will depend on the frame format and channel width, which
|
||||||
|
# changes the number of subcarriers
|
||||||
|
data = self.server.recv(65535)
|
||||||
|
try:
|
||||||
|
csidata = CSI(data)
|
||||||
|
self.logger.debug(
|
||||||
|
f"Received CSI data after {datetime.now() - prev_time}"
|
||||||
|
)
|
||||||
|
prev_time = datetime.now()
|
||||||
|
queue.put(csidata)
|
||||||
|
except struct.error:
|
||||||
|
self.logger.error("Failed to parse CSI data")
|
||||||
|
|
||||||
|
|
||||||
|
class CSIProcessor:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
receiver_connections: dict[Host, "mp.Queue[CSI]"],
|
||||||
|
) -> None:
|
||||||
|
self.pending_data: dict[Host, tuple[datetime, CSI]] = {}
|
||||||
|
self.pending_data_lock = mp.Lock()
|
||||||
|
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
||||||
|
self.last_processed = datetime.now()
|
||||||
|
self.connections = receiver_connections
|
||||||
|
|
||||||
|
def add_data(self, host: Host, data: CSI) -> None:
|
||||||
|
if (
|
||||||
|
host in self.pending_data
|
||||||
|
and self.last_processed < self.pending_data[host][0]
|
||||||
|
):
|
||||||
|
self.logger.warning(
|
||||||
|
f"Skipping data from {host} at {self.pending_data[host][0]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.pending_data_lock:
|
||||||
|
self.pending_data[host] = (datetime.now(), data)
|
||||||
|
|
||||||
|
def is_ready(self) -> bool:
|
||||||
|
for host in config.RECEIVE_HOSTS:
|
||||||
|
if (
|
||||||
|
host not in self.pending_data
|
||||||
|
or self.pending_data[host][0] <= self.last_processed
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def process_data(
|
||||||
|
self,
|
||||||
|
callback: CSICallback | None = None,
|
||||||
|
pre_merge_callback: Callable[[dict[Host, CSI]], None] | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.last_processed = datetime.now()
|
||||||
|
if pre_merge_callback is not None:
|
||||||
|
pre_merge_callback(
|
||||||
|
{host: data[1] for host, data in self.pending_data.items()}
|
||||||
|
)
|
||||||
|
antenna_data = [
|
||||||
|
np.expand_dims(self.pending_data[ip][1].matrix[:, antenna], axis=2)
|
||||||
|
for ip, antenna in config.ANTENNA_ORDER
|
||||||
|
]
|
||||||
|
|
||||||
|
# We have data from all servers
|
||||||
|
all_data = np.concat(antenna_data, axis=1)
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback(all_data)
|
||||||
|
|
||||||
|
def process_forever(
|
||||||
|
self,
|
||||||
|
callback: CSICallback | None = None,
|
||||||
|
pre_merge_callback: Callable[[dict[Host, CSI]], None] | None = None,
|
||||||
|
) -> NoReturn:
|
||||||
|
while True:
|
||||||
|
for ip, queue in self.connections.items():
|
||||||
|
while not queue.empty():
|
||||||
|
self.add_data(ip, queue.get())
|
||||||
|
if self.is_ready():
|
||||||
|
self.process_data(callback, pre_merge_callback=pre_merge_callback)
|
||||||
|
else:
|
||||||
|
self.logger.debug("Not all data is ready")
|
||||||
|
time.sleep(0.0005)
|
||||||
|
|
||||||
|
|
||||||
|
class Receiver(NamedTuple):
|
||||||
|
ip: Host
|
||||||
|
receiver: FeitReceiver
|
||||||
|
queue: "mp.Queue[CSI]"
|
||||||
|
|
||||||
|
|
||||||
|
def start_processing(
|
||||||
|
csi_callback: CSICallback | None = None,
|
||||||
|
pre_merge_callback: Callable[[dict[Host, CSI]], None] | None = None,
|
||||||
|
) -> None:
|
||||||
|
receivers = [
|
||||||
|
Receiver(ip, FeitReceiver(ip), mp.Queue(config.SAMPLE_RATE))
|
||||||
|
for ip in config.RECEIVE_HOSTS
|
||||||
|
]
|
||||||
|
|
||||||
|
FeitTransmitter()
|
||||||
|
|
||||||
|
# Start injecting CSI frames
|
||||||
|
processor = CSIProcessor({r.ip: r.queue for r in receivers})
|
||||||
|
|
||||||
|
receiver_processes = [
|
||||||
|
threading.Thread(target=r.receiver.listen, args=(r.queue,)) for r in receivers
|
||||||
|
]
|
||||||
|
|
||||||
|
for proc in receiver_processes:
|
||||||
|
proc.start()
|
||||||
|
|
||||||
|
processing_thread = mp.Process(
|
||||||
|
target=processor.process_forever, args=(csi_callback, pre_merge_callback)
|
||||||
|
)
|
||||||
|
processing_thread.start()
|
||||||
|
try:
|
||||||
|
processing_thread.join()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return
|
||||||
36
src/config.py
Normal file
36
src/config.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
PREPROCESSING_SHORT_TERM_WINDOW_SIZE = 5
|
||||||
|
PREPROCESSING_LONG_TERM_ALPHA = 0.01
|
||||||
|
|
||||||
|
PREPROCESSING_BANDPASS_LOW_CUTOFF = 2
|
||||||
|
PREPROCESSING_BANDPASS_HIGH_CUTOFF = 40
|
||||||
|
|
||||||
|
AOA_SLIDING_WINDOW_SIZE = 40
|
||||||
|
|
||||||
|
RECEIVE_HOSTS = [("cfalas.com", 10001), ("cfalas.com", 10002)]
|
||||||
|
INJECT_HOST = ("cfalas.com", 10003)
|
||||||
|
|
||||||
|
ANTENNA_ORDER = [
|
||||||
|
(("cfalas.com", 10001), 0),
|
||||||
|
(("cfalas.com", 10002), 1),
|
||||||
|
(("cfalas.com", 10002), 0),
|
||||||
|
(("cfalas.com", 10001), 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
SAMPLE_RATE = 100 # Hz
|
||||||
|
|
||||||
|
EIGVAL_THRESHOLD = 10
|
||||||
|
|
||||||
|
|
||||||
|
# DELTA_F = 78_125 # Spacing between subcarriers in Hz
|
||||||
|
DELTA_F = 312_500 # Spacing between subcarriers in Hz
|
||||||
|
CENTRAL_FREQUENCY_MHZ = 6195
|
||||||
|
ANTENNA_SPACING = 0.0285 # 2.85 cm
|
||||||
|
CHANNEL_WIDTH = 20
|
||||||
|
FRAME_FORMAT = "HT"
|
||||||
|
|
||||||
|
|
||||||
|
CENTRAL_FREQUENCY_HZ = CENTRAL_FREQUENCY_MHZ * 1_000_000
|
||||||
|
C = 299_792_458 # m/s
|
||||||
|
|
||||||
|
VISUALISE_RAW = False
|
||||||
|
HEATMAP_FPS = 10
|
||||||
137
src/processing/aoa.py
Normal file
137
src/processing/aoa.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
from .. import config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AoA:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.historical_autocorr = np.array([])
|
||||||
|
self.N_subcarriers = -1
|
||||||
|
self.N_rx = -1
|
||||||
|
self.timestamp = datetime.now()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def smooth(self, data: npt.NDArray[np.complex128]) -> npt.NDArray[np.complex128]:
|
||||||
|
assert len(data.shape) == 3
|
||||||
|
|
||||||
|
M = data.shape[0] # Number of subcarriers
|
||||||
|
N = data.shape[1] # Number of RX antennas
|
||||||
|
T = data.shape[2] # Number of TX antennas
|
||||||
|
|
||||||
|
self.N_subcarriers = M
|
||||||
|
self.N_rx = N
|
||||||
|
|
||||||
|
logger.debug(f"Smoothing: Subcarriers: {M}, RX antennas: {N}, TX antennas: {T}")
|
||||||
|
|
||||||
|
# This only works with 1 TX antenna (i.e. no MIMO) - see #4 for more details
|
||||||
|
assert T == 1, "The current implementation only supports 1 TX antenna"
|
||||||
|
|
||||||
|
H_n = np.zeros((N, M // 2, M // 2 + 1), dtype=np.complex128)
|
||||||
|
|
||||||
|
for i in range(N):
|
||||||
|
for j in range(M // 2):
|
||||||
|
H_n[i, j] = data[j : j + M // 2 + 1, i, 0]
|
||||||
|
|
||||||
|
H_sm_rows = [np.hstack(H_n[i : i + N // 2 + 1]) for i in range(N // 2)]
|
||||||
|
H_sm = np.vstack(H_sm_rows)
|
||||||
|
|
||||||
|
logger.debug(f"Smoothed: {H_sm.shape}")
|
||||||
|
|
||||||
|
return H_sm
|
||||||
|
|
||||||
|
def update(self, data: npt.NDArray[np.complex128]) -> None:
|
||||||
|
self.timestamp = datetime.now()
|
||||||
|
H_sm = self.smooth(data)
|
||||||
|
|
||||||
|
auto_corr = np.matmul(H_sm, np.conj(H_sm).T)
|
||||||
|
# This matrix is by definition Hermitian.
|
||||||
|
# Therefore, all of its eigenvectors are orthogonal.
|
||||||
|
|
||||||
|
if self.historical_autocorr.size == 0:
|
||||||
|
self.historical_autocorr = np.expand_dims(auto_corr, 0)
|
||||||
|
else:
|
||||||
|
self.historical_autocorr = np.append(
|
||||||
|
self.historical_autocorr, np.expand_dims(auto_corr, 0), axis=0
|
||||||
|
)
|
||||||
|
|
||||||
|
WINDOW_SIZE = config.AOA_SLIDING_WINDOW_SIZE
|
||||||
|
if self.historical_autocorr.shape[0] > WINDOW_SIZE:
|
||||||
|
self.historical_autocorr = self.historical_autocorr[-WINDOW_SIZE:]
|
||||||
|
|
||||||
|
# Is the moving average also Hermitian?
|
||||||
|
R = np.mean(self.historical_autocorr, axis=0)
|
||||||
|
|
||||||
|
# The smallest eigenvectors span the noise subspace,
|
||||||
|
# and the largest span the signal subspace.
|
||||||
|
eigvals, eigvecs = np.linalg.eigh(R)
|
||||||
|
self.E_n = eigvecs[:, np.abs(eigvals) < config.EIGVAL_THRESHOLD]
|
||||||
|
|
||||||
|
omega_base = np.exp(-2j * np.pi * config.DELTA_F)
|
||||||
|
phi_base = np.exp(
|
||||||
|
2j * np.pi * config.CENTRAL_FREQUENCY_HZ * config.ANTENNA_SPACING / config.C
|
||||||
|
)
|
||||||
|
|
||||||
|
def steering_vector(
|
||||||
|
self, theta: float, tof: float
|
||||||
|
) -> npt.NDArray[np.complexfloating]:
|
||||||
|
omega_t: npt.NDArray[np.complex128] = np.exp(-2j * np.pi * config.DELTA_F * tof)
|
||||||
|
phi_theta: npt.NDArray[np.complex128] = np.exp(
|
||||||
|
2j
|
||||||
|
* np.pi
|
||||||
|
* config.CENTRAL_FREQUENCY_HZ
|
||||||
|
* config.ANTENNA_SPACING
|
||||||
|
* (1 - np.cos(theta))
|
||||||
|
/ config.C
|
||||||
|
)
|
||||||
|
|
||||||
|
omega_t = np.expand_dims(omega_t, axis=-1)
|
||||||
|
phi_theta = np.expand_dims(phi_theta, axis=-1)
|
||||||
|
|
||||||
|
antenna_v = omega_t ** np.arange(self.N_subcarriers // 2)
|
||||||
|
phis = phi_theta ** np.arange(self.N_rx // 2)
|
||||||
|
antenna_v = np.expand_dims(antenna_v, axis=-1)
|
||||||
|
steering = antenna_v * phis
|
||||||
|
return steering.T.reshape(-1)
|
||||||
|
|
||||||
|
def evaluate(self, theta: float, tof: float) -> float:
|
||||||
|
try:
|
||||||
|
steering = self.steering_vector(theta, tof)
|
||||||
|
steering_h = np.conj(steering).T
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return 0
|
||||||
|
E_n = self.E_n
|
||||||
|
E_n_H = np.conj(E_n).T
|
||||||
|
c = 1 / (0.001 + (steering_h @ E_n @ E_n_H @ steering))
|
||||||
|
return np.abs(c.real)
|
||||||
|
|
||||||
|
|
||||||
|
def test_smoothing() -> None:
|
||||||
|
row, col = np.indices((4, 2))
|
||||||
|
data = row + 1j * col
|
||||||
|
aoa = AoA()
|
||||||
|
smoothed = aoa.smooth(data)
|
||||||
|
H_0 = np.array([[0 + 0j, 0 + 1j, 0 + 2j], [0 + 1j, 0 + 2j, 0 + 3j]])
|
||||||
|
H_01 = np.vstack([H_0, H_0 + 1])
|
||||||
|
H_12 = np.vstack([H_0 + 1, H_0 + 2])
|
||||||
|
expected = np.hstack([H_01, H_12])
|
||||||
|
print(expected)
|
||||||
|
assert np.allclose(smoothed, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_steering_vector() -> None:
|
||||||
|
aoa = AoA()
|
||||||
|
aoa.N_subcarriers = 10
|
||||||
|
aoa.N_rx = 2
|
||||||
|
print(aoa.omega_base)
|
||||||
|
print(aoa.phi_base)
|
||||||
|
tau = 1
|
||||||
|
theta = 0
|
||||||
|
print(aoa.steering_vector(theta, tau))
|
||||||
|
assert False
|
||||||
72
src/processing/preprocess.py
Normal file
72
src/processing/preprocess.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import logging
|
||||||
|
from queue import Queue
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
from scipy.signal import butter, correlate, sosfilt, sosfilt_zi
|
||||||
|
|
||||||
|
from .. import config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
np.seterr(invalid="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
class Preprocessor:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.prev_entries: Queue[npt.NDArray[np.complex128]] = Queue(maxsize=100)
|
||||||
|
self.short_term_avg = np.zeros((1,), dtype=np.complex128)
|
||||||
|
self.long_term_avg = np.zeros((1,), dtype=np.complex128)
|
||||||
|
self.filter = butter(
|
||||||
|
5,
|
||||||
|
[
|
||||||
|
config.PREPROCESSING_BANDPASS_LOW_CUTOFF,
|
||||||
|
config.PREPROCESSING_BANDPASS_HIGH_CUTOFF,
|
||||||
|
],
|
||||||
|
fs=config.SAMPLE_RATE,
|
||||||
|
btype="band",
|
||||||
|
output="sos",
|
||||||
|
)
|
||||||
|
|
||||||
|
def preprocess(self, h: npt.NDArray[np.complex128]) -> npt.NDArray[np.complex128]:
|
||||||
|
# CSI data is not available for pilot subcarriers.
|
||||||
|
h_hat = np.where(
|
||||||
|
np.expand_dims(h[:, 0, 0] == 0, axis=(1, 2)),
|
||||||
|
correlate(h, [[[1 / 2]], [[0]], [[1 / 2]]], mode="same"),
|
||||||
|
h,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip every other subcarrier
|
||||||
|
# h_hat = h_hat[::2, :, :]
|
||||||
|
# logger.info(f"CSI shape: {h_hat.shape}")
|
||||||
|
|
||||||
|
h_hat = np.multiply(h_hat, h_hat.conj() / abs(h_hat.conj()))
|
||||||
|
h_hat = np.nan_to_num(h_hat)
|
||||||
|
|
||||||
|
# h_hat = correlate(h_hat, np.ones((3, 1, 1)) / 3)
|
||||||
|
h_hat = correlate(h_hat, [[[1 / 4]], [[1 / 2]], [[1 / 4]]], mode="valid")
|
||||||
|
|
||||||
|
# Assume that all csi matrices will have the same shape
|
||||||
|
if self.long_term_avg.shape != h_hat.shape:
|
||||||
|
self.long_term_avg = np.zeros(h_hat.shape, dtype=np.complex128)
|
||||||
|
|
||||||
|
self.long_term_avg = (
|
||||||
|
self.long_term_avg * (1 - config.PREPROCESSING_LONG_TERM_ALPHA)
|
||||||
|
+ h_hat * config.PREPROCESSING_LONG_TERM_ALPHA
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove long term average, to remove static paths
|
||||||
|
h_hat -= self.long_term_avg
|
||||||
|
|
||||||
|
# Apply bandpass filter to remove low and high frequency noise
|
||||||
|
if not hasattr(self, "filter_zi"):
|
||||||
|
self.filter_zi = (
|
||||||
|
np.expand_dims(sosfilt_zi(self.filter), axis=(-1, -2, -3)) * h_hat
|
||||||
|
)
|
||||||
|
|
||||||
|
h_hat_filt, self.filter_zi = sosfilt(
|
||||||
|
self.filter, [h_hat], zi=self.filter_zi, axis=0
|
||||||
|
)
|
||||||
|
|
||||||
|
return h_hat_filt[0]
|
||||||
56
src/utils/antenna_order.py
Normal file
56
src/utils/antenna_order.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from ..collection import ingest
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
AntennaIdentifier = tuple[ingest.Host, int]
|
||||||
|
order: list[AntennaIdentifier] = []
|
||||||
|
prev_unplugged: set[AntennaIdentifier] = set()
|
||||||
|
antenna_average: dict[AntennaIdentifier, float] = {}
|
||||||
|
|
||||||
|
RSSI_THRESHOLD = 8
|
||||||
|
|
||||||
|
|
||||||
|
def callback(antenna_data: dict[ingest.Host, ingest.CSI]) -> None:
|
||||||
|
global prev_unplugged
|
||||||
|
global antenna_average
|
||||||
|
global order
|
||||||
|
unplugged: set[tuple[ingest.Host, int]] = set()
|
||||||
|
logger.debug(
|
||||||
|
"Antenna RSSI: "
|
||||||
|
+ str(
|
||||||
|
{
|
||||||
|
host: (csi.header.rssi1, csi.header.rssi2)
|
||||||
|
for host, csi in antenna_data.items()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for host, csi in antenna_data.items():
|
||||||
|
antenna_average[(host, 0)] = (
|
||||||
|
antenna_average.get((host, 0), csi.header.rssi1) * 0.9
|
||||||
|
+ csi.header.rssi1 * 0.1
|
||||||
|
)
|
||||||
|
antenna_average[(host, 1)] = (
|
||||||
|
antenna_average.get((host, 1), csi.header.rssi2) * 0.9
|
||||||
|
+ csi.header.rssi2 * 0.1
|
||||||
|
)
|
||||||
|
if csi.header.rssi1 > antenna_average[(host, 0)] + RSSI_THRESHOLD:
|
||||||
|
unplugged.add((host, 0))
|
||||||
|
if csi.header.rssi2 > antenna_average[(host, 1)] + RSSI_THRESHOLD:
|
||||||
|
unplugged.add((host, 1))
|
||||||
|
if prev_unplugged != unplugged:
|
||||||
|
if len(prev_unplugged) > len(unplugged):
|
||||||
|
logger.info(f"Antenna plugged in: {prev_unplugged - unplugged}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Antenna unplugged: {unplugged - prev_unplugged}")
|
||||||
|
diff = list(unplugged - prev_unplugged)
|
||||||
|
if len(diff) == 1 and diff[0] not in order:
|
||||||
|
host, antenna = diff.pop()
|
||||||
|
order.append((host, antenna))
|
||||||
|
logger.info(f"Current order: {order}")
|
||||||
|
prev_unplugged = unplugged
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ingest.start_processing(pre_merge_callback=callback)
|
||||||
148
src/visualise/__init__.py
Normal file
148
src/visualise/__init__.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
from flask import Flask, render_template, Response, request
|
||||||
|
from flask_sock import Sock
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
from simple_websocket import Server
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import multiprocessing as mp
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..processing.aoa import AoA
|
||||||
|
from .. import config
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
|
||||||
|
matplotlib.use("agg")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
sock = Sock(app)
|
||||||
|
aoa_queue = None
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
data: npt.NDArray[np.complex128] = np.array([], dtype=complex)
|
||||||
|
aoa: AoA = AoA()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/preprocessed")
|
||||||
|
def preprocessed():
|
||||||
|
return render_template("preprocessed.html")
|
||||||
|
|
||||||
|
|
||||||
|
Subscriber = Server
|
||||||
|
subscriber_settings: dict[Subscriber, tuple[int, int, int]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@sock.route("/data")
|
||||||
|
def get_data(sock: Subscriber):
|
||||||
|
while True:
|
||||||
|
msg = sock.receive()
|
||||||
|
if len(msg.split()) != 3:
|
||||||
|
break
|
||||||
|
subcarrier, rx, tx = map(int, msg.split())
|
||||||
|
subscriber_settings[sock] = (subcarrier, rx, tx)
|
||||||
|
|
||||||
|
|
||||||
|
def add_data(
|
||||||
|
raw_data: npt.NDArray[np.complex128], new_data: npt.NDArray[np.complex128]
|
||||||
|
):
|
||||||
|
if config.VISUALISE_RAW:
|
||||||
|
magn = np.abs(raw_data)
|
||||||
|
phase = np.angle(raw_data)
|
||||||
|
|
||||||
|
new_mag = np.abs(new_data)
|
||||||
|
new_phase = np.angle(new_data)
|
||||||
|
|
||||||
|
fig, axs = plt.subplots(2, 2)
|
||||||
|
axs[0, 0].plot(magn[:, 0, 0], c="b")
|
||||||
|
axs[0, 0].plot(magn[:, 1, 0], c="orange")
|
||||||
|
axs[1, 0].plot(new_mag[:, 0, 0], c="b")
|
||||||
|
axs[1, 0].plot(new_mag[:, 1, 0], c="orange")
|
||||||
|
axs[0, 1].plot(phase[:, 0, 0], c="b")
|
||||||
|
axs[0, 1].plot(phase[:, 1, 0], c="orange")
|
||||||
|
axs[1, 1].plot(new_phase[:, 0, 0], c="b")
|
||||||
|
axs[1, 1].plot(new_phase[:, 1, 0], c="orange")
|
||||||
|
fig.savefig("/tmp/plot.png")
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
global data
|
||||||
|
if data.size == 0:
|
||||||
|
data = np.expand_dims(new_data, axis=0)
|
||||||
|
else:
|
||||||
|
data = np.concat([data, np.expand_dims(new_data, axis=0)], axis=0)
|
||||||
|
|
||||||
|
# Only keep latest 100 entries
|
||||||
|
if data.shape[0] > 100:
|
||||||
|
data = data[-100:]
|
||||||
|
to_remove: list[Subscriber] = []
|
||||||
|
for subscriber in subscriber_settings:
|
||||||
|
try:
|
||||||
|
subcarrier, rx, tx = subscriber_settings[subscriber]
|
||||||
|
subscriber.send(new_data.real[subcarrier, rx, tx])
|
||||||
|
except Exception as e:
|
||||||
|
to_remove.append(subscriber)
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
for subscriber in to_remove:
|
||||||
|
del subscriber_settings[subscriber]
|
||||||
|
|
||||||
|
|
||||||
|
def make_heatmap(aoa: AoA, max_tof: float):
|
||||||
|
logger.info(f"Making heatmap with aoa of {aoa.timestamp}")
|
||||||
|
fig = plt.figure()
|
||||||
|
ax = fig.add_axes([0, 0, 1, 1], polar=True)
|
||||||
|
r = np.linspace(0, max_tof, 100) # Radius values
|
||||||
|
theta = np.linspace(0, np.pi, 50) # Angle values
|
||||||
|
R, Theta = np.meshgrid(r, theta) # Create a 2D grid of r and theta
|
||||||
|
|
||||||
|
# Compute the function values
|
||||||
|
Z = np.log(np.vectorize(aoa.evaluate)(Theta, R))
|
||||||
|
|
||||||
|
ax.pcolormesh(Theta, R, Z, edgecolors="face")
|
||||||
|
buf = io.BytesIO()
|
||||||
|
fig.savefig(buf, format="jpeg")
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
buf.seek(0)
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
def gather_aoa(max_tof: float):
|
||||||
|
assert aoa_queue is not None
|
||||||
|
|
||||||
|
prev_frame = datetime.now()
|
||||||
|
while True:
|
||||||
|
while (datetime.now() - prev_frame).total_seconds() < 1 / config.HEATMAP_FPS:
|
||||||
|
time.sleep(0.01)
|
||||||
|
while not aoa_queue.empty():
|
||||||
|
logger.debug("Receiving from aoa pipe")
|
||||||
|
aoa = aoa_queue.get()
|
||||||
|
prev_frame = datetime.now()
|
||||||
|
logger.debug(f"Generating heatmap of time {aoa.timestamp}")
|
||||||
|
buf = make_heatmap(aoa, max_tof)
|
||||||
|
yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + buf.read() + b"\r\n")
|
||||||
|
buf.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/aoa_tof")
|
||||||
|
def aoa_tof():
|
||||||
|
max_tof_str = request.args.get("max_tof")
|
||||||
|
try:
|
||||||
|
max_tof = float(max_tof_str)
|
||||||
|
except Exception:
|
||||||
|
max_tof = 5e-8
|
||||||
|
return Response(
|
||||||
|
gather_aoa(max_tof), mimetype="multipart/x-mixed-replace; boundary=frame"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def start(conn: "mp.Queue[AoA]"):
|
||||||
|
global aoa_queue
|
||||||
|
|
||||||
|
aoa_queue = conn
|
||||||
|
app.run(debug=True, use_reloader=False, host="0.0.0.0")
|
||||||
42
src/visualise/static/plot.js
Normal file
42
src/visualise/static/plot.js
Normal file
@ -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",
|
||||||
|
mode: "lines+markers", // Line + markers
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout,
|
||||||
|
);
|
||||||
|
|
||||||
|
const socket = new WebSocket("ws://" + location.host + "/data");
|
||||||
|
socket.onopen = function () {
|
||||||
|
console.log("Connected to the server");
|
||||||
|
socket.send("0 0 0");
|
||||||
|
};
|
||||||
|
socket.addEventListener("message", function (msg) {
|
||||||
|
Plotly.extendTraces(
|
||||||
|
plotElement,
|
||||||
|
{
|
||||||
|
x: [[msg.timeStamp]],
|
||||||
|
y: [[msg.data]],
|
||||||
|
},
|
||||||
|
[0],
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function changeSubcarrier() {
|
||||||
|
const subcarrier = document.getElementById("subcarrier").value;
|
||||||
|
socket.send(subcarrier + " 0 0");
|
||||||
|
}
|
||||||
15
src/visualise/templates/preprocessed.html
Normal file
15
src/visualise/templates/preprocessed.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Preprocessing</h1>
|
||||||
|
<div id="plot"></div>
|
||||||
|
<script src="{{url_for('static', filename='plot.js')}}"></script>
|
||||||
|
<input type="number" id="subcarrier" class="form-control" placeholder="Choose subcarrier" onchange="changeSubcarrier()" value="0">
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
@ -1,10 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.2.0/p5.min.js"></script>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script src="sketch.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
let target = [150, 30];
|
|
||||||
let Tx = [300, 350];
|
|
||||||
let Rx = [50, 300];
|
|
||||||
let tx_target_distance;
|
|
||||||
let rx_target_distance;
|
|
||||||
|
|
||||||
let v_target = [6, 5];
|
|
||||||
let v_target_vec;
|
|
||||||
let target_speed;
|
|
||||||
let framerate = 60;
|
|
||||||
|
|
||||||
function setup() {
|
|
||||||
createCanvas(400, 400);
|
|
||||||
frameRate(framerate);
|
|
||||||
target_speed = sqrt(sq(v_target[0]) + sq(v_target[1]));
|
|
||||||
v_target_vec = createVector(v_target[0], v_target[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let toTarget = [];
|
|
||||||
let toRx = [];
|
|
||||||
|
|
||||||
let f = 2; // Hz
|
|
||||||
let c = 50; // pixels/sec
|
|
||||||
let lambda = c / f;
|
|
||||||
|
|
||||||
let dt = 1 / framerate;
|
|
||||||
let distance_per_tick = (c / f) * dt;
|
|
||||||
let time = 0;
|
|
||||||
function draw() {
|
|
||||||
time += dt;
|
|
||||||
|
|
||||||
target[0] += v_target[0] * dt;
|
|
||||||
target[1] += v_target[1] * dt;
|
|
||||||
|
|
||||||
tx_target_distance = dist(target[0], target[1], Tx[0], Tx[1]);
|
|
||||||
rx_target_distance = dist(target[0], target[1], Rx[0], Rx[1]);
|
|
||||||
|
|
||||||
background(220);
|
|
||||||
strokeWeight(10);
|
|
||||||
stroke("black");
|
|
||||||
noFill();
|
|
||||||
point(target[0], target[1]);
|
|
||||||
point(Tx[0], Tx[1]);
|
|
||||||
point(Rx[0], Rx[1]);
|
|
||||||
|
|
||||||
if (time - int(time) < dt) {
|
|
||||||
toTarget.push(0);
|
|
||||||
}
|
|
||||||
strokeWeight(1);
|
|
||||||
stroke("blue");
|
|
||||||
for (let x in toTarget) {
|
|
||||||
toTarget[x] += distance_per_tick;
|
|
||||||
circle(Tx[0], Tx[1], 2 * toTarget[x]);
|
|
||||||
if (toTarget[x] > tx_target_distance) {
|
|
||||||
toTarget.shift();
|
|
||||||
toRx.push([target[0], target[1], 0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stroke("green");
|
|
||||||
for (let x of toRx) {
|
|
||||||
circle(x[0], x[1], 2 * x[2]);
|
|
||||||
x[2] += distance_per_tick;
|
|
||||||
if (dist(x[0], x[1], Rx[0], Rx[1]) < x[2]) {
|
|
||||||
toRx.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stroke("red");
|
|
||||||
line(
|
|
||||||
target[0],
|
|
||||||
target[1],
|
|
||||||
target[0] + 100 * v_target[0],
|
|
||||||
target[1] + 100 * v_target[1],
|
|
||||||
);
|
|
||||||
line(target[0], target[1], Rx[0], Rx[1]);
|
|
||||||
line(target[0], target[1], Tx[0], Tx[1]);
|
|
||||||
|
|
||||||
let phi_T = v_target_vec.angleBetween(
|
|
||||||
createVector(Tx[0] - target[0], Tx[1] - target[1]),
|
|
||||||
);
|
|
||||||
let phi_R = v_target_vec.angleBetween(
|
|
||||||
createVector(Rx[0] - target[0], Rx[1] - target[1]),
|
|
||||||
);
|
|
||||||
let psi = target_speed * (cos(phi_R) + cos(phi_T));
|
|
||||||
let apparent_freq = psi / lambda;
|
|
||||||
print(apparent_freq);
|
|
||||||
}
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
import logging
|
|
||||||
import multiprocessing as mp
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Any, Callable, NamedTuple
|
|
||||||
|
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from where_fi.collection import CSIMatrix, NoopCSIProducer, ingest
|
|
||||||
from where_fi.collection.csi_frame import CSI
|
|
||||||
from where_fi.collection.protocols import CSIProducer, MergedCSI
|
|
||||||
from where_fi.config import config
|
|
||||||
from where_fi.processing.preprocess import Preprocessor
|
|
||||||
from where_fi.visualise import server as visualise
|
|
||||||
from where_fi.visualise.server import figures
|
|
||||||
|
|
||||||
|
|
||||||
class Receiver(NamedTuple):
|
|
||||||
ip: ingest.Host
|
|
||||||
receiver: ingest.FeitReceiver
|
|
||||||
thread: threading.Thread
|
|
||||||
|
|
||||||
|
|
||||||
class Scheduler:
|
|
||||||
"""
|
|
||||||
A simple scheduler that runs a function at a given rate.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, rate: float, func: Callable[[], None]) -> None:
|
|
||||||
self.rate = rate
|
|
||||||
self.func = func
|
|
||||||
self.active = True
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
"""
|
|
||||||
Run the scheduler in a loop, calling the function at the given rate.
|
|
||||||
"""
|
|
||||||
while self.active:
|
|
||||||
self.func()
|
|
||||||
time.sleep(1 / self.rate)
|
|
||||||
|
|
||||||
def start(self) -> None:
|
|
||||||
"""
|
|
||||||
Start the scheduler in a separate thread.
|
|
||||||
"""
|
|
||||||
self.thread = threading.Thread(target=self.run)
|
|
||||||
self.thread.start()
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""
|
|
||||||
Stop the scheduler.
|
|
||||||
"""
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
|
|
||||||
class CSIApplication:
|
|
||||||
"""
|
|
||||||
A high-level application that manages CSI data ingestion and processing.
|
|
||||||
|
|
||||||
This allows a simple interface to be used for the main CLI logic, without having to
|
|
||||||
deal with the threads and queues directly.
|
|
||||||
|
|
||||||
It can be used using decortors to register callbacks for different stages of the
|
|
||||||
processing pipeline:
|
|
||||||
|
|
||||||
- `on_pre_merge`: Called with the raw CSI data (including headers) from each
|
|
||||||
receiver, before it is merged into a single matrix.
|
|
||||||
- `on_sample`: Called once per sample with the merged CSI data.
|
|
||||||
- `on_process`: Called at the processing sample rate, without any data (this is
|
|
||||||
mostly used as a scheduler).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, visualise_raw: bool = False) -> None:
|
|
||||||
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
||||||
|
|
||||||
self.producer = NoopCSIProducer()
|
|
||||||
|
|
||||||
self.webapp_queue: "mp.Queue[visualise.VisualiserData]" = mp.Queue(
|
|
||||||
config.processing_sample_rate
|
|
||||||
)
|
|
||||||
self.webapp = visualise.Webapp()
|
|
||||||
self.webapp_thread = threading.Thread(
|
|
||||||
target=self.webapp.start, args=(self.webapp_queue,)
|
|
||||||
)
|
|
||||||
self.visualise_raw = visualise_raw
|
|
||||||
|
|
||||||
self.pre_merge_callback = None
|
|
||||||
self.raw_csi_callback = None
|
|
||||||
self.preprocessed_csi_callback = None
|
|
||||||
self.processing_callback = None
|
|
||||||
|
|
||||||
self.preprocessor = Preprocessor()
|
|
||||||
self.custom_figures: dict[
|
|
||||||
visualise.figures.FigureId, visualise.figures.SpecificFigure
|
|
||||||
] = {}
|
|
||||||
|
|
||||||
def set_producer(self, producer: CSIProducer) -> None:
|
|
||||||
"""
|
|
||||||
Set the producer for the application. This is used to change the data source
|
|
||||||
at runtime.
|
|
||||||
"""
|
|
||||||
self.producer = producer
|
|
||||||
|
|
||||||
def register_figure(
|
|
||||||
self,
|
|
||||||
figure_id: visualise.figures.FigureId,
|
|
||||||
figure: visualise.figures.SpecificFigure,
|
|
||||||
) -> None:
|
|
||||||
self.custom_figures[figure_id] = figure
|
|
||||||
figures.all_figures[figure_id] = figure
|
|
||||||
|
|
||||||
def visualise_data(
|
|
||||||
self, data: npt.NDArray[Any], dtype: visualise.figures.FigureId
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Update a visualisation with the given data. The available visualisations are as
|
|
||||||
per visualise.server.all_figures.
|
|
||||||
"""
|
|
||||||
self.webapp_queue.put(visualise.VisualiserData(data, dtype))
|
|
||||||
|
|
||||||
def on_raw(self, func: Callable[[CSIMatrix], None]) -> Callable[[CSIMatrix], None]:
|
|
||||||
"""
|
|
||||||
Decorator to register a callback for the sample preprocessing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(data: CSIMatrix) -> None:
|
|
||||||
func(data)
|
|
||||||
|
|
||||||
self.raw_csi_callback = decorator
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def on_sample(
|
|
||||||
self, func: Callable[[CSIMatrix], None]
|
|
||||||
) -> Callable[[CSIMatrix], None]:
|
|
||||||
"""
|
|
||||||
Decorator to register a callback for the sample preprocessing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(data: CSIMatrix) -> None:
|
|
||||||
func(data)
|
|
||||||
|
|
||||||
self.preprocessed_csi_callback = decorator
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def on_pre_merge(
|
|
||||||
self, func: Callable[[dict[ingest.Host, CSI]], None]
|
|
||||||
) -> Callable[[dict[ingest.Host, CSI]], None]:
|
|
||||||
"""
|
|
||||||
Decorator to register a callback for the samples before merging.
|
|
||||||
"""
|
|
||||||
self.pre_merge_callback = func
|
|
||||||
return func
|
|
||||||
|
|
||||||
def on_process(
|
|
||||||
self, func: Callable[[CSIMatrix], None]
|
|
||||||
) -> Callable[[CSIMatrix], None]:
|
|
||||||
"""
|
|
||||||
Decorator to register a callback for the sample processing.
|
|
||||||
"""
|
|
||||||
self.processing_callback = func
|
|
||||||
return func
|
|
||||||
|
|
||||||
def process_sample(self, sample: MergedCSI) -> None:
|
|
||||||
"""Data pipeline for processing a sample.
|
|
||||||
This is the entry point for a sample being received from the producer. Depending
|
|
||||||
on the callbacks which are registered, this will call the appropriate functions
|
|
||||||
to pre-process and visualise the sample.
|
|
||||||
"""
|
|
||||||
if self.visualise_raw:
|
|
||||||
self.visualise_data(sample.matrix, visualise.figures.Figure.RAW_CSI)
|
|
||||||
if self.raw_csi_callback is not None:
|
|
||||||
self.raw_csi_callback(sample.matrix)
|
|
||||||
if self.pre_merge_callback is not None:
|
|
||||||
self.pre_merge_callback(sample.frames)
|
|
||||||
processed = self.preprocessor.preprocess(
|
|
||||||
sample.matrix, sample.frames, visualiser=self.visualise_data
|
|
||||||
)
|
|
||||||
# if self.visualise_raw:
|
|
||||||
# self.visualise_data(processed, visualise.figures.Figure.PROCESSED_CSI)
|
|
||||||
if self.preprocessed_csi_callback is not None:
|
|
||||||
self.preprocessed_csi_callback(processed)
|
|
||||||
|
|
||||||
def listen(self) -> None:
|
|
||||||
"""
|
|
||||||
Listen for incoming data from the selected ingestor, and call the appropriate
|
|
||||||
callback.
|
|
||||||
"""
|
|
||||||
data_gen = self.producer()
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
sample = next(data_gen)
|
|
||||||
self.process_sample(sample)
|
|
||||||
except StopIteration:
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Error processing sample: {e}", exc_info=True)
|
|
||||||
break
|
|
||||||
|
|
||||||
def start(self) -> None:
|
|
||||||
# for receiver in self.receivers:
|
|
||||||
# receiver.thread.start()
|
|
||||||
self.buffer_thread = threading.Thread(
|
|
||||||
target=self.listen,
|
|
||||||
)
|
|
||||||
self.buffer_thread.start()
|
|
||||||
self.webapp_thread.start()
|
|
||||||
|
|
||||||
def get_proc_sample() -> None:
|
|
||||||
sample = self.preprocessor.last_sample
|
|
||||||
if self.visualise_raw and sample is not None:
|
|
||||||
self.visualise_data(
|
|
||||||
sample,
|
|
||||||
visualise.figures.Figure.PROCESSED_CSI,
|
|
||||||
)
|
|
||||||
if sample is not None and self.processing_callback is not None:
|
|
||||||
self.processing_callback(sample)
|
|
||||||
else:
|
|
||||||
self.logger.warning("No samples to process")
|
|
||||||
|
|
||||||
self.scheduler = Scheduler(config.processing_sample_rate, get_proc_sample)
|
|
||||||
self.scheduler.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.webapp_thread.join()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
self.logger.info("Stopping application")
|
|
||||||
|
|
||||||
self.producer.stop()
|
|
||||||
|
|
||||||
if hasattr(self, "scheduler"):
|
|
||||||
self.scheduler.stop()
|
|
||||||
self.scheduler.thread.join()
|
|
||||||
self.buffer_thread.join()
|
|
||||||
|
|
||||||
self.webapp.active = False
|
|
||||||
self.webapp_thread.join()
|
|
||||||
self.logger.info("Application stopped")
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import importlib.util
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
import torch
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from where_fi.application import CSIApplication
|
|
||||||
from where_fi.processing.aoa import AoA
|
|
||||||
|
|
||||||
from . import file, globals
|
|
||||||
|
|
||||||
cli = typer.Typer(callback=globals.main)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def antennas() -> None:
|
|
||||||
"""Utility to help determine the order in which antennas are plugged in
|
|
||||||
|
|
||||||
Once the script is running, unplug and replug antennas from left to right, to get
|
|
||||||
the correct order. Every time an antenna is unplugged and replugged, the script will
|
|
||||||
print the antenna identifier. When you are done, press Ctrl+C to stop the script and
|
|
||||||
get the final order.
|
|
||||||
"""
|
|
||||||
from ..utils import antenna_order
|
|
||||||
|
|
||||||
if not globals.csi_producer.is_live:
|
|
||||||
raise NotImplementedError("This command only works with live data")
|
|
||||||
|
|
||||||
antenna_order.main()
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def heatmap() -> None:
|
|
||||||
app = CSIApplication(globals.csi_producer, visualise_raw=True)
|
|
||||||
aoa = AoA()
|
|
||||||
|
|
||||||
@app.on_sample
|
|
||||||
def _(sample: npt.NDArray[np.complex64]) -> None:
|
|
||||||
processed_tensor = torch.tensor(sample, device=device)
|
|
||||||
aoa.update(processed_tensor)
|
|
||||||
aoa.heatmap(visualiser=app.visualise_data)
|
|
||||||
|
|
||||||
app.start()
|
|
||||||
logger.info("Finished processing CSI data")
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def run(file: Path, app_name: str = "app") -> None:
|
|
||||||
"""
|
|
||||||
Run the application with the given file.
|
|
||||||
|
|
||||||
Can be used for running arbitrary CSI applications with non-default data streams
|
|
||||||
(e.g. from file or environment simulation).
|
|
||||||
"""
|
|
||||||
print(file.absolute())
|
|
||||||
if not file.exists():
|
|
||||||
print(f"Error: File '{file}' does not exist.")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
module_name = file.stem
|
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location(module_name, str(file))
|
|
||||||
if spec is None:
|
|
||||||
print(f"Could not load spec from {file}")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
if spec.loader is None:
|
|
||||||
print(f"Could not load module from {file}")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
try:
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to execute {file}: {e}")
|
|
||||||
raise typer.Exit(1) from e
|
|
||||||
|
|
||||||
if not hasattr(module, app_name):
|
|
||||||
print(f"Error: '{app_name}' not defined in the module.")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
if not isinstance(module.app, CSIApplication):
|
|
||||||
print(f"Error: '{app_name}' is not a CSIApplication.")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
module.app.producer = globals.csi_producer
|
|
||||||
module.app.start()
|
|
||||||
|
|
||||||
|
|
||||||
cli.add_typer(file.app, name="file", help="Commands for working with CSI files")
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import logging
|
|
||||||
import multiprocessing as mp
|
|
||||||
import threading
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from . import globals
|
|
||||||
|
|
||||||
app = typer.Typer()
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def write_to_file(path: Path, queue: "mp.Queue[Any]") -> None:
|
|
||||||
import h5py
|
|
||||||
|
|
||||||
with h5py.File(path, "w") as file:
|
|
||||||
logger.info("Starting writer")
|
|
||||||
while True:
|
|
||||||
data = queue.get()
|
|
||||||
if data is None:
|
|
||||||
break
|
|
||||||
logger.info("Writing data to file")
|
|
||||||
file.create_dataset(datetime.now().isoformat(), data=data)
|
|
||||||
logger.info("Writer stopped")
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
|
||||||
def capture(output_path: Path) -> None:
|
|
||||||
"""Capture CSI data to a file"""
|
|
||||||
logger.info(f"Capturing data to {output_path}")
|
|
||||||
|
|
||||||
queue: "mp.Queue[None | npt.NDArray[np.complex128]]" = mp.Queue()
|
|
||||||
writer = threading.Thread(
|
|
||||||
target=write_to_file,
|
|
||||||
args=(
|
|
||||||
output_path,
|
|
||||||
queue,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
writer.start()
|
|
||||||
|
|
||||||
def callback(antenna_data: npt.NDArray[np.complex128]) -> None:
|
|
||||||
logger.info(f"Got final CSI data with shape {antenna_data.shape}")
|
|
||||||
queue.put(antenna_data)
|
|
||||||
|
|
||||||
globals.csi_producer(csi_callback=callback)
|
|
||||||
queue.put(None)
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .. import collection
|
|
||||||
from ..collection import file, ingest, raytracing
|
|
||||||
|
|
||||||
csi_producer: collection.CSIProducer = collection.NoopCSIProducer()
|
|
||||||
is_live = True
|
|
||||||
|
|
||||||
|
|
||||||
def main(from_file: Path | None = None, from_environment: Path | None = None) -> None:
|
|
||||||
global csi_producer, is_live
|
|
||||||
if from_file and from_environment:
|
|
||||||
raise ValueError("Cannot specify both a data file and an environment file")
|
|
||||||
if from_file:
|
|
||||||
csi_producer = file.FileCSIPRoducer(path=from_file)
|
|
||||||
elif from_environment:
|
|
||||||
environment = raytracing.Environment.from_config(from_environment)
|
|
||||||
csi_producer = raytracing.SimulatedCSIProducer(environment)
|
|
||||||
else:
|
|
||||||
csi_producer = ingest.RealtimeCSIProducer()
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
from typing import Iterator
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from . import file, ingest, raytracing
|
|
||||||
from .protocols import CSIProducer, MergedCSI
|
|
||||||
|
|
||||||
|
|
||||||
class NoopCSIProducer:
|
|
||||||
"""A no-op CSI producer that does nothing"""
|
|
||||||
|
|
||||||
is_live = False
|
|
||||||
|
|
||||||
def __call__(self) -> Iterator[MergedCSI]:
|
|
||||||
return iter([])
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
CSIMatrix = npt.NDArray[np.complex64]
|
|
||||||
|
|
||||||
__all__ = ["file", "ingest", "raytracing", "NoopCSIProducer", "CSIProducer"]
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import logging
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Iterator
|
|
||||||
|
|
||||||
import h5py
|
|
||||||
|
|
||||||
from . import protocols
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FileCSIPRoducer:
|
|
||||||
is_live = False
|
|
||||||
|
|
||||||
def __init__(self, path: Path) -> None:
|
|
||||||
self.path = path
|
|
||||||
if not self.path.exists():
|
|
||||||
raise FileNotFoundError(f"File {self.path} does not exist")
|
|
||||||
|
|
||||||
def __call__(self) -> Iterator[protocols.MergedCSI]:
|
|
||||||
"""
|
|
||||||
Replay the CSI data from the file.
|
|
||||||
"""
|
|
||||||
logger.info(f"Replaying CSI data from {self.path}")
|
|
||||||
with h5py.File(self.path, "r") as file:
|
|
||||||
try:
|
|
||||||
for key in file:
|
|
||||||
datetime.fromisoformat(key)
|
|
||||||
except ValueError as e:
|
|
||||||
logger.exception(
|
|
||||||
"The file provided was not generated using this software", e
|
|
||||||
)
|
|
||||||
|
|
||||||
start_time = datetime.fromisoformat(list(file.keys())[0])
|
|
||||||
target_offset = datetime.now() - start_time
|
|
||||||
for key in file:
|
|
||||||
logger.debug(f"Sending data from {key} at {datetime.now().isoformat()}")
|
|
||||||
|
|
||||||
# TODO: add raw frames
|
|
||||||
yield protocols.MergedCSI(
|
|
||||||
frames={},
|
|
||||||
matrix=file[key][:],
|
|
||||||
)
|
|
||||||
|
|
||||||
curr_time_virtual = datetime.fromisoformat(key)
|
|
||||||
new_offset = datetime.now() - curr_time_virtual
|
|
||||||
logger.debug(f"New offset {new_offset}, target is {target_offset}")
|
|
||||||
if new_offset > target_offset + timedelta(seconds=1):
|
|
||||||
logger.warning(
|
|
||||||
f"Data is {new_offset - target_offset} behind, lagging behind..."
|
|
||||||
)
|
|
||||||
time.sleep(max(0, (target_offset - new_offset).total_seconds()))
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""
|
|
||||||
Stop the producer.
|
|
||||||
"""
|
|
||||||
logger.info("Stopping file CSI producer")
|
|
||||||
@ -1,228 +0,0 @@
|
|||||||
import logging
|
|
||||||
import multiprocessing as mp
|
|
||||||
import selectors
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Iterator
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from ..config import config
|
|
||||||
from .csi_frame import CSI
|
|
||||||
from .protocols import MergedCSI
|
|
||||||
|
|
||||||
Host = tuple[str, int]
|
|
||||||
|
|
||||||
|
|
||||||
class FeitHost:
|
|
||||||
def __init__(self, host: Host, command: str) -> None:
|
|
||||||
self.command = command
|
|
||||||
self.host = host
|
|
||||||
self.logger = logging.getLogger(
|
|
||||||
f"{__name__}.{self.__class__.__name__}-{self.host[0]}"
|
|
||||||
)
|
|
||||||
self.active = True
|
|
||||||
self.checker = threading.Thread(target=self.check_continuous)
|
|
||||||
self.checker.start()
|
|
||||||
self.selectors: list[selectors.BaseSelector] = []
|
|
||||||
|
|
||||||
def check_connection(self) -> bool:
|
|
||||||
try:
|
|
||||||
feitcsi_status = subprocess.run(
|
|
||||||
f"ssh root@{self.host[0]} pgrep feitcsi",
|
|
||||||
check=False,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
shell=True,
|
|
||||||
timeout=3,
|
|
||||||
)
|
|
||||||
return feitcsi_status.returncode == 0
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def connect(self) -> None:
|
|
||||||
self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
self.server.connect(self.host)
|
|
||||||
self.server.send(b"stop\n")
|
|
||||||
self.server.send(self.command.encode())
|
|
||||||
|
|
||||||
for selector in self.selectors:
|
|
||||||
selector.register(self.server, selectors.EVENT_READ, data=self)
|
|
||||||
|
|
||||||
self.logger.info(f"Connected to {self.host}")
|
|
||||||
|
|
||||||
def check_continuous(self) -> None:
|
|
||||||
"""
|
|
||||||
Repeatedly check if the FeitCSI service is running
|
|
||||||
|
|
||||||
Because FeitCSI is using TCP, we will get no information if the service stops,
|
|
||||||
or the computer is not reachable. In order to make debugging easier, this checks
|
|
||||||
and logs continuously if the service is running.
|
|
||||||
"""
|
|
||||||
last_status = False
|
|
||||||
while self.active:
|
|
||||||
self.logger.debug(f"Checking connection to {self.host[0]}")
|
|
||||||
if not self.check_connection():
|
|
||||||
self.logger.error(f"FeitCSI is not running on {self.host[0]}")
|
|
||||||
last_status = False
|
|
||||||
else:
|
|
||||||
if not last_status:
|
|
||||||
self.connect()
|
|
||||||
last_status = True
|
|
||||||
time.sleep(1)
|
|
||||||
self.logger.info("Stopping host checker")
|
|
||||||
|
|
||||||
|
|
||||||
class FeitTransmitter(FeitHost):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
command = (
|
|
||||||
f"feitcsi --frequency {config.central_freq} "
|
|
||||||
f"--channel-width {config.channel_width} "
|
|
||||||
f"--format {config.frame_format} "
|
|
||||||
f"--mode inject -s 1 --verbose "
|
|
||||||
f"--inject-delay {1_000_000 // config.collection_sample_rate}"
|
|
||||||
)
|
|
||||||
super().__init__(config.transmit_host, command)
|
|
||||||
|
|
||||||
|
|
||||||
class FeitReceiver(FeitHost):
|
|
||||||
"""
|
|
||||||
A class used to connect to the host running FeitCSI and receive CSI data over a UDP
|
|
||||||
socket.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, host: Host) -> None:
|
|
||||||
command = (
|
|
||||||
f"feitcsi --frequency {config.central_freq} "
|
|
||||||
f"--channel-width {config.channel_width} "
|
|
||||||
f"--format {config.frame_format} "
|
|
||||||
f"--mode measure"
|
|
||||||
)
|
|
||||||
super().__init__(host, command)
|
|
||||||
|
|
||||||
def recv(self) -> CSI:
|
|
||||||
"""
|
|
||||||
Block until a CSI frame is received and return it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Receive the data from the socket
|
|
||||||
data = self.server.recv(65535)
|
|
||||||
# Decode the data using the CSI frame format
|
|
||||||
csi = CSI(data)
|
|
||||||
return csi
|
|
||||||
|
|
||||||
def register(self, selector: selectors.BaseSelector) -> None:
|
|
||||||
"""
|
|
||||||
Register the receiver as a selector. This will allow us to multiplex the I/O
|
|
||||||
operations on a single thread.
|
|
||||||
|
|
||||||
This allows us to block until any of the receivers receive data
|
|
||||||
"""
|
|
||||||
self.selectors.append(selector)
|
|
||||||
|
|
||||||
if hasattr(self, "server"):
|
|
||||||
selector.register(self.server, selectors.EVENT_READ, data=self)
|
|
||||||
|
|
||||||
|
|
||||||
class CSIAntennaArray:
|
|
||||||
"""
|
|
||||||
Represents a set of receiver hosts that are used to receive CSI data, assumed to be
|
|
||||||
part of a linear antenna array.
|
|
||||||
|
|
||||||
This class is used to buffer a set of CSI data from multiple receivers, and once a
|
|
||||||
set of readings is ready for each receiver, it will be merged into a single sample
|
|
||||||
using the antenna order in the configuration.
|
|
||||||
|
|
||||||
When the data is ready, the following callbacks are called:
|
|
||||||
- `pre_merge_callback`: Called with the data before merging. This is useful for
|
|
||||||
per-antenna analysis, e.g. finding the correct antenna order
|
|
||||||
- `sample_callback`: Called with the merged data
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, receivers: list[FeitReceiver]) -> None:
|
|
||||||
self.pending_data: dict[Host, tuple[datetime, CSI]] = {}
|
|
||||||
self.pending_data_lock = mp.Lock()
|
|
||||||
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
||||||
self.last_processed = datetime.now()
|
|
||||||
self.receivers = receivers
|
|
||||||
self.active = True
|
|
||||||
|
|
||||||
def add_data(self, host: Host, data: CSI) -> None:
|
|
||||||
if (
|
|
||||||
host in self.pending_data
|
|
||||||
and self.last_processed < self.pending_data[host][0]
|
|
||||||
):
|
|
||||||
self.logger.warning(
|
|
||||||
f"Skipping data from {host} at {self.pending_data[host][0]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
with self.pending_data_lock:
|
|
||||||
self.pending_data[host] = (datetime.now(), data)
|
|
||||||
|
|
||||||
def is_ready(self) -> bool:
|
|
||||||
for host in config.receive_hosts:
|
|
||||||
if (
|
|
||||||
host not in self.pending_data
|
|
||||||
or self.pending_data[host][0] <= self.last_processed
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def process_data(self) -> None | MergedCSI:
|
|
||||||
if not self.is_ready():
|
|
||||||
self.logger.debug("Not all data is ready")
|
|
||||||
return None
|
|
||||||
self.last_processed = datetime.now()
|
|
||||||
|
|
||||||
frames = {host: data[1] for host, data in self.pending_data.items()}
|
|
||||||
antenna_data = [
|
|
||||||
np.expand_dims(self.pending_data[ip][1].matrix[:, antenna], axis=2)
|
|
||||||
for ip, antenna in config.antennas.order
|
|
||||||
]
|
|
||||||
|
|
||||||
# We have data from all servers
|
|
||||||
all_data = np.concat(antenna_data, axis=1)
|
|
||||||
return MergedCSI(frames=frames, matrix=all_data)
|
|
||||||
|
|
||||||
def process_forever(self) -> Iterator[MergedCSI]:
|
|
||||||
# Register the receivers with the selector
|
|
||||||
self.logger.info("Starting to process data from receivers")
|
|
||||||
selector = selectors.DefaultSelector()
|
|
||||||
|
|
||||||
for receiver in self.receivers:
|
|
||||||
receiver.register(selector)
|
|
||||||
|
|
||||||
while self.active:
|
|
||||||
events = selector.select(timeout=0.1)
|
|
||||||
if not events:
|
|
||||||
self.logger.warning("No events received in the last 0.1 seconds")
|
|
||||||
continue
|
|
||||||
|
|
||||||
for key, _ in events:
|
|
||||||
receiver = key.data
|
|
||||||
self.add_data(receiver.host, receiver.recv())
|
|
||||||
|
|
||||||
sample = self.process_data()
|
|
||||||
if sample is not None:
|
|
||||||
yield sample
|
|
||||||
|
|
||||||
|
|
||||||
class RealtimeCSIProducer:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.receivers = [FeitReceiver(ip) for ip in config.receive_hosts]
|
|
||||||
self.transmitter = FeitTransmitter()
|
|
||||||
|
|
||||||
# Start injecting CSI frames
|
|
||||||
self.buffer = CSIAntennaArray(self.receivers)
|
|
||||||
self.is_live = True
|
|
||||||
|
|
||||||
def __call__(self) -> Iterator[MergedCSI]:
|
|
||||||
return self.buffer.process_forever()
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
for r in self.receivers:
|
|
||||||
r.active = False
|
|
||||||
self.transmitter.active = False
|
|
||||||
self.buffer.active = False
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
from typing import Iterator, NamedTuple, Protocol
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from .csi_frame import CSI
|
|
||||||
|
|
||||||
CSIHost = tuple[str, int]
|
|
||||||
|
|
||||||
|
|
||||||
class MergedCSI(NamedTuple):
|
|
||||||
"""Merged CSI data from all antennas"""
|
|
||||||
|
|
||||||
frames: dict[CSIHost, CSI]
|
|
||||||
matrix: npt.NDArray[np.complex64]
|
|
||||||
|
|
||||||
|
|
||||||
class CSIProducer(Protocol):
|
|
||||||
is_live: bool
|
|
||||||
|
|
||||||
def __call__(self) -> Iterator[MergedCSI]: ...
|
|
||||||
def stop(self) -> None: ...
|
|
||||||
@ -1,259 +0,0 @@
|
|||||||
import logging
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Iterator
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from where_fi.collection.protocols import CSIProducer, MergedCSI
|
|
||||||
from where_fi.config import config
|
|
||||||
|
|
||||||
C = 299_792_458
|
|
||||||
MAX_DEPTH = 2
|
|
||||||
LOSS_EXPONENT = 0.8
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PathComponent:
|
|
||||||
delay: float
|
|
||||||
phase_offset: float
|
|
||||||
attenuation: float
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelImpulseResponse:
|
|
||||||
def __init__(self, path_components: list[PathComponent]) -> None:
|
|
||||||
self.path_components = path_components
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delayed(
|
|
||||||
other: "ChannelImpulseResponse", delay: float, reflect: bool = False
|
|
||||||
) -> "ChannelImpulseResponse":
|
|
||||||
new_path_components: list[PathComponent] = []
|
|
||||||
distance = delay * C
|
|
||||||
|
|
||||||
for component in other.path_components:
|
|
||||||
new_path_components.append(
|
|
||||||
PathComponent(
|
|
||||||
delay=component.delay + delay,
|
|
||||||
phase_offset=component.phase_offset + (np.pi if reflect else 0),
|
|
||||||
attenuation=component.attenuation
|
|
||||||
* (1 / (1 + distance) ** LOSS_EXPONENT),
|
|
||||||
# attenuation=component.attenuation
|
|
||||||
# * (np.exp(-distance * LOSS_EXPONENT)),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return ChannelImpulseResponse(new_path_components)
|
|
||||||
|
|
||||||
def __add__(self, other: "ChannelImpulseResponse") -> "ChannelImpulseResponse":
|
|
||||||
return ChannelImpulseResponse(self.path_components + other.path_components)
|
|
||||||
|
|
||||||
|
|
||||||
class PathObject:
|
|
||||||
def __init__(self, x: float, y: float, z: float) -> None:
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.z = z
|
|
||||||
self.cir = ChannelImpulseResponse([])
|
|
||||||
|
|
||||||
def distance(self, other: "PathObject") -> float:
|
|
||||||
return (
|
|
||||||
(self.x - other.x) ** 2 + (self.y - other.y) ** 2 + (self.z - other.z) ** 2
|
|
||||||
) ** 0.5
|
|
||||||
|
|
||||||
def reflect(
|
|
||||||
self,
|
|
||||||
incoming_cir: ChannelImpulseResponse,
|
|
||||||
target: list["PathObject"],
|
|
||||||
depth: int = 0,
|
|
||||||
) -> None:
|
|
||||||
self.cir = self.cir + incoming_cir
|
|
||||||
|
|
||||||
if depth > MAX_DEPTH:
|
|
||||||
return
|
|
||||||
|
|
||||||
for obj in target:
|
|
||||||
if id(obj) == id(self):
|
|
||||||
continue
|
|
||||||
distance = self.distance(obj)
|
|
||||||
assert distance > 0
|
|
||||||
delay = distance / C
|
|
||||||
obj.reflect(
|
|
||||||
ChannelImpulseResponse.delayed(incoming_cir, delay, reflect=True),
|
|
||||||
target,
|
|
||||||
depth + 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Transmitter(PathObject):
|
|
||||||
def __init__(self, x: float, y: float, z: float) -> None:
|
|
||||||
super().__init__(x, y, z)
|
|
||||||
|
|
||||||
|
|
||||||
DELTA_T = 10
|
|
||||||
GAMMA = np.pi / 4
|
|
||||||
|
|
||||||
|
|
||||||
class Receiver(PathObject):
|
|
||||||
def __init__(self, x: float, y: float, z: float, ideal: bool = True) -> None:
|
|
||||||
super().__init__(x, y, z)
|
|
||||||
self.delta_t = 0 if ideal else DELTA_T
|
|
||||||
self.gamma = 0 if ideal else GAMMA
|
|
||||||
|
|
||||||
def get_cfr(self) -> npt.NDArray[np.complex64]:
|
|
||||||
"""
|
|
||||||
Calculate the CSI matrix for this rx-tx pair. This is computed by the Fourier
|
|
||||||
Transform of the Channel Impulse Response (CIR). The CIR is calculated as in
|
|
||||||
[1], [2].
|
|
||||||
|
|
||||||
To get the FT of the CIR, we use the sifting property of the Dirac delta
|
|
||||||
|
|
||||||
[1] - https://tns.thss.tsinghua.edu.cn/wst/docs/pre
|
|
||||||
[2] - https://dl.acm.org/doi/10.1145/2543581.2543592, Equation 4
|
|
||||||
"""
|
|
||||||
cfr = np.zeros(len(config.subcarrier_frequencies), dtype=np.complex64)
|
|
||||||
for i_sub, f_sub in enumerate(config.subcarrier_frequencies):
|
|
||||||
cfr[i_sub] = sum(
|
|
||||||
[
|
|
||||||
component.attenuation
|
|
||||||
* np.exp(1j * component.phase_offset)
|
|
||||||
* np.exp(-1j * 2 * np.pi * f_sub * component.delay)
|
|
||||||
for component in self.cir.path_components
|
|
||||||
]
|
|
||||||
) * np.exp(
|
|
||||||
1j
|
|
||||||
* (
|
|
||||||
2
|
|
||||||
* np.pi
|
|
||||||
* (i_sub / len(config.subcarrier_frequencies))
|
|
||||||
* self.delta_t
|
|
||||||
+ self.gamma
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return cfr
|
|
||||||
|
|
||||||
|
|
||||||
class Environment:
|
|
||||||
def __init__(self, objects: list[PathObject]) -> None:
|
|
||||||
self.transmitters = [obj for obj in objects if isinstance(obj, Transmitter)]
|
|
||||||
self.receivers = [obj for obj in objects if isinstance(obj, Receiver)]
|
|
||||||
self.objects = [
|
|
||||||
obj for obj in objects if obj not in self.transmitters + self.receivers
|
|
||||||
]
|
|
||||||
self.count = 0
|
|
||||||
|
|
||||||
def move(self) -> None:
|
|
||||||
self.count += 1
|
|
||||||
if self.count > 4000:
|
|
||||||
print("Moving objects")
|
|
||||||
for obj in self.objects:
|
|
||||||
obj.x += np.sin(self.count / 1000 * 2 * np.pi) * 2
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_config(filename: Path) -> "Environment":
|
|
||||||
"""
|
|
||||||
Read a config file that includes a scene description and create an envionment
|
|
||||||
based on that
|
|
||||||
|
|
||||||
The config file should be a text file where each line corresponds to an object.
|
|
||||||
The first word of each line should be the type of object (TX/RX/OBJ), followed
|
|
||||||
by the x and y coordinates of the object.
|
|
||||||
"""
|
|
||||||
objects: list[PathObject] = []
|
|
||||||
with open(filename, "r") as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
if line.startswith("#"):
|
|
||||||
continue
|
|
||||||
parts = line.split(" ")
|
|
||||||
x, y, z = map(float, parts[1:])
|
|
||||||
if parts[0] == "TX":
|
|
||||||
objects.append(Transmitter(x, y, z))
|
|
||||||
elif parts[0] == "RX":
|
|
||||||
objects.append(Receiver(x, y, z))
|
|
||||||
else:
|
|
||||||
objects.append(PathObject(x, y, z))
|
|
||||||
|
|
||||||
return Environment(objects)
|
|
||||||
|
|
||||||
def add_awgn(
|
|
||||||
self, signal: npt.NDArray[np.complex64], snr_dB: float
|
|
||||||
) -> npt.NDArray[np.complex64]:
|
|
||||||
signal_power = np.mean(np.abs(signal) ** 2)
|
|
||||||
snr_linear = 10 ** (snr_dB / 10)
|
|
||||||
noise_power = signal_power / snr_linear
|
|
||||||
noise = np.sqrt(noise_power / 2) * (
|
|
||||||
np.random.randn(*signal.shape) + 1j * np.random.randn(*signal.shape)
|
|
||||||
)
|
|
||||||
return signal + noise.astype(np.complex64)
|
|
||||||
|
|
||||||
def get_csi(self) -> npt.NDArray[np.complex64]:
|
|
||||||
"""
|
|
||||||
Calculate the Channel State Information (CSI) matrix for the simulated
|
|
||||||
environment.
|
|
||||||
|
|
||||||
The returned matrix is of shape (num_subcarriers, num_receivers,
|
|
||||||
num_transmitters)
|
|
||||||
|
|
||||||
This is calculated by finding all paths leading to each receiver, and
|
|
||||||
calculating the CFR evaluated at each subcarrier.
|
|
||||||
"""
|
|
||||||
csi = np.zeros(
|
|
||||||
(
|
|
||||||
len(config.subcarrier_frequencies),
|
|
||||||
len(self.receivers),
|
|
||||||
len(self.transmitters),
|
|
||||||
),
|
|
||||||
dtype=np.complex64,
|
|
||||||
)
|
|
||||||
for i_tx, transmitter in enumerate(self.transmitters):
|
|
||||||
for obj in self.objects + self.receivers + self.transmitters:
|
|
||||||
obj.cir = ChannelImpulseResponse([])
|
|
||||||
transmitter.reflect(
|
|
||||||
ChannelImpulseResponse(
|
|
||||||
[
|
|
||||||
PathComponent(
|
|
||||||
delay=0,
|
|
||||||
phase_offset=0,
|
|
||||||
# attenuation=100000000,
|
|
||||||
attenuation=100,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
self.objects + self.receivers,
|
|
||||||
)
|
|
||||||
for i_rx, receiver in enumerate(self.receivers):
|
|
||||||
logger.debug(f"RX {i_rx} paths: {len(receiver.cir.path_components)}")
|
|
||||||
csi[:, i_rx, i_tx] = self.add_awgn(receiver.get_cfr(), snr_dB=30)
|
|
||||||
return csi
|
|
||||||
|
|
||||||
|
|
||||||
class SimulatedCSIProducer(CSIProducer):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __init__(self, environment: Environment) -> None:
|
|
||||||
self.environment = environment
|
|
||||||
self.active = True
|
|
||||||
|
|
||||||
def __call__(self) -> Iterator[MergedCSI]:
|
|
||||||
while self.active:
|
|
||||||
self.environment.move()
|
|
||||||
start = datetime.now()
|
|
||||||
csi = self.environment.get_csi()
|
|
||||||
yield MergedCSI(
|
|
||||||
frames={},
|
|
||||||
matrix=csi,
|
|
||||||
)
|
|
||||||
time.sleep(
|
|
||||||
max(
|
|
||||||
0,
|
|
||||||
1 / config.collection_sample_rate
|
|
||||||
- (datetime.now() - start).total_seconds(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
self.active = False
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import pydantic
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
try:
|
|
||||||
config = yaml.safe_load(open("config.yaml"))
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error(
|
|
||||||
"No config.yaml file found. Make sure to copy the "
|
|
||||||
"config.example.yaml file to config.yaml"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
config = models.Config(**config)
|
|
||||||
except pydantic.ValidationError as e:
|
|
||||||
logger.error(f"Invalid config.yaml file: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
from functools import cached_property
|
|
||||||
from typing import Literal, Self
|
|
||||||
|
|
||||||
from pydantic import BaseModel, model_validator
|
|
||||||
|
|
||||||
Host = tuple[str, int]
|
|
||||||
|
|
||||||
|
|
||||||
class Preprocessing(BaseModel):
|
|
||||||
class Bandpass(BaseModel):
|
|
||||||
lowcut: int
|
|
||||||
highcut: int
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bounds(self) -> tuple[int, int]:
|
|
||||||
return (self.lowcut, self.highcut)
|
|
||||||
|
|
||||||
bandpass: Bandpass | None = None
|
|
||||||
subcarrier_step: int = 1
|
|
||||||
denoising: Literal["none", "median", "mean"] = "median"
|
|
||||||
denoising_period: float = 1
|
|
||||||
|
|
||||||
steps: list[
|
|
||||||
Literal[
|
|
||||||
"fill_pilots",
|
|
||||||
"skip_subcarriers",
|
|
||||||
"remove_agc",
|
|
||||||
"remove_sfo",
|
|
||||||
"remove_sto",
|
|
||||||
"bandpass",
|
|
||||||
]
|
|
||||||
] = ["fill_pilots", "skip_subcarriers", "remove_agc", "remove_sfo"]
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def skip_in_steps(self) -> Self:
|
|
||||||
if "skip_subcarriers" not in self.steps and self.subcarrier_step != 1:
|
|
||||||
raise ValueError(
|
|
||||||
"subcarrier_step must be 1 if skip_subcarriers is not in steps"
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def bandpass_in_steps(self) -> Self:
|
|
||||||
if "bandpass" not in self.steps and self.bandpass is not None:
|
|
||||||
raise ValueError(
|
|
||||||
"bandpass must be in preprocessing steps if bandpass configuration is "
|
|
||||||
"provided"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "bandpass" in self.steps and self.bandpass is None:
|
|
||||||
raise ValueError(
|
|
||||||
"bandpass configuration must be provided if bandpass is in "
|
|
||||||
"preprocessing steps"
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class MUSIC(BaseModel):
|
|
||||||
eigval_threshold: float
|
|
||||||
window_size: int
|
|
||||||
|
|
||||||
class Heatmap(BaseModel):
|
|
||||||
theta_resolution: int
|
|
||||||
tof_resolution: int
|
|
||||||
tof_max: float
|
|
||||||
|
|
||||||
heatmap: Heatmap
|
|
||||||
|
|
||||||
|
|
||||||
class Antennas(BaseModel):
|
|
||||||
spacing: float
|
|
||||||
order: list[tuple[Host, int]]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def count(self) -> int:
|
|
||||||
return len(self.order)
|
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
|
||||||
receive_hosts: list[Host]
|
|
||||||
transmit_host: Host
|
|
||||||
|
|
||||||
antennas: Antennas
|
|
||||||
|
|
||||||
collection_sample_rate: int
|
|
||||||
processing_sample_rate: int
|
|
||||||
central_freq: int
|
|
||||||
channel_width: Literal[20, 40, 80, 160]
|
|
||||||
frame_format: Literal["NOHT", "HT", "VHT", "HESU"]
|
|
||||||
|
|
||||||
preprocessing: Preprocessing
|
|
||||||
music: MUSIC
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def central_freq_hz(self) -> int:
|
|
||||||
return self.central_freq * 1_000_000
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def band(self) -> Literal["2.4", "5", "6"]:
|
|
||||||
if self.central_freq in range(2412, 2484):
|
|
||||||
return "2.4"
|
|
||||||
if self.central_freq in range(5180, 5320):
|
|
||||||
return "5"
|
|
||||||
if self.central_freq in range(5955, 7115):
|
|
||||||
return "6"
|
|
||||||
raise ValueError(f"{self.central_freq} is not a valid Wi-Fi channel")
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def num_guards(self) -> int:
|
|
||||||
if self.channel_width == 20:
|
|
||||||
return 7
|
|
||||||
return 11
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def subcarriers(self) -> int:
|
|
||||||
nulls = {
|
|
||||||
20: 1,
|
|
||||||
40: 3,
|
|
||||||
}
|
|
||||||
return len(self.subcarrier_frequencies) + nulls[self.channel_width]
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def _delta_f_no_skipping(self) -> int:
|
|
||||||
if self.frame_format == "HESU":
|
|
||||||
return 78_125
|
|
||||||
return 312_500
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def delta_f(self) -> int:
|
|
||||||
return self._delta_f_no_skipping * self.preprocessing.subcarrier_step
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def _subcarriers_no_skipping(self) -> list[int]:
|
|
||||||
used = {
|
|
||||||
20: (1, 29),
|
|
||||||
40: (2, 59),
|
|
||||||
}
|
|
||||||
subcarrier_indices = list(
|
|
||||||
range(-used[self.channel_width][1] + 1, -used[self.channel_width][0] + 1)
|
|
||||||
) + list(range(used[self.channel_width][0], used[self.channel_width][1]))
|
|
||||||
print(subcarrier_indices)
|
|
||||||
|
|
||||||
return [
|
|
||||||
self.central_freq_hz + i * self._delta_f_no_skipping
|
|
||||||
for i in subcarrier_indices
|
|
||||||
]
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def subcarrier_frequencies(self) -> list[int]:
|
|
||||||
return self._subcarriers_no_skipping[:: self.preprocessing.subcarrier_step]
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def channels(self) -> Self:
|
|
||||||
band_start = 2412 if self.band == "2.4" else 5180 if self.band == "5" else 5955
|
|
||||||
if self.band == "2.4" and self.channel_width not in [20, 40]:
|
|
||||||
raise ValueError(
|
|
||||||
f"2.4GHz channel {self.central_freq} must "
|
|
||||||
"have a channel width of 20 or 40 MHz"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (self.band == "2.4" and (self.central_freq - band_start) % 5 != 0) or (
|
|
||||||
self.band in ["5", "6"]
|
|
||||||
and ((self.central_freq - band_start) % self.channel_width != 0)
|
|
||||||
):
|
|
||||||
raise ValueError(
|
|
||||||
f"Central frequency {self.central_freq} must be a channel as in https://en.wikipedia.org/wiki/List_of_WLAN_channels"
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
@ -1,201 +0,0 @@
|
|||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from ..config import config
|
|
||||||
from ..visualise import server as visualise
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
||||||
torch.set_default_device(device)
|
|
||||||
|
|
||||||
|
|
||||||
class AoA:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.historical_autocorr = torch.tensor([], dtype=torch.complex64)
|
|
||||||
self.N_subcarriers = config.subcarriers
|
|
||||||
self.N_rx = config.antennas.count
|
|
||||||
self.timestamp = datetime.now()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def smooth(self, data: torch.Tensor) -> torch.Tensor:
|
|
||||||
assert len(data.shape) == 3
|
|
||||||
|
|
||||||
M = data.shape[0] # Number of subcarriers
|
|
||||||
N = data.shape[1] # Number of RX antennas
|
|
||||||
T = data.shape[2] # Number of TX antennas
|
|
||||||
|
|
||||||
self.N_subcarriers = M
|
|
||||||
self.N_rx = N
|
|
||||||
|
|
||||||
logger.debug(f"Smoothing: Subcarriers: {M}, RX antennas: {N}, TX antennas: {T}")
|
|
||||||
|
|
||||||
# This only works with 1 TX antenna (i.e. no MIMO) - see #4 for more details
|
|
||||||
assert T == 1, "The current implementation only supports 1 TX antenna"
|
|
||||||
|
|
||||||
H_n = torch.zeros((N, M // 2, M // 2 + 1), dtype=torch.complex64)
|
|
||||||
|
|
||||||
for i in range(N):
|
|
||||||
for j in range(M // 2):
|
|
||||||
H_n[i, j] = data[j : j + M // 2 + 1, i, 0]
|
|
||||||
|
|
||||||
H_sm_rows = [torch.hstack(list(H_n[i : i + N // 2 + 1])) for i in range(N // 2)]
|
|
||||||
H_sm = torch.vstack(H_sm_rows)
|
|
||||||
|
|
||||||
logger.debug(f"Smoothed: {H_sm.shape}")
|
|
||||||
|
|
||||||
return H_sm
|
|
||||||
|
|
||||||
def update(self, data: torch.Tensor) -> None:
|
|
||||||
self.timestamp = datetime.now()
|
|
||||||
H_sm = self.smooth(data)
|
|
||||||
logger.debug(f"Calculated smoothed CSI matrix: {H_sm.shape}")
|
|
||||||
|
|
||||||
auto_corr = H_sm @ torch.conj(H_sm).T
|
|
||||||
# This matrix is by definition Hermitian.
|
|
||||||
# Therefore, all of its eigenvectors are orthogonal.
|
|
||||||
|
|
||||||
if len(self.historical_autocorr.shape) <= 1:
|
|
||||||
self.historical_autocorr = torch.unsqueeze(auto_corr, 0)
|
|
||||||
else:
|
|
||||||
self.historical_autocorr = torch.cat(
|
|
||||||
(self.historical_autocorr, torch.unsqueeze(auto_corr, 0))
|
|
||||||
)
|
|
||||||
|
|
||||||
WINDOW_SIZE = config.music.window_size
|
|
||||||
if self.historical_autocorr.shape[0] > WINDOW_SIZE:
|
|
||||||
self.historical_autocorr = self.historical_autocorr[-WINDOW_SIZE:]
|
|
||||||
|
|
||||||
logger.debug("Finished updating autocorrelation matrix")
|
|
||||||
|
|
||||||
def steering_vector(self, theta: torch.Tensor, tof: torch.Tensor) -> torch.Tensor:
|
|
||||||
assert theta.shape == tof.shape
|
|
||||||
assert len(theta.shape) == 1
|
|
||||||
N = theta.shape[0]
|
|
||||||
|
|
||||||
omega_t: torch.Tensor = torch.exp(-2j * np.pi * config.delta_f * tof)
|
|
||||||
phi_theta: torch.Tensor = torch.exp(
|
|
||||||
2j
|
|
||||||
* np.pi
|
|
||||||
* config.central_freq_hz
|
|
||||||
* config.antennas.spacing
|
|
||||||
* (torch.sin(theta))
|
|
||||||
/ 299_792_458
|
|
||||||
)
|
|
||||||
assert omega_t.shape == phi_theta.shape == (N,)
|
|
||||||
|
|
||||||
omega_t = torch.unsqueeze(omega_t, dim=-1)
|
|
||||||
phi_theta = torch.unsqueeze(phi_theta, dim=-1)
|
|
||||||
|
|
||||||
assert omega_t.shape == phi_theta.shape == (N, 1)
|
|
||||||
|
|
||||||
antenna_v = omega_t ** torch.arange(self.N_subcarriers, dtype=torch.float32)
|
|
||||||
phis = phi_theta ** torch.arange(self.N_rx, dtype=torch.float32)
|
|
||||||
|
|
||||||
assert antenna_v.shape == (N, self.N_subcarriers)
|
|
||||||
assert phis.shape == (N, self.N_rx)
|
|
||||||
|
|
||||||
antenna_v = torch.unsqueeze(antenna_v, dim=1)
|
|
||||||
phis = torch.unsqueeze(phis, dim=-1)
|
|
||||||
|
|
||||||
assert antenna_v.shape == (N, 1, self.N_subcarriers)
|
|
||||||
assert phis.shape == (N, self.N_rx, 1)
|
|
||||||
|
|
||||||
steering = torch.bmm(phis, antenna_v)
|
|
||||||
|
|
||||||
assert steering.shape == (N, self.N_rx, self.N_subcarriers)
|
|
||||||
return steering.reshape(N, -1)
|
|
||||||
|
|
||||||
def evaluate(
|
|
||||||
self,
|
|
||||||
theta: torch.Tensor,
|
|
||||||
tof: torch.Tensor,
|
|
||||||
visualiser: None
|
|
||||||
| Callable[[npt.NDArray[Any], visualise.figures.Figure], None] = None,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
R = torch.mean(self.historical_autocorr, dim=0)
|
|
||||||
|
|
||||||
# The smallest eigenvectors span the noise subspace,
|
|
||||||
# and the largest span the signal subspace.
|
|
||||||
logger.debug(f"Calculating eigenvectors of R: {R.shape}")
|
|
||||||
eigvals, eigvecs = torch.linalg.eig(R)
|
|
||||||
if visualiser:
|
|
||||||
visualiser(eigvals.numpy(), visualise.figures.Figure.MUSIC_EIGENVALUES)
|
|
||||||
assert isinstance(eigvals, torch.Tensor)
|
|
||||||
assert isinstance(eigvecs, torch.Tensor)
|
|
||||||
logger.debug(f"Eigenvalues: {eigvals}")
|
|
||||||
E_n = eigvecs[:, torch.abs(eigvals) < config.music.eigval_threshold]
|
|
||||||
|
|
||||||
logger.info(f"Signal subspace: {E_n.shape}")
|
|
||||||
steering = torch.unsqueeze(self.steering_vector(theta, tof), dim=-1)
|
|
||||||
print(steering.shape)
|
|
||||||
steering_h = torch.conj(steering).permute(0, 2, 1)
|
|
||||||
|
|
||||||
E_n = E_n.unsqueeze(0)
|
|
||||||
E_n_H = torch.conj(E_n).permute(0, 2, 1)
|
|
||||||
logger.debug(
|
|
||||||
f"Heatmap multiplication: {steering_h.shape}, {E_n.shape}, "
|
|
||||||
f"{E_n_H.shape}, {steering.shape}"
|
|
||||||
)
|
|
||||||
c: torch.Tensor = 1 / (steering_h @ E_n @ E_n_H @ steering)
|
|
||||||
return torch.abs(c)[:, 0, 0]
|
|
||||||
|
|
||||||
def heatmap(
|
|
||||||
self,
|
|
||||||
visualiser: None | Callable[[npt.NDArray[Any], visualise.figures.Figure], None],
|
|
||||||
) -> npt.NDArray[np.float32]:
|
|
||||||
thetas = np.linspace(
|
|
||||||
0, np.pi, config.music.heatmap.theta_resolution, dtype=np.float32
|
|
||||||
)
|
|
||||||
tofs = np.linspace(
|
|
||||||
0,
|
|
||||||
config.music.heatmap.tof_max,
|
|
||||||
config.music.heatmap.tof_resolution,
|
|
||||||
dtype=np.float32,
|
|
||||||
)
|
|
||||||
thetas_mesh, tofs_mesh = np.meshgrid(thetas, tofs)
|
|
||||||
logger.debug(
|
|
||||||
f"Calculating heatmap with {thetas_mesh.shape} and {tofs_mesh.shape}"
|
|
||||||
)
|
|
||||||
evaluated = self.evaluate(
|
|
||||||
torch.tensor(thetas_mesh.reshape(-1)),
|
|
||||||
torch.tensor(tofs_mesh.reshape(-1)),
|
|
||||||
visualiser=visualiser,
|
|
||||||
)
|
|
||||||
logger.debug(f"Evaluated heatmap: {evaluated.shape}")
|
|
||||||
heatmap: npt.NDArray[np.float32] = evaluated.reshape(
|
|
||||||
config.music.heatmap.tof_resolution,
|
|
||||||
config.music.heatmap.theta_resolution,
|
|
||||||
).numpy(force=True)
|
|
||||||
|
|
||||||
if visualiser:
|
|
||||||
visualiser(heatmap, visualise.figures.Figure.AOA_HEATMAP)
|
|
||||||
return heatmap
|
|
||||||
|
|
||||||
|
|
||||||
def test_smoothing() -> None:
|
|
||||||
row, col = np.indices((6, 4))
|
|
||||||
data = row + 1j * col
|
|
||||||
data = np.expand_dims(data, axis=2)
|
|
||||||
np.set_printoptions(linewidth=200)
|
|
||||||
print(data.shape)
|
|
||||||
aoa = AoA()
|
|
||||||
aoa.N_subcarriers = 6
|
|
||||||
aoa.N_rx = 4
|
|
||||||
smoothed = aoa.smooth(data)
|
|
||||||
print(smoothed)
|
|
||||||
|
|
||||||
|
|
||||||
def test_steering_vector() -> None:
|
|
||||||
aoa = AoA()
|
|
||||||
aoa.N_subcarriers = 10
|
|
||||||
aoa.N_rx = 4
|
|
||||||
tau = torch.Tensor([1, 0])
|
|
||||||
theta = torch.Tensor([0, 1])
|
|
||||||
print(aoa.steering_vector(theta, tau))
|
|
||||||
assert False
|
|
||||||
@ -1,233 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
from scipy.signal import butter, correlate, sosfilt, sosfilt_zi
|
|
||||||
|
|
||||||
from where_fi.collection import CSIMatrix
|
|
||||||
from where_fi.collection.csi_frame import CSI
|
|
||||||
from where_fi.collection.protocols import CSIHost
|
|
||||||
from where_fi.config import config
|
|
||||||
|
|
||||||
from ..visualise import server as visualise
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
np.seterr(invalid="ignore")
|
|
||||||
|
|
||||||
|
|
||||||
class Preprocessor:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.short_term_avg = np.zeros((1,), dtype=np.complex64)
|
|
||||||
self.long_term_avg = np.zeros((1,), dtype=np.complex64)
|
|
||||||
self._last_sample = None
|
|
||||||
self.denoising_samples = int(
|
|
||||||
config.preprocessing.denoising_period * config.collection_sample_rate
|
|
||||||
)
|
|
||||||
self.circular_buffer = np.zeros(
|
|
||||||
(
|
|
||||||
self.denoising_samples, # Number of samples
|
|
||||||
config.subcarriers, # Number of subcarriers
|
|
||||||
config.antennas.count, # Number of RX antennas
|
|
||||||
1, # Number of TX antennas
|
|
||||||
),
|
|
||||||
dtype=np.complex64,
|
|
||||||
)
|
|
||||||
self.sample_index = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_sample(self) -> None | CSIMatrix:
|
|
||||||
"""
|
|
||||||
The last sample of the preprocessor. This is used for low frequency processing
|
|
||||||
"""
|
|
||||||
match config.preprocessing.denoising:
|
|
||||||
case "none":
|
|
||||||
return self._last_sample
|
|
||||||
case "median":
|
|
||||||
# Return the median of the last samples
|
|
||||||
median_abs = np.median(np.abs(self.circular_buffer), axis=0)
|
|
||||||
median_angle = np.median(np.angle(self.circular_buffer), axis=0)
|
|
||||||
ans = median_abs * np.exp(1j * median_angle)
|
|
||||||
return ans
|
|
||||||
case "mean":
|
|
||||||
# Return the mean of the last samples
|
|
||||||
return np.mean(self.circular_buffer, axis=0)
|
|
||||||
case _:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid denoising method: {config.preprocessing.denoising}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def remove_sto(self, csi: CSIMatrix) -> CSIMatrix:
|
|
||||||
"""
|
|
||||||
Remove sampling time offsets caused by:
|
|
||||||
- Sampling frequency offset
|
|
||||||
- Packet detection delay
|
|
||||||
|
|
||||||
This is done by multiplying the CSI matrices of consecutive antennas in the
|
|
||||||
array.
|
|
||||||
|
|
||||||
According to [1]:
|
|
||||||
> Conjugate multiplication and division are the only two methods to
|
|
||||||
> eliminate the SFO and PDD.
|
|
||||||
|
|
||||||
No citation or explanation is provided, so not sure why/whether it works.
|
|
||||||
Something similar is also done in [2] without explanation.
|
|
||||||
|
|
||||||
[1] - https://tns.thss.tsinghua.edu.cn/wst/docs/sanitization
|
|
||||||
[2] - https://doi.org/10.1109/ICC51166.2024.10623053
|
|
||||||
"""
|
|
||||||
|
|
||||||
csi_remove_sto = np.zeros_like(csi)
|
|
||||||
for antenna in range(csi.shape[1]):
|
|
||||||
antenna_nxt = (antenna + 1) % csi.shape[1]
|
|
||||||
csi_remove_sto[:, antenna, :] = np.multiply(
|
|
||||||
csi[:, antenna, :], csi[:, antenna_nxt, :].conj()
|
|
||||||
)
|
|
||||||
return csi_remove_sto
|
|
||||||
|
|
||||||
def remove_sfo(
|
|
||||||
self,
|
|
||||||
csi: CSIMatrix,
|
|
||||||
visualiser: None
|
|
||||||
| Callable[[npt.NDArray[Any], visualise.figures.Figure], None] = None,
|
|
||||||
) -> CSIMatrix:
|
|
||||||
"""
|
|
||||||
Remove sampling frequency offsets by linear regression.
|
|
||||||
|
|
||||||
This is caused by the difference in sampling frequency between the transmitter
|
|
||||||
and receiver, and this is linear in frequency. We can estimate it using linear
|
|
||||||
regression and compensate for it.
|
|
||||||
"""
|
|
||||||
N_st, N_rx, _ = csi.shape
|
|
||||||
unwrapped = np.unwrap(np.angle(csi[:, :, 0]), axis=0).reshape(N_st, N_rx, 1)
|
|
||||||
|
|
||||||
if visualiser:
|
|
||||||
visualiser(unwrapped, visualise.figures.Figure.UNWRAPPED_PHASE)
|
|
||||||
|
|
||||||
X = np.vstack([np.arange(N_st), np.ones(N_st)]).T
|
|
||||||
for antenna in range(csi.shape[1]):
|
|
||||||
tau, rho = np.linalg.lstsq(X, unwrapped[:, antenna, 0])[0]
|
|
||||||
csi[:, antenna, 0] = np.abs(csi[:, antenna, 0]) * np.exp(
|
|
||||||
1j
|
|
||||||
* (np.angle(csi[:, antenna, 0]) - (tau * np.arange(csi.shape[0]) + rho))
|
|
||||||
)
|
|
||||||
|
|
||||||
return csi
|
|
||||||
|
|
||||||
def remove_agc(self, csi: CSIMatrix, frames: dict[CSIHost, CSI]) -> CSIMatrix:
|
|
||||||
"""
|
|
||||||
Normalise the magnitude of the CSI data to compensate for the effect of the
|
|
||||||
Automatic Gain Control (AGC) of the receiver
|
|
||||||
|
|
||||||
References:
|
|
||||||
[1] -
|
|
||||||
"""
|
|
||||||
rssi = [
|
|
||||||
frames[host].header.rssi1 if antenna == 0 else frames[host].header.rssi2
|
|
||||||
for host, antenna in config.antennas.order
|
|
||||||
]
|
|
||||||
rssi_linear = np.reshape(10 ** (np.array(rssi) / 10), (1, -1, 1))
|
|
||||||
csi_power = np.sum(np.abs(csi) ** 2)
|
|
||||||
return csi * np.sqrt(rssi_linear / csi_power)
|
|
||||||
|
|
||||||
def skip_subcarriers(self, csi: CSIMatrix) -> CSIMatrix:
|
|
||||||
"""
|
|
||||||
Sometimes beccause of the large amount of processing, we need to skip some
|
|
||||||
subcarriers for the processing to be able to run in real time.
|
|
||||||
|
|
||||||
The subcarriers to skip are defined in the config file, per subcarrier_step. If
|
|
||||||
subcarrier_step is set to 1 (default), no subcarriers are skipped.
|
|
||||||
"""
|
|
||||||
return csi[:: config.preprocessing.subcarrier_step, :, :]
|
|
||||||
|
|
||||||
def fill_pilots(self, csi: CSIMatrix) -> CSIMatrix:
|
|
||||||
"""
|
|
||||||
Fill the pilot subcarriers with the average of the surrounding subcarriers.
|
|
||||||
|
|
||||||
This is done by averaging the subcarriers before and after the pilot
|
|
||||||
subcarriers. Pilots are detected by checking that the value is exactly 0.
|
|
||||||
|
|
||||||
Also, it adds placeholders for the middle null subcarriers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
num_middle = 1 if config.channel_width == 20 else 3
|
|
||||||
assert csi.shape[0] + num_middle == config.subcarriers
|
|
||||||
with_middle = np.zeros(
|
|
||||||
(csi.shape[0] + num_middle, csi.shape[1], csi.shape[2]), dtype=np.complex64
|
|
||||||
)
|
|
||||||
with_middle[: csi.shape[0] // 2, :, :] = csi[: csi.shape[0] // 2, :, :]
|
|
||||||
with_middle[csi.shape[0] // 2 + num_middle :, :, :] = csi[
|
|
||||||
csi.shape[0] // 2 :, :, :
|
|
||||||
]
|
|
||||||
if num_middle == 3:
|
|
||||||
with_middle[csi.shape[0] // 2 + 1, :, :] = (
|
|
||||||
csi[csi.shape[0] // 2 - 1, :, :] + csi[csi.shape[0] // 2, :, :]
|
|
||||||
) / 2
|
|
||||||
return np.where(
|
|
||||||
np.expand_dims(with_middle[:, 0, 0] == 0, axis=(1, 2)),
|
|
||||||
correlate(with_middle, [[[1 / 2]], [[0]], [[1 / 2]]], mode="same"),
|
|
||||||
with_middle,
|
|
||||||
)
|
|
||||||
|
|
||||||
def bandpass(self, csi: CSIMatrix) -> CSIMatrix:
|
|
||||||
"""
|
|
||||||
Apply a Butterworth bandpass filter to the CSI data.
|
|
||||||
|
|
||||||
Remove low-frequency noise (caused by static paths) and high-frequency noise
|
|
||||||
(measurement variance).
|
|
||||||
"""
|
|
||||||
if not hasattr(self, "filter"):
|
|
||||||
assert config.preprocessing.bandpass is not None
|
|
||||||
self.filter = butter(
|
|
||||||
5,
|
|
||||||
config.preprocessing.bandpass.bounds,
|
|
||||||
fs=config.collection_sample_rate,
|
|
||||||
btype="band",
|
|
||||||
output="sos",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not hasattr(self, "filter_zi"):
|
|
||||||
self.filter_zi = (
|
|
||||||
np.expand_dims(sosfilt_zi(self.filter), axis=(-1, -2, -3)) * csi
|
|
||||||
)
|
|
||||||
|
|
||||||
h_hat_filt, self.filter_zi = sosfilt(
|
|
||||||
self.filter, [csi], zi=self.filter_zi, axis=0
|
|
||||||
)
|
|
||||||
|
|
||||||
return h_hat_filt[0]
|
|
||||||
|
|
||||||
def preprocess(
|
|
||||||
self,
|
|
||||||
h: CSIMatrix,
|
|
||||||
frames: dict[CSIHost, CSI],
|
|
||||||
visualiser: None
|
|
||||||
| Callable[[npt.NDArray[Any], visualise.figures.Figure], None] = None,
|
|
||||||
) -> CSIMatrix:
|
|
||||||
# CSI data is not available for pilot subcarriers.
|
|
||||||
h_hat = h
|
|
||||||
for step in config.preprocessing.steps:
|
|
||||||
match step:
|
|
||||||
case "skip_subcarriers":
|
|
||||||
h_hat = self.skip_subcarriers(h_hat)
|
|
||||||
case "remove_agc":
|
|
||||||
h_hat = self.remove_agc(h_hat, frames)
|
|
||||||
case "remove_sfo":
|
|
||||||
h_hat = self.remove_sfo(h_hat, visualiser=visualiser)
|
|
||||||
case "remove_sto":
|
|
||||||
h_hat = self.remove_sto(h_hat)
|
|
||||||
case "fill_pilots":
|
|
||||||
h_hat = self.fill_pilots(h_hat)
|
|
||||||
case "bandpass":
|
|
||||||
h_hat = self.bandpass(h_hat)
|
|
||||||
|
|
||||||
logger.debug(f"CSI shape: {h_hat.shape}")
|
|
||||||
|
|
||||||
self._last_sample = h_hat
|
|
||||||
|
|
||||||
if config.preprocessing.denoising != "none":
|
|
||||||
self.circular_buffer[self.sample_index, : h_hat.shape[0]] = h_hat
|
|
||||||
self.sample_index = (self.sample_index + 1) % self.denoising_samples
|
|
||||||
|
|
||||||
return h_hat
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import logging
|
|
||||||
from collections import deque
|
|
||||||
from typing import Any, Collection
|
|
||||||
|
|
||||||
from where_fi.application import CSIApplication
|
|
||||||
|
|
||||||
from ..collection import ingest
|
|
||||||
from ..config import config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
AntennaIdentifier = tuple[ingest.Host, int]
|
|
||||||
order: list[AntennaIdentifier] = []
|
|
||||||
prev_unplugged: set[AntennaIdentifier] = set()
|
|
||||||
long_antenna_average: dict[AntennaIdentifier, deque[int]] = {
|
|
||||||
(host, 0): deque(maxlen=15 * config.collection_sample_rate)
|
|
||||||
for host in config.receive_hosts
|
|
||||||
} | {
|
|
||||||
(host, 1): deque(maxlen=15 * config.collection_sample_rate)
|
|
||||||
for host in config.receive_hosts
|
|
||||||
}
|
|
||||||
short_antenna_average: dict[AntennaIdentifier, deque[int]] = {
|
|
||||||
(host, 0): deque(maxlen=2 * config.collection_sample_rate)
|
|
||||||
for host in config.receive_hosts
|
|
||||||
} | {
|
|
||||||
(host, 1): deque(maxlen=2 * config.collection_sample_rate)
|
|
||||||
for host in config.receive_hosts
|
|
||||||
}
|
|
||||||
|
|
||||||
RSSI_THRESHOLD = 10
|
|
||||||
|
|
||||||
|
|
||||||
def average(data: Collection[Any]) -> float:
|
|
||||||
return sum(data) / len(data)
|
|
||||||
|
|
||||||
|
|
||||||
app = CSIApplication(ingest.RealtimeCSIProducer())
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_pre_merge
|
|
||||||
def callback(antenna_data: dict[ingest.Host, ingest.CSI]) -> None:
|
|
||||||
global prev_unplugged
|
|
||||||
global antenna_average
|
|
||||||
global order
|
|
||||||
unplugged: set[tuple[ingest.Host, int]] = set()
|
|
||||||
logger.debug(
|
|
||||||
"Antenna RSSI: "
|
|
||||||
+ str(
|
|
||||||
{
|
|
||||||
host: (csi.header.rssi1, csi.header.rssi2)
|
|
||||||
for host, csi in antenna_data.items()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for host, csi in antenna_data.items():
|
|
||||||
long_antenna_average[(host, 0)].append(csi.header.rssi1)
|
|
||||||
long_antenna_average[(host, 1)].append(csi.header.rssi2)
|
|
||||||
short_antenna_average[(host, 0)].append(csi.header.rssi1)
|
|
||||||
short_antenna_average[(host, 1)].append(csi.header.rssi2)
|
|
||||||
|
|
||||||
for antenna in range(2):
|
|
||||||
if (
|
|
||||||
average(short_antenna_average[(host, antenna)])
|
|
||||||
> average(long_antenna_average[(host, antenna)]) + RSSI_THRESHOLD
|
|
||||||
):
|
|
||||||
unplugged.add((host, antenna))
|
|
||||||
|
|
||||||
if prev_unplugged != unplugged:
|
|
||||||
if len(prev_unplugged) > len(unplugged):
|
|
||||||
logger.info(f"Antenna plugged in: {prev_unplugged - unplugged}")
|
|
||||||
else:
|
|
||||||
logger.info(f"Antenna unplugged: {unplugged - prev_unplugged}")
|
|
||||||
diff = list(unplugged - prev_unplugged)
|
|
||||||
if len(diff) == 1 and diff[0] not in order:
|
|
||||||
host, antenna = diff.pop()
|
|
||||||
order.append((host, antenna))
|
|
||||||
logger.info(f"Current order: {order}")
|
|
||||||
prev_unplugged = unplugged
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
app.start()
|
|
||||||
2
where_fi/visualise/.gitignore
vendored
2
where_fi/visualise/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
generated
|
|
||||||
frontend/src/grpc
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
all: generated frontend/src/grpc
|
|
||||||
|
|
||||||
generated: protos/*.proto
|
|
||||||
mkdir -p generated
|
|
||||||
find protos/ -type f -name "*.proto" | xargs uv run python -m grpc_tools.protoc -Iprotos --python_out=generated --pyi_out=generated --grpc_python_out=generated --mypy_grpc_out=generated
|
|
||||||
|
|
||||||
find protos/ -type f -name "*.proto" | xargs uv run protol --create-package --in-place --python-out generated/ protoc --proto-path=protos/
|
|
||||||
|
|
||||||
frontend/src/grpc: protos/*.proto
|
|
||||||
mkdir -p frontend/src/grpc
|
|
||||||
cd frontend && find ../protos/ -name "*.proto" | xargs npx protoc --ts_out=src/grpc -I../protos/
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
admin:
|
|
||||||
access_log_path: /tmp/admin_access.log
|
|
||||||
address:
|
|
||||||
socket_address: { address: 0.0.0.0, port_value: 9901 }
|
|
||||||
|
|
||||||
static_resources:
|
|
||||||
listeners:
|
|
||||||
- name: listener_0
|
|
||||||
address:
|
|
||||||
socket_address: { address: 0.0.0.0, port_value: 8080 }
|
|
||||||
filter_chains:
|
|
||||||
- filters:
|
|
||||||
- name: envoy.filters.network.http_connection_manager
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
|
||||||
codec_type: auto
|
|
||||||
stat_prefix: ingress_http
|
|
||||||
route_config:
|
|
||||||
name: local_route
|
|
||||||
virtual_hosts:
|
|
||||||
- name: local_service
|
|
||||||
domains: ["*"]
|
|
||||||
routes:
|
|
||||||
- match: { prefix: "/" }
|
|
||||||
route:
|
|
||||||
cluster: echo_service
|
|
||||||
timeout: 0s
|
|
||||||
max_stream_duration:
|
|
||||||
grpc_timeout_header_max: 0s
|
|
||||||
cors:
|
|
||||||
allow_origin_string_match:
|
|
||||||
- prefix: "*"
|
|
||||||
allow_methods: GET, PUT, DELETE, POST, OPTIONS
|
|
||||||
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
|
|
||||||
max_age: "1728000"
|
|
||||||
expose_headers: custom-header-1,grpc-status,grpc-message
|
|
||||||
http_filters:
|
|
||||||
- name: envoy.filters.http.grpc_web
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
|
|
||||||
- name: envoy.filters.http.cors
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
|
|
||||||
- name: envoy.filters.http.router
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
|
||||||
clusters:
|
|
||||||
- name: echo_service
|
|
||||||
connect_timeout: 0.25s
|
|
||||||
type: logical_dns
|
|
||||||
# HTTP/2 support
|
|
||||||
typed_extension_protocol_options:
|
|
||||||
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
|
|
||||||
explicit_http_config:
|
|
||||||
http2_protocol_options: {}
|
|
||||||
lb_policy: round_robin
|
|
||||||
load_assignment:
|
|
||||||
cluster_name: cluster_0
|
|
||||||
endpoints:
|
|
||||||
- lb_endpoints:
|
|
||||||
- endpoint:
|
|
||||||
address:
|
|
||||||
socket_address:
|
|
||||||
address: 127.0.0.1
|
|
||||||
port_value: 50051
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
import grpc
|
|
||||||
|
|
||||||
from ..generated import figure_pb2, figure_pb2_grpc
|
|
||||||
|
|
||||||
|
|
||||||
def run() -> None:
|
|
||||||
# NOTE(gRPC Python Team): .close() is possible on a channel and should be
|
|
||||||
# used in circumstances in which the with statement does not fit the needs
|
|
||||||
# of the code.
|
|
||||||
with grpc.insecure_channel("localhost:50051") as channel:
|
|
||||||
stub = figure_pb2_grpc.FigureServiceStub(channel)
|
|
||||||
for figure in stub.GetFigure(figure_pb2.FigureRequest()):
|
|
||||||
print(f"Figure: {figure}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
logging.basicConfig()
|
|
||||||
run()
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
import grpc
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from ..generated import figure_pb2, figure_pb2_grpc
|
|
||||||
|
|
||||||
app = typer.Typer()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
|
||||||
def list() -> None:
|
|
||||||
"""
|
|
||||||
List all figures from the gRPC service.
|
|
||||||
"""
|
|
||||||
print("Connecting to the server...")
|
|
||||||
with grpc.insecure_channel("localhost:50051") as channel:
|
|
||||||
stub = figure_pb2_grpc.FigureServiceStub(channel)
|
|
||||||
print("Retrieving available figures...\n")
|
|
||||||
figures = [x for x in stub.GetFigure(figure_pb2.FigureRequest())]
|
|
||||||
|
|
||||||
if not figures:
|
|
||||||
print("No figures found.")
|
|
||||||
else:
|
|
||||||
for i, figure in enumerate(figures, 1):
|
|
||||||
print(f"{i}. Figure: {figure.title} (ID: {figure.uuid})")
|
|
||||||
|
|
||||||
choice = typer.prompt("Which figure to extract?", type=int)
|
|
||||||
assert isinstance(choice, int)
|
|
||||||
if 1 <= choice <= len(figures):
|
|
||||||
selected_figure = figures[choice - 1]
|
|
||||||
print(f"You selected: {selected_figure.title}")
|
|
||||||
extract(selected_figure.uuid)
|
|
||||||
else:
|
|
||||||
print("Invalid selection. Exiting.")
|
|
||||||
|
|
||||||
|
|
||||||
def get_figure(uuid: str) -> tuple[figure_pb2.Figure, figure_pb2.FigureData]:
|
|
||||||
"""
|
|
||||||
Get the figure data for a specific UUID.
|
|
||||||
"""
|
|
||||||
print(f"Connecting to the server to extract data for UUID: {uuid}...")
|
|
||||||
with grpc.insecure_channel("localhost:50051") as channel:
|
|
||||||
stub = figure_pb2_grpc.FigureServiceStub(channel)
|
|
||||||
all_figures = {x.uuid: x for x in stub.GetFigure(figure_pb2.FigureRequest())}
|
|
||||||
figure_data = stub.GetFigureUpdate(figure_pb2.FigureDataRequest(uuid=uuid))
|
|
||||||
for data in figure_data:
|
|
||||||
return (all_figures[uuid], data)
|
|
||||||
raise ValueError(f"Figure with UUID {uuid} not found.")
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
|
||||||
def extract(uuid: str, output: Path | None = None) -> None:
|
|
||||||
"""
|
|
||||||
Extract data for a specific figure identified by its UUID.
|
|
||||||
"""
|
|
||||||
_, figure = get_figure(uuid)
|
|
||||||
if not output:
|
|
||||||
output = cast(Path, typer.prompt("Enter output file path:", type=Path))
|
|
||||||
with open(output, "wb") as f:
|
|
||||||
f.write(figure.SerializeToString())
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
|
||||||
def plot(uuid: str) -> None:
|
|
||||||
"""
|
|
||||||
Plot the figure data for a specific UUID.
|
|
||||||
"""
|
|
||||||
figure, data = get_figure(uuid)
|
|
||||||
if data.line:
|
|
||||||
for line in data.line.lines:
|
|
||||||
plt.plot(
|
|
||||||
line.x if line.x else np.arange(len(line.y)), line.y, label=line.label
|
|
||||||
)
|
|
||||||
plt.xlabel(figure.x_label)
|
|
||||||
plt.ylabel(figure.y_label)
|
|
||||||
plt.title(figure.title)
|
|
||||||
elif figure.heatmap:
|
|
||||||
plt.imshow(figure.heatmap.data, cmap="hot", interpolation="nearest")
|
|
||||||
elif figure.histogram:
|
|
||||||
plt.hist(figure.histogram.data, bins=figure.histogram.bins)
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app()
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
|
||||||
charset = utf-8
|
|
||||||
indent_size = 2
|
|
||||||
indent_style = space
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
end_of_line = lf
|
|
||||||
max_line_length = 100
|
|
||||||
1
where_fi/visualise/frontend/.gitattributes
vendored
1
where_fi/visualise/frontend/.gitattributes
vendored
@ -1 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
||||||
31
where_fi/visualise/frontend/.gitignore
vendored
31
where_fi/visualise/frontend/.gitignore
vendored
@ -1,31 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
coverage
|
|
||||||
*.local
|
|
||||||
.vite/
|
|
||||||
|
|
||||||
/cypress/videos/
|
|
||||||
/cypress/screenshots/
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
*.tsbuildinfo
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/prettierrc",
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
||||||
1
where_fi/visualise/frontend/env.d.ts
vendored
1
where_fi/visualise/frontend/env.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import pluginVue from 'eslint-plugin-vue'
|
|
||||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
|
||||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
|
||||||
|
|
||||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
|
||||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
|
||||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
|
||||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
|
||||||
|
|
||||||
export default defineConfigWithVueTs(
|
|
||||||
{
|
|
||||||
name: 'app/files-to-lint',
|
|
||||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'app/files-to-ignore',
|
|
||||||
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
|
||||||
},
|
|
||||||
|
|
||||||
pluginVue.configs['flat/essential'],
|
|
||||||
vueTsConfigs.recommended,
|
|
||||||
skipFormatting,
|
|
||||||
)
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Where-Fi Visualizer</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
5326
where_fi/visualise/frontend/package-lock.json
generated
5326
where_fi/visualise/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "visualise",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"build-only": "vite build",
|
|
||||||
"type-check": "vue-tsc --build",
|
|
||||||
"lint": "eslint . --fix",
|
|
||||||
"format": "prettier --write src/"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@mdi/font": "^7.4.47",
|
|
||||||
"@protobuf-ts/grpcweb-transport": "^2.9.4",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"google-protobuf": "^3.21.4",
|
|
||||||
"grpc-web": "^1.5.0",
|
|
||||||
"plotly.js-dist": "^3.0.1",
|
|
||||||
"plotly.js-dist-min": "^3.0.1",
|
|
||||||
"vue": "^3.5.13",
|
|
||||||
"vuetify": "^3.7.12"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@protobuf-ts/plugin": "^2.9.4",
|
|
||||||
"@tsconfig/node22": "^22.0.0",
|
|
||||||
"@types/node": "^22.13.1",
|
|
||||||
"@types/plotly.js": "^2.35.2",
|
|
||||||
"@types/plotly.js-dist-min": "^2.3.4",
|
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
|
||||||
"@vue/eslint-config-prettier": "^10.1.0",
|
|
||||||
"@vue/eslint-config-typescript": "^14.3.0",
|
|
||||||
"@vue/tsconfig": "^0.7.0",
|
|
||||||
"eslint": "^9.18.0",
|
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
|
||||||
"jiti": "^2.4.2",
|
|
||||||
"npm-run-all2": "^7.0.2",
|
|
||||||
"prettier": "^3.4.2",
|
|
||||||
"typescript": "~5.7.3",
|
|
||||||
"vite": "^6.0.11",
|
|
||||||
"vite-plugin-vue-devtools": "^7.7.1",
|
|
||||||
"vue-tsc": "^2.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import PlotList from './components/PlotList.vue'
|
|
||||||
import Figure from './components/Figure.vue'
|
|
||||||
import { Figure as FigureType } from './grpc/figure'
|
|
||||||
|
|
||||||
var plots = ref<FigureType[]>([])
|
|
||||||
var paused = ref(false)
|
|
||||||
|
|
||||||
function addPlot(fig: FigureType) {
|
|
||||||
console.log('Plot added', fig)
|
|
||||||
plots.value.push(fig)
|
|
||||||
console.log(plots)
|
|
||||||
}
|
|
||||||
function removePlot(fig: FigureType) {
|
|
||||||
console.log('Plot removed', fig)
|
|
||||||
plots.value = plots.value.filter((x) => x.uuid !== fig.uuid)
|
|
||||||
console.log(plots)
|
|
||||||
}
|
|
||||||
declare module '@vue/runtime-core' {
|
|
||||||
interface ComponentCustomProperties {
|
|
||||||
$props: {
|
|
||||||
onClick?: (e: MouseEvent) => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-app>
|
|
||||||
<PlotList @figure-selected="addPlot" @figure-deselected="removePlot" />
|
|
||||||
|
|
||||||
<v-main>
|
|
||||||
<v-container fluid>
|
|
||||||
<Figure v-for="plot in plots" :key="plot.uuid" :figure="plot" :paused="paused" />
|
|
||||||
</v-container>
|
|
||||||
<v-fab location="right bottom" color="secondary" @click="paused = !paused" icon app>
|
|
||||||
<v-icon :icon="paused ? 'mdi-play' : 'mdi-pause'"></v-icon>
|
|
||||||
</v-fab>
|
|
||||||
</v-main>
|
|
||||||
</v-app>
|
|
||||||
</template>
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
|
||||||
:root {
|
|
||||||
--vt-c-white: #ffffff;
|
|
||||||
--vt-c-white-soft: #f8f8f8;
|
|
||||||
--vt-c-white-mute: #f2f2f2;
|
|
||||||
|
|
||||||
--vt-c-black: #181818;
|
|
||||||
--vt-c-black-soft: #222222;
|
|
||||||
--vt-c-black-mute: #282828;
|
|
||||||
|
|
||||||
--vt-c-indigo: #2c3e50;
|
|
||||||
|
|
||||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
|
||||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
|
||||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
|
||||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
|
||||||
|
|
||||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
|
||||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
|
||||||
--vt-c-text-dark-1: var(--vt-c-white);
|
|
||||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* semantic color variables for this project */
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-white);
|
|
||||||
--color-background-soft: var(--vt-c-white-soft);
|
|
||||||
--color-background-mute: var(--vt-c-white-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-light-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-light-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-light-1);
|
|
||||||
--color-text: var(--vt-c-text-light-1);
|
|
||||||
|
|
||||||
--section-gap: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-background: var(--vt-c-black);
|
|
||||||
--color-background-soft: var(--vt-c-black-soft);
|
|
||||||
--color-background-mute: var(--vt-c-black-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-dark-2);
|
|
||||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-dark-1);
|
|
||||||
--color-text: var(--vt-c-text-dark-2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-background);
|
|
||||||
transition:
|
|
||||||
color 0.5s,
|
|
||||||
background-color 0.5s;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import * as Plotly from 'plotly.js-dist-min'
|
|
||||||
import { Figure, FigureData } from '../grpc/figure'
|
|
||||||
import { FigureServiceClient } from '../grpc/figure.client'
|
|
||||||
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport'
|
|
||||||
import { host } from '../connect'
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { onBeforeUnmount } from 'vue'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const { figure, paused = false } = defineProps<{ figure: Figure; paused?: boolean }>()
|
|
||||||
const cancel = ref<boolean>(false)
|
|
||||||
|
|
||||||
function reshape(data: number[], width: number) {
|
|
||||||
if (data.length % width != 0) {
|
|
||||||
throw new Error('Data length is not divisible by width')
|
|
||||||
}
|
|
||||||
const height = data.length / width
|
|
||||||
const result: number[][] = new Array(height)
|
|
||||||
for (let i = 0; i < height; i++) {
|
|
||||||
result[i] = data.slice(i * width, (i + 1) * width)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = ref<FigureData | null>(null)
|
|
||||||
const plotData = computed(() => {
|
|
||||||
if (data.value) {
|
|
||||||
switch (data.value.figure.oneofKind) {
|
|
||||||
case 'line':
|
|
||||||
return data.value.figure.line.lines.map((line) => ({
|
|
||||||
x: line.x.length == 0 ? undefined : line.x,
|
|
||||||
y: line.y,
|
|
||||||
type: 'scatter' as const,
|
|
||||||
name: line.label,
|
|
||||||
color: line.color ? line.color : undefined,
|
|
||||||
}))
|
|
||||||
case 'heatmap':
|
|
||||||
const xmin = data.value.figure.heatmap.xMin ?? 0
|
|
||||||
const xmax = data.value.figure.heatmap.xMax ?? data.value.figure.heatmap.width
|
|
||||||
const ymin = data.value.figure.heatmap.yMin ?? 0
|
|
||||||
const ymax = data.value.figure.heatmap.yMax ?? data.value.figure.heatmap.height
|
|
||||||
const x = Array(data.value.figure.heatmap.width)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => xmin + ((xmax - xmin) * i) / data.value.figure.heatmap.width)
|
|
||||||
const y = Array(data.value.figure.heatmap.height)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => ymin + ((ymax - ymin) * i) / data.value.figure.heatmap.height)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
z: reshape(data.value.figure.heatmap.data, data.value.figure.heatmap.width),
|
|
||||||
x: x,
|
|
||||||
y: y,
|
|
||||||
type: 'heatmap' as const,
|
|
||||||
colorscale: 'Blues',
|
|
||||||
reversescale: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
case 'histogram':
|
|
||||||
const bin_start = data.value.figure.histogram.bins.slice(0, -1)
|
|
||||||
const bin_end = data.value.figure.histogram.bins.slice(1)
|
|
||||||
const bin_center = bin_start.map((start, i) => (start + bin_end[i]) / 2)
|
|
||||||
const bin_width = bin_start.map((start, i) => bin_end[i] - start)
|
|
||||||
console.log('Histogram data:', bin_center, bin_width)
|
|
||||||
console.log(
|
|
||||||
'Sizes:',
|
|
||||||
bin_center.length,
|
|
||||||
bin_width.length,
|
|
||||||
data.value.figure.histogram.data.length,
|
|
||||||
)
|
|
||||||
return data.value.figure.histogram.data.map((series) => ({
|
|
||||||
x: bin_center,
|
|
||||||
y: series.data,
|
|
||||||
width: bin_width,
|
|
||||||
type: 'bar' as const,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
const layout = {
|
|
||||||
title: { text: figure.title },
|
|
||||||
xaxis: { title: { text: figure.xLabel }, type: figure.logx ? 'log' : undefined },
|
|
||||||
yaxis: { title: { text: figure.yLabel }, type: figure.logy ? 'log' : undefined },
|
|
||||||
height: 700,
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGraph() {
|
|
||||||
console.log('Updating graph with', plotData.value)
|
|
||||||
Plotly.newPlot(figure.uuid, plotData.value, layout, { responsive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
console.log('Mounted')
|
|
||||||
|
|
||||||
updateGraph()
|
|
||||||
|
|
||||||
const transport = new GrpcWebFetchTransport({ baseUrl: host })
|
|
||||||
const figureService = new FigureServiceClient(transport)
|
|
||||||
const stream = figureService.getFigureUpdate({ uuid: figure.uuid })
|
|
||||||
for await (const response of stream.responses) {
|
|
||||||
if (cancel.value) {
|
|
||||||
console.log('Stopping stream...')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
console.log(`Got new value for ${figure.uuid}:`, response)
|
|
||||||
|
|
||||||
if (!paused) {
|
|
||||||
data.value = response
|
|
||||||
updateGraph()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
console.log('Component is about to unmount, stopping stream...')
|
|
||||||
cancel.value = true
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-row class="w-100">
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-card class="w-100">
|
|
||||||
<v-card-title>{{ figure.title }}</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<div :id="figure.uuid" class="w-100" />
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from 'vue'
|
|
||||||
import { Figure } from '../grpc/figure'
|
|
||||||
import { FigureServiceClient } from '../grpc/figure.client'
|
|
||||||
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport'
|
|
||||||
import { host } from '../connect'
|
|
||||||
|
|
||||||
const plots = ref<Figure[]>([])
|
|
||||||
const loading = ref(true)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
var emit = (_: any, ...args: any[]) => {
|
|
||||||
console.log('No emit function set', args)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ['figure-selected', 'figure-deselected'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selected: [],
|
|
||||||
loading: loading,
|
|
||||||
plots: plots,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(_, ctx) {
|
|
||||||
console.log('Setup')
|
|
||||||
emit = ctx.emit
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
const transport = new GrpcWebFetchTransport({ baseUrl: host })
|
|
||||||
const figureService = new FigureServiceClient(transport)
|
|
||||||
const stream = figureService.getFigure({})
|
|
||||||
for await (const response of stream.responses) {
|
|
||||||
console.log('Got new figure', response)
|
|
||||||
plots.value.push(response)
|
|
||||||
}
|
|
||||||
console.log('Done fetching figures')
|
|
||||||
loading.value = false
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
selected(newVal: string[], oldVal: string[]) {
|
|
||||||
const added = newVal.filter((x) => !oldVal.includes(x))
|
|
||||||
const removed = oldVal.filter((x) => !newVal.includes(x))
|
|
||||||
|
|
||||||
for (const uuid of added) {
|
|
||||||
console.log('Selected', uuid)
|
|
||||||
const plot = plots.value.find((x) => x.uuid === uuid)
|
|
||||||
emit('figure-selected', plot)
|
|
||||||
}
|
|
||||||
for (const uuid of removed) {
|
|
||||||
console.log('Deselected', uuid)
|
|
||||||
const plot = plots.value.find((x) => x.uuid === uuid)
|
|
||||||
emit('figure-deselected', plot)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-navigation-drawer permanent width="250">
|
|
||||||
<v-list>
|
|
||||||
<v-list-item title="Available figures" subtitle="Choose some of the figures below" />
|
|
||||||
</v-list>
|
|
||||||
<v-divider />
|
|
||||||
<v-list v-model:selected="selected" select-strategy="leaf" nav>
|
|
||||||
<v-list-item v-for="plot in plots" :key="plot.uuid" :value="plot.uuid">
|
|
||||||
<v-list-item-title>{{ plot.title }}</v-list-item-title>
|
|
||||||
<v-list-item-subtitle class="text-high-emphasis"
|
|
||||||
>{{ plot.xLabel }} w/ {{ plot.yLabel }}</v-list-item-subtitle
|
|
||||||
>
|
|
||||||
</v-list-item>
|
|
||||||
<v-skeleton-loader
|
|
||||||
type="list-item-two-line"
|
|
||||||
v-for="n in 3"
|
|
||||||
:key="n"
|
|
||||||
v-if="loading"
|
|
||||||
></v-skeleton-loader>
|
|
||||||
</v-list>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export const host = 'http://localhost:8080'
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
import 'vuetify/styles'
|
|
||||||
import { createVuetify } from 'vuetify'
|
|
||||||
import * as components from 'vuetify/components'
|
|
||||||
import * as directives from 'vuetify/directives'
|
|
||||||
import { aliases, mdi } from 'vuetify/iconsets/mdi'
|
|
||||||
import '@mdi/font/css/materialdesignicons.css'
|
|
||||||
|
|
||||||
const vuetify = createVuetify({
|
|
||||||
components,
|
|
||||||
directives,
|
|
||||||
theme: { defaultTheme: 'dark' },
|
|
||||||
icons: {
|
|
||||||
defaultSet: 'mdi',
|
|
||||||
aliases,
|
|
||||||
sets: {
|
|
||||||
mdi,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
createApp(App).use(vuetify).mount('#app')
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
|
||||||
"exclude": ["src/**/__tests__/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vueCompilerOptions": {
|
|
||||||
"fallthroughAttributes": true,
|
|
||||||
"strictTemplates": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.node.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@tsconfig/node22/tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"vite.config.*",
|
|
||||||
"vitest.config.*",
|
|
||||||
"cypress.config.*",
|
|
||||||
"nightwatch.conf.*",
|
|
||||||
"playwright.config.*",
|
|
||||||
"eslint.config.*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": true,
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"types": ["node"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue(), vueDevTools()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
import "figure_type/line.proto";
|
|
||||||
import "figure_type/heatmap.proto";
|
|
||||||
import "figure_type/histogram.proto";
|
|
||||||
|
|
||||||
message Figure {
|
|
||||||
string uuid = 1;
|
|
||||||
string title = 2;
|
|
||||||
string x_label = 3;
|
|
||||||
string y_label = 4;
|
|
||||||
|
|
||||||
bool logx = 5;
|
|
||||||
bool logy = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message FigureData {
|
|
||||||
string uuid = 1;
|
|
||||||
oneof figure {
|
|
||||||
LineChartData line = 2;
|
|
||||||
HeatmapData heatmap = 3;
|
|
||||||
HistogramData histogram = 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message FigureRequest {}
|
|
||||||
message FigureDataRequest { string uuid = 1; }
|
|
||||||
|
|
||||||
service FigureService {
|
|
||||||
rpc GetFigure(FigureRequest) returns (stream Figure) {}
|
|
||||||
rpc GetFigureUpdate(FigureDataRequest) returns (stream FigureData) {}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
message HeatmapData {
|
|
||||||
string uuid = 1;
|
|
||||||
repeated float data = 2;
|
|
||||||
uint32 width = 3;
|
|
||||||
uint32 height = 4;
|
|
||||||
string cmap = 5;
|
|
||||||
float x_min = 6;
|
|
||||||
float x_max = 7;
|
|
||||||
float y_min = 8;
|
|
||||||
float y_max = 9;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
message HistogramSeries { repeated float data = 1; }
|
|
||||||
|
|
||||||
message HistogramData {
|
|
||||||
string uuid = 1;
|
|
||||||
repeated HistogramSeries data = 2;
|
|
||||||
repeated float bins = 3;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
message LineChartData {
|
|
||||||
message Line {
|
|
||||||
string uuid = 1;
|
|
||||||
repeated float x = 2;
|
|
||||||
repeated float y = 3;
|
|
||||||
string label = 4;
|
|
||||||
string color = 5;
|
|
||||||
}
|
|
||||||
repeated Line lines = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message LineChartPoint {
|
|
||||||
string uuid = 1;
|
|
||||||
float x = 2;
|
|
||||||
float y = 3;
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
import logging
|
|
||||||
import multiprocessing as mp
|
|
||||||
import queue
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from concurrent import futures
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Generator
|
|
||||||
|
|
||||||
import grpc
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from ..generated import figure_pb2, figure_pb2_grpc
|
|
||||||
from . import figures
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class VisualiserData:
|
|
||||||
data: npt.NDArray[Any]
|
|
||||||
dtype: figures.FigureId
|
|
||||||
|
|
||||||
|
|
||||||
class FigureServer(figure_pb2_grpc.FigureServiceServicer):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
self.clients: dict[str, list[queue.Queue[figure_pb2.FigureData]]] = {}
|
|
||||||
self.clients_lock = threading.Lock()
|
|
||||||
|
|
||||||
def GetFigure(
|
|
||||||
self, request: figure_pb2.FigureRequest, context: grpc.ServicerContext
|
|
||||||
) -> Generator[figure_pb2.Figure, None, None]:
|
|
||||||
for figure_group in figures.all_figures.values():
|
|
||||||
for figure in figure_group.figures:
|
|
||||||
yield figure
|
|
||||||
|
|
||||||
def GetFigureUpdate(
|
|
||||||
self, request: figure_pb2.FigureDataRequest, context: grpc.ServicerContext
|
|
||||||
) -> Generator[figure_pb2.FigureData, None, None]:
|
|
||||||
self.logger.info(
|
|
||||||
f"Received request for figure data stream for figure {request.uuid}"
|
|
||||||
)
|
|
||||||
|
|
||||||
with self.clients_lock:
|
|
||||||
q: queue.Queue[figure_pb2.FigureData] = queue.Queue()
|
|
||||||
if request.uuid not in self.clients:
|
|
||||||
self.clients[request.uuid] = []
|
|
||||||
self.clients[request.uuid].append(q)
|
|
||||||
|
|
||||||
try:
|
|
||||||
while context.is_active():
|
|
||||||
try:
|
|
||||||
yield q.get(timeout=1)
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
with self.clients_lock:
|
|
||||||
self.clients[request.uuid].remove(q)
|
|
||||||
|
|
||||||
|
|
||||||
class Webapp:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
self.active = True
|
|
||||||
self.figure_server = FigureServer()
|
|
||||||
|
|
||||||
def add_data(
|
|
||||||
self, dtype: figures.FigureId, new_data: npt.NDArray[np.complex128]
|
|
||||||
) -> None:
|
|
||||||
self.logger.debug(f"Adding data to figure server {dtype}")
|
|
||||||
if dtype not in figures.all_figures:
|
|
||||||
self.logger.error(f"Figure {dtype} not found")
|
|
||||||
return
|
|
||||||
updates = figures.all_figures[dtype].update(new_data)
|
|
||||||
for fig_id, update in updates.items():
|
|
||||||
for client in self.figure_server.clients.get(fig_id, []):
|
|
||||||
client.put(update)
|
|
||||||
|
|
||||||
def listen_for_data(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
|
||||||
while self.active:
|
|
||||||
self.logger.debug("Listening for data")
|
|
||||||
try:
|
|
||||||
data = data_queue.get(timeout=0.1)
|
|
||||||
self.add_data(data.dtype, data.data)
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
|
|
||||||
while not data_queue.empty():
|
|
||||||
data = data_queue.get()
|
|
||||||
|
|
||||||
def start(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
|
||||||
grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
|
||||||
figure_pb2_grpc.add_FigureServiceServicer_to_server(
|
|
||||||
self.figure_server, grpc_server
|
|
||||||
)
|
|
||||||
grpc_server.add_insecure_port("[::]:50051")
|
|
||||||
grpc_server.add_insecure_port("0.0.0.0:50051")
|
|
||||||
self.logger.info("Starting server on port 50051")
|
|
||||||
grpc_server.start()
|
|
||||||
self.logger.info("Server started")
|
|
||||||
|
|
||||||
data_thread = threading.Thread(target=self.listen_for_data, args=(data_queue,))
|
|
||||||
data_thread.start()
|
|
||||||
|
|
||||||
while self.active:
|
|
||||||
time.sleep(1)
|
|
||||||
self.logger.debug("Server is running")
|
|
||||||
grpc_server.stop(0.5)
|
|
||||||
data_thread.join()
|
|
||||||
@ -1,316 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from enum import Enum
|
|
||||||
from functools import reduce
|
|
||||||
from typing import Any, Callable, Sequence, cast
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from ...config import config
|
|
||||||
from .generated import figure_pb2
|
|
||||||
from .generated.figure_type import heatmap_pb2, histogram_pb2, line_pb2
|
|
||||||
|
|
||||||
FigureUpdate = dict[str, figure_pb2.FigureData]
|
|
||||||
|
|
||||||
|
|
||||||
class SpecificFigure(ABC):
|
|
||||||
"""
|
|
||||||
This is a base class for all figures that can be visualised through the
|
|
||||||
visualisation server.
|
|
||||||
"""
|
|
||||||
|
|
||||||
figures: Sequence[figure_pb2.Figure]
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def __init__(self) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
|
||||||
"""
|
|
||||||
Update the figure with new data.
|
|
||||||
|
|
||||||
The exact shape and format of the data passed as an argument will differ
|
|
||||||
depending on the exact figure being plotted.
|
|
||||||
|
|
||||||
The return value should be a dictionary with the UUID of the figure as the key
|
|
||||||
and the new data as the value.
|
|
||||||
|
|
||||||
This allows one class to update multiple figures at once (e.g. a figure plotting
|
|
||||||
the phase and amplitude of a signal).
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleLineChart:
|
|
||||||
"""Helper class to create a simple line chart with one or more lines.
|
|
||||||
|
|
||||||
This allows generalising the creation of line charts, e.g. as in PerAntennaFigure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, title: str, x_label: str, y_label: str) -> None:
|
|
||||||
self.figure = figure_pb2.Figure(
|
|
||||||
uuid=str(uuid.uuid4()),
|
|
||||||
title=title,
|
|
||||||
x_label=x_label,
|
|
||||||
y_label=y_label,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
new_data: npt.NDArray[np.complex128],
|
|
||||||
labels: Sequence[str],
|
|
||||||
x_values: None | npt.NDArray[np.float64] = None,
|
|
||||||
) -> FigureUpdate:
|
|
||||||
lines = [
|
|
||||||
line_pb2.LineChartData.Line(
|
|
||||||
y=new_data[i],
|
|
||||||
label=labels[i],
|
|
||||||
x=x_values[i] if x_values is not None else None,
|
|
||||||
)
|
|
||||||
for i in range(new_data.shape[0])
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
self.figure.uuid: figure_pb2.FigureData(
|
|
||||||
uuid=self.figure.uuid,
|
|
||||||
line=line_pb2.LineChartData(lines=lines),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PerSubcarrierFigure(SpecificFigure):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
figures: Sequence[SimpleLineChart],
|
|
||||||
funcs: Sequence[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]],
|
|
||||||
) -> None:
|
|
||||||
self.charts = figures
|
|
||||||
self.figures = [figure.figure for figure in figures]
|
|
||||||
self.funcs = funcs
|
|
||||||
|
|
||||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
|
||||||
"""
|
|
||||||
Update the figure with new data.
|
|
||||||
|
|
||||||
The data is expected to be in the shape (subcarriers, data).
|
|
||||||
"""
|
|
||||||
subcarrier_labels = [f"Subcarrier {i}" for i in range(new_data.shape[0])]
|
|
||||||
updates = [
|
|
||||||
figure.update(func(new_data), labels=subcarrier_labels)
|
|
||||||
for func, figure in zip(self.funcs, self.charts, strict=True)
|
|
||||||
]
|
|
||||||
return reduce((lambda a, b: a | b), updates)
|
|
||||||
|
|
||||||
|
|
||||||
class LabelledMultiLineChart(SpecificFigure):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
figures: Sequence[SimpleLineChart],
|
|
||||||
funcs: list[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]],
|
|
||||||
labels: Sequence[str],
|
|
||||||
) -> None:
|
|
||||||
self.charts = figures
|
|
||||||
self.figures = [figure.figure for figure in figures]
|
|
||||||
self.funcs = funcs
|
|
||||||
self.labels = labels
|
|
||||||
|
|
||||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
|
||||||
data_by_antenna = new_data[:, :, 0].T
|
|
||||||
updates = [
|
|
||||||
figure.update(func(data_by_antenna), labels=self.labels)
|
|
||||||
for func, figure in zip(self.funcs, self.charts, strict=True)
|
|
||||||
]
|
|
||||||
return reduce((lambda a, b: a | b), updates)
|
|
||||||
|
|
||||||
|
|
||||||
class PerAntennaFigure(LabelledMultiLineChart):
|
|
||||||
"""A figure that plots data for each antenna separately.
|
|
||||||
|
|
||||||
This allows creating multiple figures, each having one line per antenna.
|
|
||||||
|
|
||||||
Each figure can have a different function that is used to transform the data before
|
|
||||||
plotting. For example, can be used to generate plots for the phase and amplitude of
|
|
||||||
a signal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
figures: Sequence[SimpleLineChart],
|
|
||||||
funcs: list[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]],
|
|
||||||
) -> None:
|
|
||||||
super().__init__(
|
|
||||||
figures, funcs, [f"Antenna {i + 1}" for i in range(config.antennas.count)]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MusicEigenvalueHistogram(SpecificFigure):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.figure = figure_pb2.Figure(
|
|
||||||
uuid=str(uuid.uuid4()),
|
|
||||||
title="AoA Eigenvalues",
|
|
||||||
x_label="Eigenvalue",
|
|
||||||
y_label="Frequency of occurrence",
|
|
||||||
logx=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.figures = [self.figure]
|
|
||||||
|
|
||||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
|
||||||
magn = np.abs(new_data)
|
|
||||||
|
|
||||||
# The frontend library used for plotting doesn't support logarithmic
|
|
||||||
# binning[1], so we have to do it manually.
|
|
||||||
# Using base 10 log for the bins to make the plots easier to comprehend.
|
|
||||||
# [1] - https://github.com/plotly/plotly.js/issues/1844
|
|
||||||
bins = cast(
|
|
||||||
npt.NDArray[np.float32],
|
|
||||||
np.logspace(np.log10(magn.min()), np.log10(magn.max()), 10),
|
|
||||||
)
|
|
||||||
hist, _ = np.histogram(magn, bins=bins)
|
|
||||||
|
|
||||||
series = [histogram_pb2.HistogramSeries(data=hist)]
|
|
||||||
histogram = histogram_pb2.HistogramData(data=series, bins=bins)
|
|
||||||
return {self.figure.uuid: figure_pb2.FigureData(histogram=histogram)}
|
|
||||||
|
|
||||||
|
|
||||||
class HeatmapFigure(SpecificFigure):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.figure = figure_pb2.Figure(
|
|
||||||
uuid=str(uuid.uuid4()),
|
|
||||||
title="Angle of arrival Heatmap",
|
|
||||||
x_label="Angle of arrival",
|
|
||||||
y_label="Time of Flight",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.figures = [self.figure]
|
|
||||||
|
|
||||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
|
||||||
heatmap = heatmap_pb2.HeatmapData(
|
|
||||||
uuid=self.figure.uuid,
|
|
||||||
data=new_data.flatten(),
|
|
||||||
width=new_data.shape[1],
|
|
||||||
height=new_data.shape[0],
|
|
||||||
x_min=0,
|
|
||||||
x_max=np.pi,
|
|
||||||
y_min=0,
|
|
||||||
y_max=config.music.heatmap.tof_max,
|
|
||||||
)
|
|
||||||
return {self.figure.uuid: figure_pb2.FigureData(heatmap=heatmap)}
|
|
||||||
|
|
||||||
|
|
||||||
class EmpiricalCDF(SpecificFigure):
|
|
||||||
"""Helper class to create a graph of the empirical cumulative distribution function
|
|
||||||
and probability density functions of a set of observations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, title: str, x_label: str) -> None:
|
|
||||||
self.chart = SimpleLineChart(title, x_label, "Cumulative Probability")
|
|
||||||
self.figures = [self.chart.figure]
|
|
||||||
|
|
||||||
def update(self, new_data: npt.NDArray[np.float64]) -> FigureUpdate:
|
|
||||||
new_data = np.sort(new_data, axis=-1)
|
|
||||||
if new_data.ndim == 1:
|
|
||||||
new_data = np.expand_dims(new_data, 0)
|
|
||||||
y_values = np.repeat(
|
|
||||||
np.linspace(0, 1, new_data.shape[-1])[np.newaxis, :],
|
|
||||||
new_data.shape[0],
|
|
||||||
axis=0,
|
|
||||||
)
|
|
||||||
return self.chart.update(y_values, labels=["Frequency"], x_values=new_data)
|
|
||||||
|
|
||||||
|
|
||||||
class RandomVariable(SpecificFigure):
|
|
||||||
def get_cdf(
|
|
||||||
self, new_data: npt.NDArray[np.float64]
|
|
||||||
) -> tuple[npt.NDArray[np.floating[Any]], npt.NDArray[np.floating[Any]]]:
|
|
||||||
"""Get the cumulative distribution function of the data.
|
|
||||||
|
|
||||||
The data is expected to be in the shape (lines, data) or (data,) for single-line
|
|
||||||
charts.
|
|
||||||
"""
|
|
||||||
new_data = np.sort(new_data, axis=-1)
|
|
||||||
if new_data.ndim == 1:
|
|
||||||
new_data = np.expand_dims(new_data, 0)
|
|
||||||
y_values = np.expand_dims(np.linspace(0, 1, new_data.size), 0)
|
|
||||||
else:
|
|
||||||
y_values = np.repeat(
|
|
||||||
np.linspace(0, 1, new_data.shape[-1])[np.newaxis, :],
|
|
||||||
new_data.shape[0],
|
|
||||||
axis=0,
|
|
||||||
)
|
|
||||||
return new_data, y_values
|
|
||||||
|
|
||||||
def __init__(self, x_label: str, line_type: str = "Subcarrier") -> None:
|
|
||||||
self.charts = [
|
|
||||||
# SimpleLineChart(
|
|
||||||
# f"{x_label} Probability Density", x_label, "Probability Density"
|
|
||||||
# ),
|
|
||||||
SimpleLineChart(
|
|
||||||
f"{x_label} Cumulative Distribution", x_label, "Cumulative Probability"
|
|
||||||
),
|
|
||||||
]
|
|
||||||
self.funcs = [
|
|
||||||
# self.get_pdf,
|
|
||||||
self.get_cdf
|
|
||||||
]
|
|
||||||
self.figures = [figure.figure for figure in self.charts]
|
|
||||||
|
|
||||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
|
||||||
"""
|
|
||||||
Update the figure with new data.
|
|
||||||
|
|
||||||
The data is expected to be in the shape (subcarriers, data).
|
|
||||||
"""
|
|
||||||
subcarrier_labels = [f"Subcarrier {i}" for i in range(new_data.shape[0])]
|
|
||||||
updates: list[FigureUpdate] = []
|
|
||||||
for func, figure in zip(self.funcs, self.charts, strict=True):
|
|
||||||
x, y = func(new_data)
|
|
||||||
updates.append(
|
|
||||||
figure.update(
|
|
||||||
y,
|
|
||||||
labels=subcarrier_labels,
|
|
||||||
x_values=x,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return reduce((lambda a, b: a | b), updates)
|
|
||||||
|
|
||||||
|
|
||||||
class Figure(Enum):
|
|
||||||
RAW_CSI = 0
|
|
||||||
UNWRAPPED_PHASE = 1
|
|
||||||
PROCESSED_CSI = 2
|
|
||||||
MUSIC_EIGENVALUES = 3
|
|
||||||
AOA_HEATMAP = 4
|
|
||||||
PHASE_ANALYSIS = 5
|
|
||||||
MAGN_ANALYSIS = 6
|
|
||||||
|
|
||||||
def figure_class(self) -> SpecificFigure:
|
|
||||||
return all_figures[self]
|
|
||||||
|
|
||||||
|
|
||||||
FigureId = str | Figure
|
|
||||||
|
|
||||||
all_figures: dict[FigureId, SpecificFigure] = {
|
|
||||||
Figure.RAW_CSI: PerAntennaFigure(
|
|
||||||
[
|
|
||||||
SimpleLineChart("Raw CSI Phase", "Subcarrier", "Phase"),
|
|
||||||
SimpleLineChart("Raw CSI Amplitude", "Subcarrier", "Amplitude"),
|
|
||||||
],
|
|
||||||
[np.angle, np.abs],
|
|
||||||
),
|
|
||||||
Figure.UNWRAPPED_PHASE: PerAntennaFigure(
|
|
||||||
[SimpleLineChart("Unwrapped CSI Phase", "Subcarrier", "Phase")], [lambda x: x]
|
|
||||||
),
|
|
||||||
Figure.PROCESSED_CSI: PerAntennaFigure(
|
|
||||||
[
|
|
||||||
SimpleLineChart("Processed CSI Phase", "Subcarrier", "Phase"),
|
|
||||||
SimpleLineChart("Processed CSI Amplitude", "Subcarrier", "Amplitude"),
|
|
||||||
],
|
|
||||||
[np.angle, np.abs],
|
|
||||||
),
|
|
||||||
Figure.MUSIC_EIGENVALUES: MusicEigenvalueHistogram(),
|
|
||||||
Figure.AOA_HEATMAP: HeatmapFigure(),
|
|
||||||
Figure.PHASE_ANALYSIS: RandomVariable("Phase"),
|
|
||||||
Figure.MAGN_ANALYSIS: RandomVariable("Magnitude"),
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user