Pedal WebSocket API
FastAPI WebSocket backend scaffold for UWB key exchange using GPS positions.
Requirements
- Python 3.11+
- Docker and Docker Compose, optional
Run Locally
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn src.main:app --reload
The API will be available at http://localhost:8000.
The deployed API is available at:
https://pedal.comtec.eecs.uni-kassel.de/
The deployed WebSocket endpoint is:
wss://pedal.comtec.eecs.uni-kassel.de/pedal-connect
Run With Docker
docker compose up --build
Build Registry Image
For Portainer deployments on a typical x86 server, build and push an amd64 Linux image:
scripts/build-linux-image.sh registry.example.com/pedal-websocket-api:latest
The script runs docker buildx build with --platform linux/amd64 and --push. For a multi-architecture image:
PLATFORM=linux/amd64,linux/arm64 scripts/build-linux-image.sh registry.example.com/pedal-websocket-api:latest
Endpoints
GET /renders this README documentation as the deployed landing page.GET /overviewreturns a basic HTML overview page with live counts, latest GPS positions, and a map.GET /overview/datareturns the JSON data used by the overview page.GET /debugreturns only the live overview counters for iPhone debugging.GET /statusreturns a basic status check.POST /uwb-connectionsacknowledges or ends UWB pair connections.WS /pedal-connectregisters clients with their UUID, UWB key, and GPS coordinates, stores them in an in-memory cache, and accepts measurement-ready messages from connected clients.WS /dead-reckoningreceives continuous iPhone dead-reckoning updates and relays each update to the other connected phones.
WebSocket sessions close automatically after 3 hours without data from the client.
/dead-reckoning is separate from /pedal-connect so high-rate ARKit dead reckoning does not interfere with GPS/UWB registration. Each payload identifies the sending phone with uuid or phone_id and includes a dead_reckoning object. The backend validates and normalizes the update, then broadcasts it to all other phones connected to /dead-reckoning; the sender does not receive its own update back.
Sessions are stored only in memory under src/data; nothing is persisted. Each open WebSocket connection owns one cache entry keyed by the UUID sent by the iPhone. Registration and position-update payloads carry uuid, GPS position, and uwb_key. Measurement-ready payloads carry uuid and GPS position, but no UWB key. New position data overwrites the GPS position and replaces the cached UWB key for that UUID. A measurement-ready payload also marks the device as ready to start the measurement session. The entry is removed when the connection closes. If the latest accepted GPS position for a session is 10 seconds old or older, the device is treated as not currently sharing GPS until a new position update arrives. The WebSocket connection remains open.
After every saved GPS position, GeoLinkService checks all other cached sessions that are currently sharing GPS. If another active GPS-sharing session is within GEO_LINK_RANGE meters, the backend sends each client a direct geo_link_established message containing the peer UUID and UWB key. The peer UUID is sent as both id and uuid for compatibility. If the same pair already has an active UWB cache entry, the backend suppresses further geo_link_established messages for that pair while they remain active.
GEO_LINK_RANGE is currently 1000.0 meters.
/pedal-connect also handles session start readiness. A device must already be connected on that WebSocket. The readiness message sends current GPS data on the same WebSocket, so the backend can verify readiness with the latest position before checking range:
{
"event": "messurement_ready",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.3127,
"longitude": 9.4797,
"accuracy": 5
}
The first readiness payload starts a 2 minute ready window. Once the backend detects the first two ready devices within GEO_LINK_RANGE, it shortens that window to 5 seconds so more nearby devices can still join the same exchange. When the window closes, the existing /pedal-connect WebSockets receive a peer message:
{
"event": "messurement_ready",
"id": "550e8400-e29b-41d4-a716-446655440001",
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"peer_uuid": "550e8400-e29b-41d4-a716-446655440001",
"peer_uuids": [
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002"
],
"ready_uuids": [
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002"
],
"ready_count": 3,
"ready": true
}
Ready exchanges are one-shot events. After the backend sends this message to a ready group, those devices are no longer treated as ready. If the same devices should start another measurement session later, they send a new readiness payload.
UWB pair state is also stored only in memory. POST /uwb-connections creates an inactive cache entry after the first device acknowledges a pair. When the second device acknowledges the same pair, the entry becomes active and both devices get a WebSocket confirmation. If one device ends UWB, all cached pair entries for that UUID are deleted and the paired devices are notified over WebSocket. The backend also deletes related UWB cache entries when a WebSocket session closes. Leaving GEO_LINK_RANGE does not end an active UWB cache entry. The internal UWB cache stores uuid1, uuid2, uwb1, uwb2, and status (active or inactive).
Overview Page
Open the deployed overview page in a browser:
https://pedal.comtec.eecs.uni-kassel.de/overview
The page is a regular HTTP endpoint, not a WebSocket. It loads once and then polls /overview/data with JavaScript, so the map view and zoom are not reset by full-page reloads. It shows:
- Count of currently open WebSocket connections.
- Count of devices currently sharing GPS, based on position updates younger than 10 seconds.
- Count of cached UWB connections.
- Device IDs for open sessions.
- Latest GPS position and accuracy per device.
- A map with one marker per open device session.
- A line between two GPS-sharing devices when they are currently within
GEO_LINK_RANGEand the backend would exchange their UWB keys.
The overview does not expose UWB keys.
Debug Counts
An iPhone can request the current live counters without loading the HTML overview page:
GET https://pedal.comtec.eecs.uni-kassel.de/debug
Example response:
{
"open_ws": 2,
"sharing_gps": 2,
"uwb_links": 1,
"geo_links": 1
}
sharing_gps only includes open sessions whose latest position update is less than 10 seconds old.
iOS can fetch the counters with a normal HTTP GET request:
import Foundation
struct PedalDebugCounts: Decodable {
let openWebSockets: Int
let sharingGPS: Int
let uwbLinks: Int
let geoLinks: Int
enum CodingKeys: String, CodingKey {
case openWebSockets = "open_ws"
case sharingGPS = "sharing_gps"
case uwbLinks = "uwb_links"
case geoLinks = "geo_links"
}
}
func fetchPedalDebugCounts(
completion: @escaping (Result<PedalDebugCounts, Error>) -> Void
) {
let url = URL(string: "https://pedal.comtec.eecs.uni-kassel.de/debug")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error {
completion(.failure(error))
return
}
guard
let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode),
let data
else {
completion(.failure(URLError(.badServerResponse)))
return
}
do {
let counts = try JSONDecoder().decode(PedalDebugCounts.self, from: data)
completion(.success(counts))
} catch {
completion(.failure(error))
}
}.resume()
}
fetchPedalDebugCounts { result in
switch result {
case .success(let counts):
print("Open WebSockets:", counts.openWebSockets)
print("Sharing GPS:", counts.sharingGPS)
print("UWB links:", counts.uwbLinks)
print("Geo links:", counts.geoLinks)
case .failure(let error):
print("Pedal debug request failed:", error)
}
}
Subscribe And Unsubscribe
Subscribe by opening a WebSocket connection to /pedal-connect and sending the device status payload after the socket opens:
const socket = new WebSocket("wss://pedal.comtec.eecs.uni-kassel.de/pedal-connect");
socket.onopen = () => {
socket.send(JSON.stringify({
uuid: "550e8400-e29b-41d4-a716-446655440000",
uwb_key: "AQIDBA==",
latitude: 51.3127,
longitude: 9.4797,
accuracy: 5
}));
};
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.event === "geo_link_established") {
console.log("Peer UUID:", message.id);
console.log("Peer UWB key:", message.uwbkey);
}
if (message.event === "uwb_connection_established") {
console.log("UWB connected with:", message.id);
}
if (message.event === "uwb_connection_ended") {
console.log("Disconnected peer UUID:", message.id);
console.log("UWB ended by:", message.ended_by);
}
};
Keep the same WebSocket open and send the same payload shape for later status updates. The cache entry is updated in place:
socket.send(JSON.stringify({
uuid: "550e8400-e29b-41d4-a716-446655440000",
uwb_key: "BQYHCA==",
latitude: 51.313,
longitude: 9.48,
accuracy: 4
}));
Unsubscribe by closing the WebSocket. The server removes the cached session entry for that UUID when the connection closes:
socket.close();
UWB Connection Acknowledgements
After a device receives a geo_link_established WebSocket event and completes the phone-to-phone UWB work, it can acknowledge the UWB pair over HTTP:
POST https://pedal.comtec.eecs.uni-kassel.de/uwb-connections
Content-Type: application/json
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"paired_uuid": "550e8400-e29b-41d4-a716-446655440001"
}
The first acknowledgement creates an inactive cache entry. When the paired device sends the reciprocal acknowledgement, the entry becomes active and both devices receive:
{
"event": "uwb_connection_established",
"id": "550e8400-e29b-41d4-a716-446655440001",
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"paired_uuid": "550e8400-e29b-41d4-a716-446655440001",
"connection": {
"uuid1": "550e8400-e29b-41d4-a716-446655440000",
"uuid2": "550e8400-e29b-41d4-a716-446655440001",
"status": "active"
}
}
To end all UWB connections for one device, post the device UUID to the same endpoint. The action field makes the intent explicit; a payload containing only uuid also ends all cached UWB connections for that device.
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"action": "end"
}
Every paired device receives:
{
"event": "uwb_connection_ended",
"id": "550e8400-e29b-41d4-a716-446655440000",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"paired_uuid": "550e8400-e29b-41d4-a716-446655440000",
"ended_by": "550e8400-e29b-41d4-a716-446655440000",
"connection": {
"uuid1": "550e8400-e29b-41d4-a716-446655440000",
"uuid2": "550e8400-e29b-41d4-a716-446655440001",
"status": "active"
}
}
iOS Client Integration
An iPhone subscribes by opening one WebSocket connection to /pedal-connect. Every location payload uses the same shape: the iPhone UUID, the current UWB key, and the current GPS position. The first payload registers the session. Later payloads on the same WebSocket update the GPS position and replace the cached UWB key. A device that is ready to start the measurement session sends a readiness payload with current GPS data on the same WebSocket. The connection is unsubscribed by closing the WebSocket.
Production URL
Use this endpoint for the deployed backend:
URL(string: "wss://pedal.comtec.eecs.uni-kassel.de/pedal-connect")!
Use this endpoint for UWB connection acknowledgements:
URL(string: "https://pedal.comtec.eecs.uni-kassel.de/uwb-connections")!
Use this endpoint for continuous dead-reckoning updates:
URL(string: "wss://pedal.comtec.eecs.uni-kassel.de/dead-reckoning")!
Development URL
For the iOS Simulator, the backend on your Mac is reachable with:
URL(string: "ws://localhost:8000/pedal-connect")!
URL(string: "ws://localhost:8000/dead-reckoning")!
For a physical iPhone, localhost means the phone itself. Run the backend on all interfaces and use your Mac's local network IP address:
uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
URL(string: "ws://192.168.178.42:8000/pedal-connect")!
URL(string: "ws://192.168.178.42:8000/dead-reckoning")!
The iPhone and backend machine must be on the same network, and the firewall must allow inbound connections to port 8000. Use wss:// in production.
iOS Permissions
Add location permission text to Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Pedal uses your location to exchange UWB keys with nearby devices.</string>
For local development against a backend on your LAN, iOS may also require:
<key>NSLocalNetworkUsageDescription</key>
<string>Pedal connects to a local development backend.</string>
If ws:// is blocked during local development, add a temporary App Transport Security exception for development builds only. Production deployments should use TLS and wss://.
Payload Models
Location payloads must contain uuid, uwb_key, and the current GPS position. Readiness payloads contain uuid and the current GPS position. On iOS, model uwb_key as Data. Because the WebSocket payload is JSON, JSONEncoder serializes Data as Base64 on the wire and JSONDecoder converts the peer uwbkey back into Data. Later updates must use the same uuid; the server overwrites the previous UWB key for that UUID whenever the payload is accepted.
import CoreLocation
import Foundation
struct PedalDeviceStatusPayload: Encodable {
let uuid: String
let uwbKey: Data
let latitude: Double
let longitude: Double
let accuracy: Double?
enum CodingKeys: String, CodingKey {
case uuid
case uwbKey = "uwb_key"
case latitude
case longitude
case accuracy
}
}
struct PedalGeoLinkMessage: Decodable {
let event: String
let id: String
let uuid: String?
let uwbkey: Data
}
struct PedalUWBAcknowledgementPayload: Encodable {
let uuid: String
let pairedUuid: String
enum CodingKeys: String, CodingKey {
case uuid
case pairedUuid = "paired_uuid"
}
}
struct PedalUWBEndPayload: Encodable {
let uuid: String
let action = "end"
}
struct PedalUWBConnectionMessage: Decodable {
let event: String
let id: String
let uuid: String
let pairedUuid: String
let endedBy: String?
enum CodingKeys: String, CodingKey {
case event
case id
case uuid
case pairedUuid = "paired_uuid"
case endedBy = "ended_by"
}
}
struct PedalMeasurementReadyPayload: Encodable {
let event = "messurement_ready"
let uuid: String
let latitude: Double
let longitude: Double
let accuracy: Double?
enum CodingKeys: String, CodingKey {
case event
case uuid
case latitude
case longitude
case accuracy
}
}
struct PedalMeasurementReadyMessage: Decodable {
let event: String
let id: String
let uuid: String
let peerUuid: String
let peerUuids: [String]?
let readyUuids: [String]?
let readyCount: Int?
let ready: Bool
enum CodingKeys: String, CodingKey {
case event
case id
case uuid
case peerUuid = "peer_uuid"
case peerUuids = "peer_uuids"
case readyUuids = "ready_uuids"
case readyCount = "ready_count"
case ready
}
}
struct DeadReckoningPayload: Encodable {
let event = "dead_reckoning_update"
let uuid: String
let phoneId: String
let deadReckoning: DeadReckoningData
enum CodingKeys: String, CodingKey {
case event
case uuid
case phoneId = "phone_id"
case deadReckoning = "dead_reckoning"
}
}
struct DeadReckoningMessage: Decodable {
let event: String
let uuid: String
let phoneId: String
let deadReckoning: DeadReckoningData
enum CodingKeys: String, CodingKey {
case event
case uuid
case phoneId = "phone_id"
case deadReckoning = "dead_reckoning"
}
}
struct DeadReckoningData: Codable {
let timestamp: String
let position: Vector3Payload
let eulerAngles: Vector3Payload
let heading: Double
let trackingState: String
enum CodingKeys: String, CodingKey {
case timestamp
case position
case eulerAngles = "euler_angles"
case heading
case trackingState = "tracking_state"
}
}
struct Vector3Payload: Codable {
let x: Double
let y: Double
let z: Double
}
WebSocket Client
Keep one URLSessionWebSocketTask open while the iPhone is available for matching. The receive loop stays active for server messages such as geo_link_established and messurement_ready.
import CoreLocation
import Foundation
final class PedalWebSocketClient {
private let uuid: String
private var uwbKey: Data
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private var task: URLSessionWebSocketTask?
init(uuid: UUID = UUID(), uwbKey: Data) {
self.uuid = uuid.uuidString
self.uwbKey = uwbKey
}
func connect(to url: URL) {
task = URLSession.shared.webSocketTask(with: url)
task?.resume()
receiveLoop()
}
func sendLocation(_ location: CLLocation) {
send(PedalDeviceStatusPayload(
uuid: uuid,
uwbKey: uwbKey,
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
accuracy: location.horizontalAccuracy
))
}
func sendReady(_ location: CLLocation) {
send(PedalMeasurementReadyPayload(
uuid: uuid,
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
accuracy: location.horizontalAccuracy
))
}
func updateUWBKey(_ newUWBKey: Data) {
uwbKey = newUWBKey
}
func disconnect() {
task?.cancel(with: .normalClosure, reason: nil)
task = nil
}
private func send<T: Encodable>(_ payload: T) {
do {
let data = try encoder.encode(payload)
guard let text = String(data: data, encoding: .utf8) else {
return
}
task?.send(.string(text)) { error in
if let error {
print("Pedal WebSocket send failed:", error)
}
}
} catch {
print("Pedal payload encoding failed:", error)
}
}
private func receiveLoop() {
task?.receive { [weak self] result in
guard let self else { return }
switch result {
case .success(let message):
self.handle(message)
self.receiveLoop()
case .failure(let error):
print("Pedal WebSocket receive failed:", error)
}
}
}
private func handle(_ message: URLSessionWebSocketTask.Message) {
let data: Data?
switch message {
case .string(let text):
data = text.data(using: .utf8)
case .data(let value):
data = value
@unknown default:
data = nil
}
guard let data else { return }
if let geoLink = try? decoder.decode(PedalGeoLinkMessage.self, from: data),
geoLink.event == "geo_link_established" {
print("Peer UUID:", geoLink.id)
print("Peer UWB key data:", geoLink.uwbkey)
}
if let readyMessage = try? decoder.decode(PedalMeasurementReadyMessage.self, from: data),
readyMessage.event == "messurement_ready",
readyMessage.ready {
print("Peers ready to start measurement:", readyMessage.peerUuids ?? [readyMessage.peerUuid])
}
if let uwbConnection = try? decoder.decode(PedalUWBConnectionMessage.self, from: data),
uwbConnection.event == "uwb_connection_established" {
print("UWB connected with:", uwbConnection.id)
}
if let uwbConnection = try? decoder.decode(PedalUWBConnectionMessage.self, from: data),
uwbConnection.event == "uwb_connection_ended" {
print("UWB disconnected peer:", uwbConnection.id)
print("UWB ended by:", uwbConnection.endedBy ?? "unknown")
}
}
}
Measurement Ready From iOS
Call sendReady(_:) on the existing /pedal-connect WebSocket with the latest CLLocation. The backend stores that GPS position first, then uses it to verify which ready devices are currently within GEO_LINK_RANGE. Readiness is batched for up to 2 minutes, then shortened to 5 seconds once the first nearby ready pair is found so three or more devices can join the same ready exchange. A successful ready exchange is one-shot, so call sendReady(_:) again for the next measurement session.
let ownUuid = UUID()
let uwbKeyData = Data([1, 2, 3, 4])
let pedalClient = PedalWebSocketClient(uuid: ownUuid, uwbKey: uwbKeyData)
pedalClient.connect(to: URL(string: "wss://pedal.comtec.eecs.uni-kassel.de/pedal-connect")!)
// currentLocation is the latest CLLocation from CLLocationManager.
pedalClient.sendReady(currentLocation)
Acknowledging UWB From iOS
Use the HTTP endpoint after the iPhone has completed its local UWB pairing work with the UUID received in geo_link_established.id.
func postJSON<T: Encodable>(_ payload: T, to url: URL) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
request.httpBody = try JSONEncoder().encode(payload)
} catch {
print("UWB payload encoding failed:", error)
return
}
URLSession.shared.dataTask(with: request) { _, _, error in
if let error {
print("UWB endpoint request failed:", error)
}
}.resume()
}
let uwbEndpoint = URL(string: "https://pedal.comtec.eecs.uni-kassel.de/uwb-connections")!
let ownUuid = UUID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!
let peerUuidString = "550e8400-e29b-41d4-a716-446655440001"
postJSON(
PedalUWBAcknowledgementPayload(
uuid: ownUuid.uuidString,
pairedUuid: peerUuidString
),
to: uwbEndpoint
)
postJSON(
PedalUWBEndPayload(uuid: ownUuid.uuidString),
to: uwbEndpoint
)
Sending Regular Position Updates
Use CLLocationManager to send the first available location and every later location update through the same WebSocket connection.
import CoreLocation
import Foundation
final class PedalLocationPublisher: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private let pedalClient: PedalWebSocketClient
init(pedalClient: PedalWebSocketClient) {
self.pedalClient = pedalClient
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 1
}
func start() {
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
func stop() {
locationManager.stopUpdatingLocation()
pedalClient.disconnect()
}
func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
guard let location = locations.last else {
return
}
pedalClient.sendLocation(location)
}
func locationManager(
_ manager: CLLocationManager,
didFailWithError error: Error
) {
print("Location updates failed:", error)
}
}
Typical usage:
let uwbKeyData = Data([1, 2, 3, 4])
let ownUuid = UUID()
let client = PedalWebSocketClient(uuid: ownUuid, uwbKey: uwbKeyData)
client.connect(to: URL(string: "wss://pedal.comtec.eecs.uni-kassel.de/pedal-connect")!)
let publisher = PedalLocationPublisher(pedalClient: client)
publisher.start()
// Later, when the user leaves the matching flow:
publisher.stop()
The server also closes idle WebSocket sessions after 3 hours without client data. Keep sending position updates while the iPhone should remain registered. If the app must keep sending location in the background, enable the iOS background location capability and request the corresponding location permission.
Quick WebSocket Test
Open the browser console while the API is running:
const socket = new WebSocket("wss://pedal.comtec.eecs.uni-kassel.de/pedal-connect");
socket.onmessage = (event) => console.log(event.data);
socket.onopen = () => socket.send(JSON.stringify({
uuid: "550e8400-e29b-41d4-a716-446655440000",
uwb_key: "AQIDBA==",
latitude: 51.3127,
longitude: 9.4797,
accuracy: 5
}));
Or use a WebSocket CLI client:
websocat wss://pedal.comtec.eecs.uni-kassel.de/pedal-connect
Device status payload:
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"uwb_key": "AQIDBA==",
"latitude": 51.3127,
"longitude": 9.4797,
"accuracy": 5
}
Subsequent position payloads on the same WebSocket must use the same shape and the same uuid. The cached UWB key for that UUID is overwritten on every accepted position payload. Manual JSON tests must send the binary key as Base64. The iOS app can keep using Data.
Measurement-ready payload on the same WebSocket:
{
"event": "messurement_ready",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"latitude": 51.3127,
"longitude": 9.4797,
"accuracy": 5
}
Dead-reckoning payload on /dead-reckoning:
{
"event": "dead_reckoning_update",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"phone_id": "550e8400-e29b-41d4-a716-446655440000",
"dead_reckoning": {
"timestamp": "2026-06-16T12:00:00.000Z",
"position": {
"x": 0.12,
"y": 0.01,
"z": -1.48
},
"euler_angles": {
"x": 0.01,
"y": -0.42,
"z": 0.03
},
"heading": 24.5,
"tracking_state": "normal"
}
}
Dead-reckoning messages are broadcast to the other /dead-reckoning clients in the same normalized shape, with an additional received_at timestamp inside dead_reckoning.
Geo link response:
{
"event": "geo_link_established",
"id": "550e8400-e29b-41d4-a716-446655440001",
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"uwbkey": "BQYHCA=="
}
Measurement-ready response when ready devices are currently within GEO_LINK_RANGE:
{
"event": "messurement_ready",
"id": "550e8400-e29b-41d4-a716-446655440001",
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"peer_uuid": "550e8400-e29b-41d4-a716-446655440001",
"peer_uuids": [
"550e8400-e29b-41d4-a716-446655440001"
],
"ready_uuids": [
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001"
],
"ready_count": 2,
"ready": true
}
Project Layout
.
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
└── src
├── data
│ ├── __init__.py
│ ├── cache.py
│ └── uwb_connections.py
├── __init__.py
├── main.py
├── repo
│ ├── __init__.py
│ ├── overview.py
│ ├── uwb_connections.py
│ └── websockets.py
└── service
├── __init__.py
└── geo_link.py