From e1eb760913e0284da19352a5b37a4108fa87ff14 Mon Sep 17 00:00:00 2001 From: AlexTemirov Date: Mon, 10 Aug 2026 17:06:11 -0700 Subject: [PATCH] fix(device): transfer hardware for PC-assisted installs --- docs/project-lifecycle.md | 8 +- editor-server/device-hardware-requirements.in | 11 + .../device-hardware-requirements.lock | 522 ++++++++++++++++++ .../device-runtime-sources.lock.json | 4 + editor-server/device_installer.py | 357 +++++++++++- editor-server/server.py | 17 + tests/test_editor_devices.py | 122 +++- 7 files changed, 1019 insertions(+), 22 deletions(-) create mode 100644 editor-server/device-hardware-requirements.in create mode 100644 editor-server/device-hardware-requirements.lock diff --git a/docs/project-lifecycle.md b/docs/project-lifecycle.md index cc1dd8c..4d4d23e 100644 --- a/docs/project-lifecycle.md +++ b/docs/project-lifecycle.md @@ -157,7 +157,13 @@ manageable. A Runtime-only managed device can add the organized Hardware checkout in place from **Attach robot → Install Hardware package**. This preserves the Runtime -pairing and does not require deleting the device. +pairing and does not require deleting the device. For a PC-assisted device, the +editor downloads the pinned Robot Hardware source and Linux dependencies, +verifies and caches them, then transfers the bundle over SSH. Hardware uses the +stack's managed Python 3.11 and does not require the device to contact GitHub or +PyPI. A recognized partial Hardware clone from an interrupted earlier install +is cleaned before the verified bundle is applied; unrelated directories remain +untouched. **Delete device** stops the selected Runtime and its deployments, removes its Runtime checkout, synchronized workflow packages, managed Linux Python, diff --git a/editor-server/device-hardware-requirements.in b/editor-server/device-hardware-requirements.in new file mode 100644 index 0000000..8746036 --- /dev/null +++ b/editor-server/device-hardware-requirements.in @@ -0,0 +1,11 @@ +# Linux Python 3.11 dependencies staged by the editor for managed Robot Hardware. +# Keep this aligned with the blacknode-robot pyproject and setup script. +feetech-servo-sdk>=1.0 +# Autobahn 26.x does not publish a CPython 3.11 manylinux2014 aarch64 wheel. +autobahn>=24.4.2,<=25.12.1 +cbor2<=5.9.0 +pyserial>=3.5 +roslibpy>=1.5 +setuptools>=68 +smbus2>=0.5 +wheel diff --git a/editor-server/device-hardware-requirements.lock b/editor-server/device-hardware-requirements.lock new file mode 100644 index 0000000..5f13878 --- /dev/null +++ b/editor-server/device-hardware-requirements.lock @@ -0,0 +1,522 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile editor-server/device-hardware-requirements.in --python-version 3.11 --python-platform aarch64-manylinux2014 --generate-hashes --output-file editor-server/device-hardware-requirements.lock +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via + # service-identity + # twisted +autobahn==25.12.1 \ + --hash=sha256:133712e97ff0122b0e2eb4675ef889702fbd0f6fb06ecfd9e5a99512d1886d38 \ + --hash=sha256:1a74a2aa3ec5e354a54266360c8f9a24710667a38f75f50cbaa6ae754feb156c \ + --hash=sha256:5300e024ad223cbd5a57643eb0c36074bfa2b8d120da4fbacaa33eb482e871bd \ + --hash=sha256:55e6b57834a2a1136f05a4572c5bc037a565d562945f253cffcaad08b44eaeff \ + --hash=sha256:72f8f108f7a63199196105e91c7711e11d916533bc1ee5ab02cbc670a0967e84 \ + --hash=sha256:76cee7e4d818bf7428bb740698a7debba4a0f56f88ce2695786b05a4ac55aa6b \ + --hash=sha256:872f569272e569fa65f3d27c0c54291dc1607b9da590588f9ef234f2d5031735 \ + --hash=sha256:877e9f4004716fb7eba1a21b266abe51ad6b5a8705810e7c7a49c18f85387af9 \ + --hash=sha256:9daf8e3ae2eafcd9763e56405822274528df3ea08ed34fd8ef00d42b68c23027 \ + --hash=sha256:a13e49f762e97a291136bb1c4e39e6b026c4275593fb1d9ef9b73e7ef22e559d \ + --hash=sha256:a91f855768b2f786b1eb141a724722bf430ff3532342a4f129df0385bc24ad9a \ + --hash=sha256:c0f3f723e1aa44acef323a7c011b65add6226ead2faa283926680a2315e40655 \ + --hash=sha256:c546454801b61db36728675f933e9f333d8015a278ff451f60c4dd7a124eddf9 \ + --hash=sha256:de2fe7e19914b4133c67b12b4fae8010ae9c2db91ffcea7390f0af7a875b3f63 \ + --hash=sha256:f176bb582b593aa5513a6818fa5156aaf08e37d8ae07e3ba044bb11e773c95aa \ + --hash=sha256:f9accd135d7fd69dac90b85a66a301d1c8384f9d0be6db1843f000dcb520988d \ + --hash=sha256:fdf5660e5287e33bea64fd1fa62e2c3355bd2a3681ae4a751b6925e6342f03dd + # via + # -r editor-server/device-hardware-requirements.in + # roslibpy +automat==25.4.16 \ + --hash=sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0 \ + --hash=sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1 + # via twisted +cbor2==5.9.0 \ + --hash=sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e \ + --hash=sha256:0485d3372fc832c5e16d4eb45fa1a20fc53e806e6c29a1d2b0d3e176cedd52b9 \ + --hash=sha256:08388ea54195738602b4c4999966bcaef6f0b17d293c9658658409d9fff96f57 \ + --hash=sha256:1d02b65f070fd726bdc310d927228975bb655d155bf059b6eb7cacefb3dca86f \ + --hash=sha256:1da96ce5d852fe3d342c1eb2c202a52d1c97edfddc9230f1be7e02674662bf26 \ + --hash=sha256:1f223dffb1bcdd2764665f04c1152943d9daa4bc124a576cd8dee1cad4264313 \ + --hash=sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560 \ + --hash=sha256:2372d357d403e7912f104ff085950ffc82a5854d6d717f1ca1ce16a40a0ef5a7 \ + --hash=sha256:25bec7beb2089465382b1be72e78667fe9090598800826559c3e3008cf0db743 \ + --hash=sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b \ + --hash=sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a \ + --hash=sha256:3095dc49e75572841a9534cbfdabc2a17487ea4ee33341436abc4a7ac7245a3a \ + --hash=sha256:34a6cb15e6ab6a8eae94ad2041731cd3ef786af43a8df99f847969af5b902ee7 \ + --hash=sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b \ + --hash=sha256:420d2490c7836c81151b4bd591c35cffc55391e33e7e333c50fda391bcea7d31 \ + --hash=sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba \ + --hash=sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec \ + --hash=sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f \ + --hash=sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b \ + --hash=sha256:5326336f633cc89dfe543c78829c16c3a6449c2c03277d1ddba99086c3323363 \ + --hash=sha256:53cfa49e0df9c639beb871d480de098eedc81eb63ff29f2dc922720d7577b676 \ + --hash=sha256:55bea0dd9a7d354e35f4e5fe58ceab393e76962713749dc3a0a64a0e5d19545e \ + --hash=sha256:5e702b02d42a5ace45425b595ffe70fe35aebaf9a3cdfdc2c758b6189c744422 \ + --hash=sha256:65f8eac3268c608533f326f0fd9010ab1b2a8a917b05edaf3853116336821669 \ + --hash=sha256:7221483fad0c63afa4244624d552abf89d7dfdbc5f5edfc56fc1ff2b4b818975 \ + --hash=sha256:7d1ddc4541e7367ac58c2470cc0df847f7137167fe4f5729e2d3cc0b993d7da4 \ + --hash=sha256:837754ece9052b3f607047e1741e5f852a538aa2b0ee3db11c82a8fa11804aa4 \ + --hash=sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea \ + --hash=sha256:86baf870d4c0bfc6f79de3801f3860a84ab76d9c8b0abb7f081f2c14c38d79d3 \ + --hash=sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073 \ + --hash=sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551 \ + --hash=sha256:a9d6e4e0f988b0e766509a8071975a8ee99f930e14a524620bf38083106158d2 \ + --hash=sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0 \ + --hash=sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc \ + --hash=sha256:cc5efec69055c3c470997935d95762be7e4bfd1248d88fb1a33bb7e0f45712e9 \ + --hash=sha256:d1a21c006760f95acd9509cc5a7d15d6fc82e58f721f94fa9039b4e77189a6e5 \ + --hash=sha256:d8524a8c142c3cc228e635f8a97499a6c0b18ca91382e8276565658035cdcb6d \ + --hash=sha256:dcf0f695873e5c94bd072d6af8698e72b8fb7f7a18f37e0bced1041b7111a6cf \ + --hash=sha256:f29e5c3abcc91c1aeefecde0e057bf33f1655588d3065c6560c30ceb3be6f333 \ + --hash=sha256:f797532d13469f2193e5c16e827d8df7a8c33674b19be755790b54ab231e6a73 \ + --hash=sha256:f7c9751a9611601ab326d8f5837f01379195bbf06175fb4effeb552140e7c9e8 \ + --hash=sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd \ + --hash=sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27 \ + --hash=sha256:fbdcf4d74acbeb7672e6413e81cd2c1ced1a4a8cf949484ac54e9af5265c3c72 + # via + # -r editor-server/device-hardware-requirements.in + # autobahn +cffi==2.1.1 \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via + # autobahn + # cryptography +constantly==23.10.4 \ + --hash=sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9 \ + --hash=sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd + # via twisted +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 + # via + # autobahn + # pyopenssl + # service-identity +feetech-servo-sdk==1.0.0 \ + --hash=sha256:d4d3832e4b1b22a8222133a414db9f868224c2fb639426a1b11d96ddfe84e69c + # via -r editor-server/device-hardware-requirements.in +hyperlink==21.0.0 \ + --hash=sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b \ + --hash=sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4 + # via + # autobahn + # twisted +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via + # hyperlink + # twisted +incremental==24.11.0 \ + --hash=sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c \ + --hash=sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3 + # via twisted +msgpack==1.2.1 \ + --hash=sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833 \ + --hash=sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a \ + --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ + --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ + --hash=sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064 \ + --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ + --hash=sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac \ + --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ + --hash=sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c \ + --hash=sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06 \ + --hash=sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 \ + --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ + --hash=sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203 \ + --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ + --hash=sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74 \ + --hash=sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22 \ + --hash=sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8 \ + --hash=sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35 \ + --hash=sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb \ + --hash=sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9 \ + --hash=sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8 \ + --hash=sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6 \ + --hash=sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07 \ + --hash=sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056 \ + --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ + --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ + --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ + --hash=sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 \ + --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ + --hash=sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0 \ + --hash=sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce \ + --hash=sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f \ + --hash=sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707 \ + --hash=sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66 \ + --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ + --hash=sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e \ + --hash=sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d \ + --hash=sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8 \ + --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ + --hash=sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 \ + --hash=sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402 \ + --hash=sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a \ + --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ + --hash=sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c \ + --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ + --hash=sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b \ + --hash=sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d \ + --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ + --hash=sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 \ + --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ + --hash=sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5 \ + --hash=sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a \ + --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ + --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ + --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ + --hash=sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c \ + --hash=sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155 \ + --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ + --hash=sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6 \ + --hash=sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1 \ + --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ + --hash=sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7 \ + --hash=sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64 \ + --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ + --hash=sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc \ + --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c + # via autobahn +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c + # via + # incremental + # wheel +py-ubjson==0.16.1 \ + --hash=sha256:b9bfb8695a1c7e3632e800fb83c943bf67ed45ddd87cd0344851610c69a5a482 + # via autobahn +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi +pyopenssl==26.4.0 \ + --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ + --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c + # via twisted +pyserial==3.5 \ + --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \ + --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 + # via + # -r editor-server/device-hardware-requirements.in + # feetech-servo-sdk +roslibpy==2.1.0 \ + --hash=sha256:05d2ec196dd2ff89c864418f7afeac4b01bff5679034c997a6b4d0c54775f352 \ + --hash=sha256:7d4af9f52c5e57249c570001bdac868543bf042c3aaf4a2c9c9ef71b20d09513 + # via -r editor-server/device-hardware-requirements.in +service-identity==26.1.0 \ + --hash=sha256:6358c52882c96e66ac4a55eb3a72c7dd4a70763f8cc6fa4e70abde2656f4bf3b \ + --hash=sha256:68c32dadbb69135fb951077677e07cd7f6031020f3a8c8f47a28cda8a0742118 + # via twisted +setuptools==84.0.0 \ + --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \ + --hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73 + # via -r editor-server/device-hardware-requirements.in +smbus2==0.6.1 \ + --hash=sha256:2b043372abf8f6029a632c3aab36b641c5d5872b1cbad599fc68e17ac4fd90a5 \ + --hash=sha256:650feeb27ca0ed58b07db4c10201c2a662c41305b7bf6e5fab9d888056f48180 + # via -r editor-server/device-hardware-requirements.in +twisted==26.4.0 \ + --hash=sha256:dbfd0fe1ee409d0243fdd7a6a6ff14f4948cec1fd78e0376291f805e1501fae9 \ + --hash=sha256:dc25ea0ebf6511c24f03232ee9f4afa54b291c5d897990e3a39cc4d14a1ef4c0 + # via roslibpy +txaio==26.6.1 \ + --hash=sha256:3ee900b2331c93457530fddbccc1a320c4e2d7ac8f9073d01c3fbe87762ccb35 \ + --hash=sha256:91a84a7825485a367c0b070c7399824c0e1a1e8c071cbdf3882dd3146dab587b + # via autobahn +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # pyopenssl + # twisted +ujson==5.13.0 \ + --hash=sha256:02200035bc80e830f076ffc1b329a94c295aee6d9de8c9043647cb9a7bd4f76f \ + --hash=sha256:0354d6b50b0d153ed7c629845b18a953d0c727b7e768fd94a94e0602abfa1f29 \ + --hash=sha256:0806683e8171ec06817e6af22f14ba0cd2f16def8a2ffb22a28a10615249355d \ + --hash=sha256:09effd42924a80df20a63b31a1ede905e66b0ce24aafe7a4cbedb05c783f8bb3 \ + --hash=sha256:12078e81def2790140583abefc1b979f3c77be15d53c524bf0e232f669822052 \ + --hash=sha256:144e9d8a454cfa727e0f755e1863738ed68068583bda5463052cb446835bd56c \ + --hash=sha256:15107aaa4f559d55201165ec32abb35c283a861be1fa67229578cb7d93fcd93a \ + --hash=sha256:17a59d5cf23ef98f7c9314524976b4b288374d83200add01d953024fb06404f9 \ + --hash=sha256:1bdf2518971586f2b413156c49d9dd8b56cc990a8647081e1bd00af60564d469 \ + --hash=sha256:1d02f31c2f59cc6a1c2c3633b377701fc2d8e876cc01950735d7a01132ccc233 \ + --hash=sha256:2275bbaaea3eddd2e8ec0863e28a420f5f520b14a760fc3f1e49fd07a974448c \ + --hash=sha256:229faf041ef249ee3fd57bac1cedb123d2718ab63f6ccd50eca95ea902eb0dca \ + --hash=sha256:24539618fb3243cfdf27dab9a850acab80798a01501e9586b61fb9ecd016a891 \ + --hash=sha256:2862f81af44b3a7e74c5d80caa118d736be1991ce6f1d5c723716fa403060cc6 \ + --hash=sha256:2c00323c13a35822c9a67a26c0b2a0787510bf1ef490922b58009b362d1a3e21 \ + --hash=sha256:2c75eb7fac0fe92925b959cf2fa18f88d9fb76b10781fd2a6ccb895d5fb89171 \ + --hash=sha256:326553ae6c063c8246974906a6c137a0780fe46d143abebc52cd2cadda0f1814 \ + --hash=sha256:32a59e7151fe2fec8fdf9a565ee66fcf87918d48827e21cdab5e15ff9f274b78 \ + --hash=sha256:38afcf994b28ed85ea2420e2a8d79a37d0a77348b3daf53850c16edda66f942d \ + --hash=sha256:3d0ad1207694988498fca7e0bb28eba7564fa33261d2f9fdf66a3aaab376b803 \ + --hash=sha256:3e074a1f7778d58aa3b3056bab7b6251aabb3f381808018ca2b7fb8dbdeef7ab \ + --hash=sha256:3f6d55c68985654a84a9b47ac51f2655adac4fd264e4189f832960c2270dcf70 \ + --hash=sha256:43dee3081b00fe447c5b9e2fe7ebfd57f7bcc5dd25b8a439c26c8c174dd581be \ + --hash=sha256:46998fc8d11aec34a20e2010905e7059732a3d192d9a3c3fe4f9ffd146c87ec8 \ + --hash=sha256:496e662a6b46d5f936d77fb68259cece19213bb2301ddd520dbd75ac7c90c5f4 \ + --hash=sha256:4b266f182d4bce74a6a9e1f86988485e8cd422efdcc7c3f537e00cc956f52678 \ + --hash=sha256:4b7badefa73f96bad9e295ea22bd06967b851c8aad68c74196437e3584f25de5 \ + --hash=sha256:4cabd73c114ce93c21d7db2e2d8e16217fd8a5b2b3ec754629eebef5c262d47f \ + --hash=sha256:5737ffe0740a788b0e6255f0ffb281db49305fd6e6a587be44c73d9e92b554c4 \ + --hash=sha256:576f35c35b918d67d41b933878062ec0a5c9f4d1e9e14e04aeef35384963feae \ + --hash=sha256:5aa9bf16f0131812720dd4ae70bc1a0cf68f79e93c07c66100d328e75944b567 \ + --hash=sha256:5c31317d5e4504dae8f98795358b6082fc0ef96e7394806db0a76a4a8717f446 \ + --hash=sha256:65c1813fdd742fe3c249d9c417fa490e5b54e8a91bf343a88486ec50d175c444 \ + --hash=sha256:6692d49ff970aaa5008f4a6fe06974bc91fd957bf13173f765e46d8ba44906ea \ + --hash=sha256:69b4e36bb7d5f413ba8c00c8006b2ec627cc5ace97301462f6aadb66ec9d2979 \ + --hash=sha256:6e343c5f0c058523f1edbf6ae4eceb4e0d934205a53bbdd8d9a945c83324662a \ + --hash=sha256:6eee5d7cce3f32a468905f9ff61807a60287a90258d849460f6fa826e810870d \ + --hash=sha256:74f3dd61aeb01b7b2a6754e400224e819279041b3867935a55ccf57fb86a43b2 \ + --hash=sha256:751ad01042472f1c7c02f5c597c7aee79834e82a6cc384ca302173bbc8e8deb8 \ + --hash=sha256:7576bdbef327c3528f011002a2d74486f6fe4e33289bdb7a042b7f1a6e9d8285 \ + --hash=sha256:770643b4752266c5a466149848b78c3874940926a4ecef304f518b2b6cdb432f \ + --hash=sha256:7892ea6dd85ede6d30fbbd22af1239b9d81dabdf9b7a8f10ca6d4464d4d9b8ab \ + --hash=sha256:7b4a05f61a96553995da6a4d502e07bc8aa5d07a90031df006239c6b00ce1c83 \ + --hash=sha256:7c5a2478a3a1fa4421f7e035b87194eea0cf44c7971a3f32ad1b42a0dfd63c03 \ + --hash=sha256:7df805aad707507a1fa165fb716218ca3a89f142125dc4b23c9fcc08fa402d97 \ + --hash=sha256:7edf16359c52ed53406e216565d83e6b98c23c3cb9a0a01673f2493f8fb15edf \ + --hash=sha256:7fd41b86444df14f8b4b7afaaa9f27bacfbf8c18380872317aeab6cd125dcede \ + --hash=sha256:820b78a6a183ab6591b2ea888020032ef0fe328d852af9a5c8d8084ababb2218 \ + --hash=sha256:82c17b904c03c2b9629486ec91a8fa46a15f10d03504284f54ed7257a917d9f1 \ + --hash=sha256:82e1393e6dbe3c95fdfc95c6c528890e191351a1f024ef51126cf1f22543af52 \ + --hash=sha256:89d0bfc986d02b4ce76b00e0f560bc8d30dfe8c05a1bfd8529e085eb6c1a77d9 \ + --hash=sha256:8b644d50f66de5490c1823c7176618cead5e8e8a88cba9f40a6308ca52e79267 \ + --hash=sha256:8bb53ef95d35875262b8d0aa28506ca612ddd07058bee2a90f609938e69dc801 \ + --hash=sha256:96e7e019b097b4b25fccddadb369d13f412c13695fcf0680b6bf906376156151 \ + --hash=sha256:9a4b92344b16e414aeb609e57f62c466500e53c94f1698f5b149dc0b7223ec3e \ + --hash=sha256:9bd0f2dd05937c3b089af316884de18c6f6182ddb8ffce597d2e7c7a9ba9f447 \ + --hash=sha256:a554b2e5bee85030369514cef8b0b913cebe1a4c2c0c13541966d50bcba22b1a \ + --hash=sha256:a8e8c1203cb1a27720debc334f840a9170da741503522f86999710cb4738fbe3 \ + --hash=sha256:ae53b3f046529c193d533ca8111492330b204d6007611cdfa20e8b764c7c1389 \ + --hash=sha256:aefd3c9c95f9b62348956396ff7b31818476f8f54dc4a4e64cbd4f0491db6fca \ + --hash=sha256:b0b5f6983b2469db00e540b68fa8297b7a0ccd0d5173c60cc3e84f336b09395f \ + --hash=sha256:b1209c985d1f4c2ae085ced7325509650cdb3533bb7294558dd1f26d48377942 \ + --hash=sha256:b49516fbe803ff30d6caa9ccc3799ec7f968992747ce3099eae4758928577b53 \ + --hash=sha256:b535e0970c96957e999cfe5ec89361f0e8d0bb987fb5d5144f6f495cb3ed9e19 \ + --hash=sha256:b63d3820f978bc8e98cc3f1fe26a33b0d2ea237733a23fe5e9cb5d51f466bd97 \ + --hash=sha256:b6eca7751d61045a9b1e7f9a8c97ac24b164f085b60bef1c4668654bb2338011 \ + --hash=sha256:b877fe7926107d25d54b655c5b8dd94b294b22c157233163ad29fdb54ac10cf4 \ + --hash=sha256:b98bf2faa5e37ecfe752226ea08290031e375a0c43d425a0b955fb3e702a2a71 \ + --hash=sha256:bf3c2c4ea55d4187903fcdc689a9bf5b0fc72d8c0eaff39db18c1f337c8832c1 \ + --hash=sha256:bf81570ac056cb058f9117b52ca5dd800bfe9381d0076d0bb30a08a54591d654 \ + --hash=sha256:bfaa8302eb9bb7f5e231f256caf83040760585e32751d19155c0f0c0225f8de1 \ + --hash=sha256:c16e07581172f08585b409246f4535dab13ee85af0e3d3cfa8684b653ca44fa8 \ + --hash=sha256:c8bbb1f5ab810954fb307b6c5e68af58210c31da8569f9e6498a3958c1859e72 \ + --hash=sha256:ca7ef2fa6c408a7c0f558e4d33d93b32ddc35ed6d3cfc505747931a64b7465d5 \ + --hash=sha256:cc9dfd41fed397ab03bb9d9fe1cbd83301211c772a17536033ce7d68877ac82b \ + --hash=sha256:cdd9618e07b3b142a02f0ab8227fd52453688b8e8e60ac0511f13a25fa8009db \ + --hash=sha256:cdf33b588a81b05d0b585c66f83050c49cb670623424d10e4d1ad37ba2f7eed9 \ + --hash=sha256:d5e206e9f849ead27e51ef8da44e52b38da7c6dbd929a7340ab44533edcda8d7 \ + --hash=sha256:d62e3d7625384c08082abad81a077af587fdef2761bb14c3822f4234b8d07d75 \ + --hash=sha256:d6bc9fa43a49e403c68c7eb164eef0feee9dd29474a7c6e0d3b6267025371990 \ + --hash=sha256:d7f19b81b73ff28f5c5022ee794f94122bfcda07a76423078e349465d71223a1 \ + --hash=sha256:dc470179775468f9a007d3a6a2734624248c94bf47c6645e808c7e50a5070d1a \ + --hash=sha256:e12192a37c6c0e476554b62647acdf6139a47b6f13d8bad8dd2316763c4cee12 \ + --hash=sha256:e1842e10adc8f0db0d3e8aa3a1f8b05ce0456b39e180c8553d7f36dd0bf24b6b \ + --hash=sha256:e2282039eb26f08ed1a1381360395f8a310f39a45ef7314cb0f258a7d2917ea3 \ + --hash=sha256:ea7204e9fa7538bfbb1396e1ee8c2bbcd3818b3633ef5bb14d4fdea52994d14d \ + --hash=sha256:ea939ff629ab03ae970d03eca6d1febd8ed55ba38ca44aec64ce997537cd3fa0 \ + --hash=sha256:ee03ce288ba25b05cf0de87203165642277a25caa4f00a437e13152e5214e310 \ + --hash=sha256:f1cf46c79498c81f088cad4165b1669a78bba7bfbeb778c7cc1aff316e062b0d \ + --hash=sha256:f3cae1c811e787b9500e2830af8632dcb32a78dea5baf15aac51d681c59a59dd \ + --hash=sha256:fb296a0aa480ab88d895ddaa90372604c08ccc72323f02590612c775426ab413 \ + --hash=sha256:fdde6341d213b29f413b5fa9fad1392d5408074c75f0900ed949e97e546fa5df \ + --hash=sha256:ffc61fc756a64f4d169a78cc638d769e3c324f45fc51997626abf4e5e5dd6460 + # via autobahn +wheel==0.47.0 \ + --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ + --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 + # via -r editor-server/device-hardware-requirements.in +zope-interface==8.5 \ + --hash=sha256:03bbecc7982af713d7499d4084bc03916413d17ffd45f89009348cc0c1d9e376 \ + --hash=sha256:09495ce9d559c06b70f2d4855b3e4f48a822a9ddc8be1d30c5b4e5be14ae1ace \ + --hash=sha256:0c8123d2a4dfde2a613c7cb772605477724782c20bc2e0ad1d9435376a6a44a3 \ + --hash=sha256:0cd6a732ac84b94eb1ef9222a117347a27efd294ee16810ffdf7ecd307677ed5 \ + --hash=sha256:0fc3a9d45f114d27eaa1e53beeb144533689edca8a9f66505b1e8e8b3f075e42 \ + --hash=sha256:126fa9d1c52295ae076d4cf968634f0a1826afa408a20808b57ff72877b8f69f \ + --hash=sha256:133999820fdbae513c36c03d6f29ef87317aaa3edef39112222b155083664714 \ + --hash=sha256:147a9442dcc2b7339ecdb1be2b3cdb098e90462e39425054053ebfb50d99125a \ + --hash=sha256:14b0e9799351d4c34fe99afd67f0cdd76e55ba15c66a98699d5fc22ea8241e08 \ + --hash=sha256:17a3114bbdddb5e75e5784cdf318944636190cbbc72d357ef9fb1a8b0351f955 \ + --hash=sha256:28e80457c134d1fa57a7d758004dece348654e1b1467ac22dcdc20fc1d127c52 \ + --hash=sha256:29d74febbae1afeb6834c4ccbf42e242a673c860060f09e53142825270456140 \ + --hash=sha256:3090e3a663d20194756a59a272e0c8508b889341e31d5894223331fe6b4f9b21 \ + --hash=sha256:31cff25b2aaedb5267e6e77b1e9be6b0ec4f622032de8a069202b8ffacda7dc2 \ + --hash=sha256:5578c9421ca409a1f39f153d6f7803e4cde01da592ec75a9ac5e1b777d18d33b \ + --hash=sha256:5e970dabea777a24b0b0bbf9dae3ab75ce8b2d8e948edf4875627034b21f3560 \ + --hash=sha256:633c8c49396f38df030340797c533e9fe460d1b5d1e42d88e55e938e525f548c \ + --hash=sha256:652b73107a04159ec6c020db6c1543d4f1e8f4d069bd2aac88a947820923517b \ + --hash=sha256:6c54725d818f1b57a7efb8b16528326e1f3c257b602b32393fd255c45af8799d \ + --hash=sha256:6d02be14f3173c6c7288bc2fdf530090c01c3cf8764ad46c68024686f364278e \ + --hash=sha256:7849ad8fa90763cc1087f4dda78ca3a233e950b3e08fac7079297c9cafbbd7bb \ + --hash=sha256:798b7c87d0e59a7d5d086d642208d0d8700ff0d55c4029134b3c479c3bfb110f \ + --hash=sha256:7a3ba1c5877f0f3e3906b02ddf793abed2becc2948116414ce0e1dd820b68d6d \ + --hash=sha256:8bd75c96966e573232f0599deaff717564828031c7f05563ccc1ac35c5ee0304 \ + --hash=sha256:8e6ee90c2e6de7c37058d5fa41f123c8b13a312db8d1e0fb5840d7f4bcdff9c9 \ + --hash=sha256:9342fb74e2afefdb081bf1df727d209ea56995c6e13f5a0540e6d7aff4beafb8 \ + --hash=sha256:a17e681224267880707c9ec9e730ad9a1ad2d65c371256843efba6cf48711b58 \ + --hash=sha256:aab6bb5bee10f38ea688b95ba054396b67f613552d2c8378be7fcb2d2fba7646 \ + --hash=sha256:afc66ccaef2a3c0bef6ca02aad40d29a39276389dad16a8eac36f9f385e4d057 \ + --hash=sha256:bf917009a4a7457c7290225a019f4a0aa706d96accd2cfdba2418d3bc1fcde2f \ + --hash=sha256:c0c8aa2bf8f3911ef37b87deb1bbe225a310e6eb6522a16d77f5d8330c4f6fbe \ + --hash=sha256:c1adc90d3576b3b4c4de4953e6002c37bef28b78d7fa54c1bbfd0c50f022fe7c \ + --hash=sha256:c28044972187245d7a309e4699319bfdbd2ffcbf7176d1d4ddf5adffb2dea80f \ + --hash=sha256:d178968a1a611df30549a717d1624cb38ca810347339e3e37b7baa6f6781a170 \ + --hash=sha256:dabeb6fe1228d411994f300811edc6866fff0cdcbc9cef98a78f05ea0da42e37 \ + --hash=sha256:e0e311f1277468c08fd59a2b41f71b43d25dff639789d364747acd1705c0df6e \ + --hash=sha256:e1bd7d96b4ca5fa311f54c9eac16dce4886b428c1531dbe06067763ccdf123b4 \ + --hash=sha256:e6347b8d8d12c5eca6502450a92be30079b7acfade2c4f693efa0deb8871b06e \ + --hash=sha256:efe234a0fafb4b6b1602e9be9245b97c2bf06d67c07af5a4bc3c0438978b555c \ + --hash=sha256:f0b48ccadaa9839e09ff81e969703cecb3f402c813bfe8b958652e699bea69f5 \ + --hash=sha256:ffaecf013251a89d0de6feb49a46eba48ad8cbbf8a40aeb6045e459e7bec6784 + # via twisted diff --git a/editor-server/device-runtime-sources.lock.json b/editor-server/device-runtime-sources.lock.json index c0bd2cc..711751f 100644 --- a/editor-server/device-runtime-sources.lock.json +++ b/editor-server/device-runtime-sources.lock.json @@ -8,5 +8,9 @@ "core": { "repository": "temiroff/Blacknode", "commit": "62f812412561f9ab59dde67d749ff291087222fb" + }, + "hardware": { + "repository": "temiroff/blacknode-robot", + "commit": "0311491e73e3b71fe0d2d6c18d64588dd38947b0" } } diff --git a/editor-server/device_installer.py b/editor-server/device_installer.py index 2cd8798..a425c5f 100644 --- a/editor-server/device_installer.py +++ b/editor-server/device_installer.py @@ -43,7 +43,9 @@ class DeviceInstallError(RuntimeError): ) _RUNTIME_REPOSITORY = "temiroff/blacknode-runtime" _CORE_REPOSITORY = "temiroff/Blacknode" +_HARDWARE_REPOSITORY = "temiroff/blacknode-robot" _OFFLINE_PYTHON_MINOR = "3.11" +_HARDWARE_BUNDLE_SCHEMA_VERSION = 1 _RUNTIME_DEVICE_SOURCE_PATHS = ( "blacknode-package.toml", "blacknode_runtime", @@ -527,6 +529,13 @@ class _RuntimeBundle: core_commit: str +@dataclass(frozen=True) +class _HardwareBundle: + path: Path + architecture: str + hardware_commit: str + + def _offline_cache_root() -> Path: configured = str(os.environ.get("BLACKNODE_DEVICE_INSTALL_CACHE") or "").strip() root = ( @@ -825,6 +834,7 @@ def _runtime_source_lock() -> dict[str, dict[str, str]]: expected = { "runtime": _RUNTIME_REPOSITORY, "core": _CORE_REPOSITORY, + "hardware": _HARDWARE_REPOSITORY, } result: dict[str, dict[str, str]] = {} for key, expected_repository in expected.items(): @@ -1093,6 +1103,181 @@ def _prepare_runtime_bundle( raise +def _prepare_hardware_wheelhouse(cache_root: Path, platform_tag: str) -> Path: + lock_path = Path(__file__).with_name("device-hardware-requirements.lock") + try: + lock_payload = lock_path.read_bytes() + except OSError as exc: + raise DeviceInstallError( + "The editor installation is missing its Robot Hardware dependency lock." + ) from exc + requirements_key = hashlib.sha256(lock_payload).hexdigest()[:12] + wheelhouse = cache_root / f"hardware-wheelhouse-cp311-{platform_tag}-{requirements_key}" + if wheelhouse.is_dir() and any(path.is_file() for path in wheelhouse.iterdir()): + return wheelhouse + temporary = cache_root / f".{wheelhouse.name}.{secrets.token_hex(6)}.tmp" + shutil.rmtree(temporary, ignore_errors=True) + temporary.mkdir(parents=True) + command = [ + sys.executable, + "-m", + "pip", + "download", + "--disable-pip-version-check", + "--dest", + str(temporary), + "--only-binary=:all:", + "--no-binary=feetech-servo-sdk,py-ubjson", + "--platform", + platform_tag, + "--implementation", + "cp", + "--python-version", + "311", + "--abi", + "cp311", + "--no-deps", + "--require-hashes", + "--requirement", + str(lock_path), + ] + try: + completed = subprocess.run( + command, + capture_output=True, + text=True, + check=False, + timeout=600.0, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + shutil.rmtree(temporary, ignore_errors=True) + raise DeviceInstallError( + "The editor computer could not prepare Robot Hardware wheels." + ) from exc + if completed.returncode != 0: + detail = "\n".join( + value.strip() + for value in (completed.stdout, completed.stderr) + if value and value.strip() + )[-1600:] + shutil.rmtree(temporary, ignore_errors=True) + raise DeviceInstallError( + "The editor computer could not prepare the locked Robot Hardware " + "dependencies." + + (f" {detail}" if detail else "") + ) + if not any(path.is_file() for path in temporary.iterdir()): + shutil.rmtree(temporary, ignore_errors=True) + raise DeviceInstallError( + "The editor computer produced an empty Robot Hardware wheelhouse." + ) + if wheelhouse.exists(): + shutil.rmtree(wheelhouse) + os.replace(temporary, wheelhouse) + return wheelhouse + + +def _read_cached_hardware_bundle( + path: Path, + architecture: str, +) -> _HardwareBundle | None: + try: + with tarfile.open(path, "r:gz") as archive: + member = archive.getmember("manifest.json") + extracted = archive.extractfile(member) + if extracted is None: + return None + payload = json.loads(extracted.read().decode("utf-8")) + except (OSError, KeyError, tarfile.TarError, UnicodeDecodeError, json.JSONDecodeError): + return None + if ( + not isinstance(payload, dict) + or int(payload.get("schema_version") or 0) != _HARDWARE_BUNDLE_SCHEMA_VERSION + or str(payload.get("architecture") or "") != architecture + or not re.fullmatch(r"[0-9a-f]{40}", str(payload.get("hardware_commit") or "")) + ): + return None + return _HardwareBundle( + path=path, + architecture=architecture, + hardware_commit=str(payload["hardware_commit"]), + ) + + +def _prepare_hardware_bundle( + architecture: str, + report: Callable[[int, str], None], +) -> _HardwareBundle: + target_arch, platform_tag = _target_architecture(architecture) + cache_root = _offline_cache_root() + with _OFFLINE_BUNDLE_LOCK: + report(8, f"Preparing Linux {target_arch} Robot Hardware on this computer") + downloads = cache_root / "downloads" + sources = _runtime_source_lock() + source_archive, hardware_commit = _repository_snapshot( + sources["hardware"]["repository"], + sources["hardware"]["commit"], + downloads, + ) + requirements_key = hashlib.sha256( + Path(__file__).with_name("device-hardware-requirements.lock").read_bytes() + ).hexdigest()[:12] + bundle_name = ( + f"hardware-offline-v{_HARDWARE_BUNDLE_SCHEMA_VERSION}-{target_arch}-" + f"{hardware_commit[:10]}-{requirements_key}.tar.gz" + ) + bundle_path = cache_root / bundle_name + cached = _read_cached_hardware_bundle(bundle_path, target_arch) + if cached is not None: + report(12, "Using the verified PC Robot Hardware cache") + return cached + report(10, "Downloading Robot Hardware dependencies on this computer") + wheelhouse = _prepare_hardware_wheelhouse(cache_root, platform_tag) + artifacts: list[tuple[Path, str]] = [(source_archive, "hardware-source.tar.gz")] + artifacts.extend( + (wheel, f"wheelhouse/{wheel.name}") + for wheel in sorted(path for path in wheelhouse.iterdir() if path.is_file()) + ) + manifest = { + "schema_version": _HARDWARE_BUNDLE_SCHEMA_VERSION, + "delivery_mode": "pc_assisted", + "architecture": target_arch, + "python_minor": _OFFLINE_PYTHON_MINOR, + "hardware_commit": hardware_commit, + "artifacts": [ + { + "path": archive_name, + "size": source.stat().st_size, + "sha256": _sha256_file(source), + } + for source, archive_name in artifacts + ], + } + temporary = bundle_path.with_name( + f".{bundle_path.name}.{secrets.token_hex(6)}.tmp" + ) + manifest_file = cache_root / f".hardware-manifest-{secrets.token_hex(6)}.json" + try: + manifest_file.write_text( + json.dumps(manifest, indent=2) + "\n", + encoding="utf-8", + ) + with tarfile.open(temporary, "w:gz") as archive: + archive.add(manifest_file, arcname="manifest.json") + for source, archive_name in artifacts: + archive.add(source, arcname=archive_name) + os.replace(temporary, bundle_path) + finally: + temporary.unlink(missing_ok=True) + manifest_file.unlink(missing_ok=True) + report(12, "Verified the PC-assisted Robot Hardware bundle") + return _HardwareBundle( + path=bundle_path, + architecture=target_arch, + hardware_commit=hardware_commit, + ) + + def _upload_sftp_file( sftp: Any, local_path: Path, @@ -2040,6 +2225,8 @@ def install_hardware_environment( password: str, host_fingerprint: str, instance_id: str, + delivery_mode: str = "device_online", + architecture: str = "", progress: Callable[[dict[str, Any]], None] | None = None, ) -> dict[str, Any]: """Install the managed Hardware package beside an existing Runtime.""" @@ -2057,7 +2244,11 @@ def report(percent: int, message: str) -> None: raise DeviceInstallError( "Check the SSH connection and confirm its host fingerprint first." ) - report(5, "Connecting to the verified device") + clean_delivery_mode = str(delivery_mode or "device_online").strip().lower() + hardware_bundle = None + if clean_delivery_mode == "pc_assisted": + hardware_bundle = _prepare_hardware_bundle(architecture, report) + report(15 if hardware_bundle is not None else 5, "Connecting to the verified device") connection = _connect( host, port, @@ -2076,6 +2267,14 @@ def report(percent: int, message: str) -> None: } export -f sudo instance="$1" +bundle_path="$2" +hardware_commit="$3" +[[ "$bundle_path" != "-" ]] || bundle_path="" +[[ "$hardware_commit" != "-" ]] || hardware_commit="" +[[ -z "$hardware_commit" || "$hardware_commit" =~ ^[0-9a-f]{40}$ ]] || { + echo "Invalid Robot Hardware source commit." + exit 2 +} [[ "$instance" == "default" || "$instance" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || { echo "Invalid Blacknode runtime instance." exit 2 @@ -2083,6 +2282,8 @@ def report(percent: int, message: str) -> None: stack_root="$HOME/Blacknode/devices/$instance" organized_runtime_dir="$stack_root/runtime" organized_hardware_dir="$stack_root/hardware" +bundle_dir="$stack_root/.hardware-install-bundle" +python_dir="$stack_root/python" service_instance="" [[ "$instance" == "default" ]] || service_instance="$instance" if [[ "$instance" == "default" ]]; then @@ -2122,26 +2323,94 @@ def report(percent: int, message: str) -> None: progress 0 "Cleaning the incomplete Hardware package" rm -rf -- "$hardware_dir" fi + rm -rf -- "$bundle_dir" exit "$exit_code" } trap cleanup_failed_install EXIT +case "$bundle_dir" in + "$HOME/Blacknode/devices/"*/.hardware-install-bundle) ;; + *) echo "Unsafe Hardware bundle directory."; exit 2 ;; +esac +valid_hardware() { + [[ -f "$1/pyproject.toml" \ + && -f "$1/blacknode_robot/__init__.py" \ + && -f "$1/install-service.sh" ]] +} +if [[ -n "$bundle_path" ]]; then + progress 30 "Verifying the Robot Hardware package transferred by the editor" + rm -rf -- "$bundle_dir" + mkdir -p "$bundle_dir" + tar -xzf "$bundle_path" -C "$bundle_dir" --no-same-owner --no-same-permissions + python3 - "$bundle_dir" "$hardware_commit" <<'PY' +import hashlib +import json +import platform +import sys +from pathlib import Path, PurePosixPath + +root = Path(sys.argv[1]).resolve() +expected_commit = sys.argv[2] +manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8")) +if manifest.get("schema_version") != __BLACKNODE_HARDWARE_BUNDLE_SCHEMA_VERSION__: + raise SystemExit("Unsupported PC-assisted Robot Hardware bundle.") +if manifest.get("hardware_commit") != expected_commit: + raise SystemExit("Robot Hardware bundle commit does not match the pinned source.") +machine = platform.machine().lower().replace("-", "_") +aliases = {"arm64": "aarch64", "amd64": "x86_64"} +if manifest.get("architecture") != aliases.get(machine, machine): + raise SystemExit("Robot Hardware bundle targets a different device architecture.") +for artifact in manifest.get("artifacts", []): + relative = str(artifact.get("path") or "") + parts = PurePosixPath(relative).parts + if not relative or relative.startswith("/") or ".." in parts: + raise SystemExit("Robot Hardware bundle contains an unsafe artifact path.") + path = (root / Path(*parts)).resolve() + if root not in path.parents or not path.is_file(): + raise SystemExit(f"Robot Hardware bundle artifact is missing: {relative}") + if path.stat().st_size != int(artifact.get("size") or -1): + raise SystemExit(f"Robot Hardware bundle artifact size changed: {relative}") + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + if digest.hexdigest() != artifact.get("sha256"): + raise SystemExit(f"Robot Hardware bundle checksum failed: {relative}") +PY +fi if [[ -e "$hardware_dir" ]]; then - [[ -d "$hardware_dir/.git" && -f "$hardware_dir/pyproject.toml" ]] || { - echo "The existing Hardware directory is not a valid package checkout:" - echo " $hardware_dir" - exit 4 - } - origin="$(git -C "$hardware_dir" remote get-url origin 2>/dev/null || true)" - [[ "$origin" =~ (github\.com[:/])temiroff/blacknode-robot(\.git)?$ ]] || { - echo "The existing Hardware checkout has an unrecognized Git origin." - exit 4 - } + if ! valid_hardware "$hardware_dir"; then + origin="$(git -C "$hardware_dir" remote get-url origin 2>/dev/null || true)" + if [[ -d "$hardware_dir/.git" \ + && "$origin" =~ (github\.com[:/])temiroff/blacknode-robot(\.git)?$ ]]; then + progress 18 "Cleaning the recognized incomplete Robot Hardware download" + rm -rf -- "$hardware_dir" + else + echo "The existing Hardware directory is not a recognized package:" + echo " $hardware_dir" + exit 4 + fi + fi +fi +if [[ -e "$hardware_dir" ]]; then + if [[ -d "$hardware_dir/.git" ]]; then + origin="$(git -C "$hardware_dir" remote get-url origin 2>/dev/null || true)" + [[ "$origin" =~ (github\.com[:/])temiroff/blacknode-robot(\.git)?$ ]] || { + echo "The existing Hardware checkout has an unrecognized Git origin." + exit 4 + } + fi progress 25 "Using the existing Robot Hardware package" +elif [[ -n "$bundle_path" ]]; then + progress 32 "Installing the Robot Hardware package transferred by the editor" + created=true + mkdir -p "$hardware_dir" + tar -xzf "$bundle_dir/hardware-source.tar.gz" -C "$hardware_dir" \ + --strip-components=1 --no-same-owner --no-same-permissions else progress 20 "Downloading the Robot Hardware package" mkdir -p "$(dirname -- "$hardware_dir")" - git clone https://github.com/temiroff/blacknode-robot.git "$hardware_dir" created=true + git clone https://github.com/temiroff/blacknode-robot.git "$hardware_dir" fi if ! grep -q 'BLACKNODE_HARDWARE_INSTANCE' "$hardware_dir/install-service.sh" \ || ! grep -q 'previous_working_directory' "$hardware_dir/install-service.sh"; then @@ -2149,12 +2418,32 @@ def report(percent: int, message: str) -> None: exit 4 fi progress 50 "Setting up the Robot Hardware environment" -( - cd "$hardware_dir" - BLACKNODE_HARDWARE_INSTANCE="$service_instance" bash ./setup_ubuntu.sh -) +if [[ -n "$bundle_path" ]]; then + [[ -x "$python_dir/bin/python3" ]] || { + echo "The managed Python 3.11 installation is missing: $python_dir" + exit 4 + } + "$python_dir/bin/python3" -m venv "$hardware_dir/.venv" + "$hardware_dir/.venv/bin/python" -m pip install \ + --disable-pip-version-check --no-index \ + --find-links "$bundle_dir/wheelhouse" setuptools wheel + "$hardware_dir/.venv/bin/python" -m pip install \ + --disable-pip-version-check --no-index \ + --find-links "$bundle_dir/wheelhouse" --no-build-isolation \ + -e "$hardware_dir" smbus2 + if getent group i2c >/dev/null 2>&1; then + sudo usermod -aG i2c "$USER" + fi + "$hardware_dir/.venv/bin/python" "$hardware_dir/scripts/hardware_doctor.py" || true +else + ( + cd "$hardware_dir" + BLACKNODE_HARDWARE_INSTANCE="$service_instance" bash ./setup_ubuntu.sh + ) +fi progress 88 "Recording the complete device stack" -python3 - "$stack_root/install.json" "$instance" "$runtime_dir" "$hardware_dir" <<'PY' +python3 - "$stack_root/install.json" "$instance" "$runtime_dir" "$hardware_dir" \ + "$hardware_commit" <<'PY' import json import sys from pathlib import Path @@ -2170,6 +2459,8 @@ def report(percent: int, message: str) -> None: "packages_dir": str(Path(sys.argv[3]) / "packages"), } payload["hardware_dir"] = sys.argv[4] +payload["hardware_commit"] = sys.argv[5] +payload["hardware_delivery_mode"] = "pc_assisted" if sys.argv[5] else "device_online" manifest_path.parent.mkdir(parents=True, exist_ok=True) manifest_path.write_text( json.dumps(payload, indent=2) + "\n", @@ -2177,16 +2468,37 @@ def report(percent: int, message: str) -> None: ) PY created=false +rm -rf -- "$bundle_dir" printf '__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":"%s","layout":"%s","stack_mode":"isolated"}\n' \ "$hardware_dir" "$layout" progress 100 "Robot Hardware package installed" """ + script = script.replace( + "__BLACKNODE_HARDWARE_BUNDLE_SCHEMA_VERSION__", + str(_HARDWARE_BUNDLE_SCHEMA_VERSION), + ) remote_script_path = ( f"/tmp/blacknode-hardware-install-{secrets.token_hex(8)}.sh" ) + remote_bundle_path = ( + f"/tmp/blacknode-hardware-bundle-{secrets.token_hex(8)}.tar.gz" + if hardware_bundle is not None + else "" + ) try: sftp = connection.client.open_sftp() try: + if hardware_bundle is not None: + report(16, "Transferring Robot Hardware from this computer") + _upload_sftp_file( + sftp, + hardware_bundle.path, + remote_bundle_path, + progress=lambda value: report( + 16 + int(value * 14 / 100), + f"Transferring Robot Hardware from this computer ({value}%)", + ), + ) with sftp.file(remote_script_path, "w") as remote_script: remote_script.write(script) sftp.chmod(remote_script_path, 0o700) @@ -2194,7 +2506,11 @@ def report(percent: int, message: str) -> None: sftp.close() output = _run( connection, - f"bash {remote_script_path} {selected_instance}", + ( + f"bash {remote_script_path} {selected_instance} " + f"{remote_bundle_path or '-'} " + f"{hardware_bundle.hardware_commit if hardware_bundle is not None else '-'}" + ), stdin_text=_sudo_input(password, attempts=8), timeout=900.0, on_output=lambda line: ( @@ -2239,9 +2555,12 @@ def report(percent: int, message: str) -> None: } finally: try: + cleanup_targets = remote_script_path + if remote_bundle_path: + cleanup_targets += f" {remote_bundle_path}" _run( connection, - f"rm -f -- {remote_script_path}", + f"rm -f -- {cleanup_targets}", timeout=10.0, ) except DeviceInstallError: diff --git a/editor-server/server.py b/editor-server/server.py index 6f3b30a..115d784 100644 --- a/editor-server/server.py +++ b/editor-server/server.py @@ -7388,6 +7388,21 @@ def _install_device_host_hardware_payload( "Enter the device SSH password to install Robot Hardware.", ) try: + inspection = ( + host.get("last_inspection") + if isinstance(host.get("last_inspection"), dict) + else {} + ) + environment = ( + inspection.get("environment") + if isinstance(inspection.get("environment"), dict) + else {} + ) + operating_system = ( + environment.get("os") + if isinstance(environment.get("os"), dict) + else {} + ) installed = install_hardware_environment( host=str(managed.get("ssh_host") or ""), port=int(managed.get("ssh_port") or 22), @@ -7395,6 +7410,8 @@ def _install_device_host_hardware_payload( password=req.password, host_fingerprint=str(managed.get("host_fingerprint") or ""), instance_id=str(managed.get("instance_id") or "default"), + delivery_mode=str(managed.get("delivery_mode") or "device_online"), + architecture=str(operating_system.get("architecture") or ""), progress=progress, ) updated_management = { diff --git a/tests/test_editor_devices.py b/tests/test_editor_devices.py index 931d4eb..b9f8ac3 100644 --- a/tests/test_editor_devices.py +++ b/tests/test_editor_devices.py @@ -1001,6 +1001,17 @@ def test_pc_assisted_linux_lock_excludes_windows_packages(self): "temiroff/blacknode-runtime", ) self.assertRegex(source_lock["core"]["commit"], r"^[0-9a-f]{40}$") + self.assertEqual( + source_lock["hardware"]["repository"], + "temiroff/blacknode-robot", + ) + self.assertRegex(source_lock["hardware"]["commit"], r"^[0-9a-f]{40}$") + hardware_lock = ( + EDITOR_SERVER_DIR / "device-hardware-requirements.lock" + ).read_text(encoding="utf-8") + self.assertIn("feetech-servo-sdk==", hardware_lock) + self.assertIn("roslibpy==", hardware_lock) + self.assertNotIn("pywin32", hardware_lock.lower()) def test_hardware_pairing_discovery_reads_only_verified_service_files(self): token = "hardware-pairing-token-1234567890" @@ -1187,8 +1198,113 @@ def fake_run(_connection, command, **kwargs): 'BLACKNODE_HARDWARE_INSTANCE="$service_instance" bash ./setup_ubuntu.sh', uploaded[0], ) - remote_python = uploaded[0].split("<<'PY'\n", 1)[1].rsplit("\nPY", 1)[0] - compile(remote_python, "", "exec") + python_blocks = re.findall( + r"(?ms)^[^\n]*<<'PY'(?:\s*&)?\n(.*?)^PY$", + uploaded[0], + ) + self.assertEqual(len(python_blocks), 2) + for index, python_block in enumerate(python_blocks, start=1): + compile( + python_block, + f"", + "exec", + ) + + def test_hardware_environment_uses_pc_assisted_bundle_and_managed_python(self): + uploaded = [] + + class RemoteFile(io.StringIO): + def __enter__(self): + return self + + def __exit__(self, *_args): + uploaded.append(self.getvalue()) + self.close() + return False + + class Sftp: + def file(self, _path, _mode): + return RemoteFile() + + def chmod(self, _path, _mode): + return None + + def close(self): + return None + + connection = SimpleNamespace( + client=SimpleNamespace(open_sftp=lambda: Sftp()), + fingerprint="SHA256:trusted-device-key", + close=lambda: None, + ) + commands = [] + bundle = device_installer._HardwareBundle( + path=Path("hardware-bundle.tar.gz"), + architecture="aarch64", + hardware_commit="a" * 40, + ) + + def fake_run(_connection, command, **_kwargs): + commands.append(command) + if command.startswith("bash "): + return ( + '__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":' + '"/home/ubuntu/Blacknode/devices/default/hardware",' + '"layout":"organized","stack_mode":"isolated"}\n' + ) + return "" + + with ( + patch.object(device_installer, "_connect", return_value=connection), + patch.object(device_installer, "_run", side_effect=fake_run), + patch.object( + device_installer, + "_prepare_hardware_bundle", + return_value=bundle, + ) as prepare, + patch.object(device_installer, "_upload_sftp_file") as upload_bundle, + ): + result = device_installer.install_hardware_environment( + host="192.168.1.171", + port=22, + username="ubuntu", + password="ssh-password", + host_fingerprint="SHA256:trusted-device-key", + instance_id="default", + delivery_mode="pc_assisted", + architecture="aarch64", + ) + + self.assertEqual(result["layout"], "organized") + prepare.assert_called_once() + self.assertEqual(prepare.call_args.args[0], "aarch64") + upload_bundle.assert_called_once() + self.assertIn("blacknode-hardware-bundle-", upload_bundle.call_args.args[2]) + self.assertIn("blacknode-hardware-bundle-", commands[0]) + self.assertIn(" " + ("a" * 40), commands[0]) + script = uploaded[0] + self.assertIn('python_dir="$stack_root/python"', script) + self.assertIn('"$python_dir/bin/python3" -m venv', script) + self.assertIn('--find-links "$bundle_dir/wheelhouse"', script) + self.assertIn( + 'progress 18 "Cleaning the recognized incomplete Robot Hardware download"', + script, + ) + self.assertLess(script.index("created=true\n git clone"), script.index("fi\nif ! grep")) + python_blocks = re.findall( + r"(?ms)^[^\n]*<<'PY'(?:\s*&)?\n(.*?)^PY$", + script, + ) + self.assertEqual(len(python_blocks), 2) + for index, python_block in enumerate(python_blocks, start=1): + compile(python_block, f"", "exec") + checked = subprocess.run( + ["bash", "-n"], + input=script.encode("utf-8"), + capture_output=True, + check=False, + ) + self.assertEqual(checked.returncode, 0, checked.stderr.decode("utf-8")) def test_hardware_environment_reports_legacy_layout_without_moving_runtime(self): class RemoteFile(io.StringIO): @@ -2417,6 +2533,8 @@ def test_runtime_only_device_can_install_managed_hardware_package(self): password="ssh-password", host_fingerprint="SHA256:trusted-device-key", instance_id="default", + delivery_mode="device_online", + architecture="", progress=None, ) self.assertEqual(