From 64037ca7ac3d1ed7eb402792d22a1eaf1a87f185 Mon Sep 17 00:00:00 2001
From: Sassan Haradji <me@sassanh.com>
Date: Tue, 24 Sep 2024 17:08:19 +0400
Subject: [PATCH] feat(rpc): add `rpc` service with `dispatch` method to let
 external services dispatch actions and events to the redux bus

---
 .gitignore                                    |    3 -
 CHANGELOG.md                                  |    1 +
 buf.yaml                                      |    3 +
 poetry.lock                                   |  140 +-
 pyproject.toml                                |   21 +-
 ubo_app/constants.py                          |    2 +
 ubo_app/main.py                               |   10 +-
 ubo_app/rpc/generated/__init__.py             |    0
 .../rpc/generated/package_info/__init__.py    |    0
 .../rpc/generated/package_info/v1/__init__.py |    8 +
 ubo_app/rpc/generated/store/__init__.py       |    0
 ubo_app/rpc/generated/store/v1/__init__.py    |  172 ++
 ubo_app/rpc/generated/ubo/__init__.py         |    0
 ubo_app/rpc/generated/ubo/v1/__init__.py      | 1946 +++++++++++++++++
 ubo_app/rpc/message_to_object.py              |  115 +
 ubo_app/rpc/proto/store/v1/store.proto        |   30 +
 ubo_app/rpc/proto/ubo/v1/ubo.proto            | 1419 ++++++++++++
 ubo_app/rpc/sample_python_client.py           |  132 ++
 ubo_app/rpc/server.py                         |   25 +
 ubo_app/rpc/service.py                        |   65 +
 20 files changed, 4084 insertions(+), 8 deletions(-)
 create mode 100644 buf.yaml
 create mode 100644 ubo_app/rpc/generated/__init__.py
 create mode 100644 ubo_app/rpc/generated/package_info/__init__.py
 create mode 100644 ubo_app/rpc/generated/package_info/v1/__init__.py
 create mode 100644 ubo_app/rpc/generated/store/__init__.py
 create mode 100644 ubo_app/rpc/generated/store/v1/__init__.py
 create mode 100644 ubo_app/rpc/generated/ubo/__init__.py
 create mode 100644 ubo_app/rpc/generated/ubo/v1/__init__.py
 create mode 100644 ubo_app/rpc/message_to_object.py
 create mode 100644 ubo_app/rpc/proto/store/v1/store.proto
 create mode 100644 ubo_app/rpc/proto/ubo/v1/ubo.proto
 create mode 100644 ubo_app/rpc/sample_python_client.py
 create mode 100644 ubo_app/rpc/server.py
 create mode 100644 ubo_app/rpc/service.py

diff --git a/.gitignore b/.gitignore
index f43ccb82..af2691c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,6 +89,3 @@ scripts/packer/output-*
 /screenshots
 /snapshot.json
 /snapshot.bin
-
-# rpc
-/ubo_app/rpc/proto/ubo/v1/ubo.proto
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9741ddff..db09ce47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Upcoming
 
 - feat(rpc): add a proto generator which parses actions and events files and generates proto files for them
+- feat(rpc): add `rpc` service with `dispatch` method to let external services dispatch actions and events to the redux bus
 
 ## Version 0.16.2
 
diff --git a/buf.yaml b/buf.yaml
new file mode 100644
index 00000000..0dbf15c1
--- /dev/null
+++ b/buf.yaml
@@ -0,0 +1,3 @@
+version: v2
+modules:
+  - path: ubo_app/rpc/proto/
diff --git a/poetry.lock b/poetry.lock
index 6782dcad..cd94a2b4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -899,6 +899,124 @@ files = [
     {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
 ]
 
+[[package]]
+name = "grpcio"
+version = "1.66.1"
+description = "HTTP/2-based RPC framework"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "grpcio-1.66.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:4877ba180591acdf127afe21ec1c7ff8a5ecf0fe2600f0d3c50e8c4a1cbc6492"},
+    {file = "grpcio-1.66.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3750c5a00bd644c75f4507f77a804d0189d97a107eb1481945a0cf3af3e7a5ac"},
+    {file = "grpcio-1.66.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:a013c5fbb12bfb5f927444b477a26f1080755a931d5d362e6a9a720ca7dbae60"},
+    {file = "grpcio-1.66.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1b24c23d51a1e8790b25514157d43f0a4dce1ac12b3f0b8e9f66a5e2c4c132f"},
+    {file = "grpcio-1.66.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7ffb8ea674d68de4cac6f57d2498fef477cef582f1fa849e9f844863af50083"},
+    {file = "grpcio-1.66.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:307b1d538140f19ccbd3aed7a93d8f71103c5d525f3c96f8616111614b14bf2a"},
+    {file = "grpcio-1.66.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c17ebcec157cfb8dd445890a03e20caf6209a5bd4ac5b040ae9dbc59eef091d"},
+    {file = "grpcio-1.66.1-cp310-cp310-win32.whl", hash = "sha256:ef82d361ed5849d34cf09105d00b94b6728d289d6b9235513cb2fcc79f7c432c"},
+    {file = "grpcio-1.66.1-cp310-cp310-win_amd64.whl", hash = "sha256:292a846b92cdcd40ecca46e694997dd6b9be6c4c01a94a0dfb3fcb75d20da858"},
+    {file = "grpcio-1.66.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:c30aeceeaff11cd5ddbc348f37c58bcb96da8d5aa93fed78ab329de5f37a0d7a"},
+    {file = "grpcio-1.66.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8a1e224ce6f740dbb6b24c58f885422deebd7eb724aff0671a847f8951857c26"},
+    {file = "grpcio-1.66.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a66fe4dc35d2330c185cfbb42959f57ad36f257e0cc4557d11d9f0a3f14311df"},
+    {file = "grpcio-1.66.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3ba04659e4fce609de2658fe4dbf7d6ed21987a94460f5f92df7579fd5d0e22"},
+    {file = "grpcio-1.66.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4573608e23f7e091acfbe3e84ac2045680b69751d8d67685ffa193a4429fedb1"},
+    {file = "grpcio-1.66.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7e06aa1f764ec8265b19d8f00140b8c4b6ca179a6dc67aa9413867c47e1fb04e"},
+    {file = "grpcio-1.66.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3885f037eb11f1cacc41f207b705f38a44b69478086f40608959bf5ad85826dd"},
+    {file = "grpcio-1.66.1-cp311-cp311-win32.whl", hash = "sha256:97ae7edd3f3f91480e48ede5d3e7d431ad6005bfdbd65c1b56913799ec79e791"},
+    {file = "grpcio-1.66.1-cp311-cp311-win_amd64.whl", hash = "sha256:cfd349de4158d797db2bd82d2020554a121674e98fbe6b15328456b3bf2495bb"},
+    {file = "grpcio-1.66.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:a92c4f58c01c77205df6ff999faa008540475c39b835277fb8883b11cada127a"},
+    {file = "grpcio-1.66.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fdb14bad0835914f325349ed34a51940bc2ad965142eb3090081593c6e347be9"},
+    {file = "grpcio-1.66.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:f03a5884c56256e08fd9e262e11b5cfacf1af96e2ce78dc095d2c41ccae2c80d"},
+    {file = "grpcio-1.66.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ca2559692d8e7e245d456877a85ee41525f3ed425aa97eb7a70fc9a79df91a0"},
+    {file = "grpcio-1.66.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ca1be089fb4446490dd1135828bd42a7c7f8421e74fa581611f7afdf7ab761"},
+    {file = "grpcio-1.66.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d639c939ad7c440c7b2819a28d559179a4508783f7e5b991166f8d7a34b52815"},
+    {file = "grpcio-1.66.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b9feb4e5ec8dc2d15709f4d5fc367794d69277f5d680baf1910fc9915c633524"},
+    {file = "grpcio-1.66.1-cp312-cp312-win32.whl", hash = "sha256:7101db1bd4cd9b880294dec41a93fcdce465bdbb602cd8dc5bd2d6362b618759"},
+    {file = "grpcio-1.66.1-cp312-cp312-win_amd64.whl", hash = "sha256:b0aa03d240b5539648d996cc60438f128c7f46050989e35b25f5c18286c86734"},
+    {file = "grpcio-1.66.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:ecfe735e7a59e5a98208447293ff8580e9db1e890e232b8b292dc8bd15afc0d2"},
+    {file = "grpcio-1.66.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4825a3aa5648010842e1c9d35a082187746aa0cdbf1b7a2a930595a94fb10fce"},
+    {file = "grpcio-1.66.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:f517fd7259fe823ef3bd21e508b653d5492e706e9f0ef82c16ce3347a8a5620c"},
+    {file = "grpcio-1.66.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1fe60d0772831d96d263b53d83fb9a3d050a94b0e94b6d004a5ad111faa5b5b"},
+    {file = "grpcio-1.66.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31a049daa428f928f21090403e5d18ea02670e3d5d172581670be006100db9ef"},
+    {file = "grpcio-1.66.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f914386e52cbdeb5d2a7ce3bf1fdfacbe9d818dd81b6099a05b741aaf3848bb"},
+    {file = "grpcio-1.66.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bff2096bdba686019fb32d2dde45b95981f0d1490e054400f70fc9a8af34b49d"},
+    {file = "grpcio-1.66.1-cp38-cp38-win32.whl", hash = "sha256:aa8ba945c96e73de29d25331b26f3e416e0c0f621e984a3ebdb2d0d0b596a3b3"},
+    {file = "grpcio-1.66.1-cp38-cp38-win_amd64.whl", hash = "sha256:161d5c535c2bdf61b95080e7f0f017a1dfcb812bf54093e71e5562b16225b4ce"},
+    {file = "grpcio-1.66.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:d0cd7050397b3609ea51727b1811e663ffda8bda39c6a5bb69525ef12414b503"},
+    {file = "grpcio-1.66.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0e6c9b42ded5d02b6b1fea3a25f036a2236eeb75d0579bfd43c0018c88bf0a3e"},
+    {file = "grpcio-1.66.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:c9f80f9fad93a8cf71c7f161778ba47fd730d13a343a46258065c4deb4b550c0"},
+    {file = "grpcio-1.66.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dd67ed9da78e5121efc5c510f0122a972216808d6de70953a740560c572eb44"},
+    {file = "grpcio-1.66.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48b0d92d45ce3be2084b92fb5bae2f64c208fea8ceed7fccf6a7b524d3c4942e"},
+    {file = "grpcio-1.66.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4d813316d1a752be6f5c4360c49f55b06d4fe212d7df03253dfdae90c8a402bb"},
+    {file = "grpcio-1.66.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9c9bebc6627873ec27a70fc800f6083a13c70b23a5564788754b9ee52c5aef6c"},
+    {file = "grpcio-1.66.1-cp39-cp39-win32.whl", hash = "sha256:30a1c2cf9390c894c90bbc70147f2372130ad189cffef161f0432d0157973f45"},
+    {file = "grpcio-1.66.1-cp39-cp39-win_amd64.whl", hash = "sha256:17663598aadbedc3cacd7bbde432f541c8e07d2496564e22b214b22c7523dac8"},
+    {file = "grpcio-1.66.1.tar.gz", hash = "sha256:35334f9c9745add3e357e3372756fd32d925bd52c41da97f4dfdafbde0bf0ee2"},
+]
+
+[package.extras]
+protobuf = ["grpcio-tools (>=1.66.1)"]
+
+[[package]]
+name = "grpcio-tools"
+version = "1.66.1"
+description = "Protobuf code generator for gRPC"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "grpcio_tools-1.66.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:e0c71405399ef59782600b1f0bdebc69ba12d7c9527cd268162a86273971d294"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:df1a174a6f9d3b4c380f005f33352d2e95464f33f021fb08084735a2eb6e23b1"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:7d789bfe53fce9e87aa80c3694a366258ce4c41b706258e9228ed4994832b780"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95c44a265ff01fd05166edae9350bc2e7d1d9a95e8f53b8cd04d2ae0a588c583"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b962a8767c3c0f9afe92e0dd6bb0b2305d35195a1053f84d4d31f585b87557ed"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d8616773126ec3cdf747b06a12e957b43ac15c34e4728def91fa67249a7c689a"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0067e79b6001560ac6acc78cca11fd3504fa27f8af46e3cdbac2f4998505e597"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-win32.whl", hash = "sha256:fa4f95a79a34afc3b5464895d091cd1911227fc3ab0441b9a37cd1817cf7db86"},
+    {file = "grpcio_tools-1.66.1-cp310-cp310-win_amd64.whl", hash = "sha256:3acce426f5e643de63019311171f4d31131da8149de518716a95c29a2c12dd38"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:9a07e24feb7472419cf70ebbb38dd4299aea696f91f191b62a99b3ee9ff03f89"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:097a069e7c640043921ecaf3e88d7af78ccd40c25dbddc91db2a4a2adbd0393d"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:016fa273dc696c9d8045091ac50e000bce766183a6b150801f51c2946e33dbe3"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ec9f4f964f8e8ed5e9cc13deb678c83d5597074c256805373220627833bc5ad"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3198815814cdd12bdb69b7580d7770a4ad4c8b2093e0bd6b987bc817618e3eec"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:796620fc41d3fbb566d9614ef22bc55df67fac1f1e19c1e0fb6ec48bc9b6a44b"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:222d8dc218560698e1abf652fb47e4015994ec7a265ef46e012fd9c9e77a4d6b"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-win32.whl", hash = "sha256:56e17a11f34df252b4c6fb8aa8cd7b44d162dba9f3333be87ddf7c8bf496622a"},
+    {file = "grpcio_tools-1.66.1-cp311-cp311-win_amd64.whl", hash = "sha256:edd52d667f2aa3c73233be0a821596937f24536647c12d96bfc54aa4cb04747d"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:869b6960d5daffda0dac1a474b44144f0dace0d4336394e499c4f400c5e2f8d9"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:68d9390bf9ba863ac147fc722d6548caa587235e887cac1bc2438212e89d1de7"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:b8660401beca7e3af28722439e07b0bcdca80b4a68f5a5a1138ae7b7780a6abf"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb67b9aa9cd69468bceb933e8e0f89fd13695746c018c4d2e6b3b84e73f3ad97"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5daceb9716e31edc0e1ba0f93303785211438c43502edddad7a919fc4cb3d664"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0a86398a4cd0665bc7f09fa90b89bac592c959d2c895bf3cf5d47a98c0f2d24c"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1b4acb53338072ab3023e418a5c7059cb15686abd1607516fa1453406dd5f69d"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-win32.whl", hash = "sha256:88e04b7546101bc79c868c941777efd5088063a9e4f03b4d7263dde796fbabf7"},
+    {file = "grpcio_tools-1.66.1-cp312-cp312-win_amd64.whl", hash = "sha256:5b4fc56abeafae74140f5da29af1093e88ce64811d77f1a81c3146e9e996fb6a"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:d4dd2ff982c1aa328ef47ce34f07af82f1f13599912fb1618ebc5fe1e14dddb8"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:066648543f786cb74b1fef5652359952455dbba37e832642026fd9fd8a219b5f"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:d19d47744c30e6bafa76b3113740e71f382d75ebb2918c1efd62ebe6ba7e20f9"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:739c53571130b359b738ac7d6d0a1f772e15779b66df7e6764bee4071cd38689"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2226ff8d3ecba83b7622946df19d6e8e15cb52f761b8d9e2f807b228db5f1b1e"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f4b1498cb8b422fbae32a491c9154e8d47650caf5852fbe6b3b34253e824343"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:93d2d9e14e81affdc63d67c42eb16a8da1b6fecc16442a703ca60eb0e7591691"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-win32.whl", hash = "sha256:d761dfd97a10e4aae73628b5120c64e56f0cded88651d0003d2d80e678c3e7c9"},
+    {file = "grpcio_tools-1.66.1-cp38-cp38-win_amd64.whl", hash = "sha256:e1c2ac0955f5fb87b8444316e475242d194c3f3cd0b7b6e54b889a7b6f05156f"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:5f1f04578b72c281e39274348a61d240c48d5321ba8d7a8838e194099ecbc322"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:da9b0c08dbbf07535ee1b75a22d0acc5675a808a3a3df9f9b21e0e73ddfbb3a9"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:e302b4e1fa856d74ff65c65888b3a37153287ce6ad5bad80b2fdf95130accec2"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fc3f62494f238774755ff90f0e66a93ac7972ea1eb7180c45acf4fd53b25cca"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cad65ff22459aa387f543d293f54834c9aac8f76fb7416a7046556df75b567"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3d17a27c567a5e4d18f487368215cb51b43e2499059fd6113b92f7ae1fee48be"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4df167e67b083f96bc277032a526f6186e98662aaa49baea1dfb8ecfe26ce117"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-win32.whl", hash = "sha256:f94d5193b2f2a9595795b83e7978b2bee1c0399da66f2f24d179c388f81fb99c"},
+    {file = "grpcio_tools-1.66.1-cp39-cp39-win_amd64.whl", hash = "sha256:66f527a1e3f063065e29cf6f3e55892434d13a5a51e3b22402e09da9521e98a3"},
+    {file = "grpcio_tools-1.66.1.tar.gz", hash = "sha256:5055ffe840ea8f505c30378be02afb4dbecb33480e554debe10b63d6b2f641c3"},
+]
+
+[package.dependencies]
+grpcio = ">=1.66.1"
+protobuf = ">=5.26.1,<6.0dev"
+setuptools = "*"
+
 [[package]]
 name = "grpclib"
 version = "0.4.7"
