Merge tag 'v0.1.1' into HEAD am: b2e8fdebd7 am: bb8118c127

Original change: https://android-review.googlesource.com/c/platform/external/rust/pica/+/2488375

Change-Id: Iaef527b4ef51e23f5ea81a2a692c78f42e59c38a
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..e422ea1
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,34 @@
+# Build, test and check the code against the linter and clippy
+name: Build, Test, Format and Clippy
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  build_and_test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [macos-latest, ubuntu-latest, windows-latest]
+    steps:
+    - uses: actions/checkout@v3
+    - name: Install Rust 1.67.1
+      uses: actions-rs/toolchain@v1
+      with:
+        toolchain: 1.67.1
+        override: true
+        components: rustfmt, clippy
+    - name: Build
+      run: cargo build
+    - name: Test
+      run: cargo test -- --skip uci_packets
+    - name: Fmt
+      run: cargo fmt --check --quiet
+    - name: Clippy
+      run: cargo clippy --no-deps -- --deny warnings
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bdbc135
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+__pycache__
+target/
diff --git a/Cargo.lock b/Cargo.lock
index 37936d2..4e1828a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,32 +3,12 @@
 version = 3
 
 [[package]]
-name = "ansi_term"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
-dependencies = [
- "winapi",
-]
-
-[[package]]
 name = "anyhow"
 version = "1.0.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
 
 [[package]]
