Repository: AkariGroup/akari_software (language: python)

Issue:
Implement grpc client
#13 を実行するための gRPC client を実装しました。
一応手元ではそれっぽく動くことを動作確認済みです。

@kazyam53 @takuya-ikeda-tri 


Discussion / hints:

Fix patch:
diff --git a/internal/akari_rpc_server/akari_rpc_server/dynamixel.py b/internal/akari_rpc_server/akari_rpc_server/dynamixel.py
index e3d73fd0..7b8faa88 100644
--- a/internal/akari_rpc_server/akari_rpc_server/dynamixel.py
+++ b/internal/akari_rpc_server/akari_rpc_server/dynamixel.py
@@ -1,4 +1,4 @@
-from typing import List
+from typing import Sequence
 
 import grpc
 from akari_client.serial.dynamixel import DynamixelController
@@ -12,7 +12,7 @@
 class DynamixelControllerServiceServicer(
     joints_controller_pb2_grpc.JointsControllerServiceServicer
 ):
-    def __init__(self, joints: List[DynamixelController]) -> None:
+    def __init__(self, joints: Sequence[DynamixelController]) -> None:
         self._joints = {j.joint_name: j for j in joints}
 
     def _select_joint(
diff --git a/internal/akari_rpc_server/akari_rpc_server/server.py b/internal/akari_rpc_server/akari_rpc_server/server.py
index 1218ca02..6de30f4d 100644
--- a/internal/akari_rpc_server/akari_rpc_server/server.py
+++ b/internal/akari_rpc_server/akari_rpc_server/server.py
@@ -1,10 +1,16 @@
 import contextlib
 import signal
 from concurrent.futures import ThreadPoolExecutor
+from typing import List, cast
 
 import grpc
-from akari_client.serial.dynamixel import create_controllers
-from akari_client.serial.dynamixel_communicator import DynamixelCommunicator
+from akari_client.akari_client import AkariClient
+from akari_client.config import (
+    JointManagerDynamixelSerialConfig,
+    M5StackSerialConfig,
+    load_config,
+)
+from akari_client.serial.dynamixel import DynamixelController
 from akari_client.serial.m5stack import M5StackSerialClient
 from akari_proto import joints_controller_pb2_grpc, m5stack_pb2_grpc
 
@@ -14,13 +20,21 @@
 
 def serve(port: int) -> None:
     with contextlib.ExitStack() as stack:
-        dynamixel_communicator = stack.enter_context(DynamixelCommunicator.open())
-        joints = create_controllers(dynamixel_communicator)
-        m5stack = stack.enter_context(M5StackSerialClient())
+        config = load_config()
+        assert isinstance(config.joint_manager, JointManagerDynamixelSerialConfig)
+        assert isinstance(config.m5stack, M5StackSerialConfig)
+
+        client: AkariClient = stack.enter_context(AkariClient(config))
+        jcs = client.joints.joint_controllers
+        assert all(isinstance(j, DynamixelController) for j in jcs)
+        joint_controllers = cast(List[DynamixelController], jcs)
+
+        m5stack = client.m5stack
+        assert isinstance(m5stack, M5StackSerialClient)
 
         server = grpc.server(ThreadPoolExecutor(max_workers=10))
         joints_controller_pb2_grpc.add_JointsControllerServiceServicer_to_server(
-            DynamixelControllerServiceServicer(joints),
+            DynamixelControllerServiceServicer(joint_controllers),
             server,
         )
         m5stack_pb2_grpc.add_M5StackServiceServicer_to_server(
diff --git a/poetry.lock b/poetry.lock
index 93231b5e..343f4e39 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,7 +1,7 @@
 [[package]]
 name = "akari-client"
 version = "0.2.2"
-description = "Akari Python package"
+description = ""
 category = "main"
 optional = false
 python-versions = "*"
@@ -9,6 +9,7 @@ develop = true
 
 [package.dependencies]
 dynamixel_sdk = "*"
+pydantic = ">=1.5,<2.0"
 
 [package.source]
 type = "directory"
@@ -17,7 +18,7 @@ url = "sdk/akari_client"
 [[package]]
 name = "akari-proto"
 version = "0.1.0"
-description = "Python package of akari protobuf definitions and utility functions"
+description = ""
 category = "main"
 optional = false
 python-versions = "*"
@@ -114,7 +115,7 @@ optional = false
 python-versions = ">=3.6"
 
 [package.extras]
-dev = ["pytest (>=5)", "pytest-cov", "coveralls", "black", "mypy", "pylint"]
+dev = ["pylint", "mypy", "black", "coveralls", "pytest-cov", "pytest (>=5)"]
 
 [[package]]
 name = "depthai"
@@ -304,8 +305,8 @@ optional = false
 python-versions = ">=3.6"
 
 [package.extras]
-dev = ["pre-commit", "tox"]
-testing = ["pytest", "pytest-benchmark"]
+testing = ["pytest-benchmark", "pytest"]
+dev = ["tox", "pre-commit"]
 
 [[package]]
 name = "protobuf"
@@ -331,6 +332,21 @@ category = "dev"
 optional = false
 python-versions = ">=3.6"
 
+[[package]]
+name = "pydantic"
+version = "1.9.1"
+description = "Data validation and settings management using python type hints"
+category = "main"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
 [[package]]
 name = "pyflakes"
 version = "2.5.0"
@@ -455,7 +471,7 @@ python-versions = "*"
 name = "typing-extensions"
 version = "4.3.0"
 description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 
@@ -475,28 +491,210 @@ content-hash = "c636fc41be53b1db424c7a1b7004f5a2011220e93a369276bc7eca23a6d703bd
 [metadata.files]
 akari-client = []
 akari-proto = []
-atomicwrites = []
-attrs = []
-black = []
-click = []
-colorama = []
-colorlog = []
-dacite = []
-depthai = []
-dynamixel-sdk = []
-flake8 = []
-gitdb = []
+atomicwrites = [
+    {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
+]
+attrs = [
+    {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+    {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
+]
+black = [
+    {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
+    {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
+    {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
+    {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
+    {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
+    {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
+    {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
+    {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
+    {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
+    {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
+    {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
+    {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
+    {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
+    {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
+    {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
+    {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
+    {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
+    {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
+    {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
+    {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
+    {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
+    {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
+    {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
+]
+click = [
+    {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
+    {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
+]
+colorama = [
+    {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
+    {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
+]
+colorlog = [
+    {file = "colorlog-4.8.0-py2.py3-none-any.whl", hash = "sha256:3dd15cb27e8119a24c1a7b5c93f9f3b455855e0f73993b1c25921b2f646f1dcd"},
+    {file = "colorlog-4.8.0.tar.gz", hash = "sha256:59b53160c60902c405cdec28d38356e09d40686659048893e026ecbd589516b1"},
+]
+dacite = [
+    {file = "dacite-1.6.0-py3-none-any.whl", hash = "sha256:4331535f7aabb505c732fa4c3c094313fc0a1d5ea19907bf4726a7819a68b93f"},
+    {file = "dacite-1.6.0.tar.gz", hash = "sha256:d48125ed0a0352d3de9f493bf980038088f45f3f9d7498f090b50a847daaa6df"},
+]
+depthai = [
+    {file = "depthai-2.17.2.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:547609e7c3afaa0e409fe231c39027cf3d6f3de8e46f444d055dd9698044bb31"},
+    {file = "depthai-2.17.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f113071780d0f5989b1b9fff165eb6f3a353558a39b6c1ee57f38e75106dd69"},
+    {file = "depthai-2.17.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c8016f270e6ff67a9f67ac2fcb763330aabd06acbe4589a407ac0567b845b14"},
+    {file = "depthai-2.17.2.0-cp310-cp310-win32.whl", hash = "sha256:cde2324db97f21a0b582051fc9fcde9f406df3a95f360980cf28c87ecbb2109b"},
+    {file = "depthai-2.17.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:f39b897ed27bee38759f34a520d8909762f41df785a66de412044dc798cb3c9c"},
+    {file = "depthai-2.17.2.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:248f3064ceb948f5e751faafbf27944bcdafb3b5b5d779ca0bfe4cfcbc5f773e"},
+    {file = "depthai-2.17.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9325341d0856466d02d9e03b8359e5cef168ffe5ae7692bd21bb5973bd88db"},
+    {file = "depthai-2.17.2.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:824d81e27db51de57842276df8422240a5a7e36c762574c7f3b88246efd63379"},
+    {file = "depthai-2.17.2.0-cp36-cp36m-win32.whl", hash = "sha256:c29a50ba02f0d3bb62d2102eee4358a38d9b25a8b02badc7802d07c9ddf1b2d1"},
+    {file = "depthai-2.17.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:fa47a941dde112f7dd16c71a0e9d5dc8491a3c33b1cec80bb7a459b945c07581"},
+    {file = "depthai-2.17.2.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:4a7f0f7228188b99a4c9971b85d9ddbf3062caf80fb2abef2adaaa77a37857cb"},
+    {file = "depthai-2.17.2.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:8d4c7e3f06199bb565d6ded34997b6966a47b7d742627418e19b9b04abd00834"},
+    {file = "depthai-2.17.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9d2811f9026bc7a87609503655c221a7c11b74eb8dca24be4a68ece730c0cc"},
+    {file = "depthai-2.17.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57b134e784c76ad17229fe2c00772c9c117f038543f295da22de083310fe126f"},
+    {file = "depthai-2.17.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa71ab24e543c2a42cb01010c2e116c15d76823f4d4010fc11dece7e239ffbfa"},
+    {file = "depthai-2.17.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:25148a7ab1358a020370c269862b743f0203cf0b71648e3092afa9f2170ee38e"},
+    {file = "depthai-2.17.2.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b5567d87dddd597ba8dfb16237a782aefc0c142f6c53591a25f2095eb7ee609c"},
+    {file = "depthai-2.17.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ea6eee5a335a255783e4111fcbaed40cad6c34e0f9a02e2d52f82f9b0738c95"},
+    {file = "depthai-2.17.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:594af17226845a70e69dc8e770202840c9af888c60970f2f170007a9eec8da08"},
+    {file = "depthai-2.17.2.0-cp38-cp38-win32.whl", hash = "sha256:fed85d97ca9dfcd25ee252ba9a38933f0058c88fecaa0e85b1aef81b909d6e78"},
+    {file = "depthai-2.17.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:036a97159c12ee4a3ac48055266eabc9b8618fc935e7e82bd5828b3cd905764d"},
+    {file = "depthai-2.17.2.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c48b5793283700a310713a77fdc6694138c2ec64830a3fd17bcb334e6429e25f"},
+    {file = "depthai-2.17.2.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:bbdf8ce8c7b1e91bfd2fe97d29983fa3154c1ebe3318d608afbe937b1aef7613"},
+    {file = "depthai-2.17.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:312d84073c574e5daeb2dfba33c22f00e5ce70a36a9bb60835390e47a2e3573d"},
+    {file = "depthai-2.17.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59db63f3b95f5f2b3e8f753ccb51307f2bc3462ec91a13c199f0d14fef788066"},
+    {file = "depthai-2.17.2.0-cp39-cp39-win32.whl", hash = "sha256:24d1308450bf4fa1c24d2ae5ed35f81abf11c8117f73ced410b0f717305b0468"},
+    {file = "depthai-2.17.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e459c5e0e777e1694d0ab0b2dd36c5a437dd63ba00e3201021d733a6bee36"},
+    {file = "depthai-2.17.2.0.tar.gz", hash = "sha256:027ebad369e8d5a52c3920fb5f14ea437647007788f64eb669d5b304b3061eff"},
+]
+dynamixel-sdk = [
+    {file = "dynamixel_sdk-3.7.31-py3-none-any.whl", hash = "sha256:74e8c112ca6b0b869b196dd8c6a44ffd5dd5c1a3cb9fe2030e9933922406b466"},
+]
+flake8 = [
+    {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
+    {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
+]
+gitdb = [
+    {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
+    {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
+]
 gitpython = [
     {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
     {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
 ]
-grpcio = []
-iniconfig = []
-isort = []
-mccabe = []
-mypy = []
-mypy-extensions = []
-numpy = []
+grpcio = [
+    {file = "grpcio-1.44.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:11f811c0fffd84fca747fbc742464575e5eb130fd4fb4d6012ccc34febd001db"},
+    {file = "grpcio-1.44.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9a86a91201f8345502ea81dee0a55ae13add5fafadf109b17acd858fe8239651"},
+    {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:5f3c54ebb5d9633a557335c01d88d3d4928e9b1b131692283b6184da1edbec0b"},
+    {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d47553b8e86ab1e59b0185ba6491a187f94a0239f414c8fc867a22b0405b798"},
+    {file = "grpcio-1.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1e22d3a510438b7f3365c0071b810672d09febac6e8ca8a47eab657ae5f347b"},
+    {file = "grpcio-1.44.0-cp310-cp310-win32.whl", hash = "sha256:41036a574cab3468f24d41d6ed2b52588fb85ed60f8feaa925d7e424a250740b"},
+    {file = "grpcio-1.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ee51964edfd0a1293a95bb0d72d134ecf889379d90d2612cbf663623ce832b4"},
+    {file = "grpcio-1.44.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:e2149077d71e060678130644670389ddf1491200bcea16c5560d4ccdc65e3f2e"},
+    {file = "grpcio-1.44.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:0ac72d4b953b76924f8fa21436af060d7e6d8581e279863f30ee14f20751ac27"},
+    {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c30a9a7d3a05920368a60b080cbbeaf06335303be23ac244034c71c03a0fd24"},
+    {file = "grpcio-1.44.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:05467acd391e3fffb05991c76cb2ed2fa1309d0e3815ac379764bc5670b4b5d4"},
+    {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_aarch64.whl", hash = "sha256:b81dc7894062ed2d25b74a2725aaa0a6895ce97ce854f432fe4e87cad5a07316"},
+    {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46d4843192e7d36278884282e100b8f305cf37d1b3d8c6b4f736d4454640a069"},
+    {file = "grpcio-1.44.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898c159148f27e23c08a337fb80d31ece6b76bb24f359d83929460d813665b74"},
+    {file = "grpcio-1.44.0-cp36-cp36m-win32.whl", hash = "sha256:b8d852329336c584c636caa9c2db990f3a332b19bc86a80f4646b58d27c142db"},
+    {file = "grpcio-1.44.0-cp36-cp36m-win_amd64.whl", hash = "sha256:790d7493337558ae168477d1be3178f4c9b8f91d8cd9b8b719d06fd9b2d48836"},
+    {file = "grpcio-1.44.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cd61b52d9cf8fcf8d9628c0b640b9e44fdc5e93d989cc268086a858540ed370c"},
+    {file = "grpcio-1.44.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:14eefcf623890f3f7dd7831decd2a2116652b5ce1e0f1d4b464b8f52110743b0"},
+    {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:bebe90b8020b4248e5a2076b56154cc6ff45691bbbe980579fc9db26717ac968"},
+    {file = "grpcio-1.44.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89b390b1c0de909965280d175c53128ce2f0f4f5c0f011382243dd7f2f894060"},
+    {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:c122dac5cb299b8ad7308d61bd9fe0413de13b0347cce465398436b3fdf1f609"},
+    {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6641a28cc826a92ef717201cca9a035c34a0185e38b0c93f3ce5f01a01a1570a"},
+    {file = "grpcio-1.44.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb0a3e0e64843441793923d9532a3a23907b07b2a1e0a7a31f186dc185bb772"},
+    {file = "grpcio-1.44.0-cp37-cp37m-win32.whl", hash = "sha256:be857b7ec2ac43455156e6ba89262f7d7ae60227049427d01a3fecd218a3f88d"},
+    {file = "grpcio-1.44.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f6a9cf0e77f72f2ac30c9c6e086bc7446c984c51bebc6c7f50fbcd718037edba"},
+    {file = "grpcio-1.44.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:19e54f0c7083c8332b5a75a9081fc5127f1dbb67b6c1a32bd7fe896ef0934918"},
+    {file = "grpcio-1.44.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:bfd36b959c3c4e945119387baed1414ea46f7116886aa23de0172302b49d7ff1"},
+    {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ccd388b8f37b19d06e4152189726ce309e36dc03b53f2216a4ea49f09a7438e6"},
+    {file = "grpcio-1.44.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:9075c0c003c1ff14ebce8f0ba55cc692158cb55c68da09cf8b0f9fc5b749e343"},
+    {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e898194f76212facbaeb6d7545debff29351afa23b53ff8f0834d66611af5139"},
+    {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fa6584046a7cf281649975a363673fa5d9c6faf9dc923f261cc0e56713b5892"},
+    {file = "grpcio-1.44.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a7bdd6ef9bca050c7ade8cba5f0e743343ea0756d5d3d520e915098a9dc503"},
+    {file = "grpcio-1.44.0-cp38-cp38-win32.whl", hash = "sha256:dc3290d0411ddd2bd49adba5793223de8de8b01588d45e9376f1a9f7d25414f4"},
+    {file = "grpcio-1.44.0-cp38-cp38-win_amd64.whl", hash = "sha256:13343e7b840c20f43b44f0e6d3bbdc037c964f0aec9735d7cb685c407731c9ff"},
+    {file = "grpcio-1.44.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c5c2f8417d13386e18ccc8c61467cb6a6f9667a1ff7000a2d7d378e5d7df693f"},
+    {file = "grpcio-1.44.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:cf220199b7b4992729ad4d55d5d3f652f4ccfe1a35b5eacdbecf189c245e1859"},
+    {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4201c597e5057a9bfef9ea5777a6d83f6252cb78044db7d57d941ec2300734a5"},
+    {file = "grpcio-1.44.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e2de61005118ae59d48d5d749283ebfd1ba4ca68cc1000f8a395cd2bdcff7ceb"},
+    {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:871078218fa9117e2a378678f327e32fda04e363ed6bc0477275444273255d4d"},
+    {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d610b7b557a7609fecee80b6dd793ecb7a9a3c3497fbdce63ce7d151cdd705"},
+    {file = "grpcio-1.44.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcb53e4eb8c271032c91b8981df5fc1bb974bc73e306ec2c27da41bd95c44b5"},
+    {file = "grpcio-1.44.0-cp39-cp39-win32.whl", hash = "sha256:e50ddea6de76c09b656df4b5a55ae222e2a56e625c44250e501ff3c904113ec1"},
+    {file = "grpcio-1.44.0-cp39-cp39-win_amd64.whl", hash = "sha256:d2ec124a986093e26420a5fb10fa3f02b2c232f924cdd7b844ddf7e846c020cd"},
+    {file = "grpcio-1.44.0.tar.gz", hash = "sha256:4bae1c99896045d3062ab95478411c8d5a52cb84b91a1517312629fa6cfeb50e"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+isort = [
+    {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
+    {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
+]
+mccabe = [
+    {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+    {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
+]
+mypy = [
+    {file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"},
+    {file = "mypy-0.950-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0"},
+    {file = "mypy-0.950-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22"},
+    {file = "mypy-0.950-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb"},
+    {file = "mypy-0.950-cp310-cp310-win_amd64.whl", hash = "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334"},
+    {file = "mypy-0.950-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f"},
+    {file = "mypy-0.950-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc"},
+    {file = "mypy-0.950-cp36-cp36m-win_amd64.whl", hash = "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2"},
+    {file = "mypy-0.950-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed"},
+    {file = "mypy-0.950-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075"},
+    {file = "mypy-0.950-cp37-cp37m-win_amd64.whl", hash = "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"},
+    {file = "mypy-0.950-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d"},
+    {file = "mypy-0.950-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a"},
+    {file = "mypy-0.950-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605"},
+    {file = "mypy-0.950-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2"},
+    {file = "mypy-0.950-cp38-cp38-win_amd64.whl", hash = "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff"},
+    {file = "mypy-0.950-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8"},
+    {file = "mypy-0.950-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038"},
+    {file = "mypy-0.950-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2"},
+    {file = "mypy-0.950-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519"},
+    {file = "mypy-0.950-cp39-cp39-win_amd64.whl", hash = "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef"},
+    {file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"},
+    {file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"},
+]
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+numpy = [
+    {file = "numpy-1.23.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b15c3f1ed08df4980e02cc79ee058b788a3d0bef2fb3c9ca90bb8cbd5b8a3a04"},
+    {file = "numpy-1.23.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ce242162015b7e88092dccd0e854548c0926b75c7924a3495e02c6067aba1f5"},
+    {file = "numpy-1.23.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d7447679ae9a7124385ccf0ea990bb85bb869cef217e2ea6c844b6a6855073"},
+    {file = "numpy-1.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3119daed207e9410eaf57dcf9591fdc68045f60483d94956bee0bfdcba790953"},
+    {file = "numpy-1.23.1-cp310-cp310-win32.whl", hash = "sha256:3ab67966c8d45d55a2bdf40701536af6443763907086c0a6d1232688e27e5447"},
+    {file = "numpy-1.23.1-cp310-cp310-win_amd64.whl", hash = "sha256:1865fdf51446839ca3fffaab172461f2b781163f6f395f1aed256b1ddc253622"},
+    {file = "numpy-1.23.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeba539285dcf0a1ba755945865ec61240ede5432df41d6e29fab305f4384db2"},
+    {file = "numpy-1.23.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e8229f3687cdadba2c4faef39204feb51ef7c1a9b669247d49a24f3e2e1617c"},
+    {file = "numpy-1.23.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68b69f52e6545af010b76516f5daaef6173e73353e3295c5cb9f96c35d755641"},
+    {file = "numpy-1.23.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1408c3527a74a0209c781ac82bde2182b0f0bf54dea6e6a363fe0cc4488a7ce7"},
+    {file = "numpy-1.23.1-cp38-cp38-win32.whl", hash = "sha256:47f10ab202fe4d8495ff484b5561c65dd59177949ca07975663f4494f7269e3e"},
+    {file = "numpy-1.23.1-cp38-cp38-win_amd64.whl", hash = "sha256:37e5ebebb0eb54c5b4a9b04e6f3018e16b8ef257d26c8945925ba8105008e645"},
+    {file = "numpy-1.23.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:173f28921b15d341afadf6c3898a34f20a0569e4ad5435297ba262ee8941e77b"},
+    {file = "numpy-1.23.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:876f60de09734fbcb4e27a97c9a286b51284df1326b1ac5f1bf0ad3678236b22"},
+    {file = "numpy-1.23.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35590b9c33c0f1c9732b3231bb6a72d1e4f77872390c47d50a615686ae7ed3fd"},
+    {file = "numpy-1.23.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c4e64dfca659fe4d0f1421fc0f05b8ed1ca8c46fb73d9e5a7f175f85696bb"},
+    {file = "numpy-1.23.1-cp39-cp39-win32.whl", hash = "sha256:c2f91f88230042a130ceb1b496932aa717dcbd665350beb821534c5c7e15881c"},
+    {file = "numpy-1.23.1-cp39-cp39-win_amd64.whl", hash = "sha256:37ece2bd095e9781a7156852e43d18044fd0d742934833335599c583618181b9"},
+    {file = "numpy-1.23.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8002574a6b46ac3b5739a003b5233376aeac5163e5dcd43dd7ad062f3e186129"},
+    {file = "numpy-1.23.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d732d17b8a9061540a10fda5bfeabca5785700ab5469a5e9b93aca5e2d3a5fb"},
+    {file = "numpy-1.23.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:55df0f7483b822855af67e38fb3a526e787adf189383b4934305565d71c4b148"},
+    {file = "numpy-1.23.1.tar.gz", hash = "sha256:d748ef349bfef2e1194b59da37ed5a29c19ea8d7e6342019921ba2ba4fd8b624"},
+]
 opencv-python = [
     {file = "opencv-python-4.6.0.66.tar.gz", hash = "sha256:c5bfae41ad4031e66bb10ec4a0a2ffd3e514d092652781e8b1ac98d1b59f1158"},
     {file = "opencv_python-4.6.0.66-cp36-abi3-macosx_10_15_x86_64.whl", hash = "sha256:e6e448b62afc95c5b58f97e87ef84699e6607fe5c58730a03301c52496005cae"},
@@ -506,35 +704,154 @@ opencv-python = [
     {file = "opencv_python-4.6.0.66-cp36-abi3-win_amd64.whl", hash = "sha256:0dc82a3d8630c099d2f3ac1b1aabee164e8188db54a786abb7a4e27eba309440"},
     {file = "opencv_python-4.6.0.66-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:6e32af22e3202748bd233ed8f538741876191863882eba44e332d1a34993165b"},
 ]
-packaging = []
-pathspec = []
-platformdirs = []
-pluggy = []
-protobuf = []
-py = []
-pycodestyle = []
-pyflakes = []
+packaging = [
+    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pathspec = [
+    {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
+    {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
+]
+platformdirs = [
+    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
+    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
+]
+pluggy = [
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+protobuf = [
+    {file = "protobuf-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1cb2ed66aac593adbf6dca4f07cd7ee7e2958b17bbc85b2cc8bc564ebeb258ec"},
+    {file = "protobuf-3.19.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:898bda9cd37ec0c781b598891e86435de80c3bfa53eb483a9dac5a11ec93e942"},
+    {file = "protobuf-3.19.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad761ef3be34c8bdc7285bec4b40372a8dad9e70cfbdc1793cd3cf4c1a4ce74"},
+    {file = "protobuf-3.19.3-cp310-cp310-win32.whl", hash = "sha256:2cddcbcc222f3144765ccccdb35d3621dc1544da57a9aca7e1944c1a4fe3db11"},
+    {file = "protobuf-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:6202df8ee8457cb00810c6e76ced480f22a1e4e02c899a14e7b6e6e1de09f938"},
+    {file = "protobuf-3.19.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:397d82f1c58b76445469c8c06b8dee1ff67b3053639d054f52599a458fac9bc6"},
+    {file = "protobuf-3.19.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e54b8650e849ee8e95e481024bff92cf98f5ec61c7650cb838d928a140adcb63"},
+    {file = "protobuf-3.19.3-cp36-cp36m-win32.whl", hash = "sha256:3bf3a07d17ba3511fe5fa916afb7351f482ab5dbab5afe71a7a384274a2cd550"},
+    {file = "protobuf-3.19.3-cp36-cp36m-win_amd64.whl", hash = "sha256:afa8122de8064fd577f49ae9eef433561c8ace97a0a7b969d56e8b1d39b5d177"},
+    {file = "protobuf-3.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18c40a1b8721026a85187640f1786d52407dc9c1ba8ec38accb57a46e84015f6"},
+    {file = "protobuf-3.19.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:af7238849fa79285d448a24db686517570099739527a03c9c2971cce99cc5ae2"},
+    {file = "protobuf-3.19.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e765e6dfbbb02c55e4d6d1145743401a84fc0b508f5a81b2c5a738cf86353139"},
+    {file = "protobuf-3.19.3-cp37-cp37m-win32.whl", hash = "sha256:c781402ed5396ab56358d7b866d78c03a77cbc26ba0598d8bb0ac32084b1a257"},
+    {file = "protobuf-3.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:544fe9705189b249380fae07952d220c97f5c6c9372a6f936cc83a79601dcb70"},
+    {file = "protobuf-3.19.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84bf3aa3efb00dbe1c7ed55da0f20800b0662541e582d7e62b3e1464d61ed365"},
+    {file = "protobuf-3.19.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3f80a3491eaca767cdd86cb8660dc778f634b44abdb0dffc9b2a8e8d0cd617d0"},
+    {file = "protobuf-3.19.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9401d96552befcc7311f5ef8f0fa7dba0ef5fd805466b158b141606cd0ab6a8"},
+    {file = "protobuf-3.19.3-cp38-cp38-win32.whl", hash = "sha256:ef02d112c025e83db5d1188a847e358beab3e4bbfbbaf10eaf69e67359af51b2"},
+    {file = "protobuf-3.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:1291a0a7db7d792745c99d4657b4c5c4942695c8b1ac1bfb993a34035ec123f7"},
+    {file = "protobuf-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49677e5e9c7ea1245a90c2e8a00d304598f22ea3aa0628f0e0a530a9e70665fa"},
+    {file = "protobuf-3.19.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:df2ba379ee42427e8fcc6a0a76843bff6efb34ef5266b17f95043939b5e25b69"},
+    {file = "protobuf-3.19.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2acd7ca329be544d1a603d5f13a4e34a3791c90d651ebaf130ba2e43ae5397c6"},
+    {file = "protobuf-3.19.3-cp39-cp39-win32.whl", hash = "sha256:b53519b2ebec70cfe24b4ddda21e9843f0918d7c3627a785393fb35d402ab8ad"},
+    {file = "protobuf-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:8ceaf5fdb72c8e1fcb7be9f2b3b07482ce058a3548180c0bdd5c7e4ac5e14165"},
+    {file = "protobuf-3.19.3-py2.py3-none-any.whl", hash = "sha256:f6d4b5b7595a57e69eb7314c67bef4a3c745b4caf91accaf72913d8e0635111b"},
+    {file = "protobuf-3.19.3.tar.gz", hash = "sha256:d975a6314fbf5c524d4981e24294739216b5fb81ef3c14b86fb4b045d6690907"},
+]
+py = [
+    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+pycodestyle = [
+    {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
+    {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
+]
+pydantic = [
+    {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"},
+    {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"},
+    {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"},
+    {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"},
+    {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"},
+    {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"},
+    {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"},
+    {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"},
+    {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"},
+    {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"},
+    {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"},
+    {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"},
+    {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"},
+    {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"},
+    {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"},
+    {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"},
+    {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"},
+    {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"},
+    {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"},
+    {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"},
+    {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"},
+    {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"},
+    {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"},
+    {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"},
+    {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"},
+    {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"},
+    {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"},
+    {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"},
+    {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"},
+    {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"},
+    {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"},
+    {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"},
+    {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"},
+    {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"},
+    {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"},
+]
+pyflakes = [
+    {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
+    {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
+]
 pyparsing = [
     {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
     {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
 ]
-pysen = []
-pyserial = []
+pysen = [
+    {file = "pysen-0.10.2-py3-none-any.whl", hash = "sha256:f669c86d12e50d75a0037898fe0e20354f8545aa9682d0934374112eb37b131f"},
+    {file = "pysen-0.10.2.tar.gz", hash = "sha256:5400124c3b6bf1a11d4a359cc44c9909c364615dc128eb35def1f735a4fbe6d2"},
+]
+pyserial = [
+    {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"},
+    {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"},
+]
 pytest = [
     {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
     {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
 ]
-pyvirtualcam = []
-six = []
+pyvirtualcam = [
+    {file = "pyvirtualcam-0.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51239623a6874d3c3cc5f170788c948ab41e6f4e254bb9dc3ab0fed60ba84584"},
+    {file = "pyvirtualcam-0.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eab7ce52105973e02725d278f12a12360d2b335165609dcb7d55a70ce19802a0"},
+    {file = "pyvirtualcam-0.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:08fd19714ff35200c1b609c03d3cfec26e09fbf3ec259b19dcc1b0b2e8084357"},
+    {file = "pyvirtualcam-0.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:000047eaedf46eed01ebd8dfdf772d99bb9a6b6a0e256b8ba725cfe43b086341"},
+    {file = "pyvirtualcam-0.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdb366187e317d08f0ca73c0a30ede76db3c90f421da95af7e7be76b404df255"},
+    {file = "pyvirtualcam-0.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:206a9b10435f6e2ebc7ee0856b88fd67f1a9d3fd97463c698f57fd282e584785"},
+    {file = "pyvirtualcam-0.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:196a021b9afd15de7b2c9eed6f1444d123897d3f014af567ee561c27caaadbf8"},
+    {file = "pyvirtualcam-0.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd524e7139581151480f6ba8688e615148ac471f0716dbf3337042e4b6a0669"},
+    {file = "pyvirtualcam-0.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:c47cb64f810255134cbdd16134df162fea2cfabcdafd53131d438932b70a0f24"},
+    {file = "pyvirtualcam-0.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a5a6088f15b6080438794b59ba21e6be949061b5848de504f91ccf444716c486"},
+    {file = "pyvirtualcam-0.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035f7881629cb673d3e9167c2b801bb125e38673b711e0b4d2f6106f854d8ccd"},
+    {file = "pyvirtualcam-0.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:c01d83700cd074c9195f79d8664e86c3afa8cd4895d163d2021215bcfcf3d2a2"},
+]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
 smmap = [
     {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
     {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
 ]
-tomli = []
-tomlkit = []
-types-protobuf = []
+tomli = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+tomlkit = [
+    {file = "tomlkit-0.11.1-py3-none-any.whl", hash = "sha256:1c5bebdf19d5051e2e1de6cf70adfc5948d47221f097fcff7a3ffc91e953eaf5"},
+    {file = "tomlkit-0.11.1.tar.gz", hash = "sha256:61901f81ff4017951119cd0d1ed9b7af31c821d6845c8c477587bbdcd5e5854e"},
+]
+types-protobuf = [
+    {file = "types-protobuf-3.19.22.tar.gz", hash = "sha256:d2b26861b0cb46a3c8669b0df507b7ef72e487da66d61f9f3576aa76ce028a83"},
+    {file = "types_protobuf-3.19.22-py3-none-any.whl", hash = "sha256:d291388678af91bb045fafa864f142dc4ac22f5d4cdca097c7d8d8a32fa9b3ab"},
+]
 typing-extensions = [
     {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
     {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
 ]
-unidiff = []
+unidiff = [
+    {file = "unidiff-0.7.4-py2.py3-none-any.whl", hash = "sha256:688622add422f84a873498cc4ff9bf50da5ea6c23dea908f19d2190fa39a8e39"},
+    {file = "unidiff-0.7.4.tar.gz", hash = "sha256:2bbcbc986e1fb97f04b1d7b864aa6002ab02f4d8a996bf03aa6e5a81447d1fc5"},
+]
diff --git a/sdk/akari_client/akari_client/akari_client.py b/sdk/akari_client/akari_client/akari_client.py
index 36f9481d..e89d3981 100644
--- a/sdk/akari_client/akari_client/akari_client.py
+++ b/sdk/akari_client/akari_client/akari_client.py
@@ -1,33 +1,20 @@
 from __future__ import annotations
 
 import contextlib
-from typing import Any
+from typing import Any, Optional
 
+from .config import AkariClientConfig, load_config
 from .joint_manager import JointManager
 from .m5stack_client import M5StackClient
-from .serial.dynamixel import create_controllers
-from .serial.dynamixel_communicator import DynamixelCommunicator
-from .serial.m5stack import M5StackSerialClient
-
-
-def _initialize_joint_manager(
-    stack: contextlib.ExitStack,
-) -> JointManager:
-    communicator = stack.enter_context(DynamixelCommunicator.open())
-    controllers = create_controllers(communicator)
-    return JointManager(controllers)
-
-
-def _initialize_m5stack(stack: contextlib.ExitStack) -> M5StackClient:
-    return stack.enter_context(M5StackSerialClient())
 
 
 class AkariClient:
-    def __init__(self) -> None:
+    def __init__(self, config: Optional[AkariClientConfig] = None) -> None:
         self._stack = contextlib.ExitStack()
 
-        self._joints = _initialize_joint_manager(self._stack)
-        self._m5stack = _initialize_m5stack(self._stack)
+        self._config = config or load_config()
+        self._joints = self._config.joint_manager.factory(self._stack)
+        self._m5stack = self._config.m5stack.factory(self._stack)
 
     def __enter__(self) -> AkariClient:
         return self
diff --git a/sdk/akari_client/akari_client/config.py b/sdk/akari_client/akari_client/config.py
new file mode 100644
index 00000000..28b63fe9
--- /dev/null
+++ b/sdk/akari_client/akari_client/config.py
@@ -0,0 +1,137 @@
+import contextlib
+import logging
+import pathlib
+from typing import List, Literal, Optional, Union
+
+import pydantic
+
+from .joint_manager import AkariJoint, JointManager
+from .m5stack_client import M5StackClient
+
+_logger = logging.getLogger(__name__)
+
+
+class DynamixelControllerConfig(pydantic.BaseModel):
+    joint_name: str
+    dynamixel_id: int
+
+
+_DEFAULT_DYNAMIXEL_CONTROLLER_CONFIG: List[DynamixelControllerConfig] = [
+    DynamixelControllerConfig(
+        joint_name=AkariJoint.PAN,
+        dynamixel_id=1,
+    ),
+    DynamixelControllerConfig(
+        joint_name=AkariJoint.TILT,
+        dynamixel_id=2,
+    ),
+]
+
+
+class JointManagerDynamixelSerialConfig(pydantic.BaseModel):
+    type: Literal["dynamixel_serial"]
+    # TODO: Remove default value from `controllers` field
+    controllers: List[DynamixelControllerConfig] = pydantic.Field(
+        default=_DEFAULT_DYNAMIXEL_CONTROLLER_CONFIG
+    )
+    serial_port: pathlib.Path = pathlib.Path("/dev/ttyUSB_dynamixel")
+    baudrate: int = 1000000
+    protocol_version: float = 2.0
+
+    def factory(self, stack: contextlib.ExitStack) -> JointManager:
+        from .serial.factory import create_joint_manager
+
+        _logger.debug("Initializing joint manager from 'dynamixel_serial' config")
+        return create_joint_manager(self, stack)
+
+
+class JointManagerGrpcConfig(pydantic.BaseModel):
+    type: Literal["grpc"]
+    endpoint: str
+    joints: Optional[List[str]]
+
+    def factory(self, stack: contextlib.ExitStack) -> JointManager:
+        from .grpc.factory import create_joint_manager
+
+        _logger.debug("Initializing joint manager from 'grpc' config")
+        return create_joint_manager(self, stack)
+
+
+JointManagerConfig = Union[JointManagerDynamixelSerialConfig, JointManagerGrpcConfig]
+
+
+class M5StackSerialConfig(pydantic.BaseModel):
+    type: Literal["serial"]
+    serial_port: pathlib.Path = pathlib.Path("/dev/ttyUSB_M5Stack")
+    baudrate: int = 500000
+    serial_timeout: float = 0.2
+
+    def factory(self, stack: contextlib.ExitStack) -> M5StackClient:
+        from .serial.factory import create_m5stack_client
+
+        _logger.debug("Initializing M5Stack client from 'serial' config")
+        return create_m5stack_client(self, stack)
+
+
+class M5StackGrpcConfig(pydantic.BaseModel):
+    type: Literal["grpc"]
+    endpoint: str
+
+    def factory(self, stack: contextlib.ExitStack) -> M5StackClient:
+        from .grpc.factory import create_m5stack_client
+
+        _logger.debug("Initializing M5Stack client from 'grpc' config")
+        return create_m5stack_client(self, stack)
+
+
+M5StackConfig = Union[M5StackSerialConfig, M5StackGrpcConfig]
+
+
+class AkariClientConfig(pydantic.BaseModel):
+    joint_manager: JointManagerConfig = pydantic.Field(..., discriminator="type")
+    m5stack: M5StackConfig = pydantic.Field(..., discriminator="type")
+
+
+class AkariClientEnv(pydantic.BaseSettings):
+    config_path: Optional[pydantic.FilePath] = None
+
+    class Config:
+        env_prefix = "AKARI_CLIENT_"
+        case_sensitive = False
+
+
+_CONFIG_LOOKUP_PATHS = [
+    pathlib.Path("~/.config/akari/client_config.json"),
+    pathlib.Path("/etc/akari/client_config.json"),
+]
+
+
+def load_env() -> AkariClientEnv:
+    return AkariClientEnv()
+
+
+def _load_config(path: pathlib.Path) -> AkariClientConfig:
+    _logger.debug(f"Load config: {path}")
+    return AkariClientConfig.parse_file(path)
+
+
+def default_serial_config() -> AkariClientConfig:
+    return AkariClientConfig(
+        joint_manager=JointManagerDynamixelSerialConfig(type="dynamixel_serial"),
+        m5stack=M5StackSerialConfig(type="serial"),
+    )
+
+
+def load_config() -> AkariClientConfig:
+    env = load_env()
+    if env.config_path is not None:
+        _logger.debug("env.config_path is set")
+        return _load_config(env.config_path)
+
+    for path in _CONFIG_LOOKUP_PATHS:
+        path = path.expanduser().resolve()
+        if path.exists():
+            return _load_config(path)
+
+    _logger.debug("Use default config")
+    return default_serial_config()
diff --git a/sdk/akari_client/akari_client/grpc/channel_pool.py b/sdk/akari_client/akari_client/grpc/channel_pool.py
new file mode 100644
index 00000000..a67a31cb
--- /dev/null
+++ b/sdk/akari_client/akari_client/grpc/channel_pool.py
@@ -0,0 +1,23 @@
+import contextlib
+import logging
+import threading
+from typing import Dict
+
+import grpc
+
+_logger = logging.getLogger(__name__)
+_lock = threading.Lock()
+_channels: Dict[str, grpc.Channel] = {}
+
+
+def create_channel(stack: contextlib.ExitStack, endpoint: str) -> grpc.Channel:
+    with _lock:
+        c = _channels.get(endpoint)
+        if c is not None:
+            _logger.debug(f"Use existing channel for endpoint: '{endpoint}'")
+            return c
+
+        channel = stack.enter_context(grpc.insecure_channel(endpoint))
+        _logger.debug(f"New channel for endpoint: '{endpoint}' created")
+        _channels[endpoint] = channel
+        return channel
diff --git a/sdk/akari_client/akari_client/grpc/factory.py b/sdk/akari_client/akari_client/grpc/factory.py
new file mode 100644
index 00000000..2f3c341b
--- /dev/null
+++ b/sdk/akari_client/akari_client/grpc/factory.py
@@ -0,0 +1,41 @@
+import contextlib
+import logging
+from typing import List
+
+from ..config import JointManagerGrpcConfig, M5StackGrpcConfig
+from ..joint_manager import JointManager
+from .channel_pool import create_channel
+from .joints_controller import GrpcJointController, _GrpcJointsController
+from .m5stack import GrpcM5StackClient
+
+_logger = logging.getLogger(__name__)
+
+
+def create_joint_manager(
+    config: JointManagerGrpcConfig, stack: contextlib.ExitStack
+) -> JointManager:
+    channel = create_channel(stack, config.endpoint)
+    service_client = _GrpcJointsController(channel)
+    service_joints = service_client.get_joint_names()
+    _logger.debug(f"Server response: service_joints='{service_joints}'")
+    controllers: List[GrpcJointController] = []
+
+    target_joints = config.joints or service_joints
+    for joint_name in target_joints:
+        if joint_name not in service_joints:
+            raise ValueError(
+                f"joint_name: '{joint_name}' is not supported in "
+                f"gRPC server: '{config.endpoint}'"
+            )
+
+        _logger.debug(f"Use grpc controller for joint: '{joint_name}'")
+        controllers.append(GrpcJointController(joint_name, service_client))
+
+    return JointManager(controllers)
+
+
+def create_m5stack_client(
+    config: M5StackGrpcConfig, stack: contextlib.ExitStack
+) -> GrpcM5StackClient:
+    channel = create_channel(stack, config.endpoint)
+    return GrpcM5StackClient(channel)
diff --git a/sdk/akari_client/akari_client/joint_controller.py b/sdk/akari_client/akari_client/joint_controller.py
index 692bfed1..1abb6ec6 100644
--- a/sdk/akari_client/akari_client/joint_controller.py
+++ b/sdk/akari_client/akari_client/joint_controller.py
@@ -1,10 +1,4 @@
 import abc
-import dataclasses
-
-
-@dataclasses.dataclass(frozen=True)
-class JointControllerConfig:
-    joint_name: str
 
 
 class RevoluteJointController(abc.ABC):
diff --git a/sdk/akari_client/akari_client/joint_manager.py b/sdk/akari_client/akari_client/joint_manager.py
index e22851b3..61918b19 100644
--- a/sdk/akari_client/akari_client/joint_manager.py
+++ b/sdk/akari_client/akari_client/joint_manager.py
@@ -21,6 +21,10 @@ def __init__(self, joint_controllers: Sequence[RevoluteJointController]) -> None
         for j in joint_controllers:
             self._joints[j.joint_name] = j
 
+    @property
+    def joint_controllers(self) -> Sequence[RevoluteJointController]:
+        return list(self._joints.values())
+
     def get_joint_names(self) -> List[str]:
         """関節名を取得する。"""
         return list(self._joints.keys())
diff --git a/sdk/akari_client/akari_client/serial/dynamixel.py b/sdk/akari_client/akari_client/serial/dynamixel.py
index 9e5a983a..31586521 100644
--- a/sdk/akari_client/akari_client/serial/dynamixel.py
+++ b/sdk/akari_client/akari_client/serial/dynamixel.py
@@ -1,9 +1,7 @@
 import dataclasses
 import math
-from typing import List
 
-from ..joint_controller import JointControllerConfig, RevoluteJointController
-from ..joint_manager import AkariJoint
+from ..joint_controller import RevoluteJointController
 from .dynamixel_communicator import DynamixelCommunicator
 
 PULSE_OFFSET = 2047
@@ -49,39 +47,31 @@ class DynamixelControlTable:
     PRESENT_POSITION = DynamixelControlItem("Present_Position", 132, 4)
 
 
-@dataclasses.dataclass(frozen=True)
-class DynamixelControllerConfig(JointControllerConfig):
-    dynamixel_id: int
-
-
 class DynamixelController(RevoluteJointController):
     def __init__(
         self,
-        config: DynamixelControllerConfig,
+        joint_name: str,
+        dynamixel_id: int,
         communicator: DynamixelCommunicator,
     ) -> None:
         """
         dynamixelのコントローラ
 
         Args:
-            config: DynamixelControllerの設定クラス
             communicator: dynamixel通信用クラス
         """
-        self._config = config
+        self._joint_name = joint_name
+        self._dynamixel_id = dynamixel_id
         self._communicator = communicator
 
     def __str__(self) -> str:
-        return self._config.joint_name
+        return self._joint_name
 
     def _read(self, item: DynamixelControlItem) -> int:
-        return self._communicator.read(
-            self._config.dynamixel_id, item.address, item.length
-        )
+        return self._communicator.read(self._dynamixel_id, item.address, item.length)
 
     def _write(self, item: DynamixelControlItem, value: int) -> None:
-        self._communicator.write(
-            self._config.dynamixel_id, item.address, item.length, value
-        )
+        self._communicator.write(self._dynamixel_id, item.address, item.length, value)
 
     def set_position_limit(self, lower_rad: float, upper_rad: float) -> None:
         """Positionの上限値と下限値を設定する。
@@ -100,7 +90,7 @@ def set_position_limit(self, lower_rad: float, upper_rad: float) -> None:
     @property
     def joint_name(self) -> str:
         """関節名を取得する。"""
-        return self._config.joint_name
+        return self._joint_name
 
     def get_servo_enabled(self) -> bool:
         """サーボの有効無効状態を取得する。"""
@@ -154,32 +144,3 @@ def get_present_position(self) -> float:
         return dynamixel_pulse_to_rad(
             self._read(DynamixelControlTable.PRESENT_POSITION)
         )
-
-
-_DEFAULT_JOINT_CONFIGS: List[DynamixelControllerConfig] = [
-    DynamixelControllerConfig(
-        joint_name=AkariJoint.PAN,
-        dynamixel_id=1,
-    ),
-    DynamixelControllerConfig(
-        joint_name=AkariJoint.TILT,
-        dynamixel_id=2,
-    ),
-]
-
-
-def create_controllers(
-    communicator: DynamixelCommunicator,
-) -> List[DynamixelController]:
-    joints: List[DynamixelController] = []
-    for config in _DEFAULT_JOINT_CONFIGS:
-        # TODO: Dispatch ControllerInitialization by a config class
-        assert isinstance(config, DynamixelControllerConfig)
-        joints.append(
-            DynamixelController(
-                config,
-                communicator,
-            )
-        )
-
-    return joints
diff --git a/sdk/akari_client/akari_client/serial/dynamixel_communicator.py b/sdk/akari_client/akari_client/serial/dynamixel_communicator.py
index 2e577ab6..164ce2a7 100644
--- a/sdk/akari_client/akari_client/serial/dynamixel_communicator.py
+++ b/sdk/akari_client/akari_client/serial/dynamixel_communicator.py
@@ -1,12 +1,13 @@
 from __future__ import annotations
 
 import contextlib
+import pathlib
 from typing import Iterator
 
 from dynamixel_sdk import COMM_SUCCESS, PacketHandler, PortHandler
 
 DEFAULT_BAUDRATE = 1000000
-DEFAULT_DEVICE_NAME = "/dev/ttyUSB_dynamixel"
+DEFAULT_DEVICE_NAME = pathlib.Path("/dev/ttyUSB_dynamixel")
 DEFAULT_PROTOCOL_VERSION = 2.0
 
 
@@ -39,13 +40,13 @@ def __init__(
     @contextlib.contextmanager
     def open(
         cls,
-        device_name: str = DEFAULT_DEVICE_NAME,
+        serial_port: pathlib.Path = DEFAULT_DEVICE_NAME,
         baudrate: int = DEFAULT_BAUDRATE,
         protocol_version: float = DEFAULT_PROTOCOL_VERSION,
     ) -> Iterator[DynamixelCommunicator]:
         """dynamixelと通信を行うクラスを初期化する"""
 
-        port_handler = PortHandler(device_name)
+        port_handler = PortHandler(str(serial_port))
         try:
             if port_handler.setBaudRate(baudrate):
                 print("Succeeded to change the baudrate")
diff --git a/sdk/akari_client/akari_client/serial/factory.py b/sdk/akari_client/akari_client/serial/factory.py
new file mode 100644
index 00000000..cbab265c
--- /dev/null
+++ b/sdk/akari_client/akari_client/serial/factory.py
@@ -0,0 +1,46 @@
+import contextlib
+from typing import List
+
+from ..config import JointManagerDynamixelSerialConfig, M5StackSerialConfig
+from ..joint_manager import JointManager
+from ..m5stack_client import M5StackClient
+from .dynamixel import DynamixelController
+from .dynamixel_communicator import DynamixelCommunicator
+from .m5stack import M5StackSerialClient
+from .m5stack_communicator import M5SerialCommunicator
+
+
+def create_joint_manager(
+    config: JointManagerDynamixelSerialConfig, stack: contextlib.ExitStack
+) -> JointManager:
+    communicator = stack.enter_context(
+        DynamixelCommunicator.open(
+            serial_port=config.serial_port,
+            baudrate=config.baudrate,
+            protocol_version=config.protocol_version,
+        )
+    )
+    controllers: List[DynamixelController] = []
+    for c in config.controllers:
+        controllers.append(
+            DynamixelController(
+                c.joint_name,
+                c.dynamixel_id,
+                communicator,
+            )
+        )
+
+    return JointManager(controllers)
+
+
+def create_m5stack_client(
+    config: M5StackSerialConfig, stack: contextlib.ExitStack
+) -> M5StackClient:
+    communicator = stack.enter_context(
+        M5SerialCommunicator(
+            baudrate=config.baudrate,
+            port=config.serial_port,
+            timeout=config.serial_timeout,
+        )
+    )
+    return M5StackSerialClient(communicator)
diff --git a/sdk/akari_client/akari_client/serial/m5stack.py b/sdk/akari_client/akari_client/serial/m5stack.py
index fbec8fe1..cb844a90 100644
--- a/sdk/akari_client/akari_client/serial/m5stack.py
+++ b/sdk/akari_client/akari_client/serial/m5stack.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import contextlib
 import dataclasses
 import enum
 import time
@@ -38,28 +37,14 @@ def serialize(self) -> Dict[str, Any]:
 
 
 class M5StackSerialClient(M5StackClient):
-    def __init__(self, communicator: Optional[M5SerialCommunicator] = None) -> None:
-        self._stack = contextlib.ExitStack()
-        if communicator is None:
-            self._communicator = self._stack.enter_context(M5SerialCommunicator())
-        else:
-            self._communicator = communicator
+    def __init__(self, communicator: M5SerialCommunicator) -> None:
+        self._communicator = communicator
 
         self._pin_out = _PinOut()
-
         self.reset_m5()
         self._communicator.start()
         time.sleep(0.1)
 
-    def __enter__(self) -> M5StackSerialClient:
-        return self
-
-    def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
-        self.close()
-
-    def close(self) -> None:
-        self._stack.close()
-
     def _write_pin_out(self, sync: bool) -> None:
         data = {
             "com": CommandId.WRITEPINVAL,
diff --git a/sdk/akari_client/akari_client/serial/m5stack_communicator.py b/sdk/akari_client/akari_client/serial/m5stack_communicator.py
index cebd10ab..20d6827b 100644
--- a/sdk/akari_client/akari_client/serial/m5stack_communicator.py
+++ b/sdk/akari_client/akari_client/serial/m5stack_communicator.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import json
+import pathlib
 import threading
 import time
 from typing import Any, Dict, Optional, cast
@@ -10,12 +11,17 @@
 from ..m5stack_client import M5ComDict
 
 BAUDRATE = 500000
-DEVICE_NAME = "/dev/ttyUSB_M5Stack"
+DEVICE_NAME = pathlib.Path("/dev/ttyUSB_M5Stack")
 TIMEOUT = 0.2
 
 
 class M5SerialCommunicator:
-    def __init__(self) -> None:
+    def __init__(
+        self,
+        baudrate: int = BAUDRATE,
+        port: pathlib.Path = DEVICE_NAME,
+        timeout: float = TIMEOUT,
+    ) -> None:
         self._reference_time = time.time()
 
         self._condition = threading.Condition()
@@ -24,7 +30,9 @@ def __init__(self) -> None:
         self._exit = False
 
         self._serial = serial.Serial(
-            baudrate=BAUDRATE, port=DEVICE_NAME, timeout=TIMEOUT
+            baudrate=baudrate,
+            port=str(port),
+            timeout=timeout,
         )
 
     def __enter__(self) -> M5SerialCommunicator:
diff --git a/sdk/akari_client/setup.py b/sdk/akari_client/setup.py
index 1dbca9b9..3263da46 100644
--- a/sdk/akari_client/setup.py
+++ b/sdk/akari_client/setup.py
@@ -8,7 +8,10 @@
     long_description=open("README.md").read(),
     author="akari",
     author_email="akari.tmc@gmail.com",
-    install_requires=["dynamixel_sdk"],
+    install_requires=[
+        "dynamixel_sdk",
+        "pydantic>=1.5,<2.0",
+    ],
     package_data={"akari_client": ["py.typed"]},
     extras_require={
         "grpc": [


Test patch:
diff --git a/sdk/akari_client/tests/akari_controller_tests/serial_tests/test_dynamixel.py b/sdk/akari_client/tests/akari_controller_tests/serial_tests/test_dynamixel.py
index 1fbf40e2..34a72a73 100644
--- a/sdk/akari_client/tests/akari_controller_tests/serial_tests/test_dynamixel.py
+++ b/sdk/akari_client/tests/akari_controller_tests/serial_tests/test_dynamixel.py
@@ -5,7 +5,6 @@
 from akari_client.serial.dynamixel import (
     DynamixelControlItem,
     DynamixelController,
-    DynamixelControllerConfig,
     DynamixelControlTable,
     dynamixel_pulse_to_rad,
     rad_per_sec2_to_rev_per_min2,
@@ -25,12 +24,8 @@ def mock_communicator() -> DynamixelCommunicator:
 
 
 def test_read_write_device(mock_communicator: DynamixelCommunicator) -> None:
-    controller1 = DynamixelController(
-        DynamixelControllerConfig("tilt", 0), mock_communicator
-    )
-    controller2 = DynamixelController(
-        DynamixelControllerConfig("pan", 1), mock_communicator
-    )
+    controller1 = DynamixelController("tilt", 0, mock_communicator)
+    controller2 = DynamixelController("pan", 1, mock_communicator)
 
     control = DynamixelControlItem("foo", 100, 10)
 
@@ -50,9 +45,7 @@ def test_read_write_device(mock_communicator: DynamixelCommunicator) -> None:
 
 
 def test_set_position_limit(mock_communicator: DynamixelCommunicator) -> None:
-    controller = DynamixelController(
-        DynamixelControllerConfig("tilt", 0), mock_communicator
-    )
+    controller = DynamixelController("tilt", 0, mock_communicator)
     assert controller.joint_name == "tilt"
     controller.set_position_limit(56, 78)
     assert controller._read(
@@ -66,9 +59,7 @@ def test_set_position_limit(mock_communicator: DynamixelCommunicator) -> None:
 def test_get_set_servo_enabled(
     mock_communicator: DynamixelCommunicator,
 ) -> None:
-    controller = DynamixelController(
-        DynamixelControllerConfig("tilt", 0), mock_communicator
-    )
+    controller = DynamixelController("tilt", 0, mock_communicator)
 
     controller.set_servo_enabled(False)
     assert not controller.get_servo_enabled()
@@ -80,9 +71,7 @@ def test_get_set_servo_enabled(
 def test_set_profile_acceleration(
     mock_communicator: DynamixelCommunicator,
 ) -> None:
-    controller = DynamixelController(
-        DynamixelControllerConfig("tilt", 0), mock_communicator
-    )
+    controller = DynamixelController("tilt", 0, mock_communicator)
 
     controller.set_profile_acceleration(123.4)
     assert controller._read(
@@ -93,9 +82,7 @@ def test_set_profile_acceleration(
 def test_set_profile_velocity(
     mock_communicator: DynamixelCommunicator,
 ) -> None:
-    controller = DynamixelController(
-        DynamixelControllerConfig("tilt", 0), mock_communicator
-    )
+    controller = DynamixelController("tilt", 0, mock_communicator)
 
     controller.set_profile_velocity(123.4)
     assert controller._read(
@@ -106,9 +93,7 @@ def test_set_profile_velocity(
 def test_set_goal_position(
     mock_communicator: DynamixelCommunicator,
 ) -> None:
-    controller = DynamixelController(
-        DynamixelControllerConfig("tilt", 0), mock_communicator
-    )
+    controller = DynamixelController("tilt", 0, mock_communicator)
 
     controller.set_goal_position(123.4)
     assert controller._read(
@@ -119,9 +104,7 @@ def test_set_goal_position(
 def test_get_present_position(
     mock_communicator: DynamixelCommunicator,
 ) -> None:
-    controller = DynamixelController(
-        DynamixelControllerConfig("tilt", 0), mock_communicator
-    )
+    controller = DynamixelController("tilt", 0, mock_communicator)
 
     controller._write(
         DynamixelControlTable.PRESENT_POSITION, rad_to_dynamixel_pulse(123.4)
diff --git a/sdk/akari_client/tests/akari_controller_tests/test_config.py b/sdk/akari_client/tests/akari_controller_tests/test_config.py
new file mode 100644
index 00000000..3b0f7380
--- /dev/null
+++ b/sdk/akari_client/tests/akari_controller_tests/test_config.py
@@ -0,0 +1,50 @@
+import os
+import pathlib
+import tempfile
+from typing import Iterator
+
+import pytest
+from akari_client.config import (
+    JointManagerDynamixelSerialConfig,
+    default_serial_config,
+    load_env,
+)
+
+
+@pytest.fixture
+def fake_os_env() -> Iterator[None]:
+    old = os.environ.copy()
+    try:
+        yield
+    finally:
+        os.environ.clear()
+        os.environ.update(old)
+
+
+@pytest.fixture
+def tempdir() -> Iterator[pathlib.Path]:
+    with tempfile.TemporaryDirectory() as td:
+        yield pathlib.Path(td)
+
+
+def test_load_env(fake_os_env: None, tempdir: pathlib.Path) -> None:
+    os.environ.clear()
+    env = load_env()
+    assert env.config_path is None
+
+    config_path = tempdir / "foo.json"
+    os.environ["AKARI_CLIENT_CONFIG_PATH"] = str(config_path)
+    with pytest.raises(Exception):
+        load_env()
+
+    config_path.touch()
+    env = load_env()
+    assert env.config_path == config_path
+
+
+def test_default_serial_config() -> None:
+    config = default_serial_config()
+    assert config.joint_manager is not None
+    assert config.m5stack is not None
+    assert isinstance(config.joint_manager, JointManagerDynamixelSerialConfig)
+    assert len(config.joint_manager.controllers) == 2