@@ -2354,6 +2472,26 @@ starlette = ["starlette (>=0.19.1)"]
 starlite = ["starlite (>=1.48)"]
 tornado = ["tornado (>=5)"]
 
+[[package]]
+name = "setuptools"
+version = "75.1.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"},
+    {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"},
+]
+
+[package.extras]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"]
+core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
+enabler = ["pytest-enabler (>=2.2)"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
+type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"]
+
 [[package]]
 name = "simpleaudio"
 version = "1.0.4"
@@ -2650,4 +2788,4 @@ dev = ["headless-kivy", "headless-kivy"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.11"
-content-hash = "e3bc8f2fb9e8eb1f442cfce506da9b25ab8906f84b4f8070af5351baa330593c"
+content-hash = "cd9d570fe0f071e447bc9f9b6ad01c1ef7b86f7ba5abb8cdbebcf2f98542bcce"
diff --git a/pyproject.toml b/pyproject.toml
index be00135f..fb12fbcd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -73,6 +73,7 @@ toml = "^0.10.2"
 pytest-mock = "^3.14.0"
 ipython = "^8.23.0"
 pyfakefs = { git = "https://github.com/pytest-dev/pyfakefs.git" }
+grpcio-tools = "^1.66.1"
 betterproto = { extras = ["compiler"], version = "^2.0.0b7" }
 
 [tool.poetry.extras]
@@ -89,12 +90,20 @@ requires = ["poetry-core"]
 build-backend = "poetry.core.masonry.api"
 
 [tool.poe.tasks]
-lint = "ruff check . --unsafe-fixes"
+lint = "ruff check ."
+"lint:fix" = "ruff check . --unsafe-fixes --fix"
 typecheck = "pyright -p pyproject.toml ."
 test = "pytest --cov=ubo_app"
 sanity = ["typecheck", "lint", "test"]
 build-docker-images = "sh -c 'docker buildx build . -f scripts/Dockerfile.dev -t ubo-app-dev && docker buildx build . -f scripts/Dockerfile.test -t ubo-app-test'"
 
+"proto:generate:raw" = "python ubo_app/rpc/generate_proto.py"
+"proto:generate" = ["proto:generate:raw", "proto:lint"]
+"proto:compile:raw" = "sh -c 'mkdir -p ubo_app/rpc/generated && python -m grpc_tools.protoc -I ubo_app/rpc/proto/ --python_betterproto_opt=typing.310 --python_betterproto_out=ubo_app/rpc/generated/ ubo_app/rpc/proto/store/v1/store.proto'"
+"proto:compile" = ["proto:compile:raw", "lint:fix"]
+"proto:lint" = "buf format -w ubo_app/rpc/proto/"
+"proto" = ["proto:generate", "proto:compile"]
+
 "device:deploy" = "poe deploy-to-device"
 "device:deploy:deps" = "poe deploy-to-device --deps"
 "device:deploy:kill" = "poe deploy-to-device --kill"
@@ -151,6 +160,14 @@ multiline-quotes = "double"
 "tests/*" = ["S101", "PLR0913", "PLR0915"]
 "**/reducer.py" = ["C901", "PLR0912", "PLR0915"]
 "ubo_app/services/*/ubo_handle.py" = ["TCH004"]
+"ubo_app/rpc/generated/*" = [
+  "ARG002",
+  "ASYNC109",
+  "D",
+  "ERA001",
+  "F401",
+  "RUF009",
+]
 
 [tool.ruff.format]
 quote-style = "single"
@@ -159,7 +176,7 @@ quote-style = "single"
 profile = "black"
 
 [tool.pyright]
-exclude = ["typings"]
+exclude = ["typings", "ubo_app/rpc/generated"]
 
 [[tool.pyright.executionEnvironments]]
 root = "ubo_app/services/000-audio"
diff --git a/ubo_app/constants.py b/ubo_app/constants.py
index ee1cc581..49f66171 100644
--- a/ubo_app/constants.py
+++ b/ubo_app/constants.py
@@ -28,6 +28,8 @@
 ENABLED_SERVICES = os.environ.get('UBO_ENABLED_SERVICES', '')
 ENABLED_SERVICES = ENABLED_SERVICES.split(',') if ENABLED_SERVICES else []
 
+DISABLE_GRPC = str_to_bool(os.environ.get('UBO_DISABLE_GRPC', 'False')) == 1
+
 UPDATE_ASSETS_PATH = Path(f'{INSTALLATION_PATH}/_update/')
 UPDATE_LOCK_PATH = UPDATE_ASSETS_PATH / 'update_is_ready.lock'
 
diff --git a/ubo_app/main.py b/ubo_app/main.py
index ae858134..80cc2c14 100644
--- a/ubo_app/main.py
+++ b/ubo_app/main.py
@@ -37,13 +37,19 @@ def main() -> None:
 
     setup()
 
-    from ubo_app.service import start_event_loop_thread
+    from ubo_app.service import start_event_loop_thread, worker_thread
 
     start_event_loop_thread(asyncio.get_event_loop())
 
+    from ubo_app.constants import DISABLE_GRPC, HEIGHT, WIDTH
+
+    if not DISABLE_GRPC:
+        from ubo_app.rpc.server import serve as grpc_serve
+
+        worker_thread.run_task(grpc_serve())
+
     import headless_kivy.config
 
-    from ubo_app.constants import HEIGHT, WIDTH
     from ubo_app.display import render_on_display
 
     headless_kivy.config.setup_headless_kivy(
diff --git a/ubo_app/rpc/generated/__init__.py b/ubo_app/rpc/generated/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ubo_app/rpc/generated/package_info/__init__.py b/ubo_app/rpc/generated/package_info/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ubo_app/rpc/generated/package_info/v1/__init__.py b/ubo_app/rpc/generated/package_info/v1/__init__.py
new file mode 100644
index 00000000..3d6e9a0a
--- /dev/null
+++ b/ubo_app/rpc/generated/package_info/v1/__init__.py
@@ -0,0 +1,8 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# sources: package_info/v1/package_info.proto
+# plugin: python-betterproto
+# This file has been @generated
+
+from dataclasses import dataclass
+
+import betterproto
diff --git a/ubo_app/rpc/generated/store/__init__.py b/ubo_app/rpc/generated/store/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ubo_app/rpc/generated/store/v1/__init__.py b/ubo_app/rpc/generated/store/v1/__init__.py
new file mode 100644
index 00000000..97b524f5
--- /dev/null
+++ b/ubo_app/rpc/generated/store/v1/__init__.py
@@ -0,0 +1,172 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# sources: store/v1/store.proto
+# plugin: python-betterproto
+# This file has been @generated
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+import betterproto
+import grpclib
+import grpclib.server
+from betterproto.grpc.grpclib_server import ServiceBase
+
+if TYPE_CHECKING:
+    from collections.abc import AsyncIterator
+
+    from betterproto.grpc.grpclib_client import MetadataLike
+    from grpclib.metadata import Deadline
+
+    from generated.ubo import v1 as __ubo_v1__
+
+
+@dataclass(eq=False, repr=False)
+class DispatchActionRequest(betterproto.Message):
+    action: '__ubo_v1__.Action' = betterproto.message_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class DispatchEventRequest(betterproto.Message):
+    event: '__ubo_v1__.Event' = betterproto.message_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class DispatchActionResponse(betterproto.Message):
+    pass
+
+
+@dataclass(eq=False, repr=False)
+class DispatchEventResponse(betterproto.Message):
+    pass
+
+
+@dataclass(eq=False, repr=False)
+class SubscribeEventRequest(betterproto.Message):
+    event: '__ubo_v1__.Event' = betterproto.message_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class SubscribeEventResponse(betterproto.Message):
+    event: '__ubo_v1__.Event' = betterproto.message_field(1)
+
+
+class StoreServiceStub(betterproto.ServiceStub):
+    async def dispatch_action(
+        self,
+        dispatch_action_request: 'DispatchActionRequest',
+        *,
+        timeout: 'float | None' = None,
+        deadline: 'Deadline | None' = None,
+        metadata: 'MetadataLike | None' = None,
+    ) -> 'DispatchActionResponse':
+        return await self._unary_unary(
+            '/store.v1.StoreService/DispatchAction',
+            dispatch_action_request,
+            DispatchActionResponse,
+            timeout=timeout,
+            deadline=deadline,
+            metadata=metadata,
+        )
+
+    async def dispatch_event(
+        self,
+        dispatch_event_request: 'DispatchEventRequest',
+        *,
+        timeout: 'float | None' = None,
+        deadline: 'Deadline | None' = None,
+        metadata: 'MetadataLike | None' = None,
+    ) -> 'DispatchEventResponse':
+        return await self._unary_unary(
+            '/store.v1.StoreService/DispatchEvent',
+            dispatch_event_request,
+            DispatchEventResponse,
+            timeout=timeout,
+            deadline=deadline,
+            metadata=metadata,
+        )
+
+    async def subscribe_event(
+        self,
+        subscribe_event_request: 'SubscribeEventRequest',
+        *,
+        timeout: 'float | None' = None,
+        deadline: 'Deadline | None' = None,
+        metadata: 'MetadataLike | None' = None,
+    ) -> 'AsyncIterator[SubscribeEventResponse]':
+        async for response in self._unary_stream(
+            '/store.v1.StoreService/SubscribeEvent',
+            subscribe_event_request,
+            SubscribeEventResponse,
+            timeout=timeout,
+            deadline=deadline,
+            metadata=metadata,
+        ):
+            yield response
+
+
+class StoreServiceBase(ServiceBase):
+
+    async def dispatch_action(
+        self, dispatch_action_request: 'DispatchActionRequest',
+    ) -> 'DispatchActionResponse':
+        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
+
+    async def dispatch_event(
+        self, dispatch_event_request: 'DispatchEventRequest',
+    ) -> 'DispatchEventResponse':
+        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
+
+    async def subscribe_event(
+        self, subscribe_event_request: 'SubscribeEventRequest',
+    ) -> 'AsyncIterator[SubscribeEventResponse]':
+        raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
+        yield SubscribeEventResponse()
+
+    async def __rpc_dispatch_action(
+        self,
+        stream: 'grpclib.server.Stream[DispatchActionRequest, DispatchActionResponse]',
+    ) -> None:
+        request = await stream.recv_message()
+        response = await self.dispatch_action(request)
+        await stream.send_message(response)
+
+    async def __rpc_dispatch_event(
+        self,
+        stream: 'grpclib.server.Stream[DispatchEventRequest, DispatchEventResponse]',
+    ) -> None:
+        request = await stream.recv_message()
+        response = await self.dispatch_event(request)
+        await stream.send_message(response)
+
+    async def __rpc_subscribe_event(
+        self,
+        stream: 'grpclib.server.Stream[SubscribeEventRequest, SubscribeEventResponse]',
+    ) -> None:
+        request = await stream.recv_message()
+        await self._call_rpc_handler_server_stream(
+            self.subscribe_event,
+            stream,
+            request,
+        )
+
+    def __mapping__(self) -> 'dict[str, grpclib.const.Handler]':
+        return {
+            '/store.v1.StoreService/DispatchAction': grpclib.const.Handler(
+                self.__rpc_dispatch_action,
+                grpclib.const.Cardinality.UNARY_UNARY,
+                DispatchActionRequest,
+                DispatchActionResponse,
+            ),
+            '/store.v1.StoreService/DispatchEvent': grpclib.const.Handler(
+                self.__rpc_dispatch_event,
+                grpclib.const.Cardinality.UNARY_UNARY,
+                DispatchEventRequest,
+                DispatchEventResponse,
+            ),
+            '/store.v1.StoreService/SubscribeEvent': grpclib.const.Handler(
+                self.__rpc_subscribe_event,
+                grpclib.const.Cardinality.UNARY_STREAM,
+                SubscribeEventRequest,
+                SubscribeEventResponse,
+            ),
+        }
diff --git a/ubo_app/rpc/generated/ubo/__init__.py b/ubo_app/rpc/generated/ubo/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ubo_app/rpc/generated/ubo/v1/__init__.py b/ubo_app/rpc/generated/ubo/v1/__init__.py
new file mode 100644
index 00000000..dd725265
--- /dev/null
+++ b/ubo_app/rpc/generated/ubo/v1/__init__.py
@@ -0,0 +1,1946 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# sources: ubo/v1/ubo.proto
+# plugin: python-betterproto
+# This file has been @generated
+import builtins
+from dataclasses import dataclass
+
+import betterproto
+
+
+class WiFiType(betterproto.Enum):
+    UNSPECIFIED = 0
+    WEP = 1
+    WPA = 2
+    WPA2 = 3
+    NOPASS = 4
+
+
+class ConnectionState(betterproto.Enum):
+    UNSPECIFIED = 0
+    CONNECTED = 1
+    CONNECTING = 2
+    DISCONNECTED = 3
+    UNKNOWN = 4
+
+
+class GlobalWiFiState(betterproto.Enum):
+    UNSPECIFIED = 0
+    CONNECTED = 1
+    DISCONNECTED = 2
+    PENDING = 3
+    NEEDS_ATTENTION = 4
+    UNKNOWN = 5
+
+
+class Sensor(betterproto.Enum):
+    UNSPECIFIED = 0
+    TEMPERATURE = 1
+    LIGHT = 2
+
+
+class DockerStatus(betterproto.Enum):
+    UNSPECIFIED = 0
+    UNKNOWN = 1
+    NOT_INSTALLED = 2
+    INSTALLING = 3
+    NOT_RUNNING = 4
+    RUNNING = 5
+    ERROR = 6
+
+
+class ImageStatus(betterproto.Enum):
+    UNSPECIFIED = 0
+    NOT_AVAILABLE = 1
+    FETCHING = 2
+    AVAILABLE = 3
+    CREATED = 4
+    RUNNING = 5
+    ERROR = 6
+
+
+class Key(betterproto.Enum):
+    UNSPECIFIED = 0
+    BACK = 1
+    HOME = 2
+    UP = 3
+    DOWN = 4
+    L1 = 5
+    L2 = 6
+    L3 = 7
+
+
+class VoiceEngine(betterproto.Enum):
+    UNSPECIFIED = 0
+    PIPER = 1
+    PICOVOICE = 2
+
+
+class GlobalEthernetState(betterproto.Enum):
+    UNSPECIFIED = 0
+    CONNECTED = 1
+    DISCONNECTED = 2
+    PENDING = 3
+    NEEDS_ATTENTION = 4
+    UNKNOWN = 5
+
+
+class Importance(betterproto.Enum):
+    UNSPECIFIED = 0
+    CRITICAL = 1
+    HIGH = 2
+    MEDIUM = 3
+    LOW = 4
+
+
+class NotificationDisplayType(betterproto.Enum):
+    UNSPECIFIED = 0
+    NOT_SET = 1
+    BACKGROUND = 2
+    FLASH = 3
+    STICKY = 4
+
+
+class Chime(betterproto.Enum):
+    UNSPECIFIED = 0
+    ADD = 1
+    DONE = 2
+    FAILURE = 3
+    VOLUME_CHANGE = 4
+
+
+class AudioDevice(betterproto.Enum):
+    UNSPECIFIED = 0
+    INPUT = 1
+    OUTPUT = 2
+
+
+@dataclass(eq=False, repr=False)
+class BaseMenu(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    title: str = betterproto.string_field(2)
+    items: 'list[Item]' = betterproto.message_field(3)
+    placeholder: 'str | None' = betterproto.string_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class HeadedMenu(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    heading: str = betterproto.string_field(2)
+    sub_heading: str = betterproto.string_field(3)
+    title: str = betterproto.string_field(4)
+    items: 'list[Item]' = betterproto.message_field(5)
+    placeholder: 'str | None' = betterproto.string_field(6, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class HeadlessMenu(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    title: str = betterproto.string_field(2)
+    items: 'list[Item]' = betterproto.message_field(3)
+    placeholder: 'str | None' = betterproto.string_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class Item(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'str | None' = betterproto.string_field(2, optional=True)
+    label: 'str | None' = betterproto.string_field(3, optional=True)
+    color: 'str | None' = betterproto.string_field(4, optional=True)
+    background_color: 'str | None' = betterproto.string_field(5, optional=True)
+    icon: 'str | None' = betterproto.string_field(6, optional=True)
+    is_short: 'bool | None' = betterproto.bool_field(7, optional=True)
+    opacity: 'float | None' = betterproto.float_field(8, optional=True)
+    progress: 'float | None' = betterproto.float_field(9, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class ActionItem(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'str | None' = betterproto.string_field(2, optional=True)
+    label: 'str | None' = betterproto.string_field(3, optional=True)
+    color: 'str | None' = betterproto.string_field(4, optional=True)
+    background_color: 'str | None' = betterproto.string_field(5, optional=True)
+    icon: 'str | None' = betterproto.string_field(6, optional=True)
+    is_short: 'bool | None' = betterproto.bool_field(7, optional=True)
+    opacity: 'float | None' = betterproto.float_field(8, optional=True)
+    progress: 'float | None' = betterproto.float_field(9, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class ApplicationItem(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    application: str = betterproto.string_field(2)
+    key: 'str | None' = betterproto.string_field(3, optional=True)
+    label: 'str | None' = betterproto.string_field(4, optional=True)
+    color: 'str | None' = betterproto.string_field(5, optional=True)
+    background_color: 'str | None' = betterproto.string_field(6, optional=True)
+    icon: 'str | None' = betterproto.string_field(7, optional=True)
+    is_short: 'bool | None' = betterproto.bool_field(8, optional=True)
+    opacity: 'float | None' = betterproto.float_field(9, optional=True)
+    progress: 'float | None' = betterproto.float_field(10, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class SubMenuItem(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    sub_menu: 'Menu' = betterproto.message_field(2)
+    key: 'str | None' = betterproto.string_field(3, optional=True)
+    label: 'str | None' = betterproto.string_field(4, optional=True)
+    color: 'str | None' = betterproto.string_field(5, optional=True)
+    background_color: 'str | None' = betterproto.string_field(6, optional=True)
+    icon: 'str | None' = betterproto.string_field(7, optional=True)
+    is_short: 'bool | None' = betterproto.bool_field(8, optional=True)
+    opacity: 'float | None' = betterproto.float_field(9, optional=True)
+    progress: 'float | None' = betterproto.float_field(10, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class Subscribable(betterproto.Message):
+    meta_field_package_name_menu: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class Menu(betterproto.Message):
+    headed_menu: 'HeadedMenu' = betterproto.message_field(1, group='menu')
+    headless_menu: 'HeadlessMenu' = betterproto.message_field(2, group='menu')
+
+
+@dataclass(eq=False, repr=False)
+class ScreenshotEvent(betterproto.Message):
+    meta_field_package_name_operations: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class SnapshotEvent(betterproto.Message):
+    meta_field_package_name_operations: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class UboAction(betterproto.Message):
+    voice_action: 'VoiceAction' = betterproto.message_field(1, group='ubo_action')
+    ssh_action: 'SshAction' = betterproto.message_field(2, group='ubo_action')
+    rgb_ring_action: 'RgbRingAction' = betterproto.message_field(3, group='ubo_action')
+    notifications_action: 'NotificationsAction' = betterproto.message_field(
+        4, group='ubo_action',
+    )
+    light_dm_action: 'LightDmAction' = betterproto.message_field(5, group='ubo_action')
+    ip_action: 'IpAction' = betterproto.message_field(6, group='ubo_action')
+    display_action: 'DisplayAction' = betterproto.message_field(7, group='ubo_action')
+    camera_action: 'CameraAction' = betterproto.message_field(8, group='ubo_action')
+    audio_action: 'AudioAction' = betterproto.message_field(9, group='ubo_action')
+    docker_action: 'DockerAction' = betterproto.message_field(10, group='ubo_action')
+    keypad_action: 'KeypadAction' = betterproto.message_field(11, group='ubo_action')
+    r_pi_connect_action: 'RPiConnectAction' = betterproto.message_field(
+        12, group='ubo_action',
+    )
+    sensors_action: 'SensorsAction' = betterproto.message_field(13, group='ubo_action')
+    users_action: 'UsersAction' = betterproto.message_field(14, group='ubo_action')
+    vs_code_action: 'VsCodeAction' = betterproto.message_field(15, group='ubo_action')
+    wi_fi_action: 'WiFiAction' = betterproto.message_field(16, group='ubo_action')
+
+
+@dataclass(eq=False, repr=False)
+class UboEvent(betterproto.Message):
+    users_event: 'UsersEvent' = betterproto.message_field(1, group='ubo_event')
+    notifications_event: 'NotificationsEvent' = betterproto.message_field(
+        2, group='ubo_event',
+    )
+    ip_event: 'IpEvent' = betterproto.message_field(3, group='ubo_event')
+    display_event: 'DisplayEvent' = betterproto.message_field(4, group='ubo_event')
+    audio_event: 'AudioEvent' = betterproto.message_field(5, group='ubo_event')
+    screenshot_event: 'ScreenshotEvent' = betterproto.message_field(
+        6, group='ubo_event',
+    )
+    camera_event: 'CameraEvent' = betterproto.message_field(7, group='ubo_event')
+    keypad_event: 'KeypadEvent' = betterproto.message_field(8, group='ubo_event')
+    snapshot_event: 'SnapshotEvent' = betterproto.message_field(9, group='ubo_event')
+    wi_fi_event: 'WiFiEvent' = betterproto.message_field(10, group='ubo_event')
+
+
+@dataclass(eq=False, repr=False)
+class DispatchItem(betterproto.Message):
+    meta_field_package_name_dispatch_action: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    operation: 'DispatchItemOperation' = betterproto.message_field(2)
+    key: 'str | None' = betterproto.string_field(3, optional=True)
+    label: 'str | None' = betterproto.string_field(4, optional=True)
+    color: 'str | None' = betterproto.string_field(5, optional=True)
+    background_color: 'str | None' = betterproto.string_field(6, optional=True)
+    icon: 'str | None' = betterproto.string_field(7, optional=True)
+    is_short: 'bool | None' = betterproto.bool_field(8, optional=True)
+    opacity: 'float | None' = betterproto.float_field(9, optional=True)
+    progress: 'float | None' = betterproto.float_field(10, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class DispatchItemOperation(betterproto.Message):
+    ubo_event: 'Event' = betterproto.message_field(1, group='operation')
+    ubo_action: 'Action' = betterproto.message_field(2, group='operation')
+
+
+@dataclass(eq=False, repr=False)
+class LightDmAction(betterproto.Message):
+    meta_field_package_name_lightdm: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class LightDmUpdateStateAction(betterproto.Message):
+    meta_field_package_name_lightdm: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_active: 'bool | None' = betterproto.bool_field(2, optional=True)
+    is_enabled: 'bool | None' = betterproto.bool_field(3, optional=True)
+    is_installed: 'bool | None' = betterproto.bool_field(4, optional=True)
+    is_installing: 'bool | None' = betterproto.bool_field(5, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class LightDmClearEnabledStateAction(betterproto.Message):
+    meta_field_package_name_lightdm: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class LightDmState(betterproto.Message):
+    meta_field_package_name_lightdm: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_active: 'bool | None' = betterproto.bool_field(2, optional=True)
+    is_enabled: 'bool | None' = betterproto.bool_field(3, optional=True)
+    is_installed: 'bool | None' = betterproto.bool_field(4, optional=True)
+    is_installing: 'bool | None' = betterproto.bool_field(5, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class WiFiConnection(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    ssid: str = betterproto.string_field(2)
+    state: 'ConnectionState | None' = betterproto.enum_field(3, optional=True)
+    signal_strength: 'int | None' = betterproto.int64_field(4, optional=True)
+    password: 'str | None' = betterproto.string_field(5, optional=True)
+    type: 'WiFiType | None' = betterproto.enum_field(6, optional=True)
+    hidden: 'bool | None' = betterproto.bool_field(7, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class WiFiAction(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class WiFiSetHasVisitedOnboardingAction(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    has_visited_onboarding: bool = betterproto.bool_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class WiFiUpdateAction(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    connections: 'list[WiFiConnection]' = betterproto.message_field(2)
+    state: 'GlobalWiFiState' = betterproto.enum_field(3)
+    current_connection: 'WiFiConnection' = betterproto.message_field(4)
+
+
+@dataclass(eq=False, repr=False)
+class WiFiUpdateRequestAction(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    reset: 'bool | None' = betterproto.bool_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class WiFiEvent(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class WiFiUpdateRequestEvent(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class WiFiState(betterproto.Message):
+    meta_field_package_name_wifi: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    connections: 'list[WiFiConnection]' = betterproto.message_field(2)
+    state: 'GlobalWiFiState' = betterproto.enum_field(3)
+    current_connection: 'WiFiConnection' = betterproto.message_field(4)
+    has_visited_onboarding: 'bool | None' = betterproto.bool_field(5, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class SensorsAction(betterproto.Message):
+    meta_field_package_name_sensors: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class SensorsReportReadingAction(betterproto.Message):
+    meta_field_package_name_sensors: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    sensor: 'Sensor' = betterproto.enum_field(2)
+    reading: float = betterproto.float_field(3)
+    timestamp: int = betterproto.int64_field(4)
+
+
+@dataclass(eq=False, repr=False)
+class SensorState(betterproto.Message):
+    meta_field_package_name_sensors: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    value: 'float | None' = betterproto.float_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class SensorsState(betterproto.Message):
+    meta_field_package_name_sensors: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    temperature: 'SensorState | None' = betterproto.message_field(2, optional=True)
+    light: 'SensorState | None' = betterproto.message_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class UsersAction(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class UsersSetUsersAction(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    users: 'list[UserState]' = betterproto.message_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class UsersCreateUserAction(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class UsersDeleteUserAction(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class UsersResetPasswordAction(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class UsersEvent(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class UsersCreateUserEvent(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class UsersDeleteUserEvent(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class UsersResetPasswordEvent(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class UserState(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+    is_removable: bool = betterproto.bool_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class UsersState(betterproto.Message):
+    meta_field_package_name_users: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    users: 'UsersStateUsers | None' = betterproto.message_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class UsersStateUsers(betterproto.Message):
+    items: 'list[UserState]' = betterproto.message_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectAction(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectEvent(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectStartDownloadingAction(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectDoneDownloadingAction(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectSetPendingAction(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectStatus(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    screen_sharing_sessions: int = betterproto.int64_field(2)
+    remote_shell_sessions: int = betterproto.int64_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectSetStatusAction(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_installed: bool = betterproto.bool_field(2)
+    is_signed_in: bool = betterproto.bool_field(3)
+    status: 'RPiConnectStatus' = betterproto.message_field(4)
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectLoginEvent(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectUpdateServiceStateAction(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_active: 'bool | None' = betterproto.bool_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RPiConnectState(betterproto.Message):
+    meta_field_package_name_rpi_connect: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_downloading: 'bool | None' = betterproto.bool_field(2, optional=True)
+    is_active: 'bool | None' = betterproto.bool_field(3, optional=True)
+    is_installed: 'bool | None' = betterproto.bool_field(4, optional=True)
+    is_signed_in: 'bool | None' = betterproto.bool_field(5, optional=True)
+    status: 'RPiConnectStatus | None' = betterproto.message_field(6, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeAction(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeEvent(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeStartDownloadingAction(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeDoneDownloadingAction(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeSetPendingAction(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeStatus(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_service_installed: bool = betterproto.bool_field(2)
+    is_running: bool = betterproto.bool_field(3)
+    name: str = betterproto.string_field(4)
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeSetStatusAction(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_binary_installed: bool = betterproto.bool_field(2)
+    is_logged_in: bool = betterproto.bool_field(3)
+    status: 'VsCodeStatus' = betterproto.message_field(4)
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeLoginEvent(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeRestartEvent(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VsCodeState(betterproto.Message):
+    meta_field_package_name_vscode: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_pending: 'bool | None' = betterproto.bool_field(2, optional=True)
+    is_downloading: 'bool | None' = betterproto.bool_field(3, optional=True)
+    is_binary_installed: 'bool | None' = betterproto.bool_field(4, optional=True)
+    is_logged_in: 'bool | None' = betterproto.bool_field(5, optional=True)
+    status: 'VsCodeStatus | None' = betterproto.message_field(6, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class DockerAction(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DockerSetStatusAction(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    status: 'DockerStatus' = betterproto.enum_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class DockerStoreUsernameAction(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    registry: str = betterproto.string_field(2)
+    username: str = betterproto.string_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class DockerRemoveUsernameAction(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    registry: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class DockerImageAction(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    image: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class DockerImageSetStatusAction(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    status: 'ImageStatus' = betterproto.enum_field(2)
+    ports: 'DockerImageSetStatusActionPorts | None' = betterproto.message_field(
+        3, optional=True,
+    )
+    ip: 'str | None' = betterproto.string_field(4, optional=True)
+    image: str = betterproto.string_field(5)
+
+
+@dataclass(eq=False, repr=False)
+class DockerImageSetStatusActionPorts(betterproto.Message):
+    items: 'list[str]' = betterproto.string_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class DockerImageSetDockerIdAction(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    docker_id: str = betterproto.string_field(2)
+    image: str = betterproto.string_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class DockerEvent(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DockerServiceState(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    status: 'DockerStatus | None' = betterproto.enum_field(2, optional=True)
+    usernames: 'DockerServiceStateUsernamesDict | None' = betterproto.message_field(
+        3, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DockerServiceStateUsernamesDict(betterproto.Message):
+    items: 'dict[str, str]' = betterproto.map_field(
+        1, betterproto.TYPE_STRING, betterproto.TYPE_STRING,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DockerImageEvent(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    image: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class DockerImageRegisterAppEvent(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    image: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class ImageState(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+    status: 'ImageStatus | None' = betterproto.enum_field(3, optional=True)
+    container_ip: 'str | None' = betterproto.string_field(4, optional=True)
+    docker_id: 'str | None' = betterproto.string_field(5, optional=True)
+    ports: 'ImageStatePorts | None' = betterproto.message_field(6, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class ImageStatePorts(betterproto.Message):
+    items: 'list[str]' = betterproto.string_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class DockerState(betterproto.Message):
+    meta_field_package_name_docker: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    service: 'DockerServiceState' = betterproto.message_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class DisplayAction(betterproto.Message):
+    meta_field_package_name_display: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DisplayEvent(betterproto.Message):
+    meta_field_package_name_display: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DisplayPauseAction(betterproto.Message):
+    meta_field_package_name_display: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DisplayResumeAction(betterproto.Message):
+    meta_field_package_name_display: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class DisplayRenderEvent(betterproto.Message):
+    meta_field_package_name_display: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    data: bytes = betterproto.bytes_field(2)
+    data_hash: int = betterproto.int64_field(3)
+    rectangle: 'list[int]' = betterproto.int64_field(4)
+
+
+@dataclass(eq=False, repr=False)
+class DisplayState(betterproto.Message):
+    meta_field_package_name_display: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_paused: 'bool | None' = betterproto.bool_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadAction(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: 'float | None' = betterproto.float_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadKeyUpAction(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: 'float | None' = betterproto.float_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadKeyDownAction(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: 'float | None' = betterproto.float_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadKeyPressAction(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: 'float | None' = betterproto.float_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadKeyReleaseAction(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: 'float | None' = betterproto.float_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadEvent(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: float = betterproto.float_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadKeyPressEvent(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: float = betterproto.float_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class KeypadKeyReleaseEvent(betterproto.Message):
+    meta_field_package_name_keypad: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    key: 'Key' = betterproto.enum_field(2)
+    time: float = betterproto.float_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class VoiceAction(betterproto.Message):
+    meta_field_package_name_voice: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VoiceEvent(betterproto.Message):
+    meta_field_package_name_voice: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class VoiceUpdateAccessKeyStatus(betterproto.Message):
+    meta_field_package_name_voice: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_access_key_set: bool = betterproto.bool_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class VoiceSetEngineAction(betterproto.Message):
+    meta_field_package_name_voice: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    engine: 'VoiceEngine' = betterproto.enum_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class VoiceReadTextAction(betterproto.Message):
+    meta_field_package_name_voice: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    text: str = betterproto.string_field(2)
+    piper_text: 'str | None' = betterproto.string_field(3, optional=True)
+    picovoice_text: 'str | None' = betterproto.string_field(4, optional=True)
+    speech_rate: 'float | None' = betterproto.float_field(5, optional=True)
+    engine: 'VoiceEngine | None' = betterproto.enum_field(6, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class VoiceSynthesizeTextEvent(betterproto.Message):
+    meta_field_package_name_voice: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    text: str = betterproto.string_field(2)
+    piper_text: str = betterproto.string_field(3)
+    picovoice_text: str = betterproto.string_field(4)
+    speech_rate: 'float | None' = betterproto.float_field(5, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class VoiceState(betterproto.Message):
+    meta_field_package_name_voice: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_access_key_set: 'bool | None' = betterproto.bool_field(2, optional=True)
+    selected_engine: 'VoiceEngine | None' = betterproto.enum_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingEvent(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingSetIsConnectedAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_connected: 'bool | None' = betterproto.bool_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingSetIsBusyAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_busy: 'bool | None' = betterproto.bool_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingCommandAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingWaitableCommandAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    wait: 'int | None' = betterproto.int64_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingColorfulCommandAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    color: 'RgbColor | None' = betterproto.message_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingSetEnabledAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    enabled: 'bool | None' = betterproto.bool_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingSetAllAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    color: 'RgbColor | None' = betterproto.message_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingSetBrightnessAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    brightness: 'float | None' = betterproto.float_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingBlankAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingRainbowAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    rounds: int = betterproto.int64_field(2)
+    wait: 'int | None' = betterproto.int64_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingProgressWheelStepAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    color: 'RgbColor | None' = betterproto.message_field(2, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingPulseAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    repetitions: 'int | None' = betterproto.int64_field(2, optional=True)
+    wait: 'int | None' = betterproto.int64_field(3, optional=True)
+    color: 'RgbColor | None' = betterproto.message_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingBlinkAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    repetitions: 'int | None' = betterproto.int64_field(2, optional=True)
+    wait: 'int | None' = betterproto.int64_field(3, optional=True)
+    color: 'RgbColor | None' = betterproto.message_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingSpinningWheelAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    length: 'int | None' = betterproto.int64_field(2, optional=True)
+    repetitions: 'int | None' = betterproto.int64_field(3, optional=True)
+    wait: 'int | None' = betterproto.int64_field(4, optional=True)
+    color: 'RgbColor | None' = betterproto.message_field(5, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingProgressWheelAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    percentage: 'float | None' = betterproto.float_field(2, optional=True)
+    color: 'RgbColor | None' = betterproto.message_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingFillUptoAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    percentage: 'float | None' = betterproto.float_field(2, optional=True)
+    wait: 'int | None' = betterproto.int64_field(3, optional=True)
+    color: 'RgbColor | None' = betterproto.message_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingFillDownfromAction(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    percentage: 'float | None' = betterproto.float_field(2, optional=True)
+    wait: 'int | None' = betterproto.int64_field(3, optional=True)
+    color: 'RgbColor | None' = betterproto.message_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingCommandEvent(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    command: 'list[str]' = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class RgbRingState(betterproto.Message):
+    meta_field_package_name_rgb_ring: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_connected: bool = betterproto.bool_field(2)
+    is_busy: bool = betterproto.bool_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class RgbColorElement(betterproto.Message):
+    float: builtins.float = betterproto.float_field(1, group='rgb_color_element')
+    int64: int = betterproto.int64_field(2, group='rgb_color_element')
+
+
+@dataclass(eq=False, repr=False)
+class RgbColor(betterproto.Message):
+    items: 'list[RgbColorElement]' = betterproto.message_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class CameraAction(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class CameraStartViewfinderAction(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+    pattern: str = betterproto.string_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class CameraEvent(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class CameraStartViewfinderEvent(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    pattern: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class CameraStopViewfinderEvent(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class CameraReportBarcodeAction(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    codes: 'list[str]' = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class CameraBarcodeEvent(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+    code: str = betterproto.string_field(3)
+    group_dict: 'dict[str, str]' = betterproto.map_field(
+        4, betterproto.TYPE_STRING, betterproto.TYPE_STRING,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class InputDescription(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+    pattern: str = betterproto.string_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class CameraState(betterproto.Message):
+    meta_field_package_name_camera: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    current: 'InputDescription | None' = betterproto.message_field(2, optional=True)
+    is_viewfinder_active: bool = betterproto.bool_field(3)
+    queue: 'list[InputDescription]' = betterproto.message_field(4)
+
+
+@dataclass(eq=False, repr=False)
+class IpAction(betterproto.Message):
+    meta_field_package_name_ip: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class IpEvent(betterproto.Message):
+    meta_field_package_name_ip: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class IpUpdateInterfacesAction(betterproto.Message):
+    meta_field_package_name_ip: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    interfaces: 'list[IpNetworkInterface]' = betterproto.message_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class IpSetIsConnectedAction(betterproto.Message):
+    meta_field_package_name_ip: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_connected: bool = betterproto.bool_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class IpNetworkInterface(betterproto.Message):
+    meta_field_package_name_ip: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    name: str = betterproto.string_field(2)
+    ip_addresses: 'list[str]' = betterproto.string_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class IpState(betterproto.Message):
+    meta_field_package_name_ip: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    interfaces: 'list[IpNetworkInterface]' = betterproto.message_field(2)
+    is_connected: 'bool | None' = betterproto.bool_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationActionItem(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    background_color: 'str | None' = betterproto.string_field(2, optional=True)
+    dismiss_notification: 'bool | None' = betterproto.bool_field(3, optional=True)
+    key: 'str | None' = betterproto.string_field(4, optional=True)
+    label: 'str | None' = betterproto.string_field(5, optional=True)
+    color: 'str | None' = betterproto.string_field(6, optional=True)
+    icon: 'str | None' = betterproto.string_field(7, optional=True)
+    is_short: 'bool | None' = betterproto.bool_field(8, optional=True)
+    opacity: 'float | None' = betterproto.float_field(9, optional=True)
+    progress: 'float | None' = betterproto.float_field(10, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationDispatchItem(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    operation: 'NotificationDispatchItemOperation' = betterproto.message_field(2)
+    key: 'str | None' = betterproto.string_field(3, optional=True)
+    label: 'str | None' = betterproto.string_field(4, optional=True)
+    color: 'str | None' = betterproto.string_field(5, optional=True)
+    background_color: 'str | None' = betterproto.string_field(6, optional=True)
+    icon: 'str | None' = betterproto.string_field(7, optional=True)
+    is_short: 'bool | None' = betterproto.bool_field(8, optional=True)
+    opacity: 'float | None' = betterproto.float_field(9, optional=True)
+    progress: 'float | None' = betterproto.float_field(10, optional=True)
+    dismiss_notification: 'bool | None' = betterproto.bool_field(11, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationDispatchItemOperation(betterproto.Message):
+    ubo_event: 'Event' = betterproto.message_field(1, group='operation')
+    ubo_action: 'Action' = betterproto.message_field(2, group='operation')
+
+
+@dataclass(eq=False, repr=False)
+class NotificationExtraInformation(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    text: str = betterproto.string_field(2)
+    piper_text: 'str | None' = betterproto.string_field(3, optional=True)
+    picovoice_text: 'str | None' = betterproto.string_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class Notification(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: 'str | None' = betterproto.string_field(2, optional=True)
+    title: str = betterproto.string_field(3)
+    content: str = betterproto.string_field(4)
+    extra_information: 'NotificationExtraInformation | None' = (
+        betterproto.message_field(5, optional=True)
+    )
+    importance: 'Importance | None' = betterproto.enum_field(6, optional=True)
+    chime: 'Chime | None' = betterproto.enum_field(7, optional=True)
+    timestamp: 'int | None' = betterproto.int64_field(8, optional=True)
+    is_read: 'bool | None' = betterproto.bool_field(9, optional=True)
+    sender: 'str | None' = betterproto.string_field(10, optional=True)
+    actions: 'NotificationActions | None' = betterproto.message_field(11, optional=True)
+    icon: 'str | None' = betterproto.string_field(12, optional=True)
+    color: 'str | None' = betterproto.string_field(13, optional=True)
+    expiration_timestamp: 'int | None' = betterproto.int64_field(14, optional=True)
+    display_type: 'NotificationDisplayType | None' = betterproto.enum_field(
+        15, optional=True,
+    )
+    flash_time: 'float | None' = betterproto.float_field(16, optional=True)
+    dismissable: 'bool | None' = betterproto.bool_field(17, optional=True)
+    dismiss_on_close: 'bool | None' = betterproto.bool_field(18, optional=True)
+    on_close: 'NotificationOnClose | None' = betterproto.message_field(
+        19, optional=True,
+    )
+    blink: 'bool | None' = betterproto.bool_field(20, optional=True)
+    progress: 'float | None' = betterproto.float_field(21, optional=True)
+    progress_weight: 'float | None' = betterproto.float_field(22, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationActionsItem(betterproto.Message):
+    notification_action_item: 'NotificationActionItem' = betterproto.message_field(
+        1, group='actions_item',
+    )
+    notification_dispatch_item: 'NotificationDispatchItem' = betterproto.message_field(
+        2, group='actions_item',
+    )
+
+
+@dataclass(eq=False, repr=False)
+class NotificationActions(betterproto.Message):
+    items: 'list[NotificationActionsItem]' = betterproto.message_field(1)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationOnClose(betterproto.Message):
+    pass
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsAction(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsAddAction(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    notification: 'Notification' = betterproto.message_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsClearAction(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    notification: 'Notification' = betterproto.message_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsClearByIdAction(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsClearAllAction(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsEvent(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsClearEvent(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    notification: 'Notification' = betterproto.message_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsDisplayEvent(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    notification: 'Notification' = betterproto.message_field(2)
+    index: 'int | None' = betterproto.int64_field(3, optional=True)
+    count: 'int | None' = betterproto.int64_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class NotificationsState(betterproto.Message):
+    meta_field_package_name_notifications: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    notifications: 'list[Notification]' = betterproto.message_field(2)
+    unread_count: int = betterproto.int64_field(3)
+    progress: 'float | None' = betterproto.float_field(4, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class AudioAction(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class AudioSetVolumeAction(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    volume: float = betterproto.float_field(2)
+    device: 'AudioDevice' = betterproto.enum_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class AudioChangeVolumeAction(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    amount: float = betterproto.float_field(2)
+    device: 'AudioDevice' = betterproto.enum_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class AudioSetMuteStatusAction(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_mute: bool = betterproto.bool_field(2)
+    device: 'AudioDevice' = betterproto.enum_field(3)
+
+
+@dataclass(eq=False, repr=False)
+class AudioToggleMuteStatusAction(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    device: 'AudioDevice' = betterproto.enum_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class AudioPlayChimeAction(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    name: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class AudioPlayAudioAction(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: 'str | None' = betterproto.string_field(2, optional=True)
+    sample: bytes = betterproto.bytes_field(3)
+    channels: int = betterproto.int64_field(4)
+    rate: int = betterproto.int64_field(5)
+    width: int = betterproto.int64_field(6)
+
+
+@dataclass(eq=False, repr=False)
+class AudioEvent(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class AudioPlayChimeEvent(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    name: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class AudioPlayAudioEvent(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: 'str | None' = betterproto.string_field(2, optional=True)
+    sample: bytes = betterproto.bytes_field(3)
+    channels: int = betterproto.int64_field(4)
+    rate: int = betterproto.int64_field(5)
+    width: int = betterproto.int64_field(6)
+
+
+@dataclass(eq=False, repr=False)
+class AudioPlaybackDoneEvent(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    id: str = betterproto.string_field(2)
+
+
+@dataclass(eq=False, repr=False)
+class AudioState(betterproto.Message):
+    meta_field_package_name_audio: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    playback_volume: 'float | None' = betterproto.float_field(2, optional=True)
+    is_playback_mute: 'bool | None' = betterproto.bool_field(3, optional=True)
+    capture_volume: 'float | None' = betterproto.float_field(4, optional=True)
+    is_capture_mute: 'bool | None' = betterproto.bool_field(5, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class SshAction(betterproto.Message):
+    meta_field_package_name_ssh: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class SshUpdateStateAction(betterproto.Message):
+    meta_field_package_name_ssh: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_active: 'bool | None' = betterproto.bool_field(2, optional=True)
+    is_enabled: 'bool | None' = betterproto.bool_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class SshClearEnabledStateAction(betterproto.Message):
+    meta_field_package_name_ssh: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+
+
+@dataclass(eq=False, repr=False)
+class SshState(betterproto.Message):
+    meta_field_package_name_ssh: 'str | None' = betterproto.string_field(
+        1, optional=True,
+    )
+    is_active: 'bool | None' = betterproto.bool_field(2, optional=True)
+    is_enabled: 'bool | None' = betterproto.bool_field(3, optional=True)
+
+
+@dataclass(eq=False, repr=False)
+class Action(betterproto.Message):
+    light_dm_action: 'LightDmAction' = betterproto.message_field(1, group='action')
+    light_dm_update_state_action: 'LightDmUpdateStateAction' = (
+        betterproto.message_field(2, group='action')
+    )
+    light_dm_clear_enabled_state_action: 'LightDmClearEnabledStateAction' = (
+        betterproto.message_field(3, group='action')
+    )
+    wi_fi_action: 'WiFiAction' = betterproto.message_field(4, group='action')
+    wi_fi_set_has_visited_onboarding_action: 'WiFiSetHasVisitedOnboardingAction' = (
+        betterproto.message_field(5, group='action')
+    )
+    wi_fi_update_action: 'WiFiUpdateAction' = betterproto.message_field(
+        6, group='action',
+    )
+    wi_fi_update_request_action: 'WiFiUpdateRequestAction' = betterproto.message_field(
+        7, group='action',
+    )
+    sensors_action: 'SensorsAction' = betterproto.message_field(8, group='action')
+    sensors_report_reading_action: 'SensorsReportReadingAction' = (
+        betterproto.message_field(9, group='action')
+    )
+    users_action: 'UsersAction' = betterproto.message_field(10, group='action')
+    users_set_users_action: 'UsersSetUsersAction' = betterproto.message_field(
+        11, group='action',
+    )
+    users_create_user_action: 'UsersCreateUserAction' = betterproto.message_field(
+        12, group='action',
+    )
+    users_delete_user_action: 'UsersDeleteUserAction' = betterproto.message_field(
+        13, group='action',
+    )
+    users_reset_password_action: 'UsersResetPasswordAction' = betterproto.message_field(
+        14, group='action',
+    )
+    r_pi_connect_action: 'RPiConnectAction' = betterproto.message_field(
+        15, group='action',
+    )
+    r_pi_connect_start_downloading_action: 'RPiConnectStartDownloadingAction' = (
+        betterproto.message_field(16, group='action')
+    )
+    r_pi_connect_done_downloading_action: 'RPiConnectDoneDownloadingAction' = (
+        betterproto.message_field(17, group='action')
+    )
+    r_pi_connect_set_pending_action: 'RPiConnectSetPendingAction' = (
+        betterproto.message_field(18, group='action')
+    )
+    r_pi_connect_set_status_action: 'RPiConnectSetStatusAction' = (
+        betterproto.message_field(19, group='action')
+    )
+    r_pi_connect_update_service_state_action: 'RPiConnectUpdateServiceStateAction' = (
+        betterproto.message_field(20, group='action')
+    )
+    vs_code_action: 'VsCodeAction' = betterproto.message_field(21, group='action')
+    vs_code_start_downloading_action: 'VsCodeStartDownloadingAction' = (
+        betterproto.message_field(22, group='action')
+    )
+    vs_code_done_downloading_action: 'VsCodeDoneDownloadingAction' = (
+        betterproto.message_field(23, group='action')
+    )
+    vs_code_set_pending_action: 'VsCodeSetPendingAction' = betterproto.message_field(
+        24, group='action',
+    )
+    vs_code_set_status_action: 'VsCodeSetStatusAction' = betterproto.message_field(
+        25, group='action',
+    )
+    docker_action: 'DockerAction' = betterproto.message_field(26, group='action')
+    docker_set_status_action: 'DockerSetStatusAction' = betterproto.message_field(
+        27, group='action',
+    )
+    docker_store_username_action: 'DockerStoreUsernameAction' = (
+        betterproto.message_field(28, group='action')
+    )
+    docker_remove_username_action: 'DockerRemoveUsernameAction' = (
+        betterproto.message_field(29, group='action')
+    )
+    docker_image_action: 'DockerImageAction' = betterproto.message_field(
+        30, group='action',
+    )
+    docker_image_set_status_action: 'DockerImageSetStatusAction' = (
+        betterproto.message_field(31, group='action')
+    )
+    docker_image_set_docker_id_action: 'DockerImageSetDockerIdAction' = (
+        betterproto.message_field(32, group='action')
+    )
+    display_action: 'DisplayAction' = betterproto.message_field(33, group='action')
+    display_pause_action: 'DisplayPauseAction' = betterproto.message_field(
+        34, group='action',
+    )
+    display_resume_action: 'DisplayResumeAction' = betterproto.message_field(
+        35, group='action',
+    )
+    keypad_action: 'KeypadAction' = betterproto.message_field(36, group='action')
+    keypad_key_up_action: 'KeypadKeyUpAction' = betterproto.message_field(
+        37, group='action',
+    )
+    keypad_key_down_action: 'KeypadKeyDownAction' = betterproto.message_field(
+        38, group='action',
+    )
+    keypad_key_press_action: 'KeypadKeyPressAction' = betterproto.message_field(
+        39, group='action',
+    )
+    keypad_key_release_action: 'KeypadKeyReleaseAction' = betterproto.message_field(
+        40, group='action',
+    )
+    voice_action: 'VoiceAction' = betterproto.message_field(41, group='action')
+    voice_set_engine_action: 'VoiceSetEngineAction' = betterproto.message_field(
+        42, group='action',
+    )
+    voice_read_text_action: 'VoiceReadTextAction' = betterproto.message_field(
+        43, group='action',
+    )
+    rgb_ring_action: 'RgbRingAction' = betterproto.message_field(44, group='action')
+    rgb_ring_set_is_connected_action: 'RgbRingSetIsConnectedAction' = (
+        betterproto.message_field(45, group='action')
+    )
+    rgb_ring_set_is_busy_action: 'RgbRingSetIsBusyAction' = betterproto.message_field(
+        46, group='action',
+    )
+    rgb_ring_command_action: 'RgbRingCommandAction' = betterproto.message_field(
+        47, group='action',
+    )
+    rgb_ring_waitable_command_action: 'RgbRingWaitableCommandAction' = (
+        betterproto.message_field(48, group='action')
+    )
+    rgb_ring_colorful_command_action: 'RgbRingColorfulCommandAction' = (
+        betterproto.message_field(49, group='action')
+    )
+    rgb_ring_set_enabled_action: 'RgbRingSetEnabledAction' = betterproto.message_field(
+        50, group='action',
+    )
+    rgb_ring_set_all_action: 'RgbRingSetAllAction' = betterproto.message_field(
+        51, group='action',
+    )
+    rgb_ring_set_brightness_action: 'RgbRingSetBrightnessAction' = (
+        betterproto.message_field(52, group='action')
+    )
+    rgb_ring_blank_action: 'RgbRingBlankAction' = betterproto.message_field(
+        53, group='action',
+    )
+    rgb_ring_rainbow_action: 'RgbRingRainbowAction' = betterproto.message_field(
+        54, group='action',
+    )
+    rgb_ring_progress_wheel_step_action: 'RgbRingProgressWheelStepAction' = (
+        betterproto.message_field(55, group='action')
+    )
+    rgb_ring_pulse_action: 'RgbRingPulseAction' = betterproto.message_field(
+        56, group='action',
+    )
+    rgb_ring_blink_action: 'RgbRingBlinkAction' = betterproto.message_field(
+        57, group='action',
+    )
+    rgb_ring_spinning_wheel_action: 'RgbRingSpinningWheelAction' = (
+        betterproto.message_field(58, group='action')
+    )
+    rgb_ring_progress_wheel_action: 'RgbRingProgressWheelAction' = (
+        betterproto.message_field(59, group='action')
+    )
+    rgb_ring_fill_upto_action: 'RgbRingFillUptoAction' = betterproto.message_field(
+        60, group='action',
+    )
+    rgb_ring_fill_downfrom_action: 'RgbRingFillDownfromAction' = (
+        betterproto.message_field(61, group='action')
+    )
+    camera_action: 'CameraAction' = betterproto.message_field(62, group='action')
+    camera_start_viewfinder_action: 'CameraStartViewfinderAction' = (
+        betterproto.message_field(63, group='action')
+    )
+    camera_report_barcode_action: 'CameraReportBarcodeAction' = (
+        betterproto.message_field(64, group='action')
+    )
+    ip_action: 'IpAction' = betterproto.message_field(65, group='action')
+    ip_update_interfaces_action: 'IpUpdateInterfacesAction' = betterproto.message_field(
+        66, group='action',
+    )
+    ip_set_is_connected_action: 'IpSetIsConnectedAction' = betterproto.message_field(
+        67, group='action',
+    )
+    notifications_action: 'NotificationsAction' = betterproto.message_field(
+        68, group='action',
+    )
+    notifications_add_action: 'NotificationsAddAction' = betterproto.message_field(
+        69, group='action',
+    )
+    notifications_clear_action: 'NotificationsClearAction' = betterproto.message_field(
+        70, group='action',
+    )
+    notifications_clear_by_id_action: 'NotificationsClearByIdAction' = (
+        betterproto.message_field(71, group='action')
+    )
+    notifications_clear_all_action: 'NotificationsClearAllAction' = (
+        betterproto.message_field(72, group='action')
+    )
+    audio_action: 'AudioAction' = betterproto.message_field(73, group='action')
+    audio_set_volume_action: 'AudioSetVolumeAction' = betterproto.message_field(
+        74, group='action',
+    )
+    audio_change_volume_action: 'AudioChangeVolumeAction' = betterproto.message_field(
+        75, group='action',
+    )
+    audio_set_mute_status_action: 'AudioSetMuteStatusAction' = (
+        betterproto.message_field(76, group='action')
+    )
+    audio_toggle_mute_status_action: 'AudioToggleMuteStatusAction' = (
+        betterproto.message_field(77, group='action')
+    )
+    audio_play_chime_action: 'AudioPlayChimeAction' = betterproto.message_field(
+        78, group='action',
+    )
+    audio_play_audio_action: 'AudioPlayAudioAction' = betterproto.message_field(
+        79, group='action',
+    )
+    ssh_action: 'SshAction' = betterproto.message_field(80, group='action')
+    ssh_update_state_action: 'SshUpdateStateAction' = betterproto.message_field(
+        81, group='action',
+    )
+    ssh_clear_enabled_state_action: 'SshClearEnabledStateAction' = (
+        betterproto.message_field(82, group='action')
+    )
+
+
+@dataclass(eq=False, repr=False)
+class Event(betterproto.Message):
+    screenshot_event: 'ScreenshotEvent' = betterproto.message_field(1, group='event')
+    snapshot_event: 'SnapshotEvent' = betterproto.message_field(2, group='event')
+    wi_fi_event: 'WiFiEvent' = betterproto.message_field(3, group='event')
+    wi_fi_update_request_event: 'WiFiUpdateRequestEvent' = betterproto.message_field(
+        4, group='event',
+    )
+    users_event: 'UsersEvent' = betterproto.message_field(5, group='event')
+    users_create_user_event: 'UsersCreateUserEvent' = betterproto.message_field(
+        6, group='event',
+    )
+    users_delete_user_event: 'UsersDeleteUserEvent' = betterproto.message_field(
+        7, group='event',
+    )
+    users_reset_password_event: 'UsersResetPasswordEvent' = betterproto.message_field(
+        8, group='event',
+    )
+    r_pi_connect_event: 'RPiConnectEvent' = betterproto.message_field(9, group='event')
+    r_pi_connect_login_event: 'RPiConnectLoginEvent' = betterproto.message_field(
+        10, group='event',
+    )
+    vs_code_event: 'VsCodeEvent' = betterproto.message_field(11, group='event')
+    vs_code_login_event: 'VsCodeLoginEvent' = betterproto.message_field(
+        12, group='event',
+    )
+    vs_code_restart_event: 'VsCodeRestartEvent' = betterproto.message_field(
+        13, group='event',
+    )
+    docker_event: 'DockerEvent' = betterproto.message_field(14, group='event')
+    docker_image_event: 'DockerImageEvent' = betterproto.message_field(
+        15, group='event',
+    )
+    docker_image_register_app_event: 'DockerImageRegisterAppEvent' = (
+        betterproto.message_field(16, group='event')
+    )
+    display_event: 'DisplayEvent' = betterproto.message_field(17, group='event')
+    display_render_event: 'DisplayRenderEvent' = betterproto.message_field(
+        18, group='event',
+    )
+    keypad_event: 'KeypadEvent' = betterproto.message_field(19, group='event')
+    keypad_key_press_event: 'KeypadKeyPressEvent' = betterproto.message_field(
+        20, group='event',
+    )
+    keypad_key_release_event: 'KeypadKeyReleaseEvent' = betterproto.message_field(
+        21, group='event',
+    )
+    voice_event: 'VoiceEvent' = betterproto.message_field(22, group='event')
+    voice_synthesize_text_event: 'VoiceSynthesizeTextEvent' = betterproto.message_field(
+        23, group='event',
+    )
+    rgb_ring_event: 'RgbRingEvent' = betterproto.message_field(24, group='event')
+    rgb_ring_command_event: 'RgbRingCommandEvent' = betterproto.message_field(
+        25, group='event',
+    )
+    camera_event: 'CameraEvent' = betterproto.message_field(26, group='event')
+    camera_start_viewfinder_event: 'CameraStartViewfinderEvent' = (
+        betterproto.message_field(27, group='event')
+    )
+    camera_stop_viewfinder_event: 'CameraStopViewfinderEvent' = (
+        betterproto.message_field(28, group='event')
+    )
+    camera_barcode_event: 'CameraBarcodeEvent' = betterproto.message_field(
+        29, group='event',
+    )
+    ip_event: 'IpEvent' = betterproto.message_field(30, group='event')
+    notifications_event: 'NotificationsEvent' = betterproto.message_field(
+        31, group='event',
+    )
+    notifications_clear_event: 'NotificationsClearEvent' = betterproto.message_field(
+        32, group='event',
+    )
+    notifications_display_event: 'NotificationsDisplayEvent' = (
+        betterproto.message_field(33, group='event')
+    )
+    audio_event: 'AudioEvent' = betterproto.message_field(34, group='event')
+    audio_play_chime_event: 'AudioPlayChimeEvent' = betterproto.message_field(
+        35, group='event',
+    )
+    audio_play_audio_event: 'AudioPlayAudioEvent' = betterproto.message_field(
+        36, group='event',
+    )
+    audio_playback_done_event: 'AudioPlaybackDoneEvent' = betterproto.message_field(
+        37, group='event',
+    )
diff --git a/ubo_app/rpc/message_to_object.py b/ubo_app/rpc/message_to_object.py
new file mode 100644
index 00000000..bf5b9d76
--- /dev/null
+++ b/ubo_app/rpc/message_to_object.py
@@ -0,0 +1,115 @@
+# ruff: noqa: SLF001, S101, D100, D103
+from __future__ import annotations
+
+import enum
+import importlib
+from datetime import UTC, datetime
+from typing import TypeAlias, TypeVar, cast, overload
+
+import betterproto
+import betterproto.casing
+from betterproto import Enum
+from immutable import Immutable
+
+ReturnType: TypeAlias = (
+    Immutable
+    | enum.Enum
+    | int
+    | float
+    | str
+    | bool
+    | None
+    | datetime
+    | list['ReturnType']
+)
+
+META_FIELD_PREFIX_PACKAGE_NAME = 'meta_field_package_name_'
+
+
+def get_class(message: betterproto.Message) -> type | None:
+    source_class = type(message)
+    class_name = source_class.__name__
+
+    first_field_name = source_class._betterproto.field_name_by_number[1]
+    if first_field_name.startswith(META_FIELD_PREFIX_PACKAGE_NAME):
+        package_name = first_field_name[len(META_FIELD_PREFIX_PACKAGE_NAME) :]
+    else:
+        return None
+
+    destination_module_path = f'ubo_app.store.services.{package_name}'
+    destination_module = importlib.import_module(destination_module_path)
+
+    return getattr(destination_module, class_name, None)
+
+
+def reduce_group(message: betterproto.Message) -> betterproto.Message:
+    assert len(message._group_current) == 1
+    attribute = next(iter(message._group_current.values()))
+
+    return getattr(message, attribute)
+
+
+T = TypeVar('T', bound=Immutable)
+
+
+@overload
+def rebuild_object(
+    message: betterproto.Message | list[betterproto.Message],
+    expected_type: type[T],
+) -> T: ...
+@overload
+def rebuild_object(
+    message: betterproto.Message | list[betterproto.Message],
+) -> ReturnType: ...
+def rebuild_object(  # noqa: C901
+    message: betterproto.Message | list[betterproto.Message],
+    expected_type: type[T] | None = None,
+) -> ReturnType | T:
+    if isinstance(message, int | float | str | bool | None):
+        return cast(ReturnType, message)
+
+    if isinstance(message, list):
+        return [rebuild_object(item) for item in message]
+
+    if hasattr(message, '_group_current') and len(message._group_current) > 0:
+        return rebuild_object(reduce_group(message))
+
+    keys = message._betterproto_meta.sorted_field_names
+    if len(keys) == 1 and keys[0] == 'items':
+        return [rebuild_object(item) for item in getattr(message, 'items', [])]
+
+    destination_class = get_class(message)
+    if expected_type and (
+        destination_class is None or not issubclass(destination_class, expected_type)
+    ):
+        msg = f'Expected {expected_type}, got {destination_class}'
+        raise ValueError(msg)
+
+    fields = {
+        betterproto.casing.snake_case(key): datetime.fromtimestamp(
+            getattr(message, key),
+            tz=UTC,
+        )
+        if key.endswith('_timestamp')
+        else rebuild_object(getattr(message, key))
+        for key in keys
+        if not key.startswith('meta_field_') and getattr(message, key) is not None
+    }
+
+    if len(fields) == 1 and 'list' in fields:
+        return fields['list']
+
+    if destination_class is None:
+        msg = f'Class not found for {message}'
+        raise ValueError(msg)
+
+    if isinstance(message, Enum) and message.name:
+        if message.name == 'UNSPECIFIED':
+            return None
+        return getattr(destination_class, message.name)
+
+    if isinstance(destination_class, type) and issubclass(destination_class, Immutable):
+        return destination_class(**fields)
+
+    msg = f'Parsing {message} is not implemented yet'
+    raise NotImplementedError(msg)
diff --git a/ubo_app/rpc/proto/store/v1/store.proto b/ubo_app/rpc/proto/store/v1/store.proto
new file mode 100644
index 00000000..1b8fcf67
--- /dev/null
+++ b/ubo_app/rpc/proto/store/v1/store.proto
@@ -0,0 +1,30 @@
+syntax = "proto3";
+
+package store.v1;
+
+import "ubo/v1/ubo.proto";
+
+message DispatchActionRequest {
+  ubo.v1.Action action = 1;
+}
+
+message DispatchEventRequest {
+  ubo.v1.Event event = 1;
+}
+
+message DispatchActionResponse {}
+message DispatchEventResponse {}
+
+message SubscribeEventRequest {
+  ubo.v1.Event event = 1;
+}
+
+message SubscribeEventResponse {
+  ubo.v1.Event event = 1;
+}
+
+service StoreService {
+  rpc DispatchAction(DispatchActionRequest) returns (DispatchActionResponse);
+  rpc DispatchEvent(DispatchEventRequest) returns (DispatchEventResponse);
+  rpc SubscribeEvent(SubscribeEventRequest) returns (stream SubscribeEventResponse);
+}
diff --git a/ubo_app/rpc/proto/ubo/v1/ubo.proto b/ubo_app/rpc/proto/ubo/v1/ubo.proto
new file mode 100644
index 00000000..9fc6908f
--- /dev/null
+++ b/ubo_app/rpc/proto/ubo/v1/ubo.proto
@@ -0,0 +1,1419 @@
+syntax = "proto3";
+
+package ubo.v1;
+
+import "package_info/v1/package_info.proto";
+
+message BaseMenu {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+  string title = 2;
+  repeated Item items = 3;
+  optional string placeholder = 4;
+}
+
+message HeadedMenu {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+  string heading = 2;
+  string sub_heading = 3;
+  string title = 4;
+  repeated Item items = 5;
+  optional string placeholder = 6;
+}
+
+message HeadlessMenu {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+  string title = 2;
+  repeated Item items = 3;
+  optional string placeholder = 4;
+}
+
+message Item {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+  optional string key = 2;
+  optional string label = 3;
+  optional string color = 4;
+  optional string background_color = 5;
+  optional string icon = 6;
+  optional bool is_short = 7;
+  optional float opacity = 8;
+  optional float progress = 9;
+}
+
+message ActionItem {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+  optional string key = 2;
+  optional string label = 3;
+  optional string color = 4;
+  optional string background_color = 5;
+  optional string icon = 6;
+  optional bool is_short = 7;
+  optional float opacity = 8;
+  optional float progress = 9;
+}
+
+message ApplicationItem {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+  string application = 2;
+  optional string key = 3;
+  optional string label = 4;
+  optional string color = 5;
+  optional string background_color = 6;
+  optional string icon = 7;
+  optional bool is_short = 8;
+  optional float opacity = 9;
+  optional float progress = 10;
+}
+
+message SubMenuItem {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+  Menu sub_menu = 2;
+  optional string key = 3;
+  optional string label = 4;
+  optional string color = 5;
+  optional string background_color = 6;
+  optional string icon = 7;
+  optional bool is_short = 8;
+  optional float opacity = 9;
+  optional float progress = 10;
+}
+
+message Subscribable {
+  option (package_info.v1.package_name) = "menu";
+  optional string meta_field_package_name_menu = 1;
+}
+
+message Menu {
+  oneof menu {
+    HeadedMenu headed_menu = 1;
+    HeadlessMenu headless_menu = 2;
+  }
+}
+message ScreenshotEvent {
+  option (package_info.v1.package_name) = "operations";
+  optional string meta_field_package_name_operations = 1;
+}
+
+message SnapshotEvent {
+  option (package_info.v1.package_name) = "operations";
+  optional string meta_field_package_name_operations = 1;
+}
+
+message UboAction {
+  oneof ubo_action {
+    VoiceAction voice_action = 1;
+    SSHAction ssh_action = 2;
+    RgbRingAction rgb_ring_action = 3;
+    NotificationsAction notifications_action = 4;
+    LightDMAction light_dm_action = 5;
+    IpAction ip_action = 6;
+    DisplayAction display_action = 7;
+    CameraAction camera_action = 8;
+    AudioAction audio_action = 9;
+    DockerAction docker_action = 10;
+    KeypadAction keypad_action = 11;
+    RPiConnectAction r_pi_connect_action = 12;
+    SensorsAction sensors_action = 13;
+    UsersAction users_action = 14;
+    VSCodeAction vs_code_action = 15;
+    WiFiAction wi_fi_action = 16;
+  }
+}
+
+message UboEvent {
+  oneof ubo_event {
+    UsersEvent users_event = 1;
+    NotificationsEvent notifications_event = 2;
+    IpEvent ip_event = 3;
+    DisplayEvent display_event = 4;
+    AudioEvent audio_event = 5;
+    ScreenshotEvent screenshot_event = 6;
+    CameraEvent camera_event = 7;
+    KeypadEvent keypad_event = 8;
+    SnapshotEvent snapshot_event = 9;
+    WiFiEvent wi_fi_event = 10;
+  }
+}
+message DispatchItem {
+  option (package_info.v1.package_name) = "dispatch_action";
+  optional string meta_field_package_name_dispatch_action = 1;
+
+  message Operation {
+    oneof operation {
+      Event ubo_event = 1;
+      Action ubo_action = 2;
+    }
+  }
+  Operation operation = 2;
+  optional string key = 3;
+  optional string label = 4;
+  optional string color = 5;
+  optional string background_color = 6;
+  optional string icon = 7;
+  optional bool is_short = 8;
+  optional float opacity = 9;
+  optional float progress = 10;
+}
+
+message LightDMAction {
+  option (package_info.v1.package_name) = "lightdm";
+  optional string meta_field_package_name_lightdm = 1;
+}
+
+message LightDMUpdateStateAction {
+  option (package_info.v1.package_name) = "lightdm";
+  optional string meta_field_package_name_lightdm = 1;
+  optional bool is_active = 2;
+  optional bool is_enabled = 3;
+  optional bool is_installed = 4;
+  optional bool is_installing = 5;
+}
+
+message LightDMClearEnabledStateAction {
+  option (package_info.v1.package_name) = "lightdm";
+  optional string meta_field_package_name_lightdm = 1;
+}
+
+message LightDMState {
+  option (package_info.v1.package_name) = "lightdm";
+  optional string meta_field_package_name_lightdm = 1;
+  optional bool is_active = 2;
+  optional bool is_enabled = 3;
+  optional bool is_installed = 4;
+  optional bool is_installing = 5;
+}
+
+enum WiFiType {
+  WI_FI_TYPE_UNSPECIFIED = 0;
+  WI_FI_TYPE_WEP = 1;
+  WI_FI_TYPE_WPA = 2;
+  WI_FI_TYPE_WPA2 = 3;
+  WI_FI_TYPE_NOPASS = 4;
+}
+
+enum ConnectionState {
+  CONNECTION_STATE_UNSPECIFIED = 0;
+  CONNECTION_STATE_CONNECTED = 1;
+  CONNECTION_STATE_CONNECTING = 2;
+  CONNECTION_STATE_DISCONNECTED = 3;
+  CONNECTION_STATE_UNKNOWN = 4;
+}
+
+enum GlobalWiFiState {
+  GLOBAL_WI_FI_STATE_UNSPECIFIED = 0;
+  GLOBAL_WI_FI_STATE_CONNECTED = 1;
+  GLOBAL_WI_FI_STATE_DISCONNECTED = 2;
+  GLOBAL_WI_FI_STATE_PENDING = 3;
+  GLOBAL_WI_FI_STATE_NEEDS_ATTENTION = 4;
+  GLOBAL_WI_FI_STATE_UNKNOWN = 5;
+}
+
+message WiFiConnection {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+  string ssid = 2;
+  optional ConnectionState state = 3;
+  optional int64 signal_strength = 4;
+  optional string password = 5;
+  optional WiFiType type = 6;
+  optional bool hidden = 7;
+}
+
+message WiFiAction {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+}
+
+message WiFiSetHasVisitedOnboardingAction {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+  bool has_visited_onboarding = 2;
+}
+
+message WiFiUpdateAction {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+  repeated WiFiConnection connections = 2;
+  GlobalWiFiState state = 3;
+  WiFiConnection current_connection = 4;
+}
+
+message WiFiUpdateRequestAction {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+  optional bool reset = 2;
+}
+
+message WiFiEvent {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+}
+
+message WiFiUpdateRequestEvent {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+}
+
+message WiFiState {
+  option (package_info.v1.package_name) = "wifi";
+  optional string meta_field_package_name_wifi = 1;
+  repeated WiFiConnection connections = 2;
+  GlobalWiFiState state = 3;
+  WiFiConnection current_connection = 4;
+  optional bool has_visited_onboarding = 5;
+}
+
+enum Sensor {
+  SENSOR_UNSPECIFIED = 0;
+  SENSOR_TEMPERATURE = 1;
+  SENSOR_LIGHT = 2;
+}
+
+message SensorsAction {
+  option (package_info.v1.package_name) = "sensors";
+  optional string meta_field_package_name_sensors = 1;
+}
+
+message SensorsReportReadingAction {
+  option (package_info.v1.package_name) = "sensors";
+  optional string meta_field_package_name_sensors = 1;
+  Sensor sensor = 2;
+  float reading = 3;
+  int64 timestamp = 4;
+}
+
+message SensorState {
+  option (package_info.v1.package_name) = "sensors";
+  optional string meta_field_package_name_sensors = 1;
+  optional float value = 2;
+}
+
+message SensorsState {
+  option (package_info.v1.package_name) = "sensors";
+  optional string meta_field_package_name_sensors = 1;
+  optional SensorState temperature = 2;
+  optional SensorState light = 3;
+}
+
+message UsersAction {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+}
+
+message UsersSetUsersAction {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+  repeated UserState users = 2;
+}
+
+message UsersCreateUserAction {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+}
+
+message UsersDeleteUserAction {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+  string id = 2;
+}
+
+message UsersResetPasswordAction {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+  string id = 2;
+}
+
+message UsersEvent {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+}
+
+message UsersCreateUserEvent {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+}
+
+message UsersDeleteUserEvent {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+  string id = 2;
+}
+
+message UsersResetPasswordEvent {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+  string id = 2;
+}
+
+message UserState {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+  string id = 2;
+  bool is_removable = 3;
+}
+
+message UsersState {
+  option (package_info.v1.package_name) = "users";
+  optional string meta_field_package_name_users = 1;
+
+  message Users {
+    repeated UserState items = 1;
+  }
+  optional Users users = 2;
+}
+
+message RPiConnectAction {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+}
+
+message RPiConnectEvent {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+}
+
+message RPiConnectStartDownloadingAction {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+}
+
+message RPiConnectDoneDownloadingAction {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+}
+
+message RPiConnectSetPendingAction {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+}
+
+message RPiConnectStatus {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+  int64 screen_sharing_sessions = 2;
+  int64 remote_shell_sessions = 3;
+}
+
+message RPiConnectSetStatusAction {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+  bool is_installed = 2;
+  bool is_signed_in = 3;
+  RPiConnectStatus status = 4;
+}
+
+message RPiConnectLoginEvent {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+}
+
+message RPiConnectUpdateServiceStateAction {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+  optional bool is_active = 2;
+}
+
+message RPiConnectState {
+  option (package_info.v1.package_name) = "rpi_connect";
+  optional string meta_field_package_name_rpi_connect = 1;
+  optional bool is_downloading = 2;
+  optional bool is_active = 3;
+  optional bool is_installed = 4;
+  optional bool is_signed_in = 5;
+  optional RPiConnectStatus status = 6;
+}
+
+message VSCodeAction {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+}
+
+message VSCodeEvent {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+}
+
+message VSCodeStartDownloadingAction {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+}
+
+message VSCodeDoneDownloadingAction {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+}
+
+message VSCodeSetPendingAction {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+}
+
+message VSCodeStatus {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+  bool is_service_installed = 2;
+  bool is_running = 3;
+  string name = 4;
+}
+
+message VSCodeSetStatusAction {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+  bool is_binary_installed = 2;
+  bool is_logged_in = 3;
+  VSCodeStatus status = 4;
+}
+
+message VSCodeLoginEvent {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+}
+
+message VSCodeRestartEvent {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+}
+
+message VSCodeState {
+  option (package_info.v1.package_name) = "vscode";
+  optional string meta_field_package_name_vscode = 1;
+  optional bool is_pending = 2;
+  optional bool is_downloading = 3;
+  optional bool is_binary_installed = 4;
+  optional bool is_logged_in = 5;
+  optional VSCodeStatus status = 6;
+}
+
+enum DockerStatus {
+  DOCKER_STATUS_UNSPECIFIED = 0;
+  DOCKER_STATUS_UNKNOWN = 1;
+  DOCKER_STATUS_NOT_INSTALLED = 2;
+  DOCKER_STATUS_INSTALLING = 3;
+  DOCKER_STATUS_NOT_RUNNING = 4;
+  DOCKER_STATUS_RUNNING = 5;
+  DOCKER_STATUS_ERROR = 6;
+}
+
+enum ImageStatus {
+  IMAGE_STATUS_UNSPECIFIED = 0;
+  IMAGE_STATUS_NOT_AVAILABLE = 1;
+  IMAGE_STATUS_FETCHING = 2;
+  IMAGE_STATUS_AVAILABLE = 3;
+  IMAGE_STATUS_CREATED = 4;
+  IMAGE_STATUS_RUNNING = 5;
+  IMAGE_STATUS_ERROR = 6;
+}
+
+message DockerAction {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+}
+
+message DockerSetStatusAction {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  DockerStatus status = 2;
+}
+
+message DockerStoreUsernameAction {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  string registry = 2;
+  string username = 3;
+}
+
+message DockerRemoveUsernameAction {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  string registry = 2;
+}
+
+message DockerImageAction {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  string image = 2;
+}
+
+message DockerImageSetStatusAction {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+
+  message Ports {
+    repeated string items = 1;
+  }
+  ImageStatus status = 2;
+  optional Ports ports = 3;
+  optional string ip = 4;
+  string image = 5;
+}
+
+message DockerImageSetDockerIdAction {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  string docker_id = 2;
+  string image = 3;
+}
+
+message DockerEvent {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+}
+
+message DockerServiceState {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+
+  message UsernamesDict {
+    map<string, string> items = 1;
+  }
+  optional DockerStatus status = 2;
+  optional UsernamesDict usernames = 3;
+}
+
+message DockerImageEvent {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  string image = 2;
+}
+
+message DockerImageRegisterAppEvent {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  string image = 2;
+}
+
+message ImageState {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+
+  message Ports {
+    repeated string items = 1;
+  }
+  string id = 2;
+  optional ImageStatus status = 3;
+  optional string container_ip = 4;
+  optional string docker_id = 5;
+  optional Ports ports = 6;
+}
+
+message DockerState {
+  option (package_info.v1.package_name) = "docker";
+  optional string meta_field_package_name_docker = 1;
+  DockerServiceState service = 2;
+}
+
+message DisplayAction {
+  option (package_info.v1.package_name) = "display";
+  optional string meta_field_package_name_display = 1;
+}
+
+message DisplayEvent {
+  option (package_info.v1.package_name) = "display";
+  optional string meta_field_package_name_display = 1;
+}
+
+message DisplayPauseAction {
+  option (package_info.v1.package_name) = "display";
+  optional string meta_field_package_name_display = 1;
+}
+
+message DisplayResumeAction {
+  option (package_info.v1.package_name) = "display";
+  optional string meta_field_package_name_display = 1;
+}
+
+message DisplayRenderEvent {
+  option (package_info.v1.package_name) = "display";
+  optional string meta_field_package_name_display = 1;
+  bytes data = 2;
+  int64 data_hash = 3;
+  repeated int64 rectangle = 4;
+}
+
+message DisplayState {
+  option (package_info.v1.package_name) = "display";
+  optional string meta_field_package_name_display = 1;
+  optional bool is_paused = 2;
+}
+
+enum Key {
+  KEY_UNSPECIFIED = 0;
+  KEY_BACK = 1;
+  KEY_HOME = 2;
+  KEY_UP = 3;
+  KEY_DOWN = 4;
+  KEY_L1 = 5;
+  KEY_L2 = 6;
+  KEY_L3 = 7;
+}
+
+message KeypadAction {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  optional float time = 3;
+}
+
+message KeypadKeyUpAction {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  optional float time = 3;
+}
+
+message KeypadKeyDownAction {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  optional float time = 3;
+}
+
+message KeypadKeyPressAction {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  optional float time = 3;
+}
+
+message KeypadKeyReleaseAction {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  optional float time = 3;
+}
+
+message KeypadEvent {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  float time = 3;
+}
+
+message KeypadKeyPressEvent {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  float time = 3;
+}
+
+message KeypadKeyReleaseEvent {
+  option (package_info.v1.package_name) = "keypad";
+  optional string meta_field_package_name_keypad = 1;
+  Key key = 2;
+  float time = 3;
+}
+
+enum VoiceEngine {
+  VOICE_ENGINE_UNSPECIFIED = 0;
+  VOICE_ENGINE_PIPER = 1;
+  VOICE_ENGINE_PICOVOICE = 2;
+}
+
+message VoiceAction {
+  option (package_info.v1.package_name) = "voice";
+  optional string meta_field_package_name_voice = 1;
+}
+
+message VoiceEvent {
+  option (package_info.v1.package_name) = "voice";
+  optional string meta_field_package_name_voice = 1;
+}
+
+message VoiceUpdateAccessKeyStatus {
+  option (package_info.v1.package_name) = "voice";
+  optional string meta_field_package_name_voice = 1;
+  bool is_access_key_set = 2;
+}
+
+message VoiceSetEngineAction {
+  option (package_info.v1.package_name) = "voice";
+  optional string meta_field_package_name_voice = 1;
+  VoiceEngine engine = 2;
+}
+
+message VoiceReadTextAction {
+  option (package_info.v1.package_name) = "voice";
+  optional string meta_field_package_name_voice = 1;
+  string text = 2;
+  optional string piper_text = 3;
+  optional string picovoice_text = 4;
+  optional float speech_rate = 5;
+  optional VoiceEngine engine = 6;
+}
+
+message VoiceSynthesizeTextEvent {
+  option (package_info.v1.package_name) = "voice";
+  optional string meta_field_package_name_voice = 1;
+  string text = 2;
+  string piper_text = 3;
+  string picovoice_text = 4;
+  optional float speech_rate = 5;
+}
+
+message VoiceState {
+  option (package_info.v1.package_name) = "voice";
+  optional string meta_field_package_name_voice = 1;
+  optional bool is_access_key_set = 2;
+  optional VoiceEngine selected_engine = 3;
+}
+
+enum GlobalEthernetState {
+  GLOBAL_ETHERNET_STATE_UNSPECIFIED = 0;
+  GLOBAL_ETHERNET_STATE_CONNECTED = 1;
+  GLOBAL_ETHERNET_STATE_DISCONNECTED = 2;
+  GLOBAL_ETHERNET_STATE_PENDING = 3;
+  GLOBAL_ETHERNET_STATE_NEEDS_ATTENTION = 4;
+  GLOBAL_ETHERNET_STATE_UNKNOWN = 5;
+}
+
+message RgbRingAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+}
+
+message RgbRingEvent {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+}
+
+message RgbRingSetIsConnectedAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional bool is_connected = 2;
+}
+
+message RgbRingSetIsBusyAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional bool is_busy = 2;
+}
+
+message RgbRingCommandAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+}
+
+message RgbRingWaitableCommandAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional int64 wait = 2;
+}
+
+message RgbRingColorfulCommandAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional RgbColor color = 2;
+}
+
+message RgbRingSetEnabledAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional bool enabled = 2;
+}
+
+message RgbRingSetAllAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional RgbColor color = 2;
+}
+
+message RgbRingSetBrightnessAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional float brightness = 2;
+}
+
+message RgbRingBlankAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+}
+
+message RgbRingRainbowAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  int64 rounds = 2;
+  optional int64 wait = 3;
+}
+
+message RgbRingProgressWheelStepAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional RgbColor color = 2;
+}
+
+message RgbRingPulseAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional int64 repetitions = 2;
+  optional int64 wait = 3;
+  optional RgbColor color = 4;
+}
+
+message RgbRingBlinkAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional int64 repetitions = 2;
+  optional int64 wait = 3;
+  optional RgbColor color = 4;
+}
+
+message RgbRingSpinningWheelAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional int64 length = 2;
+  optional int64 repetitions = 3;
+  optional int64 wait = 4;
+  optional RgbColor color = 5;
+}
+
+message RgbRingProgressWheelAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional float percentage = 2;
+  optional RgbColor color = 3;
+}
+
+message RgbRingFillUptoAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional float percentage = 2;
+  optional int64 wait = 3;
+  optional RgbColor color = 4;
+}
+
+message RgbRingFillDownfromAction {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  optional float percentage = 2;
+  optional int64 wait = 3;
+  optional RgbColor color = 4;
+}
+
+message RgbRingCommandEvent {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  repeated string command = 2;
+}
+
+message RgbRingState {
+  option (package_info.v1.package_name) = "rgb_ring";
+  optional string meta_field_package_name_rgb_ring = 1;
+  bool is_connected = 2;
+  bool is_busy = 3;
+}
+
+message RgbColorElement {
+  oneof rgb_color_element {
+    float float = 1;
+    int64 int64 = 2;
+  }
+}
+
+message RgbColor {
+  repeated RgbColorElement items = 1;
+}
+message CameraAction {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+}
+
+message CameraStartViewfinderAction {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+  string id = 2;
+  string pattern = 3;
+}
+
+message CameraEvent {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+}
+
+message CameraStartViewfinderEvent {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+  string pattern = 2;
+}
+
+message CameraStopViewfinderEvent {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+  string id = 2;
+}
+
+message CameraReportBarcodeAction {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+  repeated string codes = 2;
+}
+
+message CameraBarcodeEvent {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+
+  string id = 2;
+  string code = 3;
+  map<string, string> group_dict = 4;
+}
+
+message InputDescription {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+  string id = 2;
+  string pattern = 3;
+}
+
+message CameraState {
+  option (package_info.v1.package_name) = "camera";
+  optional string meta_field_package_name_camera = 1;
+  optional InputDescription current = 2;
+  bool is_viewfinder_active = 3;
+  repeated InputDescription queue = 4;
+}
+
+message IpAction {
+  option (package_info.v1.package_name) = "ip";
+  optional string meta_field_package_name_ip = 1;
+}
+
+message IpEvent {
+  option (package_info.v1.package_name) = "ip";
+  optional string meta_field_package_name_ip = 1;
+}
+
+message IpUpdateInterfacesAction {
+  option (package_info.v1.package_name) = "ip";
+  optional string meta_field_package_name_ip = 1;
+  repeated IpNetworkInterface interfaces = 2;
+}
+
+message IpSetIsConnectedAction {
+  option (package_info.v1.package_name) = "ip";
+  optional string meta_field_package_name_ip = 1;
+  bool is_connected = 2;
+}
+
+message IpNetworkInterface {
+  option (package_info.v1.package_name) = "ip";
+  optional string meta_field_package_name_ip = 1;
+  string name = 2;
+  repeated string ip_addresses = 3;
+}
+
+message IpState {
+  option (package_info.v1.package_name) = "ip";
+  optional string meta_field_package_name_ip = 1;
+  repeated IpNetworkInterface interfaces = 2;
+  optional bool is_connected = 3;
+}
+
+enum Importance {
+  IMPORTANCE_UNSPECIFIED = 0;
+  IMPORTANCE_CRITICAL = 1;
+  IMPORTANCE_HIGH = 2;
+  IMPORTANCE_MEDIUM = 3;
+  IMPORTANCE_LOW = 4;
+}
+
+enum NotificationDisplayType {
+  NOTIFICATION_DISPLAY_TYPE_UNSPECIFIED = 0;
+  NOTIFICATION_DISPLAY_TYPE_NOT_SET = 1;
+  NOTIFICATION_DISPLAY_TYPE_BACKGROUND = 2;
+  NOTIFICATION_DISPLAY_TYPE_FLASH = 3;
+  NOTIFICATION_DISPLAY_TYPE_STICKY = 4;
+}
+
+enum Chime {
+  CHIME_UNSPECIFIED = 0;
+  CHIME_ADD = 1;
+  CHIME_DONE = 2;
+  CHIME_FAILURE = 3;
+  CHIME_VOLUME_CHANGE = 4;
+}
+
+message NotificationActionItem {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  optional string background_color = 2;
+  optional bool dismiss_notification = 3;
+  optional string key = 4;
+  optional string label = 5;
+  optional string color = 6;
+  optional string icon = 7;
+  optional bool is_short = 8;
+  optional float opacity = 9;
+  optional float progress = 10;
+}
+
+message NotificationDispatchItem {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+
+  message Operation {
+    oneof operation {
+      Event ubo_event = 1;
+      Action ubo_action = 2;
+    }
+  }
+  Operation operation = 2;
+  optional string key = 3;
+  optional string label = 4;
+  optional string color = 5;
+  optional string background_color = 6;
+  optional string icon = 7;
+  optional bool is_short = 8;
+  optional float opacity = 9;
+  optional float progress = 10;
+  optional bool dismiss_notification = 11;
+}
+
+message NotificationExtraInformation {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  string text = 2;
+  optional string piper_text = 3;
+  optional string picovoice_text = 4;
+}
+
+message Notification {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+
+  message ActionsItem {
+    oneof actions_item {
+      NotificationActionItem notification_action_item = 1;
+      NotificationDispatchItem notification_dispatch_item = 2;
+    }
+  }
+
+  message Actions {
+    repeated ActionsItem items = 1;
+  }
+
+  message OnClose {}
+  optional string id = 2;
+  string title = 3;
+  string content = 4;
+  optional NotificationExtraInformation extra_information = 5;
+  optional Importance importance = 6;
+  optional Chime chime = 7;
+  optional int64 timestamp = 8;
+  optional bool is_read = 9;
+  optional string sender = 10;
+  optional Actions actions = 11;
+  optional string icon = 12;
+  optional string color = 13;
+  optional int64 expiration_timestamp = 14;
+  optional NotificationDisplayType display_type = 15;
+  optional float flash_time = 16;
+  optional bool dismissable = 17;
+  optional bool dismiss_on_close = 18;
+  optional OnClose on_close = 19;
+  optional bool blink = 20;
+  optional float progress = 21;
+  optional float progress_weight = 22;
+}
+
+message NotificationsAction {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+}
+
+message NotificationsAddAction {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  Notification notification = 2;
+}
+
+message NotificationsClearAction {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  Notification notification = 2;
+}
+
+message NotificationsClearByIdAction {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  string id = 2;
+}
+
+message NotificationsClearAllAction {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+}
+
+message NotificationsEvent {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+}
+
+message NotificationsClearEvent {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  Notification notification = 2;
+}
+
+message NotificationsDisplayEvent {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  Notification notification = 2;
+  optional int64 index = 3;
+  optional int64 count = 4;
+}
+
+message NotificationsState {
+  option (package_info.v1.package_name) = "notifications";
+  optional string meta_field_package_name_notifications = 1;
+  repeated Notification notifications = 2;
+  int64 unread_count = 3;
+  optional float progress = 4;
+}
+
+enum AudioDevice {
+  AUDIO_DEVICE_UNSPECIFIED = 0;
+  AUDIO_DEVICE_INPUT = 1;
+  AUDIO_DEVICE_OUTPUT = 2;
+}
+
+message AudioAction {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+}
+
+message AudioSetVolumeAction {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  float volume = 2;
+  AudioDevice device = 3;
+}
+
+message AudioChangeVolumeAction {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  float amount = 2;
+  AudioDevice device = 3;
+}
+
+message AudioSetMuteStatusAction {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  bool is_mute = 2;
+  AudioDevice device = 3;
+}
+
+message AudioToggleMuteStatusAction {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  AudioDevice device = 2;
+}
+
+message AudioPlayChimeAction {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  string name = 2;
+}
+
+message AudioPlayAudioAction {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  optional string id = 2;
+  bytes sample = 3;
+  int64 channels = 4;
+  int64 rate = 5;
+  int64 width = 6;
+}
+
+message AudioEvent {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+}
+
+message AudioPlayChimeEvent {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  string name = 2;
+}
+
+message AudioPlayAudioEvent {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  optional string id = 2;
+  bytes sample = 3;
+  int64 channels = 4;
+  int64 rate = 5;
+  int64 width = 6;
+}
+
+message AudioPlaybackDoneEvent {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  string id = 2;
+}
+
+message AudioState {
+  option (package_info.v1.package_name) = "audio";
+  optional string meta_field_package_name_audio = 1;
+  optional float playback_volume = 2;
+  optional bool is_playback_mute = 3;
+  optional float capture_volume = 4;
+  optional bool is_capture_mute = 5;
+}
+
+message SSHAction {
+  option (package_info.v1.package_name) = "ssh";
+  optional string meta_field_package_name_ssh = 1;
+}
+
+message SSHUpdateStateAction {
+  option (package_info.v1.package_name) = "ssh";
+  optional string meta_field_package_name_ssh = 1;
+  optional bool is_active = 2;
+  optional bool is_enabled = 3;
+}
+
+message SSHClearEnabledStateAction {
+  option (package_info.v1.package_name) = "ssh";
+  optional string meta_field_package_name_ssh = 1;
+}
+
+message SSHState {
+  option (package_info.v1.package_name) = "ssh";
+  optional string meta_field_package_name_ssh = 1;
+  optional bool is_active = 2;
+  optional bool is_enabled = 3;
+}
+
+message Action {
+  oneof action {
+    LightDMAction light_dm_action = 1;
+    LightDMUpdateStateAction light_dm_update_state_action = 2;
+    LightDMClearEnabledStateAction light_dm_clear_enabled_state_action = 3;
+    WiFiAction wi_fi_action = 4;
+    WiFiSetHasVisitedOnboardingAction wi_fi_set_has_visited_onboarding_action = 5;
+    WiFiUpdateAction wi_fi_update_action = 6;
+    WiFiUpdateRequestAction wi_fi_update_request_action = 7;
+    SensorsAction sensors_action = 8;
+    SensorsReportReadingAction sensors_report_reading_action = 9;
+    UsersAction users_action = 10;
+    UsersSetUsersAction users_set_users_action = 11;
+    UsersCreateUserAction users_create_user_action = 12;
+    UsersDeleteUserAction users_delete_user_action = 13;
+    UsersResetPasswordAction users_reset_password_action = 14;
+    RPiConnectAction r_pi_connect_action = 15;
+    RPiConnectStartDownloadingAction r_pi_connect_start_downloading_action = 16;
+    RPiConnectDoneDownloadingAction r_pi_connect_done_downloading_action = 17;
+    RPiConnectSetPendingAction r_pi_connect_set_pending_action = 18;
+    RPiConnectSetStatusAction r_pi_connect_set_status_action = 19;
+    RPiConnectUpdateServiceStateAction r_pi_connect_update_service_state_action = 20;
+    VSCodeAction vs_code_action = 21;
+    VSCodeStartDownloadingAction vs_code_start_downloading_action = 22;
+    VSCodeDoneDownloadingAction vs_code_done_downloading_action = 23;
+    VSCodeSetPendingAction vs_code_set_pending_action = 24;
+    VSCodeSetStatusAction vs_code_set_status_action = 25;
+    DockerAction docker_action = 26;
+    DockerSetStatusAction docker_set_status_action = 27;
+    DockerStoreUsernameAction docker_store_username_action = 28;
+    DockerRemoveUsernameAction docker_remove_username_action = 29;
+    DockerImageAction docker_image_action = 30;
+    DockerImageSetStatusAction docker_image_set_status_action = 31;
+    DockerImageSetDockerIdAction docker_image_set_docker_id_action = 32;
+    DisplayAction display_action = 33;
+    DisplayPauseAction display_pause_action = 34;
+    DisplayResumeAction display_resume_action = 35;
+    KeypadAction keypad_action = 36;
+    KeypadKeyUpAction keypad_key_up_action = 37;
+    KeypadKeyDownAction keypad_key_down_action = 38;
+    KeypadKeyPressAction keypad_key_press_action = 39;
+    KeypadKeyReleaseAction keypad_key_release_action = 40;
+    VoiceAction voice_action = 41;
+    VoiceSetEngineAction voice_set_engine_action = 42;
+    VoiceReadTextAction voice_read_text_action = 43;
+    RgbRingAction rgb_ring_action = 44;
+    RgbRingSetIsConnectedAction rgb_ring_set_is_connected_action = 45;
+    RgbRingSetIsBusyAction rgb_ring_set_is_busy_action = 46;
+    RgbRingCommandAction rgb_ring_command_action = 47;
+    RgbRingWaitableCommandAction rgb_ring_waitable_command_action = 48;
+    RgbRingColorfulCommandAction rgb_ring_colorful_command_action = 49;
+    RgbRingSetEnabledAction rgb_ring_set_enabled_action = 50;
+    RgbRingSetAllAction rgb_ring_set_all_action = 51;
+    RgbRingSetBrightnessAction rgb_ring_set_brightness_action = 52;
+    RgbRingBlankAction rgb_ring_blank_action = 53;
+    RgbRingRainbowAction rgb_ring_rainbow_action = 54;
+    RgbRingProgressWheelStepAction rgb_ring_progress_wheel_step_action = 55;
+    RgbRingPulseAction rgb_ring_pulse_action = 56;
+    RgbRingBlinkAction rgb_ring_blink_action = 57;
+    RgbRingSpinningWheelAction rgb_ring_spinning_wheel_action = 58;
+    RgbRingProgressWheelAction rgb_ring_progress_wheel_action = 59;
+    RgbRingFillUptoAction rgb_ring_fill_upto_action = 60;
+    RgbRingFillDownfromAction rgb_ring_fill_downfrom_action = 61;
+    CameraAction camera_action = 62;
+    CameraStartViewfinderAction camera_start_viewfinder_action = 63;
+    CameraReportBarcodeAction camera_report_barcode_action = 64;
+    IpAction ip_action = 65;
+    IpUpdateInterfacesAction ip_update_interfaces_action = 66;
+    IpSetIsConnectedAction ip_set_is_connected_action = 67;
+    NotificationsAction notifications_action = 68;
+    NotificationsAddAction notifications_add_action = 69;
+    NotificationsClearAction notifications_clear_action = 70;
+    NotificationsClearByIdAction notifications_clear_by_id_action = 71;
+    NotificationsClearAllAction notifications_clear_all_action = 72;
+    AudioAction audio_action = 73;
+    AudioSetVolumeAction audio_set_volume_action = 74;
+    AudioChangeVolumeAction audio_change_volume_action = 75;
+    AudioSetMuteStatusAction audio_set_mute_status_action = 76;
+    AudioToggleMuteStatusAction audio_toggle_mute_status_action = 77;
+    AudioPlayChimeAction audio_play_chime_action = 78;
+    AudioPlayAudioAction audio_play_audio_action = 79;
+    SSHAction ssh_action = 80;
+    SSHUpdateStateAction ssh_update_state_action = 81;
+    SSHClearEnabledStateAction ssh_clear_enabled_state_action = 82;
+  }
+}
+
+message Event {
+  oneof event {
+    ScreenshotEvent screenshot_event = 1;
+    SnapshotEvent snapshot_event = 2;
+    WiFiEvent wi_fi_event = 3;
+    WiFiUpdateRequestEvent wi_fi_update_request_event = 4;
+    UsersEvent users_event = 5;
+    UsersCreateUserEvent users_create_user_event = 6;
+    UsersDeleteUserEvent users_delete_user_event = 7;
+    UsersResetPasswordEvent users_reset_password_event = 8;
+    RPiConnectEvent r_pi_connect_event = 9;
+    RPiConnectLoginEvent r_pi_connect_login_event = 10;
+    VSCodeEvent vs_code_event = 11;
+    VSCodeLoginEvent vs_code_login_event = 12;
+    VSCodeRestartEvent vs_code_restart_event = 13;
+    DockerEvent docker_event = 14;
+    DockerImageEvent docker_image_event = 15;
+    DockerImageRegisterAppEvent docker_image_register_app_event = 16;
+    DisplayEvent display_event = 17;
+    DisplayRenderEvent display_render_event = 18;
+    KeypadEvent keypad_event = 19;
+    KeypadKeyPressEvent keypad_key_press_event = 20;
+    KeypadKeyReleaseEvent keypad_key_release_event = 21;
+    VoiceEvent voice_event = 22;
+    VoiceSynthesizeTextEvent voice_synthesize_text_event = 23;
+    RgbRingEvent rgb_ring_event = 24;
+    RgbRingCommandEvent rgb_ring_command_event = 25;
+    CameraEvent camera_event = 26;
+    CameraStartViewfinderEvent camera_start_viewfinder_event = 27;
+    CameraStopViewfinderEvent camera_stop_viewfinder_event = 28;
+    CameraBarcodeEvent camera_barcode_event = 29;
+    IpEvent ip_event = 30;
+    NotificationsEvent notifications_event = 31;
+    NotificationsClearEvent notifications_clear_event = 32;
+    NotificationsDisplayEvent notifications_display_event = 33;
+    AudioEvent audio_event = 34;
+    AudioPlayChimeEvent audio_play_chime_event = 35;
+    AudioPlayAudioEvent audio_play_audio_event = 36;
+    AudioPlaybackDoneEvent audio_playback_done_event = 37;
+  }
+}
diff --git a/ubo_app/rpc/sample_python_client.py b/ubo_app/rpc/sample_python_client.py
new file mode 100644
index 00000000..cddf1ff8
--- /dev/null
+++ b/ubo_app/rpc/sample_python_client.py
@@ -0,0 +1,132 @@
+"""Client for the remote store."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, overload
+
+from grpclib.client import Channel
+
+from ubo_app.rpc.generated.store.v1 import (
+    DispatchActionRequest,
+    DispatchEventRequest,
+    StoreServiceStub,
+    SubscribeEventRequest,
+)
+from ubo_app.rpc.generated.ubo.v1 import (
+    Action,
+    Event,
+    Key,
+    KeypadKeyPressAction,
+    Notification,
+    NotificationActions,
+    NotificationActionsItem,
+    NotificationDispatchItem,
+    NotificationDispatchItemOperation,
+    NotificationsAddAction,
+)
+
+if TYPE_CHECKING:
+    from collections.abc import Callable
+
+SERVER_HOST = '127.0.0.1'
+SERVER_PORT = 50051
+
+
+class AsyncRemoteStore:
+    """Async remote store for dispatching operations to a gRPC server."""
+
+    def __init__(
+        self: AsyncRemoteStore,
+        host: str,
+        port: int,
+    ) -> None:
+        """Initialize the async remote store."""
+        self.channel = Channel(host=host, port=port)
+        self.service = StoreServiceStub(self.channel)
+
+    @overload
+    async def dispatch_async(
+        self: AsyncRemoteStore,
+        *,
+        action: Action,
+    ) -> None: ...
+    @overload
+    async def dispatch_async(
+        self: AsyncRemoteStore,
+        *,
+        event: Event,
+    ) -> None: ...
+    async def dispatch_async(
+        self: AsyncRemoteStore,
+        *,
+        action: Action | None = None,
+        event: Event | None = None,
+    ) -> None:
+        """Dispatch an operation to the remote store."""
+        return
+        """Dispatch an operation to the remote store."""
+        if action is not None:
+            await self.service.dispatch_action(DispatchActionRequest(action=action))
+        if event is not None:
+            await self.service.dispatch_event(DispatchEventRequest(event=event))
+
+    async def subscribe_event(
+        self: AsyncRemoteStore,
+        event_type: Event,
+        callback: Callable[[Event], None],
+    ) -> None:
+        """Subscribe to the remote store."""
+        async for response in self.service.subscribe_event(
+            SubscribeEventRequest(event=event_type),
+        ):
+            callback(response.event)
+
+
+async def connect() -> None:
+    """Connect to the gRPC server."""
+    store = AsyncRemoteStore(SERVER_HOST, SERVER_PORT)
+    await store.dispatch_async(
+        action=Action(
+            keypad_key_press_action=KeypadKeyPressAction(
+                key=Key.L1,
+                time=0.0,
+            ),
+        ),
+    )
+    await store.dispatch_async(
+        action=Action(
+            notifications_add_action=NotificationsAddAction(
+                notification=Notification(
+                    title='Hello',
+                    content='World',
+                    actions=NotificationActions(
+                        items=[
+                            NotificationActionsItem(
+                                notification_dispatch_item=NotificationDispatchItem(
+                                    label='custom action',
+                                    color='#ff0000',
+                                    background_color='#00ff00',
+                                    icon='󰑣',
+                                    operation=NotificationDispatchItemOperation(
+                                        ubo_action=Action(
+                                            keypad_key_press_action=KeypadKeyPressAction(
+                                                key=Key.L1,
+                                                time=0.0,
+                                            ),
+                                        ),
+                                    ),
+                                ),
+                            ),
+                        ],
+                    ),
+                ),
+            ),
+        ),
+    )
+    store.channel.close()
+
+
+if __name__ == '__main__':
+    import asyncio
+
+    asyncio.run(connect())
diff --git a/ubo_app/rpc/server.py b/ubo_app/rpc/server.py
new file mode 100644
index 00000000..fec0959e
--- /dev/null
+++ b/ubo_app/rpc/server.py
@@ -0,0 +1,25 @@
+# ruff: noqa: N802
+"""gRPC server for the store service."""
+
+from __future__ import annotations
+
+from grpclib.server import Server
+
+from ubo_app.logging import logger
+from ubo_app.rpc.service import StoreService
+
+LISTEN_HOST = '127.0.0.1'
+LISTEN_PORT = 50051
+
+
+async def serve() -> None:
+    """Serve the gRPC server."""
+    server = Server([StoreService()])
+
+    logger.error(
+        'Starting gRPC server',
+        extra={'host': LISTEN_HOST, 'port': LISTEN_PORT},
+    )
+    await server.start(LISTEN_HOST, LISTEN_PORT)
+
+    await server.wait_closed()
diff --git a/ubo_app/rpc/service.py b/ubo_app/rpc/service.py
new file mode 100644
index 00000000..5ba6a0ac
--- /dev/null
+++ b/ubo_app/rpc/service.py
@@ -0,0 +1,65 @@
+"""gRPC service that implements the Store service."""
+
+from __future__ import annotations
+
+from typing import cast
+
+from redux import BaseAction, BaseEvent
+
+from ubo_app.logging import logger
+from ubo_app.rpc.generated.store.v1 import (
+    DispatchActionRequest,
+    DispatchActionResponse,
+    DispatchEventRequest,
+    DispatchEventResponse,
+    StoreServiceBase,
+)
+from ubo_app.rpc.message_to_object import rebuild_object
+from ubo_app.store.main import store
+from ubo_app.store.operations import UboAction, UboEvent
+
+
+class StoreService(StoreServiceBase):
+    """gRPC service class that implements the Store service."""
+
+    async def dispatch_action(
+        self: StoreService,
+        dispatch_action_request: DispatchActionRequest,
+    ) -> DispatchActionResponse:
+        """Dispatch an action to the store."""
+        logger.info(
+            'Received action to be dispatched over gRPC',
+            extra={
+                'request': dispatch_action_request,
+            },
+        )
+        if not dispatch_action_request.action:
+            return DispatchActionResponse()
+        try:
+            action = rebuild_object(dispatch_action_request.action, BaseAction)
+        except Exception:
+            logger.exception('Failed to build object from dispatch action request')
+        else:
+            store.dispatch(cast(UboAction, action))
+        return DispatchActionResponse()
+
+    async def dispatch_event(
+        self: StoreService,
+        dispatch_event_request: DispatchEventRequest,
+    ) -> DispatchEventResponse:
+        """Dispatch an event to the store."""
+        logger.info(
+            'Received event to be dispatched over gRPC',
+            extra={
+                'request': dispatch_event_request,
+            },
+        )
+        if not dispatch_event_request.event:
+            return DispatchEventResponse()
+        try:
+            event = rebuild_object(dispatch_event_request.event, BaseEvent)
+        except Exception:
+            logger.exception('Failed to build object from dispatch event request')
+        else:
+            store.dispatch(cast(UboEvent, event))
+        return DispatchEventResponse()