-name = "atty"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
-dependencies = [
- "hermit-abi",
- "libc",
- "winapi",
-]
-
-[[package]]
 name = "autocfg"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -47,6 +27,12 @@
 checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
 
 [[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -54,17 +40,60 @@
 
 [[package]]
 name = "clap"
-version = "2.34.0"
+version = "4.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5"
 dependencies = [
- "ansi_term",
- "atty",
  "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "is-terminal",
+ "once_cell",
  "strsim",
- "textwrap",
- "unicode-width",
- "vec_map",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
 ]
 
 [[package]]
@@ -120,12 +149,9 @@
 
 [[package]]
 name = "heck"
-version = "0.3.3"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
 name = "hermit-abi"
@@ -137,6 +163,12 @@
 ]
 
 [[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
 name = "hex"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -200,6 +232,28 @@
 ]
 
 [[package]]
+name = "io-lifetimes"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
+dependencies = [
+ "libc",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
 name = "itoa"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -213,9 +267,15 @@
 
 [[package]]
 name = "libc"
-version = "0.2.121"
+version = "0.2.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
 
 [[package]]
 name = "lock_api"
@@ -299,15 +359,21 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
 ]
 
 [[package]]
 name = "once_cell"
-version = "1.10.0"
+version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
 
 [[package]]
 name = "parking_lot"
@@ -329,7 +395,7 @@
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-sys",
+ "windows-sys 0.32.0",
 ]
 
 [[package]]
@@ -338,6 +404,7 @@
 dependencies = [
  "anyhow",
  "bytes",
+ "clap",
  "glam",
  "hex",
  "hyper",
@@ -345,7 +412,6 @@
  "num-traits",
  "serde",
  "serde_json",
- "structopt",
  "thiserror",
  "tokio",
  "tokio-stream",
@@ -389,11 +455,11 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.36"
+version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
 dependencies = [
- "unicode-xid",
+ "unicode-ident",
 ]
 
 [[package]]
@@ -415,6 +481,20 @@
 ]
 
 [[package]]
+name = "rustix"
+version = "0.36.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
 name = "ryu"
 version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -484,33 +564,9 @@
 
 [[package]]
 name = "strsim"
-version = "0.8.0"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-
-[[package]]
-name = "structopt"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
-dependencies = [
- "clap",
- "lazy_static",
- "structopt-derive",
-]
-
-[[package]]
-name = "structopt-derive"
-version = "0.4.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
-dependencies = [
- "heck",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
 [[package]]
 name = "syn"
@@ -524,12 +580,12 @@
 ]
 
 [[package]]
-name = "textwrap"
-version = "0.11.0"
+name = "termcolor"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
 dependencies = [
- "unicode-width",
+ "winapi-util",
 ]
 
 [[package]]
@@ -554,16 +610,15 @@
 
 [[package]]
 name = "tokio"
-version = "1.17.0"
+version = "1.18.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
+checksum = "0e050c618355082ae5a89ec63bbf897225d5ffe84c7c4e036874e4d185a5044e"
 dependencies = [
  "bytes",
  "libc",
  "memchr",
  "mio",
  "num_cpus",
- "once_cell",
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
@@ -642,16 +697,10 @@
 checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
 [[package]]
-name = "unicode-segmentation"
-version = "1.9.0"
+name = "unicode-ident"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
 
 [[package]]
 name = "unicode-xid"
@@ -660,12 +709,6 @@
 checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
 
 [[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
-[[package]]
 name = "version_check"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -704,6 +747,15 @@
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -715,39 +767,105 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6"
 dependencies = [
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_msvc",
+ "windows_aarch64_msvc 0.32.0",
+ "windows_i686_gnu 0.32.0",
+ "windows_i686_msvc 0.32.0",
+ "windows_x86_64_gnu 0.32.0",
+ "windows_x86_64_msvc 0.32.0",
 ]
 
 [[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
 name = "windows_aarch64_msvc"
 version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
 
 [[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
 name = "windows_i686_gnu"
 version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
 
 [[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
 name = "windows_i686_msvc"
 version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
 
 [[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
 name = "windows_x86_64_gnu"
 version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
 
 [[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
 name = "windows_x86_64_msvc"
 version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
diff --git a/Cargo.toml b/Cargo.toml
index da3611e..7b2582b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,13 +6,25 @@
   "David Duarte",
   "Henri Chataing",
 ]
-version = "0.1.0"
+version = "0.1.1"
 edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+[lib]
+name = "pica"
+path = "src/lib.rs"
+
+[[bin]]
+name = "server"
+path = "src/bin/server/mod.rs"
+
+[features]
+default = ["web"]
+web = ["hyper"]
+
 [dependencies]
-tokio = { version = "1.17.0", features = ["full"] }
+tokio = { version = "1.18.5", features = ["full"] }
 tokio-stream = { version = "0.1.8", features = ["sync"] }
 bytes = "1"
 anyhow = "1.0.56"
@@ -20,8 +32,8 @@
 num-traits = "*"
 thiserror = "*"
 glam = "0.20.3"
-hyper = { version = "0.14", features = ["server", "stream", "http1", "tcp"] }
+hyper = { version = "0.14", features = ["server", "stream", "http1", "tcp"], optional = true }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-structopt = "0.3.23"
 hex = "0.4.3"
+clap = { version = "4.1.8", features = ["derive"] }
diff --git a/README.md b/README.md
index 8a0ca22..aeee11e 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
 - Pica provides HTTP commands to interact with the scene directly such as create and destroy
   virtual anchors.
 
-# Build and run
+# Build and Run
 
 ```bash
 $> git clone https://github.com/google/pica.git
@@ -20,7 +20,7 @@
 $> cargo run
 ```
 
-You should have the following output:
+You should receive the following output:
 
 ```
 Pica: Listening on: 7000
diff --git a/scripts/__pycache__/uci_packets.cpython-310.pyc b/scripts/__pycache__/uci_packets.cpython-310.pyc
deleted file mode 100644
index 9487b58..0000000
--- a/scripts/__pycache__/uci_packets.cpython-310.pyc
+++ /dev/null
Binary files differ
diff --git a/scripts/console.py b/scripts/console.py
index 644b6ff..c06400a 100755
--- a/scripts/console.py
+++ b/scripts/console.py
@@ -28,9 +28,6 @@
 from concurrent.futures import ThreadPoolExecutor
 import uci_packets
 
-MAX_PAYLOAD_SIZE = 1024
-
-
 def encode_position(x: int, y: int, z: int, yaw: int, pitch: int, roll: int) -> bytes:
     return (struct.pack('<h', x)
             + struct.pack('<h', y)
diff --git a/src/bin/server/mod.rs b/src/bin/server/mod.rs
new file mode 100644
index 0000000..bfa0bdb
--- /dev/null
+++ b/src/bin/server/mod.rs
@@ -0,0 +1,86 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate bytes;
+extern crate num_derive;
+extern crate num_traits;
+extern crate thiserror;
+
+#[cfg(feature = "web")]
+mod web;
+
+use anyhow::Result;
+use clap::Parser;
+use pica::{Pica, PicaCommand};
+use std::net::{Ipv4Addr, SocketAddrV4};
+use std::path::PathBuf;
+use tokio::net::TcpListener;
+use tokio::sync::{broadcast, mpsc};
+use tokio::try_join;
+
+const DEFAULT_UCI_PORT: u16 = 7000;
+const DEFAULT_WEB_PORT: u16 = 3000;
+
+async fn accept_incoming(tx: mpsc::Sender<PicaCommand>, uci_port: u16) -> Result<()> {
+    let uci_socket = SocketAddrV4::new(Ipv4Addr::LOCALHOST, uci_port);
+    let uci_listener = TcpListener::bind(uci_socket).await?;
+    println!("Pica: Listening on: {}", uci_port);
+
+    loop {
+        let (socket, addr) = uci_listener.accept().await?;
+        println!("Uwb host addr: {}", addr);
+        tx.send(PicaCommand::Connect(socket)).await?
+    }
+}
+
+#[derive(Parser, Debug)]
+#[command(name = "pica", about = "Virtual UWB subsystem")]
+struct Args {
+    /// Output directory for storing .pcapng traces.
+    /// If provided, .pcapng traces of client connections are automatically
+    /// saved under the name `device-{handle}.pcapng`.
+    #[arg(short, long, value_name = "PCAPNG_DIR")]
+    pcapng_dir: Option<PathBuf>,
+    /// Configure the TCP port for the UCI server.
+    #[arg(short, long, value_name = "UCI_PORT", default_value_t = DEFAULT_UCI_PORT)]
+    uci_port: u16,
+    /// Configure the HTTP port for the web interface.
+    #[arg(short, long, value_name = "WEB_PORT", default_value_t = DEFAULT_WEB_PORT)]
+    web_port: u16,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let args = Args::parse();
+    assert_ne!(
+        args.uci_port, args.web_port,
+        "UCI port and Web port shall be different."
+    );
+    let (event_tx, _) = broadcast::channel(16);
+
+    let mut pica = Pica::new(event_tx.clone(), args.pcapng_dir);
+    let pica_tx = pica.tx();
+
+    #[cfg(feature = "web")]
+    try_join!(
+        accept_incoming(pica_tx.clone(), args.uci_port),
+        pica.run(),
+        web::serve(pica_tx, event_tx, args.web_port)
+    )?;
+
+    #[cfg(not(feature = "web"))]
+    try_join!(accept_incoming(pica_tx.clone(), args.uci_port), pica.run(),)?;
+
+    Ok(())
+}
diff --git a/src/web.rs b/src/bin/server/web.rs
similarity index 79%
rename from src/web.rs
rename to src/bin/server/web.rs
index 5744c8b..0af876f 100644
--- a/src/web.rs
+++ b/src/bin/server/web.rs
@@ -23,38 +23,37 @@
 use tokio::sync::{broadcast, mpsc, oneshot};
 use tokio_stream::{wrappers::BroadcastStream, StreamExt};
 
-use crate::position::Position;
-use crate::{Anchor, MacAddress, PicaCommand, PicaCommandError, PicaCommandStatus, PicaEvent};
+use pica::{
+    Category, MacAddress, PicaCommand, PicaCommandError, PicaCommandStatus, PicaEvent, Position,
+};
 use PicaEvent::{DeviceAdded, DeviceRemoved, DeviceUpdated, NeighborUpdated};
 
-const WEB_PORT: u16 = 3000;
-
 const STATIC_FILES: &[(&str, &str, &str)] = &[
-    ("/", "text/html", include_str!("../static/index.html")),
+    ("/", "text/html", include_str!("../../../static/index.html")),
     (
         "/openapi",
         "text/html",
-        include_str!("../static/openapi.html"),
+        include_str!("../../../static/openapi.html"),
     ),
     (
         "/openapi.yaml",
         "text/yaml",
-        include_str!("../static/openapi.yaml"),
+        include_str!("../../../static/openapi.yaml"),
     ),
     (
         "/src/components/Map.js",
         "application/javascript",
-        include_str!("../static/src/components/Map.js"),
+        include_str!("../../../static/src/components/Map.js"),
     ),
     (
         "/src/components/DeviceInfo.js",
         "application/javascript",
-        include_str!("../static/src/components/DeviceInfo.js"),
+        include_str!("../../../static/src/components/DeviceInfo.js"),
     ),
     (
         "/src/components/Orientation.js",
         "application/javascript",
-        include_str!("../static/src/components/Orientation.js"),
+        include_str!("../../../static/src/components/Orientation.js"),
     ),
 ];
 
@@ -101,40 +100,14 @@
     };
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
-pub enum Category {
-    Uci,
-    Anchor,
-}
-
 #[derive(Debug, Serialize, Clone)]
-pub struct Device {
+struct Device {
     pub category: Category,
     pub mac_address: String,
     #[serde(flatten)]
     pub position: Position,
 }
 
-impl Device {
-    pub fn new(category: Category, mac_address: MacAddress, position: Position) -> Self {
-        Self {
-            category,
-            mac_address: mac_address.to_string(),
-            position,
-        }
-    }
-}
-
-impl From<Anchor> for Device {
-    fn from(anchor: Anchor) -> Self {
-        Self {
-            category: Category::Anchor,
-            mac_address: anchor.mac_address.to_string(),
-            position: anchor.position,
-        }
-    }
-}
-
 fn event_name(event: &PicaEvent) -> &'static str {
     match event {
         DeviceAdded { .. } => "device-added",
@@ -168,16 +141,23 @@
         tx.send(pica_cmd).await.unwrap();
         let (status, description) = match pica_cmd_rsp_rx.await {
             Ok(Ok(_)) => (HttpStatusCode::OK, "success".into()),
-            Ok(Err(err)) => (match err {
-                PicaCommandError::DeviceAlreadyExists(_) => HttpStatusCode::CONFLICT,
-                PicaCommandError::DeviceNotFound(_) => HttpStatusCode::NOT_FOUND,
-            }, format!("{}", err)),
-            Err(err) =>
-                (HttpStatusCode::INTERNAL_SERVER_ERROR,
-                    format!("Error getting command response: {}", err))
+            Ok(Err(err)) => (
+                match err {
+                    PicaCommandError::DeviceAlreadyExists(_) => HttpStatusCode::CONFLICT,
+                    PicaCommandError::DeviceNotFound(_) => HttpStatusCode::NOT_FOUND,
+                },
+                format!("{}", err),
+            ),
+            Err(err) => (
+                HttpStatusCode::INTERNAL_SERVER_ERROR,
+                format!("Error getting command response: {}", err),
+            ),
         };
         println!("  status: {}, {}", status, description);
-        Response::builder().status(status).body(description.into()).unwrap()
+        Response::builder()
+            .status(status)
+            .body(description.into())
+            .unwrap()
     };
 
     match req
@@ -239,10 +219,19 @@
                 devices: Vec<Device>,
             }
             println!("PicaCommand: GetState");
-            let (state_tx, state_rx) = oneshot::channel::<Vec<Device>>();
+            let (state_tx, state_rx) = oneshot::channel::<Vec<_>>();
             tx.send(PicaCommand::GetState(state_tx)).await.unwrap();
             let devices = match state_rx.await {
-                Ok(devices) => GetStateResponse { devices },
+                Ok(devices) => GetStateResponse {
+                    devices: devices
+                        .into_iter()
+                        .map(|(category, mac_address, position)| Device {
+                            category,
+                            mac_address: mac_address.into(),
+                            position,
+                        })
+                        .collect(),
+                },
                 Err(_) => GetStateResponse { devices: vec![] },
             };
             let body = serde_json::to_string(&devices).unwrap();
@@ -258,8 +247,9 @@
 pub async fn serve(
     tx: mpsc::Sender<PicaCommand>,
     events: broadcast::Sender<PicaEvent>,
+    web_port: u16,
 ) -> Result<()> {
-    let addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, WEB_PORT);
+    let addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, web_port);
 
     let make_svc = make_service_fn(move |_conn| {
         let tx = tx.clone();
@@ -273,7 +263,7 @@
 
     let server = Server::bind(&addr.into()).serve(make_svc);
 
-    println!("Pica: Web server started on http://0.0.0.0:{}", WEB_PORT);
+    println!("Pica: Web server started on http://0.0.0.0:{}", web_port);
 
     server.await.context("Web Server Error")
 }
diff --git a/src/lib.rs b/src/lib.rs
index 66e07f7..e1ed9b7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,7 +14,7 @@
 
 use anyhow::Result;
 use bytes::{Bytes, BytesMut};
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 use std::fmt::Display;
 use std::path::PathBuf;
@@ -28,7 +28,7 @@
 mod pcapng;
 
 mod position;
-use position::Position;
+pub use position::Position;
 
 mod uci_packets;
 use uci_packets::StatusCode as UciStatusCode;
@@ -40,13 +40,13 @@
 mod session;
 use session::MAX_SESSION;
 
-pub mod web;
-use web::Category;
+mod mac_address;
+pub use mac_address::MacAddress;
 
-pub mod mac_address;
-use mac_address::MacAddress;
-
-const MAX_PAYLOAD_SIZE: usize = 4096;
+// UCI Generic Specification v1.1.0 § 4.4
+const HEADER_SIZE: usize = 4;
+const MAX_PAYLOAD_SIZE: usize = 255;
+const MAX_PACKET_SIZE: usize = HEADER_SIZE + MAX_PAYLOAD_SIZE;
 
 struct Connection {
     socket: TcpStream,
@@ -58,7 +58,7 @@
     fn new(socket: TcpStream, pcapng_file: Option<pcapng::File>) -> Self {
         Connection {
             socket,
-            buffer: BytesMut::with_capacity(MAX_PAYLOAD_SIZE),
+            buffer: BytesMut::with_capacity(MAX_PACKET_SIZE),
             pcapng_file,
         }
     }
@@ -118,7 +118,7 @@
     // Destroy Anchor
     DestroyAnchor(MacAddress, oneshot::Sender<PicaCommandStatus>),
     // Get State
-    GetState(oneshot::Sender<Vec<web::Device>>),
+    GetState(oneshot::Sender<Vec<(Category, MacAddress, Position)>>),
 }
 
 impl Display for PicaCommand {
@@ -143,25 +143,40 @@
 pub enum PicaEvent {
     // A Device was added
     DeviceAdded {
-        device: web::Device,
+        category: Category,
+        mac_address: MacAddress,
+        #[serde(flatten)]
+        position: Position,
     },
     // A Device was removed
     DeviceRemoved {
-        device: web::Device,
+        category: Category,
+        mac_address: MacAddress,
     },
     // A Device position has changed
     DeviceUpdated {
-        device: web::Device,
+        category: Category,
+        mac_address: MacAddress,
+        #[serde(flatten)]
+        position: Position,
     },
     NeighborUpdated {
-        source_device: web::Device,
-        destination_device: web::Device,
+        source_category: Category,
+        source_mac_address: MacAddress,
+        destination_category: Category,
+        destination_mac_address: MacAddress,
         distance: u16,
         azimuth: i16,
         elevation: i8,
     },
 }
 
+#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+pub enum Category {
+    Uci,
+    Anchor,
+}
+
 #[derive(Debug, Clone, Copy)]
 struct Anchor {
     mac_address: MacAddress,
@@ -298,7 +313,9 @@
         device.init();
 
         self.send_event(PicaEvent::DeviceAdded {
-            device: web::Device::new(Category::Uci, device.mac_address, device.position),
+            category: Category::Uci,
+            mac_address: device.mac_address,
+            position: device.position,
         });
 
         self.devices.insert(device_handle, device);
@@ -357,7 +374,8 @@
         {
             Ok(device) => {
                 self.send_event(PicaEvent::DeviceRemoved {
-                    device: web::Device::new(Category::Uci, device.mac_address, device.position),
+                    category: Category::Uci,
+                    mac_address: device.mac_address,
                 });
                 self.devices.remove(&device_handle);
             }
@@ -417,7 +435,7 @@
                 // TODO: support extended address
                 ShortMacTwoWayRangeDataNtfBuilder {
                     sequence_number: session.sequence_number,
-                    session_id: session_id as u32,
+                    session_id,
                     rcr_indicator: 0,            //TODO
                     current_ranging_interval: 0, //TODO
                     two_way_ranging_measurements: measurements,
@@ -448,9 +466,11 @@
         {
             Ok(device) => {
                 let response = device.command(cmd).into();
-                device.tx.send(response).await.unwrap_or_else(|err| {
-                    println!("Failed to send UCI command response: {}", err)
-                });
+                device
+                    .tx
+                    .send(response)
+                    .await
+                    .unwrap_or_else(|err| println!("Failed to send UCI command response: {}", err));
             }
             Err(err) => println!("{}", err),
         }
@@ -549,13 +569,15 @@
             }
         };
         self.send_event(PicaEvent::DeviceUpdated {
-            device: web::Device::new(category, mac_address, position),
+            category,
+            mac_address,
+            position,
         });
 
         let devices = self.devices.values().map(|d| (d.mac_address, d.position));
         let anchors = self.anchors.values().map(|b| (b.mac_address, b.position));
 
-        let update_neighbors = |category, device_mac_address, device_position| {
+        let update_neighbors = |device_category, device_mac_address, device_position| {
             if mac_address != device_mac_address {
                 let local = position.compute_range_azimuth_elevation(&device_position);
                 let remote = device_position.compute_range_azimuth_elevation(&position);
@@ -563,20 +585,20 @@
                 assert!(local.0 == remote.0);
 
                 self.send_event(PicaEvent::NeighborUpdated {
-                    source_device: web::Device::new(category, mac_address, position),
-                    destination_device: web::Device::new(
-                        category,
-                        device_mac_address,
-                        device_position,
-                    ),
+                    source_category: category,
+                    source_mac_address: mac_address,
+                    destination_category: device_category,
+                    destination_mac_address: device_mac_address,
                     distance: local.0,
                     azimuth: local.1,
                     elevation: local.2,
                 });
 
                 self.send_event(PicaEvent::NeighborUpdated {
-                    source_device: web::Device::new(category, device_mac_address, device_position),
-                    destination_device: web::Device::new(category, mac_address, position),
+                    source_category: device_category,
+                    source_mac_address: device_mac_address,
+                    destination_category: category,
+                    destination_mac_address: mac_address,
                     distance: remote.0,
                     azimuth: remote.1,
                     elevation: remote.2,
@@ -601,7 +623,9 @@
             Err(PicaCommandError::DeviceAlreadyExists(mac_address))
         } else {
             self.send_event(PicaEvent::DeviceAdded {
-                device: web::Device::new(Category::Anchor, mac_address, position),
+                category: Category::Anchor,
+                mac_address,
+                position,
             });
             assert!(self
                 .anchors
@@ -633,7 +657,8 @@
             Err(PicaCommandError::DeviceNotFound(mac_address))
         } else {
             self.send_event(PicaEvent::DeviceRemoved {
-                device: web::Device::new(Category::Anchor, mac_address, Position::default()),
+                category: Category::Anchor,
+                mac_address,
             });
             Ok(())
         };
@@ -642,17 +667,21 @@
         })
     }
 
-    fn get_state(&self, state_tx: oneshot::Sender<Vec<web::Device>>) {
+    fn get_state(&self, state_tx: oneshot::Sender<Vec<(Category, MacAddress, Position)>>) {
         println!("[_] Get State");
-        let web_devices: Vec<web::Device> = self
-            .anchors
-            .iter()
-            .map(|(_, anchor)| web::Device::from(*anchor))
-            .chain(self.devices.iter().map(|(_, uci_device)| {
-                web::Device::new(Category::Uci, uci_device.mac_address, uci_device.position)
-            }))
-            .collect();
 
-        state_tx.send(web_devices).unwrap();
+        state_tx
+            .send(
+                self.anchors
+                    .values()
+                    .map(|anchor| (Category::Anchor, anchor.mac_address, anchor.position))
+                    .chain(
+                        self.devices
+                            .values()
+                            .map(|device| (Category::Uci, device.mac_address, device.position)),
+                    )
+                    .collect(),
+            )
+            .unwrap();
     }
 }
diff --git a/src/mac_address.rs b/src/mac_address.rs
index 768bb6d..cc9d2c4 100644
--- a/src/mac_address.rs
+++ b/src/mac_address.rs
@@ -15,6 +15,7 @@
 use std::fmt::Display;
 
 use hex::FromHex;
+use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
 const SHORT_MAC_ADDRESS_SIZE: usize = 2;
@@ -28,7 +29,8 @@
     #[error("MacAddress has the wrong format: 0")]
     MacAddressWrongFormat(String),
 }
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(try_from = "String", into = "String")]
 pub enum MacAddress {
     Short([u8; SHORT_MAC_ADDRESS_SIZE]),
     Extend([u8; EXTEND_MAC_ADDRESS_SIZE]),
@@ -36,6 +38,19 @@
 
 impl MacAddress {
     pub fn new(mac_address: String) -> Result<Self, Error> {
+        mac_address.try_into()
+    }
+}
+
+impl From<usize> for MacAddress {
+    fn from(device_handle: usize) -> Self {
+        MacAddress::Extend(device_handle.to_be_bytes())
+    }
+}
+
+impl TryFrom<String> for MacAddress {
+    type Error = Error;
+    fn try_from(mac_address: String) -> std::result::Result<Self, Error> {
         let mac_address = mac_address.replace(':', "");
         let mac_address = mac_address.replace("%3A", "");
         let uwb_mac_address = match mac_address.len() {
@@ -53,14 +68,8 @@
     }
 }
 
-impl From<usize> for MacAddress {
-    fn from(device_handle: usize) -> Self {
-        MacAddress::Extend(device_handle.to_be_bytes())
-    }
-}
-
-impl Display for MacAddress {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl From<&MacAddress> for String {
+    fn from(mac_address: &MacAddress) -> Self {
         let to_string = |addr: &[u8]| -> String {
             let mac_address: Vec<_> = addr.iter().map(|byte| format!("{:02X}:", byte)).collect();
             let s = mac_address
@@ -69,13 +78,25 @@
                 .collect::<String>();
             s.trim_end_matches(':').into()
         };
-        match *self {
-            MacAddress::Short(address) => write!(f, "{}", to_string(&address)),
-            MacAddress::Extend(address) => write!(f, "{}", to_string(&address)),
+        match mac_address {
+            MacAddress::Short(address) => to_string(address),
+            MacAddress::Extend(address) => to_string(address),
         }
     }
 }
 
+impl From<MacAddress> for String {
+    fn from(mac_address: MacAddress) -> Self {
+        String::from(&mac_address)
+    }
+}
+
+impl Display for MacAddress {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", String::from(self))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index f789d57..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-extern crate bytes;
-extern crate num_derive;
-extern crate num_traits;
-extern crate thiserror;
-
-use pica::{web, Pica, PicaCommand};
-use std::path::PathBuf;
-
-use anyhow::Result;
-use std::net::{Ipv4Addr, SocketAddrV4};
-use structopt::StructOpt;
-use tokio::net::TcpListener;
-use tokio::sync::{broadcast, mpsc};
-use tokio::try_join;
-
-const UCI_PORT: u16 = 7000;
-
-async fn accept_incoming(tx: mpsc::Sender<PicaCommand>) -> Result<()> {
-    let uci_socket = SocketAddrV4::new(Ipv4Addr::LOCALHOST, UCI_PORT);
-    let uci_listener = TcpListener::bind(uci_socket).await?;
-    println!("Pica: Listening on: {}", UCI_PORT);
-
-    loop {
-        let (socket, addr) = uci_listener.accept().await?;
-        println!("Uwb host addr: {}", addr);
-        tx.send(PicaCommand::Connect(socket)).await?
-    }
-}
-
-#[derive(Debug, StructOpt)]
-#[structopt(name = "pica", about = "Virtual UWB subsystem")]
-struct Opts {
-    /// Output directory for storing .pcapng traces.
-    /// If provided, .pcapng traces of client connections are automatically
-    /// saved under the name `device-{handle}.pcapng`.
-    #[structopt(short, long, parse(from_os_str))]
-    pcapng_dir: Option<PathBuf>,
-}
-
-#[tokio::main]
-async fn main() -> Result<()> {
-    let opts = Opts::from_args();
-    let (event_tx, _) = broadcast::channel(16);
-
-    let mut pica = Pica::new(event_tx.clone(), opts.pcapng_dir);
-    let pica_tx = pica.tx();
-
-    try_join!(
-        accept_incoming(pica_tx.clone()),
-        pica.run(),
-        web::serve(pica_tx, event_tx)
-    )?;
-
-    Ok(())
-}
diff --git a/static/index.html b/static/index.html
index f33c5f1..84f83c2 100644
--- a/static/index.html
+++ b/static/index.html
@@ -123,7 +123,7 @@
     console.log("Device Added", data);
 
     const {
-      device: { mac_address, x, y, z, yaw, pitch, roll },
+      mac_address, x, y, z, yaw, pitch, roll,
     } = data;
     map.devices = [
       ...map.devices,
@@ -143,7 +143,7 @@
     console.log("Device Removed", data);
 
     const {
-      device: { mac_address },
+      mac_address,
     } = data;
     if (info.device?.mac_address === mac_address) {
       info.device = null;
@@ -165,7 +165,7 @@
     console.log("Position updated", data);
 
     const {
-      device: { mac_address, x, y, z, yaw, pitch, roll },
+      mac_address, x, y, z, yaw, pitch, roll,
     } = data;
 
     const device = map.devices.find(
@@ -186,20 +186,20 @@
     console.log("Neighbor updated", data);
 
     const {
-      source_device: { mac_address: src_mac_address },
-      destination_device: { mac_address: dest_mac_address },
+      source_mac_address,
+      destination_mac_address,
       distance,
       azimuth,
       elevation,
     } = data;
 
     const device = map.devices.find(
-      (device) => device.mac_address === src_mac_address
+      (device) => device.mac_address === source_mac_address
     );
 
     const neighbor = device.neighbors.find(
-      (device) => device.mac_address == dest_mac_address
-    ) || { mac_address: dest_mac_address };
+      (device) => device.mac_address == destination_mac_address
+    ) || { mac_address: destination_mac_address };
 
     neighbor.distance = distance;
     neighbor.azimuth = azimuth;
diff --git a/static/openapi.yaml b/static/openapi.yaml
index e1af749..7193836 100644
--- a/static/openapi.yaml
+++ b/static/openapi.yaml
@@ -54,19 +54,23 @@
         or an UCI Device as described in the Fira UCI Specification, noted `uci`.
       type: object
       properties:
-        Category:
-          description: Represents the device's category, uci or anchor.
-          type: string
-          enum: [uci, anchor]
-        MacAddress:
-          description: |
-            Valid UWB mac addresses must follow the above format
-              * Short Mode: "XX:XX"
-              * Extend Mode: "XX:XX:XX:XX:XX:XX:XX:XX"
-            where X is an hexadecimal number.
-          type: string
-        Position:
+        category:
+            $ref: "#/components/schemas/Category"
+        mac_address:
+            $ref: "#/components/schemas/MacAddress"
+        position:
             $ref: "#/components/schemas/Position"
+    Category:
+      description: Represents the device's category, uci or anchor.
+      type: string
+      enum: [uci, anchor]
+    MacAddress:
+      description: |
+        Valid UWB mac addresses must follow the above format
+          * Short Mode: "XX:XX"
+          * Extend Mode: "XX:XX:XX:XX:XX:XX:XX:XX"
+        where X is an hexadecimal number.
+      type: string
     Position:
       description:
         The position includes the Cartesian coordinates in cm, and the yaw, pitch, roll angles in degrees.
@@ -214,10 +218,7 @@
                              const: device-added
                              description: Device added to the scene
                            data:
-                             type: object
-                             properties:
-                               device:
-                                 $ref: "#/components/schemas/Device"
+                             $ref: "#/components/schemas/Device"
                       - type: object
                         properties:
                            event:
@@ -226,18 +227,17 @@
                            data:
                              type: object
                              properties:
-                               device:
-                                 $ref: "#/components/schemas/Device"
+                              category:
+                                  $ref: "#/components/schemas/Category"
+                              mac_address:
+                                  $ref: "#/components/schemas/MacAddress"
                       - type: object
                         properties:
                            event:
                              const: device-updated
                              description: Device position updated
                            data:
-                             type: object
-                             properties:
-                               device:
-                                 $ref: "#/components/schemas/Device"
+                             $ref: "#/components/schemas/Device"
                       - type: object
                         properties:
                            event:
@@ -246,10 +246,14 @@
                            data:
                              type: object
                              properties:
-                               source-device:
-                                 $ref: "#/components/schemas/Device"
-                               destination-device:
-                                 $ref: "#/components/schemas/Device"
+                               source_category:
+                                 $ref: "#/components/schemas/Category"
+                               source_mac_address:
+                                 $ref: "#/components/schemas/MacAddress"
+                               destination_category:
+                                 $ref: "#/components/schemas/Category"
+                               destination_mac_address:
+                                 $ref: "#/components/schemas/MacAddress"
                                distance:
                                  description: Distance in cm.
                                  type: integer # u16
@@ -258,7 +262,7 @@
                                azimuth:
                                  description: Azimuth in degrees
                                  type: integer
-                                 minium: -180
+                                 minimum: -180
                                  maximum: 180
                                elevation:
                                  description: Elevation is degrees
diff --git a/static/src/components/Map.js b/static/src/components/Map.js
index d1e86c4..e239414 100644
--- a/static/src/components/Map.js
+++ b/static/src/components/Map.js
@@ -66,7 +66,7 @@
     if (event.target.classList?.contains("handle")) {
       this.changingElevation = true;
     } else {
-      const element = event.path.find((el) => el.classList?.contains("marker"));
+      const element = event.composedPath().find((el) => el.classList?.contains("marker"));
       if (element) {
         const key = element.getAttribute("key");
         this.selected = this.devices[key];