From 357576735314e748b07db0ba5a3a3c8661dd4b1d Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 15:14:52 -0400 Subject: [PATCH 01/20] feat(hardware): add shared-data for errors --- hardware/Pipfile | 3 +- hardware/Pipfile.lock | 731 +++++++++++++++++++++++++----------------- hardware/setup.py | 6 +- 3 files changed, 451 insertions(+), 289 deletions(-) diff --git a/hardware/Pipfile b/hardware/Pipfile index adae953e9b0..ef1497cee78 100644 --- a/hardware/Pipfile +++ b/hardware/Pipfile @@ -11,7 +11,7 @@ numpy = "==1.21.2" pydantic = "==1.8.2" [dev-packages] -pytest = "==6.1.0" +pytest = "==7.0.1" pytest-lazy-fixture = "==0.6.3" pytest-cov = "==2.10.1" mypy = "==0.910" @@ -25,6 +25,7 @@ types-mock = "==4.0.1" hypothesis = "~=6.36.1" pytest-asyncio = "~=0.18" matplotlib = "*" +opentrons-shared-data = { editable = true, path = "../shared-data/python" } [requires] python_version = "3.7" diff --git a/hardware/Pipfile.lock b/hardware/Pipfile.lock index efff4b0a04c..29a90038227 100644 --- a/hardware/Pipfile.lock +++ b/hardware/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6ae1b3a213054f85d8838d5751f105f56157f52679015be32e34e375989ab443" + "sha256": "e648179e894ecd8eab6eaf4d0a75f1bde4c9aa087ab04e79d8058210f4318c8b" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aenum": { "hashes": [ - "sha256:12ae89967f2e25c0ce28c293955d643f891603488bc3d9946158ba2b35203638", - "sha256:525b4870a27d0b471c265bda692bc657f1e0dd7597ad4186d072c59f9db666f6", - "sha256:aed2c273547ae72a0d5ee869719c02a643da16bf507c80958faadc7e038e3f73" + "sha256:27b1710b9d084de6e2e695dab78fe9f269de924b51ae2850170ee7e1ca6288a5", + "sha256:8cbd76cd18c4f870ff39b24284d3ea028fbe8731a58df3aa581e434c575b9559", + "sha256:e0dfaeea4c2bd362144b87377e2c61d91958c5ed0b4daf89cb6f45ae23af6288" ], - "version": "==3.1.11" + "version": "==3.1.15" }, "numpy": { "hashes": [ @@ -105,91 +105,102 @@ }, "typing-extensions": { "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", + "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" ], "index": "pypi", - "version": "==4.2.0" + "version": "==4.6.3" }, "wrapt": { "hashes": [ - "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", - "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", - "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", - "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", - "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", - "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", - "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", - "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", - "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", - "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", - "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", - "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", - "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", - "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", - "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", - "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", - "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", - "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", - "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", - "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", - "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", - "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", - "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", - "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", - "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", - "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", - "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", - "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", - "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", - "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", - "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", - "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", - "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", - "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", - "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", - "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", - "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", - "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", - "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", - "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", - "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", - "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", - "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", - "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", - "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", - "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", - "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", - "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", - "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", - "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", - "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", - "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", - "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", - "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", - "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", - "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", - "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", - "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", - "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", - "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", - "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", - "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", - "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", - "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" + "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", + "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", + "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", + "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", + "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", + "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", + "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", + "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", + "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", + "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", + "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", + "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", + "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", + "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", + "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", + "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", + "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", + "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", + "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", + "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", + "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", + "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", + "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", + "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", + "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", + "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", + "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", + "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", + "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", + "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", + "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", + "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", + "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", + "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", + "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", + "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", + "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", + "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", + "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", + "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", + "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", + "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", + "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", + "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", + "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", + "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", + "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", + "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", + "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", + "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", + "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", + "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", + "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", + "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", + "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", + "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", + "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", + "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", + "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", + "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", + "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", + "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", + "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", + "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", + "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", + "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", + "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", + "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", + "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", + "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", + "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", + "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", + "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", + "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", + "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.14.1" + "version": "==1.15.0" } }, "develop": { "attrs": { "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" + "markers": "python_version >= '3.7'", + "version": "==23.1.0" }, "black": { "hashes": [ @@ -230,50 +241,69 @@ }, "coverage": { "hashes": [ - "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", - "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d", - "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf", - "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7", - "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6", - "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4", - "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059", - "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39", - "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536", - "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac", - "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c", - "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903", - "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d", - "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05", - "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684", - "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1", - "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f", - "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7", - "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca", - "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad", - "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca", - "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d", - "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92", - "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4", - "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf", - "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6", - "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1", - "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4", - "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359", - "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3", - "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620", - "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512", - "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69", - "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2", - "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518", - "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0", - "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa", - "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4", - "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e", - "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1", - "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2" + "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", + "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", + "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", + "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", + "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", + "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", + "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", + "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", + "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", + "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", + "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", + "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", + "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", + "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", + "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", + "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", + "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", + "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", + "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", + "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", + "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", + "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", + "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", + "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", + "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", + "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", + "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", + "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", + "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", + "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", + "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", + "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", + "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", + "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", + "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", + "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", + "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", + "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", + "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", + "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", + "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", + "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", + "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", + "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", + "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", + "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", + "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", + "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", + "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", + "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", + "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", + "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", + "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", + "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", + "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", + "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", + "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", + "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", + "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", + "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3" ], "markers": "python_version >= '3.7'", - "version": "==6.3.2" + "version": "==7.2.7" }, "cycler": { "hashes": [ @@ -309,18 +339,19 @@ }, "flake8-noqa": { "hashes": [ - "sha256:629b87b542f9b4cbd7ee6de10b2c669e460a200145a7577b98092b7e94373153" + "sha256:26d92ca6b72dec732d294e587a2bdeb66dab01acc609ed6a064693d6baa4e789", + "sha256:445618162e0bbae1b9d983326d4e39066c5c6de71ba0c444ca2d4d1fa5b2cdb7" ], "index": "pypi", - "version": "==1.2.1" + "version": "==1.2.9" }, "fonttools": { "hashes": [ - "sha256:c0fdcfa8ceebd7c1b2021240bd46ef77aa8e7408cf10434be55df52384865f8e", - "sha256:f829c579a8678fa939a1d9e9894d01941db869de44390adb49ce67055a06cc2a" + "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1", + "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb" ], "markers": "python_version >= '3.7'", - "version": "==4.33.3" + "version": "==4.38.0" }, "hypothesis": { "hashes": [ @@ -332,108 +363,141 @@ }, "importlib-metadata": { "hashes": [ - "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", - "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" + "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116", + "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d" ], - "markers": "python_version < '3.8'", - "version": "==4.11.3" + "markers": "python_full_version < '3.8.0' and python_full_version < '3.8.0' and python_full_version < '3.8.0' and python_full_version < '3.8.0' and python_full_version < '3.8.0' and python_full_version < '3.8.0' and python_full_version < '3.8.0'", + "version": "==4.13.0" }, "iniconfig": { "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jsonschema": { + "hashes": [ + "sha256:5f9c0a719ca2ce14c5de2fd350a64fd2d13e8539db29836a86adc990bb1a068f", + "sha256:8d4a2b7b6c2237e0199c8ea1a6d3e05bf118e289ae2b9d7ba444182a2959560d" ], - "version": "==1.1.1" + "version": "==3.0.2" }, "kiwisolver": { "hashes": [ - "sha256:0b7f50a1a25361da3440f07c58cd1d79957c2244209e4f166990e770256b6b0b", - "sha256:0c380bb5ae20d829c1a5473cfcae64267b73aaa4060adc091f6df1743784aae0", - "sha256:0d98dca86f77b851350c250f0149aa5852b36572514d20feeadd3c6b1efe38d0", - "sha256:0e45e780a74416ef2f173189ef4387e44b5494f45e290bcb1f03735faa6779bf", - "sha256:0e8afdf533b613122e4bbaf3c1e42c2a5e9e2d1dd3a0a017749a7658757cb377", - "sha256:1008346a7741620ab9cc6c96e8ad9b46f7a74ce839dbb8805ddf6b119d5fc6c2", - "sha256:1d1078ba770d6165abed3d9a1be1f9e79b61515de1dd00d942fa53bba79f01ae", - "sha256:1dcade8f6fe12a2bb4efe2cbe22116556e3b6899728d3b2a0d3b367db323eacc", - "sha256:240009fdf4fa87844f805e23f48995537a8cb8f8c361e35fda6b5ac97fcb906f", - "sha256:240c2d51d098395c012ddbcb9bd7b3ba5de412a1d11840698859f51d0e643c4f", - "sha256:262c248c60f22c2b547683ad521e8a3db5909c71f679b93876921549107a0c24", - "sha256:2e6cda72db409eefad6b021e8a4f964965a629f577812afc7860c69df7bdb84a", - "sha256:3c032c41ae4c3a321b43a3650e6ecc7406b99ff3e5279f24c9b310f41bc98479", - "sha256:42f6ef9b640deb6f7d438e0a371aedd8bef6ddfde30683491b2e6f568b4e884e", - "sha256:484f2a5f0307bc944bc79db235f41048bae4106ffa764168a068d88b644b305d", - "sha256:69b2d6c12f2ad5f55104a36a356192cfb680c049fe5e7c1f6620fc37f119cdc2", - "sha256:6e395ece147f0692ca7cdb05a028d31b83b72c369f7b4a2c1798f4b96af1e3d8", - "sha256:6ece2e12e4b57bc5646b354f436416cd2a6f090c1dadcd92b0ca4542190d7190", - "sha256:71469b5845b9876b8d3d252e201bef6f47bf7456804d2fbe9a1d6e19e78a1e65", - "sha256:7f606d91b8a8816be476513a77fd30abe66227039bd6f8b406c348cb0247dcc9", - "sha256:7f88c4b8e449908eeddb3bbd4242bd4dc2c7a15a7aa44bb33df893203f02dc2d", - "sha256:81237957b15469ea9151ec8ca08ce05656090ffabc476a752ef5ad7e2644c526", - "sha256:89b57c2984f4464840e4b768affeff6b6809c6150d1166938ade3e22fbe22db8", - "sha256:8a830a03970c462d1a2311c90e05679da56d3bd8e78a4ba9985cb78ef7836c9f", - "sha256:8ae5a071185f1a93777c79a9a1e67ac46544d4607f18d07131eece08d415083a", - "sha256:8b6086aa6936865962b2cee0e7aaecf01ab6778ce099288354a7229b4d9f1408", - "sha256:8ec2e55bf31b43aabe32089125dca3b46fdfe9f50afbf0756ae11e14c97b80ca", - "sha256:8ff3033e43e7ca1389ee59fb7ecb8303abb8713c008a1da49b00869e92e3dd7c", - "sha256:91eb4916271655dfe3a952249cb37a5c00b6ba68b4417ee15af9ba549b5ba61d", - "sha256:9d2bb56309fb75a811d81ed55fbe2208aa77a3a09ff5f546ca95e7bb5fac6eff", - "sha256:a4e8f072db1d6fb7a7cc05a6dbef8442c93001f4bb604f1081d8c2db3ca97159", - "sha256:b1605c7c38cc6a85212dfd6a641f3905a33412e49f7c003f35f9ac6d71f67720", - "sha256:b3e251e5c38ac623c5d786adb21477f018712f8c6fa54781bd38aa1c60b60fc2", - "sha256:b978afdb913ca953cf128d57181da2e8798e8b6153be866ae2a9c446c6162f40", - "sha256:be9a650890fb60393e60aacb65878c4a38bb334720aa5ecb1c13d0dac54dd73b", - "sha256:c222f91a45da9e01a9bc4f760727ae49050f8e8345c4ff6525495f7a164c8973", - "sha256:c839bf28e45d7ddad4ae8f986928dbf5a6d42ff79760d54ec8ada8fb263e097c", - "sha256:cbb5eb4a2ea1ffec26268d49766cafa8f957fe5c1b41ad00733763fae77f9436", - "sha256:e348f1904a4fab4153407f7ccc27e43b2a139752e8acf12e6640ba683093dd96", - "sha256:e677cc3626287f343de751e11b1e8a5b915a6ac897e8aecdbc996cd34de753a0", - "sha256:f74f2a13af201559e3d32b9ddfc303c94ae63d63d7f4326d06ce6fe67e7a8255", - "sha256:fa4d97d7d2b2c082e67907c0b8d9f31b85aa5d3ba0d33096b7116f03f8061261", - "sha256:ffbdb9a96c536f0405895b5e21ee39ec579cb0ed97bdbd169ae2b55f41d73219" + "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b", + "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166", + "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c", + "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c", + "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0", + "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4", + "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9", + "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286", + "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767", + "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c", + "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6", + "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b", + "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004", + "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf", + "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494", + "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac", + "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626", + "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766", + "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514", + "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6", + "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f", + "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d", + "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191", + "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d", + "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51", + "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f", + "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8", + "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454", + "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb", + "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da", + "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8", + "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de", + "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a", + "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9", + "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008", + "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3", + "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32", + "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938", + "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1", + "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9", + "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d", + "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824", + "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b", + "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd", + "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2", + "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5", + "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69", + "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3", + "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae", + "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597", + "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e", + "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955", + "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca", + "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a", + "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea", + "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede", + "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4", + "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6", + "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686", + "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408", + "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871", + "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29", + "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750", + "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897", + "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0", + "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2", + "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09", + "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c" ], "markers": "python_version >= '3.7'", - "version": "==1.4.2" + "version": "==1.4.4" }, "matplotlib": { "hashes": [ - "sha256:03bbb3f5f78836855e127b5dab228d99551ad0642918ccbf3067fcd52ac7ac5e", - "sha256:24173c23d1bcbaed5bf47b8785d27933a1ac26a5d772200a0f3e0e38f471b001", - "sha256:2a0967d4156adbd0d46db06bc1a877f0370bce28d10206a5071f9ecd6dc60b79", - "sha256:2e8bda1088b941ead50caabd682601bece983cadb2283cafff56e8fcddbf7d7f", - "sha256:31fbc2af27ebb820763f077ec7adc79b5a031c2f3f7af446bd7909674cd59460", - "sha256:364e6bca34edc10a96aa3b1d7cd76eb2eea19a4097198c1b19e89bee47ed5781", - "sha256:3d8e129af95b156b41cb3be0d9a7512cc6d73e2b2109f82108f566dbabdbf377", - "sha256:44c6436868186564450df8fd2fc20ed9daaef5caad699aa04069e87099f9b5a8", - "sha256:48cf850ce14fa18067f2d9e0d646763681948487a8080ec0af2686468b4607a2", - "sha256:49a5938ed6ef9dda560f26ea930a2baae11ea99e1c2080c8714341ecfda72a89", - "sha256:4a05f2b37222319753a5d43c0a4fd97ed4ff15ab502113e3f2625c26728040cf", - "sha256:4a44cdfdb9d1b2f18b1e7d315eb3843abb097869cd1ef89cfce6a488cd1b5182", - "sha256:4fa28ca76ac5c2b2d54bc058b3dad8e22ee85d26d1ee1b116a6fd4d2277b6a04", - "sha256:5844cea45d804174bf0fac219b4ab50774e504bef477fc10f8f730ce2d623441", - "sha256:5a32ea6e12e80dedaca2d4795d9ed40f97bfa56e6011e14f31502fdd528b9c89", - "sha256:6c623b355d605a81c661546af7f24414165a8a2022cddbe7380a31a4170fa2e9", - "sha256:751d3815b555dcd6187ad35b21736dc12ce6925fc3fa363bbc6dc0f86f16484f", - "sha256:75c406c527a3aa07638689586343f4b344fcc7ab1f79c396699eb550cd2b91f7", - "sha256:77157be0fc4469cbfb901270c205e7d8adb3607af23cef8bd11419600647ceed", - "sha256:7d7705022df2c42bb02937a2a824f4ec3cca915700dd80dc23916af47ff05f1a", - "sha256:7f409716119fa39b03da3d9602bd9b41142fab7a0568758cd136cd80b1bf36c8", - "sha256:9480842d5aadb6e754f0b8f4ebeb73065ac8be1855baa93cd082e46e770591e9", - "sha256:9776e1a10636ee5f06ca8efe0122c6de57ffe7e8c843e0fb6e001e9d9256ec95", - "sha256:a91426ae910819383d337ba0dc7971c7cefdaa38599868476d94389a329e599b", - "sha256:b4fedaa5a9aa9ce14001541812849ed1713112651295fdddd640ea6620e6cf98", - "sha256:b6c63cd01cad0ea8704f1fd586e9dc5777ccedcd42f63cbbaa3eae8dd41172a1", - "sha256:b8d3f4e71e26307e8c120b72c16671d70c5cd08ae412355c11254aa8254fb87f", - "sha256:c4b82c2ae6d305fcbeb0eb9c93df2602ebd2f174f6e8c8a5d92f9445baa0c1d3", - "sha256:c772264631e5ae61f0bd41313bbe48e1b9bcc95b974033e1118c9caa1a84d5c6", - "sha256:c87973ddec10812bddc6c286b88fdd654a666080fbe846a1f7a3b4ba7b11ab78", - "sha256:e2b696699386766ef171a259d72b203a3c75d99d03ec383b97fc2054f52e15cf", - "sha256:ea75df8e567743207e2b479ba3d8843537be1c146d4b1e3e395319a4e1a77fe9", - "sha256:ebc27ad11df3c1661f4677a7762e57a8a91dd41b466c3605e90717c9a5f90c82", - "sha256:ee0b8e586ac07f83bb2950717e66cb305e2859baf6f00a9c39cc576e0ce9629c", - "sha256:ee175a571e692fc8ae8e41ac353c0e07259113f4cb063b0ec769eff9717e84bb" + "sha256:0bcdfcb0f976e1bac6721d7d457c17be23cf7501f977b6a38f9d38a3762841f7", + "sha256:1e64ac9be9da6bfff0a732e62116484b93b02a0b4d4b19934fb4f8e7ad26ad6a", + "sha256:22227c976ad4dc8c5a5057540421f0d8708c6560744ad2ad638d48e2984e1dbc", + "sha256:2886cc009f40e2984c083687251821f305d811d38e3df8ded414265e4583f0c5", + "sha256:2e6d184ebe291b9e8f7e78bbab7987d269c38ea3e062eace1fe7d898042ef804", + "sha256:3211ba82b9f1518d346f6309df137b50c3dc4421b4ed4815d1d7eadc617f45a1", + "sha256:339cac48b80ddbc8bfd05daae0a3a73414651a8596904c2a881cfd1edb65f26c", + "sha256:35a8ad4dddebd51f94c5d24bec689ec0ec66173bf614374a1244c6241c1595e0", + "sha256:3b4fa56159dc3c7f9250df88f653f085068bcd32dcd38e479bba58909254af7f", + "sha256:43e9d3fa077bf0cc95ded13d331d2156f9973dce17c6f0c8b49ccd57af94dbd9", + "sha256:57f1b4e69f438a99bb64d7f2c340db1b096b41ebaa515cf61ea72624279220ce", + "sha256:5c096363b206a3caf43773abebdbb5a23ea13faef71d701b21a9c27fdcef72f4", + "sha256:6bb93a0492d68461bd458eba878f52fdc8ac7bdb6c4acdfe43dba684787838c2", + "sha256:6ea6aef5c4338e58d8d376068e28f80a24f54e69f09479d1c90b7172bad9f25b", + "sha256:6fe807e8a22620b4cd95cfbc795ba310dc80151d43b037257250faf0bfcd82bc", + "sha256:73dd93dc35c85dece610cca8358003bf0760d7986f70b223e2306b4ea6d1406b", + "sha256:839d47b8ead7ad9669aaacdbc03f29656dc21f0d41a6fea2d473d856c39c8b1c", + "sha256:874df7505ba820e0400e7091199decf3ff1fde0583652120c50cd60d5820ca9a", + "sha256:879c7e5fce4939c6aa04581dfe08d57eb6102a71f2e202e3314d5fbc072fd5a0", + "sha256:94ff86af56a3869a4ae26a9637a849effd7643858a1a04dd5ee50e9ab75069a7", + "sha256:99482b83ebf4eb6d5fc6813d7aacdefdd480f0d9c0b52dcf9f1cc3b2c4b3361a", + "sha256:9ab29589cef03bc88acfa3a1490359000c18186fc30374d8aa77d33cc4a51a4a", + "sha256:9befa5954cdbc085e37d974ff6053da269474177921dd61facdad8023c4aeb51", + "sha256:a206a1b762b39398efea838f528b3a6d60cdb26fe9d58b48265787e29cd1d693", + "sha256:ab8d26f07fe64f6f6736d635cce7bfd7f625320490ed5bfc347f2cdb4fae0e56", + "sha256:b28de401d928890187c589036857a270a032961411934bdac4cf12dde3d43094", + "sha256:b428076a55fb1c084c76cb93e68006f27d247169f056412607c5c88828d08f88", + "sha256:bf618a825deb6205f015df6dfe6167a5d9b351203b03fab82043ae1d30f16511", + "sha256:c995f7d9568f18b5db131ab124c64e51b6820a92d10246d4f2b3f3a66698a15b", + "sha256:cd45a6f3e93a780185f70f05cf2a383daed13c3489233faad83e81720f7ede24", + "sha256:d2484b350bf3d32cae43f85dcfc89b3ed7bd2bcd781ef351f93eb6fb2cc483f9", + "sha256:d62880e1f60e5a30a2a8484432bcb3a5056969dc97258d7326ad465feb7ae069", + "sha256:dacddf5bfcec60e3f26ec5c0ae3d0274853a258b6c3fc5ef2f06a8eb23e042be", + "sha256:f3840c280ebc87a48488a46f760ea1c0c0c83fcf7abbe2e6baf99d033fd35fd8", + "sha256:f814504e459c68118bf2246a530ed953ebd18213dc20e3da524174d84ed010b2" ], "index": "pypi", - "version": "==3.5.2" + "version": "==3.5.3" }, "mccabe": { "hashes": [ @@ -481,10 +545,10 @@ }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd" ], - "version": "==0.4.3" + "markers": "python_version >= '2.7'", + "version": "==0.4.4" }, "numpy": { "hashes": [ @@ -522,80 +586,113 @@ "index": "pypi", "version": "==1.21.2" }, + "opentrons-shared-data": { + "editable": true, + "path": "./../shared-data/python" + }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.1" }, "pathspec": { "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", + "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" ], - "version": "==0.9.0" + "markers": "python_version >= '3.7'", + "version": "==0.11.1" }, "pillow": { "hashes": [ - "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", - "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", - "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", - "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", - "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", - "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", - "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", - "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", - "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", - "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", - "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", - "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", - "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", - "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", - "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", - "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", - "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", - "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", - "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", - "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", - "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", - "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", - "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", - "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", - "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", - "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", - "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", - "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", - "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", - "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", - "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", - "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", - "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", - "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", - "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", - "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", - "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", - "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" + "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1", + "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba", + "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a", + "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799", + "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51", + "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb", + "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5", + "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270", + "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6", + "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47", + "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf", + "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e", + "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b", + "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66", + "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865", + "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec", + "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c", + "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1", + "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38", + "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906", + "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705", + "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef", + "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc", + "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f", + "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf", + "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392", + "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d", + "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe", + "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32", + "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5", + "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7", + "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44", + "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d", + "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3", + "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625", + "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e", + "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829", + "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089", + "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3", + "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78", + "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96", + "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964", + "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597", + "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99", + "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a", + "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140", + "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7", + "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16", + "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903", + "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1", + "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296", + "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572", + "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115", + "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a", + "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd", + "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4", + "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1", + "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb", + "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa", + "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a", + "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569", + "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c", + "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf", + "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082", + "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062", + "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579" ], "markers": "python_version >= '3.7'", - "version": "==9.1.0" + "version": "==9.5.0" }, "platformdirs": { "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" + "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc", + "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e" ], "markers": "python_version >= '3.7'", - "version": "==2.5.2" + "version": "==3.8.0" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", + "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" + "markers": "python_version >= '3.7'", + "version": "==1.2.0" }, "py": { "hashes": [ @@ -613,13 +710,41 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, + "pydantic": { + "hashes": [ + "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", + "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", + "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", + "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", + "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", + "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", + "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", + "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", + "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", + "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", + "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", + "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", + "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", + "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", + "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", + "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", + "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", + "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", + "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", + "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", + "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", + "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" + ], + "index": "pypi", + "version": "==1.8.2" + }, "pydocstyle": { "hashes": [ - "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc", - "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4" + "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", + "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" ], "markers": "python_version >= '3.6'", - "version": "==6.1.1" + "version": "==6.3.0" }, "pyflakes": { "hashes": [ @@ -631,28 +756,60 @@ }, "pyparsing": { "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + "sha256:d554a96d1a7d3ddaf7183104485bc19fd80543ad6ac5bdb6426719d766fb06c1", + "sha256:edb662d6fe322d6e990b1594b5feaeadf806803359e3d4d42f11e295e588f0ea" ], "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" + "version": "==3.1.0" + }, + "pyrsistent": { + "hashes": [ + "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", + "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", + "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", + "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", + "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", + "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", + "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", + "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", + "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", + "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", + "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", + "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", + "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", + "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", + "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", + "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", + "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", + "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", + "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", + "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", + "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", + "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", + "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", + "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", + "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", + "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", + "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" + ], + "markers": "python_version >= '3.7'", + "version": "==0.19.3" }, "pytest": { "hashes": [ - "sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33", - "sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7" + "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db", + "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171" ], "index": "pypi", - "version": "==6.1.0" + "version": "==7.0.1" }, "pytest-asyncio": { "hashes": [ - "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213", - "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91", - "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84" + "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b", + "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c" ], "index": "pypi", - "version": "==0.18.3" + "version": "==0.21.0" }, "pytest-cov": { "hashes": [ @@ -749,7 +906,7 @@ "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" ], - "markers": "python_version < '3.8' and implementation_name == 'cpython'", + "markers": "python_full_version < '3.8.0' and implementation_name == 'cpython' and python_full_version < '3.8.0' and python_full_version < '3.8.0'", "version": "==1.4.3" }, "types-mock": { @@ -762,19 +919,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", + "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" ], "index": "pypi", - "version": "==4.2.0" + "version": "==4.6.3" }, "zipp": { "hashes": [ - "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", - "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" + "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", + "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" ], "markers": "python_version >= '3.7'", - "version": "==3.8.0" + "version": "==3.15.0" } } } diff --git a/hardware/setup.py b/hardware/setup.py index f285eaf3128..8dbb43965a3 100644 --- a/hardware/setup.py +++ b/hardware/setup.py @@ -46,7 +46,11 @@ def get_version() -> str: KEYWORDS = ["robots", "protocols", "synbio", "pcr", "automation", "lab"] DESCRIPTION = "Hardware control for Opentrons Robots." PACKAGES = find_packages(where=".", exclude=["tests.*", "tests"]) -INSTALL_REQUIRES = ["python-can==3.3.4", "pyserial==3.5"] +INSTALL_REQUIRES = [ + "python-can==3.3.4", + "pyserial==3.5", + f"opentrons_shared_data=={VERSION}", +] def read(*parts: str) -> str: From 69c64886905f01f3613030f0d395c5a37bb7a49f Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 15:15:11 -0400 Subject: [PATCH 02/20] add internal message format error --- shared-data/errors/definitions/1/errors.json | 4 ++++ .../python/opentrons_shared_data/errors/codes.py | 1 + .../opentrons_shared_data/errors/exceptions.py | 12 ++++++++++++ 3 files changed, 17 insertions(+) diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index 4a8a5c3e683..d8dbf950f7e 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -42,6 +42,10 @@ "detail": "Firmware Update Failed", "category": "hardwareCommunicationError" }, + "1006": { + "detail": "Internal Message Format Error", + "category": "hardwareCommunicationError" + }, "2000": { "detail": "Robotics Control Error", "category": "roboticsControlError" diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index 99ec11f9537..30dd8ed9aa7 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -40,6 +40,7 @@ class ErrorCodes(Enum): MODULE_COMMUNICATION_ERROR = _code_from_dict_entry("1003") COMMAND_TIMED_OUT = _code_from_dict_entry("1004") FIRMWARE_UPDATE_FAILED = _code_from_dict_entry("1005") + INTERNAL_MESSAGE_FORMAT_ERROR = _code_from_dict_entry('1006') ROBOTICS_CONTROL_ERROR = _code_from_dict_entry("2000") MOTION_FAILED = _code_from_dict_entry("2001") HOMING_FAILED = _code_from_dict_entry("2002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 6b2bb305b38..1b45d48bc8f 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -286,6 +286,18 @@ def __init__( super().__init__(ErrorCodes.FIRMWARE_UPDATE_FAILED, message, detail, wrapping) +class InternalMessageFormatError(CommunicationError): + """An error indicating that an internal message was formatted incorrectly.""" + + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an InternalMesasgeFormatError.""" + super().__init__(ErrorCodes.FIRMWARE_UPDATE_FAILED, message, detail, wrapping) + class MotionFailedError(RoboticsControlError): """An error indicating that a motion failed.""" From d6e045e03aeae4f4cb7a5cc7a2a07eed860ce1ab Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 15:15:19 -0400 Subject: [PATCH 03/20] use some errors in hardware --- hardware/mypy.ini | 1 + .../firmware_bindings/messages/payloads.py | 18 +++--- .../opentrons_hardware/sensors/scheduler.py | 60 +++++++++++++++---- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/hardware/mypy.ini b/hardware/mypy.ini index c4dca9c9f39..0fd2feea554 100644 --- a/hardware/mypy.ini +++ b/hardware/mypy.ini @@ -1,5 +1,6 @@ [mypy] show_error_codes = True +warn_unused_configs = True strict = True [mypy-can.*] diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index 223a929c942..5b084a896d3 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -3,9 +3,11 @@ # dataclass fields interpretation. # from __future__ import annotations from dataclasses import dataclass, field, asdict -from . import message_definitions from typing import Iterator, List +from opentrons_shared_data.errors.exceptions import InternalMessageFormatError + +from . import message_definitions from .fields import ( FirmwareShortSHADataField, VersionFlagsField, @@ -314,14 +316,16 @@ def __post_init__(self) -> None: data_length = len(self.data.value) address = self.address.value if address % 8 != 0: - raise ValueError( - f"Data address needs to be doubleword aligned." - f" {address} mod 8 equals {address % 8} and should be 0" + raise InternalMessageFormatError( + f"FirmwareUpdateData: Data address needs to be doubleword aligned." + f" {address} mod 8 equals {address % 8} and should be 0", + detail={"address": address}, ) if data_length > FirmwareUpdateDataField.NUM_BYTES: - raise ValueError( - f"Data cannot be more than" - f" {FirmwareUpdateDataField.NUM_BYTES} bytes got {data_length}." + raise InternalMessageFormatError( + f"FirmwareUpdateData: Data cannot be more than" + f" {FirmwareUpdateDataField.NUM_BYTES} bytes got {data_length}.", + detail={"size": data_length}, ) @classmethod diff --git a/hardware/opentrons_hardware/sensors/scheduler.py b/hardware/opentrons_hardware/sensors/scheduler.py index dd0ee6744c1..edda0da3bb9 100644 --- a/hardware/opentrons_hardware/sensors/scheduler.py +++ b/hardware/opentrons_hardware/sensors/scheduler.py @@ -5,6 +5,8 @@ from typing import Optional, TypeVar, Callable, AsyncIterator, List, Tuple +from opentrons_shared_data.errors.exceptions import CommandTimedOutError + from opentrons_hardware.firmware_bindings.constants import ( NodeId, SensorOutputBinding, @@ -135,8 +137,17 @@ async def run_baseline( self._multi_wait_for_response(reader, _format_sensor_response), timeout, ) - except asyncio.TimeoutError: - log.warning("Sensor poll timed out") + except asyncio.TimeoutError as te: + msg = f"Sensor poll of {sensor_info.node_id.name} timed out" + log.warning(msg) + raise CommandTimedOutError( + message=msg, + detail={ + "node": sensor_info.node_id.name, + "sensor": sensor_info.sensor_type.name, + "sensor_id": sensor_info.sensor_id.name, + }, + ) from te finally: return data_list @@ -160,7 +171,7 @@ async def send_write( ) if error != ErrorCode.ok: log.error( - f"recieved error {str(error)} trying to write sensor info to {str(sensor_info.node_id)}" + f"recieved error {str(error)} trying to write sensor info to {sensor_info.node_id.name}" ) async def send_read( @@ -195,8 +206,17 @@ async def send_read( self._multi_wait_for_response(reader, _format_sensor_response), timeout, ) - except asyncio.TimeoutError: - log.warning("Sensor Read timed out") + except asyncio.TimeoutError as te: + msg = f"Sensor Read from {sensor_info.node_id.name} timed out" + log.warning(msg) + raise CommandTimedOutError( + message=msg, + detail={ + "node": sensor_info.node_id.name, + "sensor": sensor_info.sensor_type.name, + "sensor_id": sensor_info.sensor_id.name, + }, + ) from te finally: return data_list @@ -226,7 +246,7 @@ async def read( timeout, ) except asyncio.TimeoutError: - log.warning("Sensor Read timed out") + log.warning(f"Sensor Read from {node_id.name} timed out") finally: return data_list @@ -268,9 +288,17 @@ def _format(response: MessageDefinition) -> SensorDataType: self._wait_for_response(reader, _format), timeout, ) - except asyncio.TimeoutError: - log.error(f"Sensor Threshold Read from {sensor_info.node_id} timed out") - raise + except asyncio.TimeoutError as te: + msg = f"Sensor Threshold Read from {sensor_info.node_id.name} timed out" + log.error(msg) + raise CommandTimedOutError( + message=msg, + detail={ + "node": sensor_info.node_id.name, + "sensor": sensor_info.sensor_type.name, + "sensor_id": sensor_info.sensor_id.name, + }, + ) from te @staticmethod async def _multi_wait_for_response( @@ -334,8 +362,18 @@ async def request_peripheral_status( return await asyncio.wait_for( self._read_peripheral_response(reader), timeout ) - except asyncio.TimeoutError: - log.warning(f"No PeripheralStatusResponse from node {node_id}") + except asyncio.TimeoutError as te: + msg = f"No PeripheralStatusResponse from node {node_id}" + log.warning(msg) + raise CommandTimedOutError( + message=msg, + detail={ + "node": node_id.name, + "sensor": sensor.name, + "sensor_id": sensor_id.name, + "timeout": str(timeout), + }, + ) from te return False @staticmethod From 5f2673f999cb4c1414d8653ed440f8d078bab0cf Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 16:21:17 -0400 Subject: [PATCH 04/20] fix message format --- shared-data/python/opentrons_shared_data/errors/exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 1b45d48bc8f..936c970f258 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -296,7 +296,8 @@ def __init__( wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InternalMesasgeFormatError.""" - super().__init__(ErrorCodes.FIRMWARE_UPDATE_FAILED, message, detail, wrapping) + super().__init__(ErrorCodes.INTERNAL_MESSAGE_FORMAT_ERROR, message, detail, wrapping) + class MotionFailedError(RoboticsControlError): """An error indicating that a motion failed.""" From 7eb83c5c85eeb20a7c16e5020f662fc2573e9b35 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 16:21:25 -0400 Subject: [PATCH 05/20] add configuration error --- shared-data/errors/definitions/1/errors.json | 4 ++++ .../python/opentrons_shared_data/errors/codes.py | 1 + .../python/opentrons_shared_data/errors/exceptions.py | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index d8dbf950f7e..278539815fe 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -46,6 +46,10 @@ "detail": "Internal Message Format Error", "category": "hardwareCommunicationError" }, + "1007": { + "detail": "CANBus Configuration Error", + "category": "hardwareCommunicationError" + }, "2000": { "detail": "Robotics Control Error", "category": "roboticsControlError" diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index 30dd8ed9aa7..e51cc2d70ae 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -41,6 +41,7 @@ class ErrorCodes(Enum): COMMAND_TIMED_OUT = _code_from_dict_entry("1004") FIRMWARE_UPDATE_FAILED = _code_from_dict_entry("1005") INTERNAL_MESSAGE_FORMAT_ERROR = _code_from_dict_entry('1006') + CANBUS_CONFIGURATION_ERROR = _code_from_dict_entry('1007') ROBOTICS_CONTROL_ERROR = _code_from_dict_entry("2000") MOTION_FAILED = _code_from_dict_entry("2001") HOMING_FAILED = _code_from_dict_entry("2002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 936c970f258..79ba6c95fe1 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -298,6 +298,17 @@ def __init__( """Build an InternalMesasgeFormatError.""" super().__init__(ErrorCodes.INTERNAL_MESSAGE_FORMAT_ERROR, message, detail, wrapping) +class CANBusConfigurationError(CommunicationError): + """An error indicating a misconfiguration of the CANbus.""" + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a CANBus Configuration Error.""" + super().__init__(ErrorCodes.CANBUS_CONFIGURATION_ERROR, message, detail, wrapping) + class MotionFailedError(RoboticsControlError): """An error indicating that a motion failed.""" From 389d221bb5078e197d12be8d77a180416f46037e Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 16:21:37 -0400 Subject: [PATCH 06/20] use configuration error --- .../drivers/can_bus/settings.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/can_bus/settings.py b/hardware/opentrons_hardware/drivers/can_bus/settings.py index 3f8dd9d5305..0fbddb1827d 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/settings.py +++ b/hardware/opentrons_hardware/drivers/can_bus/settings.py @@ -3,6 +3,8 @@ from typing import Optional from pydantic import BaseSettings, Field +from opentrons_shared_data.errors.exceptions import CANBusConfigurationError + from math import floor DEFAULT_INTERFACE: Final = "socketcan" @@ -72,13 +74,13 @@ def _check_calculated_bit_timing_values( brp: float, tseg_1: float, tseg_2: float ) -> None: if not brp.is_integer(): - raise ValueError(f"BRP is {brp} and must be an integer") + raise CANBusConfigurationError(message=f"BRP is {brp} and must be an integer", detail={'brp': str(brp)}) if brp > MAX_BRP: - raise ValueError(f"Calculated BRP {brp} exceeds max value {MAX_BRP}") + raise CANBusConfigurationError(message=f"Calculated BRP {brp} exceeds max value {MAX_BRP}", detail={'brp': str(brp), 'max': str(MAX_BRP)}) if tseg_1 > MAX_TSEG1: - raise ValueError(f"Calculated TSEG1 {tseg_1} exceeds max value {MAX_TSEG1}") + raise CANBusConfigurationError(message=f"Calculated TSEG1 {tseg_1} exceeds max value {MAX_TSEG1}", detail={'tseg1': str(tseg_1), 'max': str(MAX_TSEG1)}) if tseg_2 > MAX_TSEG2: - raise ValueError(f"Calculated TSEG2 {tseg_2} exceeds max value {MAX_TSEG2}") + raise CANBusConfigurationError(message=f"Calculated TSEG2 {tseg_2} exceeds max value {MAX_TSEG2}", detail={'tseg2': str(tseg_2), 'max': str(MAX_TSEG2)}) def calculate_fdcan_parameters( @@ -107,12 +109,14 @@ def calculate_fdcan_parameters( jump_width = DEFAULT_JUMP_WIDTH_SEG if fcan_clock > MAX_FCAN_CLK: - raise ValueError( - f"Clock value {fcan_clock} exceeds max value of {MAX_FCAN_CLK}" + raise CANBusConfigurationError( + message=f"Clock value {fcan_clock} exceeds max value of {MAX_FCAN_CLK}", + detail={'clock': str(fcan_clock), 'max': str(MAX_FCAN_CLK)} ) if jump_width > MAX_SJW: raise ValueError( - f"Jump width value {jump_width} exceeds max value of {MAX_SJW}" + message=f"Jump width value {jump_width} exceeds max value of {MAX_SJW}", + detail={'sjw': str(jump_width), 'max': str(max_sjw)} ) sample_rate_percent = sample_rate / 100 From 125c28b7c91a9bd6b4fd234b3df8c716bad622e3 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 16:50:35 -0400 Subject: [PATCH 07/20] add a bus error for e.g. error frames --- shared-data/errors/definitions/1/errors.json | 4 ++++ .../python/opentrons_shared_data/errors/codes.py | 1 + .../python/opentrons_shared_data/errors/exceptions.py | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index 278539815fe..ea051a19ae5 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -50,6 +50,10 @@ "detail": "CANBus Configuration Error", "category": "hardwareCommunicationError" }, + "1008": { + "detail": "CANBus Bus Error", + "category": "hardwareCommunicationError" + }, "2000": { "detail": "Robotics Control Error", "category": "roboticsControlError" diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index e51cc2d70ae..6dd5f27d0cf 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -42,6 +42,7 @@ class ErrorCodes(Enum): FIRMWARE_UPDATE_FAILED = _code_from_dict_entry("1005") INTERNAL_MESSAGE_FORMAT_ERROR = _code_from_dict_entry('1006') CANBUS_CONFIGURATION_ERROR = _code_from_dict_entry('1007') + CANBUS_BUS_ERROR = _code_from_dict_entry('1008') ROBOTICS_CONTROL_ERROR = _code_from_dict_entry("2000") MOTION_FAILED = _code_from_dict_entry("2001") HOMING_FAILED = _code_from_dict_entry("2002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 79ba6c95fe1..76422dd2cbf 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -309,6 +309,17 @@ def __init__( """Build a CANBus Configuration Error.""" super().__init__(ErrorCodes.CANBUS_CONFIGURATION_ERROR, message, detail, wrapping) +class CANBusBusError(CommunicationError): + """An error indicating a low-level bus error on the CANbus like an error frame.""" + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a CANBus Bus Error.""" + super().__init__(ErrorCodes.CANBUS_BUS_ERROR, message, detail, wrapping) + class MotionFailedError(RoboticsControlError): """An error indicating that a motion failed.""" From 3ff490e8f69b439b07104ba0b28f14e8e469f8df Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 16:50:48 -0400 Subject: [PATCH 08/20] use bus error instead of hw error --- .../drivers/can_bus/driver.py | 8 +++--- .../drivers/can_bus/errors.py | 14 ----------- .../drivers/can_bus/settings.py | 25 +++++++++++++------ .../drivers/can_bus/socket_driver.py | 11 +++++--- .../scripts/sim_socket_can.py | 4 +-- .../drivers/can_bus/test_driver.py | 4 +-- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/can_bus/driver.py b/hardware/opentrons_hardware/drivers/can_bus/driver.py index 8c1ffc68803..083f4ec5a2d 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/driver.py +++ b/hardware/opentrons_hardware/drivers/can_bus/driver.py @@ -6,11 +6,11 @@ from typing import Optional, Union, Dict, Any import concurrent.futures +from opentrons_shared_data.errors.exceptions import CANBusBusError from can import Notifier, Bus, AsyncBufferedReader, Message from opentrons_hardware.firmware_bindings.arbitration_id import ArbitrationId from opentrons_hardware.firmware_bindings.message import CanMessage -from .errors import ErrorFrameCanError from .abstract_driver import AbstractCanDriver from .settings import calculate_fdcan_parameters, PCANParameters @@ -124,12 +124,14 @@ async def read(self) -> CanMessage: A can message Raises: - ErrorFrameCanError + CANBusBusError """ m: Message = await self._reader.get_message() if m.is_error_frame: log.error("Error frame encountered") - raise ErrorFrameCanError(message=repr(m)) + raise CANBusBusError( + message="Error frame encountered", detail={"frame": repr(m)} + ) return CanMessage( arbitration_id=ArbitrationId(id=m.arbitration_id), data=m.data diff --git a/hardware/opentrons_hardware/drivers/can_bus/errors.py b/hardware/opentrons_hardware/drivers/can_bus/errors.py index 2d5765437d9..eb8cbf68d44 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/errors.py +++ b/hardware/opentrons_hardware/drivers/can_bus/errors.py @@ -3,20 +3,6 @@ from opentrons_hardware.firmware_bindings.constants import ErrorCode, ErrorSeverity -class CanError(Exception): - """Can bus error.""" - - def __init__(self, message: str) -> None: - """Constructor.""" - super().__init__(message) - - -class ErrorFrameCanError(CanError): - """An error frame was received on the can bus.""" - - pass - - class AsyncHardwareError(RuntimeError): """An error generated from firmware that was not caused by a command sent from hardware controller.""" diff --git a/hardware/opentrons_hardware/drivers/can_bus/settings.py b/hardware/opentrons_hardware/drivers/can_bus/settings.py index 0fbddb1827d..c36bd791fb9 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/settings.py +++ b/hardware/opentrons_hardware/drivers/can_bus/settings.py @@ -74,13 +74,24 @@ def _check_calculated_bit_timing_values( brp: float, tseg_1: float, tseg_2: float ) -> None: if not brp.is_integer(): - raise CANBusConfigurationError(message=f"BRP is {brp} and must be an integer", detail={'brp': str(brp)}) + raise CANBusConfigurationError( + message=f"BRP is {brp} and must be an integer", detail={"brp": str(brp)} + ) if brp > MAX_BRP: - raise CANBusConfigurationError(message=f"Calculated BRP {brp} exceeds max value {MAX_BRP}", detail={'brp': str(brp), 'max': str(MAX_BRP)}) + raise CANBusConfigurationError( + message=f"Calculated BRP {brp} exceeds max value {MAX_BRP}", + detail={"brp": str(brp), "max": str(MAX_BRP)}, + ) if tseg_1 > MAX_TSEG1: - raise CANBusConfigurationError(message=f"Calculated TSEG1 {tseg_1} exceeds max value {MAX_TSEG1}", detail={'tseg1': str(tseg_1), 'max': str(MAX_TSEG1)}) + raise CANBusConfigurationError( + message=f"Calculated TSEG1 {tseg_1} exceeds max value {MAX_TSEG1}", + detail={"tseg1": str(tseg_1), "max": str(MAX_TSEG1)}, + ) if tseg_2 > MAX_TSEG2: - raise CANBusConfigurationError(message=f"Calculated TSEG2 {tseg_2} exceeds max value {MAX_TSEG2}", detail={'tseg2': str(tseg_2), 'max': str(MAX_TSEG2)}) + raise CANBusConfigurationError( + message=f"Calculated TSEG2 {tseg_2} exceeds max value {MAX_TSEG2}", + detail={"tseg2": str(tseg_2), "max": str(MAX_TSEG2)}, + ) def calculate_fdcan_parameters( @@ -111,12 +122,12 @@ def calculate_fdcan_parameters( if fcan_clock > MAX_FCAN_CLK: raise CANBusConfigurationError( message=f"Clock value {fcan_clock} exceeds max value of {MAX_FCAN_CLK}", - detail={'clock': str(fcan_clock), 'max': str(MAX_FCAN_CLK)} + detail={"clock": str(fcan_clock), "max": str(MAX_FCAN_CLK)}, ) if jump_width > MAX_SJW: - raise ValueError( + raise CANBusConfigurationError( message=f"Jump width value {jump_width} exceeds max value of {MAX_SJW}", - detail={'sjw': str(jump_width), 'max': str(max_sjw)} + detail={"sjw": str(jump_width), "max": str(MAX_SJW)}, ) sample_rate_percent = sample_rate / 100 diff --git a/hardware/opentrons_hardware/drivers/can_bus/socket_driver.py b/hardware/opentrons_hardware/drivers/can_bus/socket_driver.py index d3b0cac04a8..753beb9bf52 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/socket_driver.py +++ b/hardware/opentrons_hardware/drivers/can_bus/socket_driver.py @@ -5,10 +5,11 @@ import struct import asyncio +from opentrons_shared_data.errors.exceptions import CanbusCommunicationError + from . import ArbitrationId from opentrons_hardware.firmware_bindings import CanMessage from .abstract_driver import AbstractCanDriver -from .errors import CanError log = logging.getLogger(__name__) @@ -62,13 +63,17 @@ async def read(self) -> CanMessage: header = await self._reader.readexactly(self.header_length) arbitration_id, length = struct.unpack(">LL", header) except asyncio.IncompleteReadError as e: - raise CanError(str(e)) + raise CanbusCommunicationError( + message=f"Error reading socket header: {str(e)}" + ) if length > 0: try: data = await self._reader.readexactly(length) except asyncio.IncompleteReadError as e: - raise CanError(str(e)) + raise CanbusCommunicationError( + message=f"Error reading socket payload: {str(e)}" + ) else: data = b"" return CanMessage(arbitration_id=ArbitrationId(id=arbitration_id), data=data) diff --git a/hardware/opentrons_hardware/scripts/sim_socket_can.py b/hardware/opentrons_hardware/scripts/sim_socket_can.py index fee1edcb516..de25ae7f717 100644 --- a/hardware/opentrons_hardware/scripts/sim_socket_can.py +++ b/hardware/opentrons_hardware/scripts/sim_socket_can.py @@ -7,7 +7,7 @@ from logging.config import dictConfig from typing import List -from opentrons_hardware.drivers.can_bus.errors import CanError +from opentrons_shared_data.errors.exceptions import CANBusBusError from opentrons_hardware.drivers.can_bus.socket_driver import SocketDriver log = logging.getLogger(__name__) @@ -35,7 +35,7 @@ async def __call__( while not reader.at_eof(): try: data = await driver.read() - except CanError as e: + except CANBusBusError as e: log.error(f"Read error: {e}.") break diff --git a/hardware/tests/opentrons_hardware/drivers/can_bus/test_driver.py b/hardware/tests/opentrons_hardware/drivers/can_bus/test_driver.py index fc294a6019b..2956d8dd360 100644 --- a/hardware/tests/opentrons_hardware/drivers/can_bus/test_driver.py +++ b/hardware/tests/opentrons_hardware/drivers/can_bus/test_driver.py @@ -4,8 +4,8 @@ import pytest from can import Bus, Message +from opentrons_shared_data.errors.exceptions import CANBusBusError from opentrons_hardware.drivers.can_bus import CanDriver, ArbitrationId, CanMessage -from opentrons_hardware.drivers.can_bus.errors import ErrorFrameCanError @pytest.fixture @@ -100,5 +100,5 @@ async def test_raise_error_frame_error(subject: CanDriver, can_bus: Bus) -> None data=bytearray([1, 2, 3, 4]), ) can_bus.send(m) - with pytest.raises(ErrorFrameCanError): + with pytest.raises(CANBusBusError): await subject.read() From 6cdd1532d1843e9b5889012dcdac04f9bce7bdfe Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 27 Jun 2023 17:08:13 -0400 Subject: [PATCH 09/20] all can errors except async hardware --- .../drivers/can_bus/can_messenger.py | 48 +++++++++++-------- .../hardware_control/tools/detector.py | 4 +- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index 02d8e0bec06..ac724367bf6 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -16,7 +16,11 @@ import logging -from opentrons_hardware.drivers.errors import CANCommunicationError +from opentrons_shared_data.errors.exceptions import ( + CanbusCommunicationError, + EnumeratedError, + PythonException, +) from opentrons_hardware.drivers.can_bus.abstract_driver import AbstractCanDriver from opentrons_hardware.firmware_bindings.arbitration_id import ( ArbitrationId, @@ -238,11 +242,13 @@ async def _send(self, node_id: NodeId, message: MessageDefinition) -> None: await self._drive.send( message=CanMessage(arbitration_id=arbitration_id, data=data) ) - except CANCommunicationError: + except EnumeratedError: raise except Exception as exc: log.exception("Exception in CAN send") - raise CANCommunicationError(exc=exc) from exc + raise CanbusCommunicationError( + message="Exception in canbus.send", wrapping=[PythonException(exc)] + ) async def ensure_send_exclusive( self, @@ -293,11 +299,13 @@ async def _ensure_send( ) try: return await listener.send_and_verify_recieved() - except CANCommunicationError: + except EnumeratedError: raise except Exception as exc: log.exception("Exception in CAN ensure_send") - raise CANCommunicationError(exc=exc) from exc + raise CanbusCommunicationError( + message="Exception in CAN ensure_send", wrapping=[PythonException(exc)] + ) async def __aenter__(self: CanMessenger) -> CanMessenger: """Start messenger.""" @@ -313,9 +321,10 @@ async def __aexit__( """Stop messenger.""" await self.stop() if exc_val: - if isinstance(exc_val, CANCommunicationError): + if isinstance(exc_val, EnumeratedError): raise exc_val - raise CANCommunicationError(exc=exc_val) + # Don't want a specific error here because this wraps other code + raise PythonException(exc_val) def start(self) -> None: """Start the reader task.""" @@ -349,18 +358,19 @@ def remove_listener(self, listener: MessageListenerCallback) -> None: self._listeners.pop(listener) async def _read_task_shield(self) -> None: - while True: - try: - await self._read_task() - except (asyncio.CancelledError, StopAsyncIteration): - return - except (CANCommunicationError, AsyncHardwareError, CanError) as e: - log.exception(f"Nonfatal error in CAN read task: {e}") - continue - except Exception as e: - # Log this separately if it's some unknown error - log.exception(f"Unexpected error in CAN read task: {e}") - continue + try: + await self._read_task() + except asyncio.CancelledError: + pass + except EnumeratedError: + raise + except AsyncHardwareError: + raise + except Exception as exc: + log.exception("Exception in read") + raise CanbusCommunicationError( + message="Exception in read", wrapping=[PythonException(exc)] + ) async def _read_task(self) -> None: """Read task.""" diff --git a/hardware/opentrons_hardware/hardware_control/tools/detector.py b/hardware/opentrons_hardware/hardware_control/tools/detector.py index 694573396df..fd526406405 100644 --- a/hardware/opentrons_hardware/hardware_control/tools/detector.py +++ b/hardware/opentrons_hardware/hardware_control/tools/detector.py @@ -4,6 +4,8 @@ import asyncio from typing import AsyncIterator, Set, Dict, Tuple, Union +from opentrons_shared_data.errors.exceptions import CanbusCommunicationError + from opentrons_hardware.drivers.can_bus.can_messenger import WaitableCallback from opentrons_hardware.firmware_bindings.constants import ToolType, PipetteName from opentrons_hardware.firmware_bindings.messages import message_definitions @@ -43,7 +45,7 @@ async def _await_one_result(callback: WaitableCallback) -> ToolDetectionResult: return _handle_detection_result(response) if isinstance(response, message_definitions.ErrorMessage): log.error(f"Recieved error message {str(response)}") - raise RuntimeError("Messenger closed before a tool was found") + raise CanbusCommunicationError(message="Messenger closed before a tool was found") def _decode_or_default(orig: bytes) -> str: From 66ba34dd51bacde149449eda174a1d05ea84b56c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 28 Jun 2023 14:49:08 -0400 Subject: [PATCH 10/20] add some new lovely error codes --- shared-data/errors/definitions/1/errors.json | 8 +++++++ .../opentrons_shared_data/errors/codes.py | 2 ++ .../errors/exceptions.py | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index ea051a19ae5..266a0d80217 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -74,6 +74,10 @@ "detail": "Motion Planning Failure", "category": "roboticsControlError" }, + "2005": { + "detail": "Position Estimation Invalid", + "category": "roboticsControlError" + }, "3000": { "detail": "A robotics interaction error occurred.", "category": "roboticsInteractionError" @@ -134,6 +138,10 @@ "detail": "Module Not Present", "category": "roboticsInteractionError" }, + "3016": { + "detail": "Invalid Instrument Data", + "category": "roboticsInteractionError" + }, "4000": { "detail": "Unknown or Uncategorized Error", "category": "generalError" diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index 6dd5f27d0cf..2f711f18821 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -48,6 +48,7 @@ class ErrorCodes(Enum): HOMING_FAILED = _code_from_dict_entry("2002") STALL_OR_COLLISION_DETECTED = _code_from_dict_entry("2003") MOTION_PLANNING_FAILURE = _code_from_dict_entry("2004") + POSITION_ESTIMATION_INVALID = _code_from_dict_entry('2005') ROBOTICS_INTERACTION_ERROR = _code_from_dict_entry("3000") LABWARE_DROPPED = _code_from_dict_entry("3001") LABWARE_NOT_PICKED_UP = _code_from_dict_entry("3002") @@ -63,6 +64,7 @@ class ErrorCodes(Enum): FIRMWARE_UPDATE_REQUIRED = _code_from_dict_entry("3013") INVALID_ACTUATOR = _code_from_dict_entry("3014") MODULE_NOT_PRESENT = _code_from_dict_entry("3015") + INVALID_INSTRUMENT_DATA = _code_from_dict_entry('3016') GENERAL_ERROR = _code_from_dict_entry("4000") ROBOT_IN_USE = _code_from_dict_entry("4001") API_REMOVED = _code_from_dict_entry("4002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 76422dd2cbf..79b78c13679 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -375,6 +375,18 @@ def __init__( super().__init__(ErrorCodes.MOTION_PLANNING_FAILURE, message, detail, wrapping) +class PositionEstimationInvalidError(RoboticsControlError): + """An error indicating that motion planning failed.""" + + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a PositionEstimationFailedError.""" + super().__init__(ErrorCodes.POSITION_ESTIMATION_INVALID, message, detail, wrapping) + class LabwareDroppedError(RoboticsInteractionError): """An error indicating that the gripper dropped labware it was holding.""" @@ -549,6 +561,18 @@ def __init__( ErrorCodes.MODULE_NOT_PRESENT, checked_message, checked_detail, wrapping ) +class InvalidInstrumentData(RoboticsInteractionError): + """An error indicating that instrument data is invalid.""" + + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an GripperNotPresentError.""" + super().__init__(ErrorCodes.INVALID_INSTRUMENT_DATA, message, detail, wrapping) + class APIRemoved(GeneralError): """An error indicating that a specific API is no longer available.""" From 96b141c5191e755646457840909b4524a9bdebde Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 28 Jun 2023 14:49:21 -0400 Subject: [PATCH 11/20] use some more error codes --- .../drivers/binary_usb/bin_serial.py | 19 ++- .../drivers/binary_usb/binary_messenger.py | 52 +++++--- hardware/opentrons_hardware/drivers/errors.py | 8 -- .../utils/binary_serializable.py | 48 ++++++-- .../firmware_update/downloader.py | 8 +- .../firmware_update/eraser.py | 4 +- .../firmware_update/errors.py | 71 +++++++---- .../firmware_update/hex_file.py | 114 +++++++++++++----- .../firmware_update/initiator.py | 4 +- .../opentrons_hardware/firmware_update/run.py | 82 ++++++++++--- .../hardware_control/motion_planning/types.py | 20 ++- .../hardware_control/motor_position_status.py | 22 +++- .../hardware_control/tool_sensors.py | 11 +- .../instruments/gripper/serials.py | 18 ++- .../instruments/pipettes/serials.py | 33 +++-- .../firmware_update/test_hex_file.py | 10 +- 16 files changed, 377 insertions(+), 147 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/binary_usb/bin_serial.py b/hardware/opentrons_hardware/drivers/binary_usb/bin_serial.py index ac4051f8e40..0ce04bb2d8a 100644 --- a/hardware/opentrons_hardware/drivers/binary_usb/bin_serial.py +++ b/hardware/opentrons_hardware/drivers/binary_usb/bin_serial.py @@ -1,17 +1,21 @@ """The usb binary protocol over serial transport.""" +import asyncio +import logging +import concurrent.futures +from functools import partial +from typing import Optional, Type, Tuple import serial # type: ignore[import] from serial.tools.list_ports import comports # type: ignore[import] -from functools import partial + +from opentrons_shared_data.errors.exceptions import InternalUSBCommunicationError + from opentrons_hardware.firmware_bindings.messages.binary_message_definitions import ( BinaryMessageDefinition, get_binary_definition, ) -import asyncio -import logging -import concurrent.futures -from typing import Optional, Type, Tuple + from opentrons_hardware.firmware_bindings import utils from opentrons_hardware.firmware_bindings.binary_constants import BinaryMessageId @@ -37,7 +41,10 @@ def find_and_connect( """Initialize a serial connection to a usb device that uses the binary messaging protocol.""" _port_name = self._find_serial_port(vid, pid) if _port_name is None: - raise IOError("unable to find serial device") + raise InternalUSBCommunicationError( + message="unable to find serial device", + detail={"vid": str(vid), "pid": str(pid)}, + ) self._vid = vid self._pid = pid self._baudrate = baudrate diff --git a/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py b/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py index 148a45c8e80..49c1584526d 100644 --- a/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py +++ b/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py @@ -7,9 +7,14 @@ import logging +from opentrons_shared_data.errors.exceptions import ( + EnumeratedError, + InternalUSBCommunicationError, + PythonException, +) + from opentrons_hardware.drivers.binary_usb.bin_serial import SerialUsbDriver from opentrons_hardware.firmware_bindings.binary_constants import BinaryMessageId -from opentrons_hardware.drivers.errors import USBCommunicationError from opentrons_hardware.firmware_bindings.messages.binary_message_definitions import ( @@ -106,10 +111,14 @@ async def send(self, message: BinaryMessageDefinition) -> bool: return bool( (await self._drive.write(message=message)) == message.get_size() ) - except USBCommunicationError: + except EnumeratedError: raise - except Exception as exc: - raise USBCommunicationError(exc=exc) from exc + except BaseException as exc: + raise InternalUSBCommunicationError( + message="Error in USB send", + detail={"message": str(message)}, + wrapping=[PythonException(exc)], + ) async def __aenter__(self) -> BinaryMessenger: """Start messenger.""" @@ -124,9 +133,9 @@ async def __aexit__( if exc_val: # type ignore because it's unclear what the type of exc_tb should be log.error(format_exception(exc_type, exc_val, exc_tb)) # type: ignore - if isinstance(exc_val, USBCommunicationError): + if isinstance(exc_val, EnumeratedError): raise exc_val - raise USBCommunicationError(exc=exc_val) + raise PythonException(exc_val) def start(self) -> None: """Start the reader task.""" @@ -165,15 +174,18 @@ def remove_listener(self, listener: BinaryMessageListenerCallback) -> None: self._listeners.pop(listener) async def _read_task_shield(self) -> None: - try: - await self._read_task() - except asyncio.CancelledError: - pass - except USBCommunicationError: - raise - except Exception as exc: - log.exception("Exception in read") - raise USBCommunicationError(exc=exc) from exc + while True: + try: + await self._read_task() + except (asyncio.CancelledError, StopAsyncIteration): + return + except (InternalUSBCommunicationError) as e: + log.exception(f"Nonfatal error in USB read task: {e}") + continue + except BaseException as e: + # Log this separately if it's some unknown error + log.exception(f"Unexpected error in USB read task: {e}") + continue async def _read_task(self) -> None: """Read task.""" @@ -214,11 +226,15 @@ async def send_and_receive( listener = SendAndReceiveListener(self, response_type, timeout) try: return await listener.send_and_receive(message) - except USBCommunicationError: + except EnumeratedError: raise - except Exception as exc: + except BaseException as exc: log.exception("Exception in send_and_receive") - raise USBCommunicationError(exc=exc) from exc + raise InternalUSBCommunicationError( + message="Exception in internal USB send_and_receive", + detail={"message": str(message)}, + wrapping=[PythonException(exc=exc)], + ) class BinaryWaitableCallback: diff --git a/hardware/opentrons_hardware/drivers/errors.py b/hardware/opentrons_hardware/drivers/errors.py index 8b2de183ff8..4f0cd2e4468 100644 --- a/hardware/opentrons_hardware/drivers/errors.py +++ b/hardware/opentrons_hardware/drivers/errors.py @@ -7,11 +7,3 @@ class CommunicationError(RuntimeError): def __init__(self, exc: BaseException) -> None: """Build an exception for easier catching that wraps another.""" self.wrapped_exc = exc - - -class USBCommunicationError(CommunicationError): - """There was an error in communications with a USB device.""" - - -class CANCommunicationError(CommunicationError): - """There was an error in communications with a canbus device.""" diff --git a/hardware/opentrons_hardware/firmware_bindings/utils/binary_serializable.py b/hardware/opentrons_hardware/firmware_bindings/utils/binary_serializable.py index ff82412c635..987483b98ff 100644 --- a/hardware/opentrons_hardware/firmware_bindings/utils/binary_serializable.py +++ b/hardware/opentrons_hardware/firmware_bindings/utils/binary_serializable.py @@ -3,25 +3,53 @@ from __future__ import annotations import struct from dataclasses import dataclass, fields, astuple -from typing import TypeVar, Generic, Type +from typing import TypeVar, Generic, Type, Optional, Dict, Any, Sequence +from opentrons_shared_data.errors.exceptions import ( + InternalMessageFormatError, + EnumeratedError, + PythonException, +) -class BinarySerializableException(BaseException): + +class BinarySerializableException(InternalMessageFormatError): """Exception.""" - pass + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a BinarySerializableException.""" + submessage = f": {message}" if message else "" + super().__init__( + f"Could not create message object{submessage}", + detail=detail, + wrapping=wrapping, + ) class InvalidFieldException(BinarySerializableException): """Field is wrong type.""" - pass + def __init__(self, message: str, intended_bytes: bytes, exc: BaseException) -> None: + """Build an InvalidFieldException.""" + super().__init__( + message=message, + detail={ + "data": repr(intended_bytes), + }, + wrapping=[PythonException(exc)], + ) class SerializationException(BinarySerializableException): """Serialization error.""" - pass + def __init__(self, exc: BaseException) -> None: + """Build a SerializationException.""" + super().__init__("Serialization failed", wrapping=[PythonException(exc)]) T = TypeVar("T") @@ -162,7 +190,7 @@ def serialize(self) -> bytes: try: return struct.pack(string, *vals) except struct.error as e: - raise SerializationException(str(e)) + raise SerializationException(e) @classmethod def build(cls, data: bytes) -> BinarySerializable: @@ -207,7 +235,7 @@ def build(cls, data: bytes) -> BinarySerializable: ret_instance.message_index = message_index # type: ignore[attr-defined] return ret_instance except struct.error as e: - raise InvalidFieldException(str(e)) + raise InvalidFieldException("Bad data for field", data, e) @classmethod def _get_format_string(cls) -> str: @@ -221,8 +249,10 @@ def _get_format_string(cls) -> str: format_string = ( f"{cls.ENDIAN}{''.join(v.type.FORMAT for v in dataclass_fields)}" ) - except AttributeError: - raise InvalidFieldException(f"All fields must be of type {BinaryFieldBase}") + except AttributeError as e: + raise InvalidFieldException( + "All fields must be of type BinaryFieldBase", b"", e + ) return format_string diff --git a/hardware/opentrons_hardware/firmware_update/downloader.py b/hardware/opentrons_hardware/firmware_update/downloader.py index 54b9ffafb6f..e4b6d2a750a 100644 --- a/hardware/opentrons_hardware/firmware_update/downloader.py +++ b/hardware/opentrons_hardware/firmware_update/downloader.py @@ -69,7 +69,7 @@ async def run( self._wait_data_message_ack(node_id, reader), ack_wait_seconds ) except asyncio.TimeoutError: - raise TimeoutResponse(data_message) + raise TimeoutResponse(data_message, node_id) crc32 = binascii.crc32(data, crc32) num_messages += 1 @@ -88,7 +88,7 @@ async def run( self._wait_update_complete_ack(node_id, reader), ack_wait_seconds ) except asyncio.TimeoutError: - raise TimeoutResponse(complete_message) + raise TimeoutResponse(complete_message, node_id) @staticmethod async def _wait_data_message_ack(node_id: NodeId, reader: WaitableCallback) -> None: @@ -99,7 +99,7 @@ async def _wait_data_message_ack(node_id: NodeId, reader: WaitableCallback) -> N response, message_definitions.FirmwareUpdateDataAcknowledge ): if response.payload.error_code.value != ErrorCode.ok: - raise ErrorResponse(response) + raise ErrorResponse(response, node_id) break @staticmethod @@ -113,5 +113,5 @@ async def _wait_update_complete_ack( response, message_definitions.FirmwareUpdateCompleteAcknowledge ): if response.payload.error_code.value != ErrorCode.ok: - raise ErrorResponse(response) + raise ErrorResponse(response, node_id) break diff --git a/hardware/opentrons_hardware/firmware_update/eraser.py b/hardware/opentrons_hardware/firmware_update/eraser.py index 178a6a6df0a..8b7abf821f6 100644 --- a/hardware/opentrons_hardware/firmware_update/eraser.py +++ b/hardware/opentrons_hardware/firmware_update/eraser.py @@ -40,7 +40,7 @@ async def run(self, node_id: NodeId, timeout_sec: float) -> None: self._wait_response(node_id, reader), timeout_sec ) except asyncio.TimeoutError: - raise TimeoutResponse(request) + raise TimeoutResponse(request, node_id) @staticmethod async def _wait_response(node_id: NodeId, reader: WaitableCallback) -> None: @@ -52,6 +52,6 @@ async def _wait_response(node_id: NodeId, reader: WaitableCallback) -> None: and arbitration_id.parts.originating_node_id == node_id ): if response.payload.error_code.value != ErrorCode.ok: - raise ErrorResponse(response) + raise ErrorResponse(response, node_id) else: break diff --git a/hardware/opentrons_hardware/firmware_update/errors.py b/hardware/opentrons_hardware/firmware_update/errors.py index efc8d96c297..0200b5c784a 100644 --- a/hardware/opentrons_hardware/firmware_update/errors.py +++ b/hardware/opentrons_hardware/firmware_update/errors.py @@ -1,36 +1,59 @@ """Firmware update exceptions.""" +from typing import Optional, Sequence +from opentrons_shared_data.errors.exceptions import ( + FirmwareUpdateFailedError, + EnumeratedError, +) from opentrons_hardware.firmware_bindings.messages import MessageDefinition +from opentrons_hardware.firmware_bindings import FirmwareTarget -class FirmwareUpdateException(Exception): - """Base exception.""" - - pass - - -class ErrorResponse(FirmwareUpdateException): +class ErrorResponse(FirmwareUpdateFailedError): """Error response exception.""" - message: MessageDefinition - - def __init__(self, message: MessageDefinition) -> None: - """Constructor.""" - self.message = message - super().__init__(f"Got error response {message}.") + def __init__( + self, + error_message: MessageDefinition, + target: FirmwareTarget, + ) -> None: + """Build an ErrorResponse.""" + super().__init__( + message="Error response during firmware update", + detail={"node": target.application_for().name, "error": str(error_message)}, + ) -class TimeoutResponse(FirmwareUpdateException): +class TimeoutResponse(FirmwareUpdateFailedError): """No response exception.""" - message: MessageDefinition - - def __init__(self, message: MessageDefinition) -> None: - """Constructor.""" - self.message = message - super().__init__(f"Timed out waiting for response to {message}") - - -class BootloaderNotReady(FirmwareUpdateException): + def __init__( + self, + message: MessageDefinition, + target: FirmwareTarget, + ) -> None: + """Build a TimeoutResponse.""" + super().__init__( + message="Device response timeout during firmware update", + detail={ + "node": target.application_for().name, + "message": str(message), + }, + ) + + +class BootloaderNotReady(FirmwareUpdateFailedError): """Bootloader is not ready.""" - pass + def __init__( + self, + target: FirmwareTarget, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a BootloaderNotReady.""" + super().__init__( + message="Device did not enter bootloader for firmware update", + detail={ + "node": target.application_for().name, + }, + wrapping=wrapping, + ) diff --git a/hardware/opentrons_hardware/firmware_update/hex_file.py b/hardware/opentrons_hardware/firmware_update/hex_file.py index 3e4188b718c..32e97bb93cf 100644 --- a/hardware/opentrons_hardware/firmware_update/hex_file.py +++ b/hardware/opentrons_hardware/firmware_update/hex_file.py @@ -8,6 +8,8 @@ import struct import logging +from opentrons_shared_data.errors.exceptions import FirmwareUpdateFailedError + from typing_extensions import Final @@ -36,34 +38,68 @@ class HexRecord: checksum: int -class HexFileException(BaseException): - """Base of all hex file exceptions.""" - - pass - - -class MalformedLineException(HexFileException): +class MalformedLineException(FirmwareUpdateFailedError): """Line is malformed.""" - pass + def __init__(self, message: str, line: str, line_no: int, filename: str) -> None: + """Build a MalformedLineException.""" + super().__init__( + message=f"Could not parse firmware update file: {message}", + detail={"line": line, "line_number": str(line_no), "filename": filename}, + ) -class ChecksumException(HexFileException): +class ChecksumException(FirmwareUpdateFailedError): """Wrong checksum.""" - pass - - -class StartAddressException(HexFileException): + def __init__( + self, + line: str, + line_no: int, + filename: str, + calculated_checksum: int, + expected_checksum: int, + ) -> None: + """Build a ChecksumException.""" + super().__init__( + message="Bad line checksum in firmware update file", + detail={ + "line": line, + "line_number": str(line_no), + "filename": filename, + "calculated": str(calculated_checksum), + "expected": str(expected_checksum), + }, + ) + + +class StartAddressException(FirmwareUpdateFailedError): """Start address error.""" - pass - - -class BadChunkSizeException(HexFileException): + def __init__( + self, line: str, line_no: int, filename: str, bad_address: int + ) -> None: + """Build a StartAddressException.""" + super().__init__( + message="Bad start address in firmware update file", + detail={ + "line": line, + "line_number": str(line_no), + "filename": filename, + "address": str(bad_address), + }, + ) + + +class BadChunkSizeException(FirmwareUpdateFailedError): """Invalid chunk size.""" - pass + def __init__(self, filename: str, actual_size: int) -> None: + """Build a BadChunkSizeException.""" + super().__init__( + message="Bad chunk size: must be >0", + detail={"filename": filename, "actual": str(actual_size)}, + ) def from_hex_file_path(file_path: Path) -> Iterable[HexRecord]: @@ -74,18 +110,26 @@ def from_hex_file_path(file_path: Path) -> Iterable[HexRecord]: def from_hex_file(hex_file: TextIO) -> Iterable[HexRecord]: """A generator that processes a hex file contents.""" - for line in hex_file.readlines(): - yield process_line(line) + for idx, line in enumerate(hex_file.readlines()): + yield process_line(line, idx, hex_file.name) -def process_line(line: str) -> HexRecord: +def process_line( + line: str, + line_no_for_error: int, + filename_for_error: str, +) -> HexRecord: """Convert a line in a HEX file into a HexRecord.""" if len(line) < 11: # 11 = 1 (':') + 2 (byte count) + 4 (address) + 2 (record type) + 2 (checksum) - raise MalformedLineException(f"Line is missing fields '{line}'") + raise MalformedLineException( + "Line is missing fields", line, line_no_for_error, filename_for_error + ) if line[0] != ":": - raise MalformedLineException(f"Missing ':' in '{line}'") + raise MalformedLineException( + "Missing ':'", line, line_no_for_error, filename_for_error + ) # Skip the ':' and strip binary_line = binascii.unhexlify(line[1:].rstrip()) @@ -98,11 +142,18 @@ def process_line(line: str) -> HexRecord: try: record_type = RecordType(binary_line[3]) except ValueError: - raise MalformedLineException(f"'{binary_line[3]}' is not a valid record type.") + raise MalformedLineException( + f"Invalid record type '{binary_line[3]}'", + line, + line_no_for_error, + filename_for_error, + ) # 5 is 1 for byte count, 2 for address, 1 for record type, and 1 for checksum if len(binary_line) != (byte_count + 5): - raise MalformedLineException("Incorrect byte count") + raise MalformedLineException( + "Incorrect byte count", line, line_no_for_error, filename_for_error + ) byte_count_index: Final = 4 checksum_index: Final = byte_count_index + byte_count @@ -116,7 +167,9 @@ def process_line(line: str) -> HexRecord: computed_checksum = 0xFF & (~sum(binary_line[:checksum_index]) + 1) if computed_checksum != checksum: - raise ChecksumException(f"Expected {checksum} but computed {computed_checksum}") + raise ChecksumException( + line, line_no_for_error, filename_for_error, computed_checksum, checksum + ) return HexRecord( byte_count=byte_count, @@ -141,20 +194,21 @@ class HexRecordProcessor: Iterate through the process generator to get data chunks and start_address. """ - def __init__(self, records: Iterable[HexRecord]) -> None: + def __init__(self, records: Iterable[HexRecord], filename: str) -> None: """Constructor.""" self._records = records self._start_address: int = 0 + self._filename = filename @classmethod def from_file_path(cls, file_path: Path) -> HexRecordProcessor: """Construct from file.""" - return HexRecordProcessor(from_hex_file_path(file_path)) + return HexRecordProcessor(from_hex_file_path(file_path), str(file_path)) @classmethod def from_file(cls, hex_file: TextIO) -> HexRecordProcessor: """Construct from file.""" - return HexRecordProcessor(from_hex_file(hex_file)) + return HexRecordProcessor(from_hex_file(hex_file), hex_file.name) @property def start_address(self) -> int: @@ -175,7 +229,7 @@ def process(self, chunk_size: int) -> Generator[Chunk, None, None]: # noqa: C90 """ if chunk_size <= 0: - raise BadChunkSizeException("chunk size must be greater than 0.") + raise BadChunkSizeException(self._filename, chunk_size) # Address offset set by the StartLinearAddress record type address_offset = 0 diff --git a/hardware/opentrons_hardware/firmware_update/initiator.py b/hardware/opentrons_hardware/firmware_update/initiator.py index b8c3e0cf50f..e27d1016d19 100644 --- a/hardware/opentrons_hardware/firmware_update/initiator.py +++ b/hardware/opentrons_hardware/firmware_update/initiator.py @@ -63,7 +63,9 @@ async def run( if i < retry_count: i += 1 else: - raise BootloaderNotReady() + raise BootloaderNotReady( + target.bootloader_node.application_for() + ) logger.info("initiate: released exclusive") async def _wait_bootloader(self, reader: WaitableCallback, target: Target) -> None: diff --git a/hardware/opentrons_hardware/firmware_update/run.py b/hardware/opentrons_hardware/firmware_update/run.py index 69e6946fdbc..2e896d77023 100644 --- a/hardware/opentrons_hardware/firmware_update/run.py +++ b/hardware/opentrons_hardware/firmware_update/run.py @@ -3,7 +3,14 @@ import asyncio import os from typing import Optional, Dict, Tuple, AsyncIterator, Any -from .types import FirmwareUpdateStatus, StatusElement + + +from opentrons_shared_data.errors.exceptions import ( + InternalUSBCommunicationError, + FirmwareUpdateFailedError, + EnumeratedError, + PythonException, +) from opentrons_hardware.drivers.can_bus import CanMessenger from opentrons_hardware.drivers.binary_usb import BinaryMessenger @@ -26,6 +33,7 @@ ) from opentrons_hardware.firmware_update.errors import BootloaderNotReady from opentrons_hardware.firmware_update.target import Target +from .types import FirmwareUpdateStatus, StatusElement logger = logging.getLogger(__name__) DFU_PID = "df11" @@ -60,7 +68,15 @@ async def find_dfu_device(pid: str, expected_device_count: int) -> str: if stdout is None and stderr is None: continue if stderr: - raise RuntimeError(f"Error finding dfu device: {stderr.decode()}") + raise BootloaderNotReady( + USBTarget.rear_panel, + wrapping=[ + InternalUSBCommunicationError( + message="Error finding dfu device", + detail={"stderr": stderr.decode(), "target-pid": pid}, + ) + ], + ) result = stdout.decode() if pid not in result: @@ -76,11 +92,24 @@ async def find_dfu_device(pid: str, expected_device_count: int) -> str: # rear panel has 3 endpoints return serial elif devices_found > expected_device_count: - raise OSError("Multiple new bootloader devices" "found on mode switch") + raise BootloaderNotReady( + USBTarget.rear_panel, + wrapping=[ + InternalUSBCommunicationError( + message="Multiple new bootloader devices found on mode switch", + detail={"devices": result, "target-pid": pid}, + ) + ], + ) - raise RuntimeError( - "Could not update firmware via dfu. Possible issues- dfu-util" - " not working or specified dfu device not found" + raise BootloaderNotReady( + USBTarget.rear_panel, + wrapping=[ + InternalUSBCommunicationError( + message="Could not find dfu device to update firmware. dfu-util may be broken or the device may not be present.", + detail={"target-pid": pid}, + ) + ], ) @@ -281,7 +310,7 @@ async def _prep_can_update( (FirmwareUpdateStatus.updating, prep_progress), ) ) - except BootloaderNotReady as e: + except BaseException as e: logger.error(f"Firmware Update failed for {target} {e}.") await self._status_queue.put( ( @@ -289,7 +318,20 @@ async def _prep_can_update( (FirmwareUpdateStatus.updating, prep_progress), ) ) - raise + if isinstance(e, FirmwareUpdateFailedError): + raise + elif isinstance(e, EnumeratedError): + raise FirmwareUpdateFailedError( + message="Device did not enter bootloader", + detail={"node": target.bootloader_node.application_for().name}, + wrapping=[e], + ) + else: + raise FirmwareUpdateFailedError( + "Unhandled exception during firmware update", + detail={"node": target.bootloader_node.application_for().name}, + wrapping=[PythonException(e)], + ) else: logger.info("Skipping erase step.") return prep_progress @@ -307,19 +349,19 @@ async def _run_can_update( """Perform a firmware update on a node target.""" if not os.path.exists(filepath): logger.error(f"Subsystem update file not found {filepath}") - raise FileNotFoundError - - try: - download_start_progress = await self._prep_can_update( - messenger, - node_id, - retry_count, - timeout_seconds, - erase, - erase_timeout_seconds, + raise FirmwareUpdateFailedError( + message="Subsystem update file not found", + detail={"filepath": filepath, "target": node_id.application_for().name}, ) - except BootloaderNotReady: - return + + download_start_progress = await self._prep_can_update( + messenger, + node_id, + retry_count, + timeout_seconds, + erase, + erase_timeout_seconds, + ) target = Target.from_single_node(node_id) logger.info(f"Downloading {filepath} to {target.bootloader_node}.") diff --git a/hardware/opentrons_hardware/hardware_control/motion_planning/types.py b/hardware/opentrons_hardware/hardware_control/motion_planning/types.py index e1b8c777c21..f78eeb0b22e 100644 --- a/hardware/opentrons_hardware/hardware_control/motion_planning/types.py +++ b/hardware/opentrons_hardware/hardware_control/motion_planning/types.py @@ -19,6 +19,8 @@ TYPE_CHECKING, ) +from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError + if TYPE_CHECKING: from numpy.typing import NDArray @@ -104,7 +106,14 @@ def __init__( """Constructor.""" # verify unit vector before creating Move if not is_unit_vector(unit_vector): - raise ValueError(f"{unit_vector} is not a valid unit vector.") + raise MotionPlanningFailureError( + f"Invalid unit vector: {unit_vector}", + detail={ + "unit_vector": str(unit_vector), + "distance": str(distance), + "max_speed": str(max_speed), + }, + ) self.unit_vector = unit_vector self.distance = distance self.max_speed = max_speed @@ -241,7 +250,9 @@ def build( SystemConstraints = Dict[AxisKey, AxisConstraints] -class ZeroLengthMoveError(ValueError, Generic[AxisKey, CoordinateValue]): +class ZeroLengthMoveError( + MotionPlanningFailureError, Generic[AxisKey, CoordinateValue] +): """Error that handles trying to make a unit vector from a 0-length input. A unit vector would be undefined in this scenario, so this is the only safe way to @@ -258,7 +269,10 @@ def __init__( """Build the exception with the data that caused it.""" self._origin: Coordinates[AxisKey, CoordinateValue] = origin self._destination: Coordinates[AxisKey, CoordinateValue] = destination - super().__init__() + super(MotionPlanningFailureError, self).__init__( + message="Zero length move", + detail={"origin": str(origin), "destination": str(destination)}, + ) def __repr__(self) -> str: """Stringify.""" diff --git a/hardware/opentrons_hardware/hardware_control/motor_position_status.py b/hardware/opentrons_hardware/hardware_control/motor_position_status.py index 1dd747d1d93..faa51d236b7 100644 --- a/hardware/opentrons_hardware/hardware_control/motor_position_status.py +++ b/hardware/opentrons_hardware/hardware_control/motor_position_status.py @@ -2,6 +2,11 @@ import asyncio from typing import Set, Tuple import logging + +from opentrons_shared_data.errors.exceptions import ( + RoboticsControlError, + CommandTimedOutError, +) from opentrons_hardware.drivers.can_bus.can_messenger import ( CanMessenger, WaitableCallback, @@ -135,11 +140,22 @@ def _listener_filter(arbitration_id: ArbitrationId) -> bool: ) if not data[node][2]: # If the stepper_ok flag isn't set, that means the node didn't update position. - raise RuntimeError( - f"Failed to update motor position for node: {node}" + # This probably is because the motor is off. It's rare. + raise RoboticsControlError( + message="Failed to update motor position", + detail={ + "node": node.name, + }, ) except asyncio.TimeoutError: log.warning("Update motor position estimation timed out") - raise StopAsyncIteration + raise CommandTimedOutError( + "Update motor position estimation timed out", + detail={ + "missing-nodes": ", ".join( + node.name for node in set(nodes).difference(set(data)) + ) + }, + ) return data diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index ebc5a66fffb..a4417fe225b 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -7,6 +7,8 @@ from math import copysign from typing_extensions import Literal +from opentrons_shared_data.errors.exceptions import CanbusCommunicationError + from opentrons_hardware.firmware_bindings.constants import ( NodeId, SensorId, @@ -178,7 +180,14 @@ async def capacitive_probe( messenger, ) if not threshold: - raise RuntimeError("Could not set threshold for probe") + raise CanbusCommunicationError( + message="Could not set threshold for probe", + detail={ + "tool": tool.name, + "sensor": sensor_id.name, + "threshold": relative_threshold_pf, + }, + ) LOG.info(f"starting capacitive probe with threshold {threshold.to_float()}") pass_group = _build_pass_step([mover], {mover: distance}, {mover: speed}) runner = MoveGroupRunner(move_groups=[[pass_group]]) diff --git a/hardware/opentrons_hardware/instruments/gripper/serials.py b/hardware/opentrons_hardware/instruments/gripper/serials.py index de2f9c0581a..09f41d92dd6 100644 --- a/hardware/opentrons_hardware/instruments/gripper/serials.py +++ b/hardware/opentrons_hardware/instruments/gripper/serials.py @@ -3,6 +3,10 @@ from typing import Tuple import struct +from opentrons_shared_data.errors.exceptions import ( + InvalidInstrumentData, + PythonException, +) from opentrons_hardware.instruments.serial_utils import ensure_serial_length # Separate string into 2 groups @@ -37,8 +41,9 @@ def gripper_info_from_serial_string(serialval: str) -> Tuple[int, bytes]: """ matches = SERIAL_RE.match(serialval.strip()) if not matches: - raise ValueError( - f"The serial number {serialval.strip()} is not valid. {SERIAL_FORMAT_MSG}" + raise InvalidInstrumentData( + message=f"The serial number {serialval.strip()} is not valid. {SERIAL_FORMAT_MSG}", + detail={"serial": serialval}, ) model = int(matches.group("model")) @@ -60,4 +65,11 @@ def gripper_serial_val_from_parts(model: int, serialval: bytes) -> bytes: you will not get what you put in. """ - return struct.pack(">H16s", model, ensure_serial_length(serialval)) + try: + return struct.pack(">H16s", model, ensure_serial_length(serialval)) + except struct.error as e: + raise InvalidInstrumentData( + message="Invalid serial data", + detail={"model": model, "serial": serialval}, + wrapping=[PythonException(e)], + ) diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py index 01a23b13394..3d5d0b3c54e 100644 --- a/hardware/opentrons_hardware/instruments/pipettes/serials.py +++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py @@ -2,6 +2,10 @@ import re from typing import Dict, Tuple import struct +from opentrons_shared_data.errors.exceptions import ( + InvalidInstrumentData, + PythonException, +) from opentrons_hardware.firmware_bindings.constants import PipetteName from opentrons_hardware.instruments.serial_utils import ensure_serial_length @@ -47,14 +51,16 @@ def info_from_serial_string(serialval: str) -> Tuple[PipetteName, int, bytes]: """ matches = SERIAL_RE.match(serialval.strip()) if not matches: - raise ValueError( - f"The serial number {serialval.strip()} is not valid. {SERIAL_FORMAT_MSG}" + raise InvalidInstrumentData( + message=f"The serial number {serialval.strip()} is not valid. {SERIAL_FORMAT_MSG}", + detail={"serial": serialval}, ) try: name = NAME_LOOKUP[matches.group("name")] except KeyError: - raise ValueError( - f"The pipette name part of the serial number ({matches.group('name')}) is unknown. {SERIAL_FORMAT_MSG}" + raise InvalidInstrumentData( + message=f"The pipette name part of the serial number ({matches.group('name')}) is unknown. {SERIAL_FORMAT_MSG}", + detail={"serial": name, "name": matches.group("name")}, ) model = int(matches.group("model")) @@ -77,9 +83,16 @@ def serial_val_from_parts(name: PipetteName, model: int, serialval: bytes) -> by you will not get what you put in. """ - return struct.pack( - ">HH16s", - name.value, - model, - ensure_serial_length(serialval), - ) + try: + return struct.pack( + ">HH16s", + name.value, + model, + ensure_serial_length(serialval), + ) + except struct.error as e: + raise InvalidInstrumentData( + message="Invalid pipette serial", + detail={"name": name, "model": model, "serial": str(serialval)}, + wrapping=[PythonException(e)], + ) diff --git a/hardware/tests/opentrons_hardware/firmware_update/test_hex_file.py b/hardware/tests/opentrons_hardware/firmware_update/test_hex_file.py index 686edc6422c..e2429d9e439 100644 --- a/hardware/tests/opentrons_hardware/firmware_update/test_hex_file.py +++ b/hardware/tests/opentrons_hardware/firmware_update/test_hex_file.py @@ -63,7 +63,7 @@ ) def test_process_line(line: str, expected: hex_file.HexRecord) -> None: """It should process line successfully.""" - assert hex_file.process_line(line) == expected + assert hex_file.process_line(line, 2, "dummy-name") == expected @pytest.mark.parametrize( @@ -94,7 +94,7 @@ def test_process_line(line: str, expected: hex_file.HexRecord) -> None: def test_process_bad_line(line: str) -> None: """It should fail to process malformed line.""" with pytest.raises(hex_file.MalformedLineException): - hex_file.process_line(line) + hex_file.process_line(line, 2, "dummy-name") @pytest.mark.parametrize( @@ -114,7 +114,7 @@ def test_process_bad_line(line: str) -> None: def test_process_bad_checksum(line: str) -> None: """It should raise a checksum exception.""" with pytest.raises(hex_file.ChecksumException): - hex_file.process_line(line) + hex_file.process_line(line, 3, "dummy-name") @pytest.fixture(scope="session") @@ -246,7 +246,7 @@ def test_process( hex_records: Iterable[hex_file.HexRecord], size: int, expected: List[hex_file.Chunk] ) -> None: """It should read n sized chunks from a stream of HexRecord objects.""" - subject = hex_file.HexRecordProcessor(records=hex_records) + subject = hex_file.HexRecordProcessor(records=hex_records, filename="dummy-name") assert list(subject.process(size)) == expected assert subject.start_address == 0x8090A0B0 @@ -254,6 +254,6 @@ def test_process( def test_process_failure_zero_size(hex_records: Iterable[hex_file.HexRecord]) -> None: """It should fail if 0 is the requested size.""" - subject = hex_file.HexRecordProcessor(records=hex_records) + subject = hex_file.HexRecordProcessor(records=hex_records, filename="dummy-name") with pytest.raises(hex_file.BadChunkSizeException): list(subject.process(0)) From 6d455d76891c1232be328cedd068447cd657edd4 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 28 Jun 2023 16:11:55 -0400 Subject: [PATCH 12/20] add a move condition error --- shared-data/errors/definitions/1/errors.json | 4 ++++ .../opentrons_shared_data/errors/codes.py | 1 + .../opentrons_shared_data/errors/exceptions.py | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index 266a0d80217..8c749ae3383 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -78,6 +78,10 @@ "detail": "Position Estimation Invalid", "category": "roboticsControlError" }, + "2006": { + "detail": "Move Condition Not Met", + "category": "roboticsControlError" + }, "3000": { "detail": "A robotics interaction error occurred.", "category": "roboticsInteractionError" diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index 2f711f18821..6449b68424e 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -49,6 +49,7 @@ class ErrorCodes(Enum): STALL_OR_COLLISION_DETECTED = _code_from_dict_entry("2003") MOTION_PLANNING_FAILURE = _code_from_dict_entry("2004") POSITION_ESTIMATION_INVALID = _code_from_dict_entry('2005') + MOVE_CONDITION_NOT_MET = _code_from_dict_entry('2006') ROBOTICS_INTERACTION_ERROR = _code_from_dict_entry("3000") LABWARE_DROPPED = _code_from_dict_entry("3001") LABWARE_NOT_PICKED_UP = _code_from_dict_entry("3002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 79b78c13679..50dd00bf8ef 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -376,7 +376,7 @@ def __init__( class PositionEstimationInvalidError(RoboticsControlError): - """An error indicating that motion planning failed.""" + """An error indicating that a command failed because position estimation was invalid.""" def __init__( self, @@ -387,6 +387,22 @@ def __init__( """Build a PositionEstimationFailedError.""" super().__init__(ErrorCodes.POSITION_ESTIMATION_INVALID, message, detail, wrapping) + +class MoveConditionNotMetError(RoboticsControlError): + """An error indicating that a move completed without its condition being met.""" + + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a MoveConditionNotMetError.""" + super().__init__( + ErrorCodes.MOVE_CONDITION_NOT_MET, + message or 'Move completed without its complete condition being met', detail, wrapping) + + class LabwareDroppedError(RoboticsInteractionError): """An error indicating that the gripper dropped labware it was holding.""" From cf88046aba580502b84834fa21a62cf3b7109004 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 28 Jun 2023 16:12:06 -0400 Subject: [PATCH 13/20] add handling for error messages from hardware --- hardware/opentrons_hardware/errors.py | 150 ++++++++++++++++++ .../motion_planning/move_utils.py | 4 - .../hardware_control/move_group_runner.py | 76 ++++++--- .../test_move_group_runner.py | 7 +- 4 files changed, 203 insertions(+), 34 deletions(-) create mode 100644 hardware/opentrons_hardware/errors.py diff --git a/hardware/opentrons_hardware/errors.py b/hardware/opentrons_hardware/errors.py new file mode 100644 index 00000000000..fbb33ebfbbb --- /dev/null +++ b/hardware/opentrons_hardware/errors.py @@ -0,0 +1,150 @@ +"""Module to convert message errors to exceptions.""" +from typing import Dict, Optional, Tuple +import logging + +from opentrons_shared_data.errors.exceptions import ( + InternalMessageFormatError, + RoboticsControlError, + RoboticsInteractionError, + CommandTimedOutError, + EStopActivatedError, + StallOrCollisionDetectedError, + PipetteOverpressureError, + LabwareDroppedError, + PythonException, +) + +from opentrons_hardware.firmware_bindings.messages.message_definitions import ( + ErrorMessage, +) +from opentrons_hardware.firmware_bindings.messages import MessageDefinition +from opentrons_hardware.firmware_bindings.constants import ( + ErrorSeverity, + ErrorCode, + NodeId, +) +from opentrons_hardware.firmware_bindings.arbitration_id import ArbitrationId + + +log = logging.getLogger(__name__) + + +def nice_name_for_error(code: ErrorCode) -> str: + """Build a quick nice name for an error code instance.""" + return code.name.replace("_", " ") + + +def _safe_details_from_message( + message: ErrorMessage, arbitration_id: Optional[ArbitrationId] +) -> Tuple[Optional[NodeId], ErrorCode, ErrorSeverity]: + + detail_dict = { + "hardware-error": str(message.payload.error_code.value), + "hardware-severity": str(message.payload.severity.value), + } + if arbitration_id: + detail_dict["hardware-node"] = str( + arbitration_id.parts.originating_node_id.value + ) + try: + originator = NodeId(arbitration_id.parts.originating_node_id.value) + except BaseException as e: + raise InternalMessageFormatError( + message="Invalid or unknown message sender", + detail=detail_dict, + wrapping=[PythonException(e)], + ) + node: Optional[NodeId] = originator + else: + node = None + try: + error_code = ErrorCode(message.payload.error_code.value) + except BaseException as e: + raise InternalMessageFormatError( + message="Invalid or unknown error code", + detail=detail_dict, + wrapping=[PythonException(e)], + ) + try: + error_severity = ErrorSeverity(message.payload.severity.value) + except BaseException as e: + raise InternalMessageFormatError( + message="Invalid or unknown error severity", + detail=detail_dict, + wrapping=[PythonException(e)], + ) + return node, error_code, error_severity + + +def raise_from_error_message( # noqa: C901 + message: ErrorMessage, + arbitration_id: Optional[ArbitrationId] = None, + *, + detail: Optional[Dict[str, str]] = None, +) -> ErrorMessage: + """Raise a proper enumerated error based on an error message if required, or return.""" + detail_dict = detail or {} + maybe_node, error_code, error_severity = _safe_details_from_message( + message, arbitration_id + ) + if error_severity == ErrorSeverity.warning: + return message + if error_code == ErrorCode.ok: + log.warning(f"Error message with ok error code: {message}") + return message + + detail_dict["error-code"] = error_code.name + detail_dict["error-severity"] = error_severity.name + if maybe_node: + detail_dict["node"] = maybe_node.name + + if error_code in ( + ErrorCode.invalid_size, + ErrorCode.bad_checksum, + ErrorCode.invalid_input, + ): + raise InternalMessageFormatError( + message=f"Message format error: {nice_name_for_error(error_code)}", + detail=detail_dict, + ) + + if error_code in (ErrorCode.motor_busy,): + raise RoboticsInteractionError( + message="Motor busy when operation requested", detail=detail_dict + ) + + if error_code in (ErrorCode.timeout,): + raise CommandTimedOutError( + message="Command timeout from hardware", detail=detail_dict + ) + + if error_code in (ErrorCode.estop_detected,): + raise EStopActivatedError(detail=detail_dict) + + if error_code in (ErrorCode.collision_detected,): + raise StallOrCollisionDetectedError(detail=detail_dict) + + if error_code in (ErrorCode.over_pressure,): + raise PipetteOverpressureError(detail=detail_dict) + + if error_code in (ErrorCode.labware_dropped,): + raise LabwareDroppedError(detail=detail_dict) + + if error_code in (ErrorCode.stop_requested, ErrorCode.estop_released): + raise RoboticsControlError( + message="Unexpected robotics error", detail=detail_dict + ) + + raise RoboticsControlError(message="Hardware error", detail=detail_dict) + + +def message_or_raise( + message: MessageDefinition, + arbitration_id: Optional[ArbitrationId] = None, + *, + detail: Optional[Dict[str, str]] = None, +) -> MessageDefinition: + """Raise an error for an error message or return a non-error message.""" + if isinstance(message, ErrorMessage): + return raise_from_error_message(message, arbitration_id, detail=detail) + return message diff --git a/hardware/opentrons_hardware/hardware_control/motion_planning/move_utils.py b/hardware/opentrons_hardware/hardware_control/motion_planning/move_utils.py index fc78d45092a..318deea5221 100644 --- a/hardware/opentrons_hardware/hardware_control/motion_planning/move_utils.py +++ b/hardware/opentrons_hardware/hardware_control/motion_planning/move_utils.py @@ -27,10 +27,6 @@ MINIMUM_DISPLACEMENT = 0.05 -class MoveConditionNotMet(ValueError): - """Error raised if a move does not meet its stop condition before finishing.""" - - def apply_constraint(constraint: np.float64, input: np.float64) -> np.float64: """Keep the sign of the input but cap the numeric value at the constraint value.""" return cast(np.float64, np.copysign(np.minimum(abs(constraint), abs(input)), input)) diff --git a/hardware/opentrons_hardware/hardware_control/move_group_runner.py b/hardware/opentrons_hardware/hardware_control/move_group_runner.py index 8dc5b4304ca..ddf2385af4a 100644 --- a/hardware/opentrons_hardware/hardware_control/move_group_runner.py +++ b/hardware/opentrons_hardware/hardware_control/move_group_runner.py @@ -6,6 +6,14 @@ import numpy as np import time +from opentrons_shared_data.errors.exceptions import ( + GeneralError, + MoveConditionNotMetError, + EnumeratedError, + MotionFailedError, + PythonException +) + from opentrons_hardware.firmware_bindings import ArbitrationId from opentrons_hardware.firmware_bindings.constants import ( NodeId, @@ -44,6 +52,7 @@ tip_interrupts_per_sec, brushed_motor_interrupts_per_sec, ) +from opentrons_hardware.errors import raise_from_error_message from opentrons_hardware.hardware_control.motion import ( MoveGroups, MoveGroupSingleAxisStep, @@ -62,9 +71,7 @@ MoveStopConditionField, ) from opentrons_hardware.hardware_control.motion import MoveStopCondition -from opentrons_hardware.hardware_control.motion_planning.move_utils import ( - MoveConditionNotMet, -) + from .types import NodeDict log = logging.getLogger(__name__) @@ -130,12 +137,10 @@ async def execute( log.debug("No moves. Nothing to do.") return {} if not self._is_prepped: - raise RuntimeError("A group must be prepped before it can be executed.") - try: - move_completion_data = await self._move(can_messenger, self._start_at_index) - except (RuntimeError, asyncio.TimeoutError): - log.error("raising error from Move group runner") - raise + raise GeneralError( + message="A move group must be prepped before it can be executed." + ) + move_completion_data = await self._move(can_messenger, self._start_at_index) return self._accumulate_move_completions(move_completion_data) async def run( @@ -361,7 +366,7 @@ def __init__(self, move_groups: MoveGroups, start_at_index: int = 0) -> None: log.debug(f"Move scheduler running for groups {move_groups}") self._completion_queue: asyncio.Queue[_CompletionPacket] = asyncio.Queue() self._event = asyncio.Event() - self._error: Optional[ErrorMessage] = None + self._errors: List[EnumeratedError] = [] self._current_group: Optional[int] = None self._should_stop = False @@ -395,7 +400,10 @@ def _remove_move_group( def _handle_error( self, message: ErrorMessage, arbitration_id: ArbitrationId ) -> None: - self._error = message + try: + message = raise_from_error_message(message, arbitration_id) + except EnumeratedError as e: + self._errors.append(e) severity = message.payload.severity.value node_name = NodeId(arbitration_id.parts.node_id).name log.error(f"Error during move group from {node_name} : {message}") @@ -425,6 +433,11 @@ def _handle_move_completed( log.error( f"Homing move from node {node_id} completed without meeting condition {stop_cond}" ) + self._errors.append( + MoveConditionNotMetError( + detail={"node": node_id.name, "stop-condition": stop_cond.name} + ) + ) self._should_stop = True self._event.set() if ( @@ -477,15 +490,20 @@ async def _send_stop_if_necessary( ) if err != ErrorCode.stop_requested: log.warning("Stop request failed") - if self._error: - raise RuntimeError( - f"Unrecoverable firmware error during move group {group_id}: {self._error}" - ) + if self._errors: + if len(self._errors) > 1: + raise MotionFailedError( + "Motion failed with multiple errors", wrapping=self._errors + ) + else: + raise self._errors[0] else: # This happens when the move completed without stop condition - raise MoveConditionNotMet - elif self._error is not None: - log.warning(f"Recoverable firmware error during {group_id}: {self._error}") + raise MoveConditionNotMetError(detail={"group-id": str(group_id)}) + elif self._errors: + log.warning( + f"Recoverable firmware errors during {group_id}: {self._errors}" + ) async def run(self, can_messenger: CanMessenger) -> _Completions: """Start each move group after the prior has completed.""" @@ -538,16 +556,22 @@ async def run(self, can_messenger: CanMessenger) -> _Completions: f"Move set {str(group_id)} took longer ({duration} seconds) than expected ({expected_time} seconds)." ) except asyncio.TimeoutError: - log.warning( - f"Move set {str(group_id)} timed out of max duration {full_timeout}. Expected time: {expected_time}" - ) - log.warning( - f"Expected nodes in group {str(group_id)}: {str(self._get_nodes_in_move_group(group_id))}" + missing_node_msg = ', '.join(node.name for node in self._get_nodes_in_move_group(group_id)) + log.error( + f"Move set {str(group_id)} timed out of max duration {full_timeout}. Expected time: {expected_time}. Missing: {missing_node_Msg}" ) + + raise MotionFailedError(message='Command timed out', detail={ + 'missing-nodes': missing_node_msg, + 'full-timeout': str(full_timeout), + 'expected-time': expected_time, + 'elapsed': str(time.time() - start_time)}) + except EnumeratedError: + log.exception('Cancelling move group scheduler') raise - except (RuntimeError, MoveConditionNotMet) as e: - log.error("canceling move group scheduler") - raise e + except BaseException as e: + log.exception("canceling move group scheduler") + raise PythonException(e) from e def _reify_queue_iter() -> Iterator[_CompletionPacket]: while not self._completion_queue.empty(): diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py b/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py index 98c6522bdbb..d169eac9651 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py @@ -4,6 +4,7 @@ from numpy import float64, float32, int32 from mock import AsyncMock, call, MagicMock, patch import asyncio +from opentrons_shared_data.errors.exceptions import MoveConditionNotMetError from opentrons_hardware.firmware_bindings import ArbitrationId, ArbitrationIdParts from opentrons_hardware.firmware_bindings.constants import ( @@ -54,9 +55,7 @@ MoveScheduler, _CompletionPacket, ) -from opentrons_hardware.hardware_control.motion_planning.move_utils import ( - MoveConditionNotMet, -) + from opentrons_hardware.hardware_control.types import NodeMap from opentrons_hardware.firmware_bindings.messages import ( message_definitions as md, @@ -745,7 +744,7 @@ async def test_home_timeout( mock_sender = MockSendMoveCompleter(move_group_home_single, subject, ack_id=3) mock_can_messenger.ensure_send.side_effect = mock_sender.mock_ensure_send mock_can_messenger.send.side_effect = mock_sender.mock_send - with pytest.raises(MoveConditionNotMet): + with pytest.raises(MoveConditionNotMetError): await subject.run(can_messenger=mock_can_messenger) From 51c460b35e59176941ea7822e7e5c4ecd0228457 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 28 Jun 2023 16:19:29 -0400 Subject: [PATCH 14/20] remove asynchardwareerror This doesn't, like, do anything. It just blew up the reader task and nothing else, and we don't even do that anymore. It didn't go anywhere. Remove it. --- .../drivers/can_bus/can_messenger.py | 34 ++++++------------- .../drivers/can_bus/errors.py | 15 -------- hardware/opentrons_hardware/drivers/errors.py | 9 ----- hardware/opentrons_hardware/errors.py | 5 +-- .../hardware_control/network.py | 5 ++- 5 files changed, 16 insertions(+), 52 deletions(-) delete mode 100644 hardware/opentrons_hardware/drivers/can_bus/errors.py delete mode 100644 hardware/opentrons_hardware/drivers/errors.py diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index ac724367bf6..a5da8165f9b 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -53,7 +53,7 @@ from opentrons_hardware.firmware_bindings.utils import BinarySerializableException -from .errors import AsyncHardwareError, CanError +from opentrons_hardware.errors import raise_from_error_message log = logging.getLogger(__name__) @@ -364,8 +364,6 @@ async def _read_task_shield(self) -> None: pass except EnumeratedError: raise - except AsyncHardwareError: - raise except Exception as exc: log.exception("Exception in read") raise CanbusCommunicationError( @@ -385,37 +383,27 @@ async def _read_task(self) -> None: f"Received <--\n\tarbitration_id: {message.arbitration_id},\n\t" f"payload: {build}" ) + handled = False for listener, filter in self._listeners.values(): if filter and not filter(message.arbitration_id): log.debug("message ignored by filter") continue listener(message_definition(payload=build), message.arbitration_id) # type: ignore[arg-type] - if ( - message.arbitration_id.parts.message_id - == MessageId.error_message - ): - await self._handle_error(build) + handled = True + if not handled: + if ( + message.arbitration_id.parts.message_id + == MessageId.error_message + ): + log.error(f"Asynchronous error message ignored: {message}") + else: + log.info(f"Message ignored: {message}") except BinarySerializableException: log.exception(f"Failed to build from {message}") else: log.error(f"Message {message} is not recognized.") raise StopAsyncIteration - async def _handle_error(self, build: BinarySerializable) -> None: - err_msg = ErrorMessage(payload=build) # type: ignore[arg-type] - error_payload: ErrorMessagePayload = err_msg.payload - err_msg.log_error(log) - - if error_payload.message_index == 0: - log.error( - f"error {str(err_msg)} recieved is asyncronous, raising exception" - ) - raise AsyncHardwareError( - "Async firmware error: " + str(err_msg), - ErrorCode(error_payload.error_code.value), - ErrorSeverity(error_payload.severity.value), - ) - @property def exclusive_writer(self) -> asyncio.Lock: """A caller may acquire this context manager to temporarily gain exclusive control of the bus. diff --git a/hardware/opentrons_hardware/drivers/can_bus/errors.py b/hardware/opentrons_hardware/drivers/can_bus/errors.py deleted file mode 100644 index eb8cbf68d44..00000000000 --- a/hardware/opentrons_hardware/drivers/can_bus/errors.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Can bus errors.""" - -from opentrons_hardware.firmware_bindings.constants import ErrorCode, ErrorSeverity - - -class AsyncHardwareError(RuntimeError): - """An error generated from firmware that was not caused by a command sent from hardware controller.""" - - def __init__( - self, description: str, error_code: ErrorCode, error_severity: ErrorSeverity - ) -> None: - """Build an async error.""" - self.description = description - self.error_code = error_code - self.error_severity = error_severity diff --git a/hardware/opentrons_hardware/drivers/errors.py b/hardware/opentrons_hardware/drivers/errors.py deleted file mode 100644 index 4f0cd2e4468..00000000000 --- a/hardware/opentrons_hardware/drivers/errors.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Driver errors for collecting all the pokemon.""" - - -class CommunicationError(RuntimeError): - """There was an error in communications with a device.""" - - def __init__(self, exc: BaseException) -> None: - """Build an exception for easier catching that wraps another.""" - self.wrapped_exc = exc diff --git a/hardware/opentrons_hardware/errors.py b/hardware/opentrons_hardware/errors.py index fbb33ebfbbb..32e2d4a13eb 100644 --- a/hardware/opentrons_hardware/errors.py +++ b/hardware/opentrons_hardware/errors.py @@ -81,15 +81,16 @@ def raise_from_error_message( # noqa: C901 arbitration_id: Optional[ArbitrationId] = None, *, detail: Optional[Dict[str, str]] = None, + ignore_severity: bool = False, ) -> ErrorMessage: """Raise a proper enumerated error based on an error message if required, or return.""" detail_dict = detail or {} maybe_node, error_code, error_severity = _safe_details_from_message( message, arbitration_id ) - if error_severity == ErrorSeverity.warning: + if error_severity == ErrorSeverity.warning and not ignore_severity: return message - if error_code == ErrorCode.ok: + if error_code == ErrorCode.ok and not ignore_severity: log.warning(f"Error message with ok error code: {message}") return message diff --git a/hardware/opentrons_hardware/hardware_control/network.py b/hardware/opentrons_hardware/hardware_control/network.py index 2572fe91505..a58ac85ca03 100644 --- a/hardware/opentrons_hardware/hardware_control/network.py +++ b/hardware/opentrons_hardware/hardware_control/network.py @@ -14,7 +14,6 @@ from opentrons_hardware.drivers.can_bus.can_messenger import ( CanMessenger, ) -from opentrons_hardware.drivers.errors import CommunicationError from opentrons_hardware.drivers.binary_usb import BinaryMessenger from opentrons_hardware.firmware_bindings.messages.message_definitions import ( DeviceInfoRequest as CanDeviceInfoRequest, @@ -239,7 +238,7 @@ def listener(message: BinaryMessageDefinition) -> None: self._log_failure( devices, set(iter(targets.keys())), "Timeout during probe_specific" ) - except CommunicationError: + except BaseException: self._log_failure( devices, set(iter(targets.keys())), @@ -293,7 +292,7 @@ def listener(message: BinaryMessageDefinition) -> None: self._log_failure( expected_targets, set(iter(targets.keys())), "Timeout during probe" ) - except CommunicationError: + except BaseException: self._log_failure( expected_targets, set(iter(targets.keys())), From 127ac557542fe51528535a6723cd97c6dd5737bf Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 28 Jun 2023 16:53:49 -0400 Subject: [PATCH 15/20] fix tests --- .../drivers/can_bus/can_messenger.py | 12 ------------ hardware/opentrons_hardware/errors.py | 6 ++---- .../hardware_control/move_group_runner.py | 5 ++++- .../instruments/pipettes/serials.py | 2 +- .../drivers/can_bus/test_settings.py | 3 ++- .../hardware_control/test_move_group_runner.py | 9 ++++++--- .../opentrons_hardware/instruments/test_serials.py | 7 ++++--- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index a5da8165f9b..b8e8b22a6c5 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -27,34 +27,22 @@ ArbitrationIdParts, ) from opentrons_hardware.firmware_bindings.message import CanMessage -from opentrons_hardware.firmware_bindings.utils.binary_serializable import ( - BinarySerializable, -) - from opentrons_hardware.firmware_bindings.constants import ( NodeId, MessageId, FunctionCode, - ErrorSeverity, ErrorCode, ) - from opentrons_hardware.firmware_bindings.messages.message_definitions import ( Acknowledgement, ErrorMessage, ) - from opentrons_hardware.firmware_bindings.messages.messages import ( MessageDefinition, get_definition, ) - -from opentrons_hardware.firmware_bindings.messages.payloads import ErrorMessagePayload - from opentrons_hardware.firmware_bindings.utils import BinarySerializableException -from opentrons_hardware.errors import raise_from_error_message - log = logging.getLogger(__name__) diff --git a/hardware/opentrons_hardware/errors.py b/hardware/opentrons_hardware/errors.py index 32e2d4a13eb..3bc5310f4b7 100644 --- a/hardware/opentrons_hardware/errors.py +++ b/hardware/opentrons_hardware/errors.py @@ -43,11 +43,9 @@ def _safe_details_from_message( "hardware-severity": str(message.payload.severity.value), } if arbitration_id: - detail_dict["hardware-node"] = str( - arbitration_id.parts.originating_node_id.value - ) + detail_dict["hardware-node"] = str(arbitration_id.parts.originating_node_id) try: - originator = NodeId(arbitration_id.parts.originating_node_id.value) + originator = NodeId(arbitration_id.parts.originating_node_id) except BaseException as e: raise InternalMessageFormatError( message="Invalid or unknown message sender", diff --git a/hardware/opentrons_hardware/hardware_control/move_group_runner.py b/hardware/opentrons_hardware/hardware_control/move_group_runner.py index ddf2385af4a..8e123257058 100644 --- a/hardware/opentrons_hardware/hardware_control/move_group_runner.py +++ b/hardware/opentrons_hardware/hardware_control/move_group_runner.py @@ -435,7 +435,10 @@ def _handle_move_completed( ) self._errors.append( MoveConditionNotMetError( - detail={"node": node_id.name, "stop-condition": stop_cond.name} + detail={ + "node": NodeId(node_id).name, + "stop-condition": stop_cond.name, + } ) ) self._should_stop = True diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py index 3d5d0b3c54e..b7b43cd3b59 100644 --- a/hardware/opentrons_hardware/instruments/pipettes/serials.py +++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py @@ -60,7 +60,7 @@ def info_from_serial_string(serialval: str) -> Tuple[PipetteName, int, bytes]: except KeyError: raise InvalidInstrumentData( message=f"The pipette name part of the serial number ({matches.group('name')}) is unknown. {SERIAL_FORMAT_MSG}", - detail={"serial": name, "name": matches.group("name")}, + detail={"name": matches.group("name")}, ) model = int(matches.group("model")) diff --git a/hardware/tests/opentrons_hardware/drivers/can_bus/test_settings.py b/hardware/tests/opentrons_hardware/drivers/can_bus/test_settings.py index bda7f20b530..1833509a4d6 100644 --- a/hardware/tests/opentrons_hardware/drivers/can_bus/test_settings.py +++ b/hardware/tests/opentrons_hardware/drivers/can_bus/test_settings.py @@ -2,6 +2,7 @@ import pytest from typing import Optional +from opentrons_shared_data.errors.exceptions import CANBusConfigurationError from opentrons_hardware.drivers.can_bus import settings @@ -94,7 +95,7 @@ def test_invalid_calculate_bit_timings( match_str: str, ) -> None: """Test invalid bit timing calculations.""" - with pytest.raises(ValueError, match=match_str): + with pytest.raises(CANBusConfigurationError, match=match_str): settings.calculate_fdcan_parameters( fcan_clock, bitrate, sample_rate, jump_width ) diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py b/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py index d169eac9651..10762e1cf21 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py @@ -4,7 +4,10 @@ from numpy import float64, float32, int32 from mock import AsyncMock, call, MagicMock, patch import asyncio -from opentrons_shared_data.errors.exceptions import MoveConditionNotMetError +from opentrons_shared_data.errors.exceptions import ( + MoveConditionNotMetError, + EnumeratedError, +) from opentrons_hardware.firmware_bindings import ArbitrationId, ArbitrationIdParts from opentrons_hardware.firmware_bindings.constants import ( @@ -1213,7 +1216,7 @@ async def test_single_move_error( mock_sender = MockSendMoveErrorCompleter(move_group_single, subject) mock_can_messenger.ensure_send.side_effect = mock_sender.mock_ensure_send mock_can_messenger.send.side_effect = mock_sender.mock_send - with pytest.raises(RuntimeError): + with pytest.raises(EnumeratedError): await subject.run(can_messenger=mock_can_messenger) assert mock_sender.call_count == 1 @@ -1252,7 +1255,7 @@ async def test_multiple_move_error( mock_sender = MockSendMoveErrorCompleter(move_group_multiple_axes, subject) mock_can_messenger.ensure_send.side_effect = mock_sender.mock_ensure_send mock_can_messenger.send.side_effect = mock_sender.mock_send - with pytest.raises(RuntimeError): + with pytest.raises(EnumeratedError): await subject.run(can_messenger=mock_can_messenger) assert mock_sender.call_count == 2 diff --git a/hardware/tests/opentrons_hardware/instruments/test_serials.py b/hardware/tests/opentrons_hardware/instruments/test_serials.py index 75d57eafb42..7b398eda286 100644 --- a/hardware/tests/opentrons_hardware/instruments/test_serials.py +++ b/hardware/tests/opentrons_hardware/instruments/test_serials.py @@ -1,5 +1,6 @@ """Test serial setting.""" import pytest +from opentrons_shared_data.errors.exceptions import InvalidInstrumentData from opentrons_hardware.instruments.serial_utils import model_versionstring_from_int from opentrons_hardware.instruments.pipettes import serials as pip_serials from opentrons_hardware.instruments.gripper import serials as grip_serials @@ -56,7 +57,7 @@ def test_scan_valid_pipette_serials( @pytest.mark.parametrize("scannedval", ["P111V02", "P1ksV22", "P3HSV12"]) def test_pipette_name_validity(scannedval: str) -> None: """Pipette name lookup matching.""" - with pytest.raises(ValueError, match="The pipette name part.*"): + with pytest.raises(InvalidInstrumentData, match="The pipette name part.*"): pip_serials.info_from_serial_string(scannedval) @@ -75,7 +76,7 @@ def test_pipette_name_validity(scannedval: str) -> None: ) def test_pipette_serial_validity(scannedval: str) -> None: """Various regex failures.""" - with pytest.raises(ValueError, match="The serial number.*"): + with pytest.raises(InvalidInstrumentData, match="The serial number.*"): pip_serials.info_from_serial_string(scannedval) @@ -133,7 +134,7 @@ def test_scan_valid_gripper_serials( ) def test_gripper_serial_validity(scannedval: str) -> None: """Various regex failures for gripper.""" - with pytest.raises(ValueError, match="The serial number.*"): + with pytest.raises(InvalidInstrumentData, match="The serial number.*"): grip_serials.gripper_info_from_serial_string(scannedval) From 32b5dc8bf084456eafeac5ffc1805f8ee3af9c05 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 28 Jun 2023 16:54:34 -0400 Subject: [PATCH 16/20] shared-data format --- .../opentrons_shared_data/errors/codes.py | 12 +++++----- .../errors/exceptions.py | 22 +++++++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index 6449b68424e..d8327508029 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -40,16 +40,16 @@ class ErrorCodes(Enum): MODULE_COMMUNICATION_ERROR = _code_from_dict_entry("1003") COMMAND_TIMED_OUT = _code_from_dict_entry("1004") FIRMWARE_UPDATE_FAILED = _code_from_dict_entry("1005") - INTERNAL_MESSAGE_FORMAT_ERROR = _code_from_dict_entry('1006') - CANBUS_CONFIGURATION_ERROR = _code_from_dict_entry('1007') - CANBUS_BUS_ERROR = _code_from_dict_entry('1008') + INTERNAL_MESSAGE_FORMAT_ERROR = _code_from_dict_entry("1006") + CANBUS_CONFIGURATION_ERROR = _code_from_dict_entry("1007") + CANBUS_BUS_ERROR = _code_from_dict_entry("1008") ROBOTICS_CONTROL_ERROR = _code_from_dict_entry("2000") MOTION_FAILED = _code_from_dict_entry("2001") HOMING_FAILED = _code_from_dict_entry("2002") STALL_OR_COLLISION_DETECTED = _code_from_dict_entry("2003") MOTION_PLANNING_FAILURE = _code_from_dict_entry("2004") - POSITION_ESTIMATION_INVALID = _code_from_dict_entry('2005') - MOVE_CONDITION_NOT_MET = _code_from_dict_entry('2006') + POSITION_ESTIMATION_INVALID = _code_from_dict_entry("2005") + MOVE_CONDITION_NOT_MET = _code_from_dict_entry("2006") ROBOTICS_INTERACTION_ERROR = _code_from_dict_entry("3000") LABWARE_DROPPED = _code_from_dict_entry("3001") LABWARE_NOT_PICKED_UP = _code_from_dict_entry("3002") @@ -65,7 +65,7 @@ class ErrorCodes(Enum): FIRMWARE_UPDATE_REQUIRED = _code_from_dict_entry("3013") INVALID_ACTUATOR = _code_from_dict_entry("3014") MODULE_NOT_PRESENT = _code_from_dict_entry("3015") - INVALID_INSTRUMENT_DATA = _code_from_dict_entry('3016') + INVALID_INSTRUMENT_DATA = _code_from_dict_entry("3016") GENERAL_ERROR = _code_from_dict_entry("4000") ROBOT_IN_USE = _code_from_dict_entry("4001") API_REMOVED = _code_from_dict_entry("4002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 50dd00bf8ef..f74853e3fd6 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -296,10 +296,14 @@ def __init__( wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InternalMesasgeFormatError.""" - super().__init__(ErrorCodes.INTERNAL_MESSAGE_FORMAT_ERROR, message, detail, wrapping) + super().__init__( + ErrorCodes.INTERNAL_MESSAGE_FORMAT_ERROR, message, detail, wrapping + ) + class CANBusConfigurationError(CommunicationError): """An error indicating a misconfiguration of the CANbus.""" + def __init__( self, message: Optional[str] = None, @@ -307,10 +311,14 @@ def __init__( wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CANBus Configuration Error.""" - super().__init__(ErrorCodes.CANBUS_CONFIGURATION_ERROR, message, detail, wrapping) + super().__init__( + ErrorCodes.CANBUS_CONFIGURATION_ERROR, message, detail, wrapping + ) + class CANBusBusError(CommunicationError): """An error indicating a low-level bus error on the CANbus like an error frame.""" + def __init__( self, message: Optional[str] = None, @@ -385,7 +393,9 @@ def __init__( wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a PositionEstimationFailedError.""" - super().__init__(ErrorCodes.POSITION_ESTIMATION_INVALID, message, detail, wrapping) + super().__init__( + ErrorCodes.POSITION_ESTIMATION_INVALID, message, detail, wrapping + ) class MoveConditionNotMetError(RoboticsControlError): @@ -400,7 +410,10 @@ def __init__( """Build a MoveConditionNotMetError.""" super().__init__( ErrorCodes.MOVE_CONDITION_NOT_MET, - message or 'Move completed without its complete condition being met', detail, wrapping) + message or "Move completed without its complete condition being met", + detail, + wrapping, + ) class LabwareDroppedError(RoboticsInteractionError): @@ -577,6 +590,7 @@ def __init__( ErrorCodes.MODULE_NOT_PRESENT, checked_message, checked_detail, wrapping ) + class InvalidInstrumentData(RoboticsInteractionError): """An error indicating that instrument data is invalid.""" From c6b828ddafd53d136d6a199c73444d55111a8f6b Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 29 Jun 2023 09:20:17 -0400 Subject: [PATCH 17/20] fix up api --- api/src/opentrons/hardware_control/ot3api.py | 6 ++---- api/tests/opentrons/hardware_control/test_moves.py | 9 ++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index fb40e0b0fbf..9d2d45184c3 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -142,9 +142,7 @@ InstrumentDict, GripperDict, ) -from opentrons_hardware.hardware_control.motion_planning.move_utils import ( - MoveConditionNotMet, -) + from .status_bar_state import StatusBarStateController @@ -1227,7 +1225,7 @@ async def _home(self, axes: Sequence[OT3Axis]) -> None: except ZeroLengthMoveError: self._log.info(f"{axis} already at home position, skip homing") continue - except (MoveConditionNotMet, Exception) as e: + except BaseException as e: self._log.exception(f"Homing failed: {e}") self._current_position.clear() raise diff --git a/api/tests/opentrons/hardware_control/test_moves.py b/api/tests/opentrons/hardware_control/test_moves.py index 3d2888cd89c..cd4ce3be829 100644 --- a/api/tests/opentrons/hardware_control/test_moves.py +++ b/api/tests/opentrons/hardware_control/test_moves.py @@ -27,6 +27,8 @@ ) from opentrons.hardware_control.types import OT3Axis +from opentrons_shared_data.errors.exceptions import MoveConditionNotMetError + async def test_controller_must_home(hardware_api): abs_position = types.Point(30, 20, 10) @@ -100,12 +102,9 @@ async def test_home(ot3_hardware, mock_home): async def test_home_unmet(ot3_hardware, mock_home): - from opentrons_hardware.hardware_control.motion_planning.move_utils import ( - MoveConditionNotMet, - ) - mock_home.side_effect = MoveConditionNotMet() - with pytest.raises(MoveConditionNotMet): + mock_home.side_effect = MoveConditionNotMetError() + with pytest.raises(MoveConditionNotMetError): await ot3_hardware.home([OT3Axis.X]) assert ot3_hardware.gantry_load == GantryLoad.LOW_THROUGHPUT mock_home.assert_called_once_with([OT3Axis.X], GantryLoad.LOW_THROUGHPUT) From c82248aa3caf7d59f2e4179e995a641716ceee3d Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 6 Jul 2023 14:30:53 -0400 Subject: [PATCH 18/20] fix accidental change to can messenger --- .../drivers/can_bus/can_messenger.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index b8e8b22a6c5..3aa325c46ee 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -346,17 +346,14 @@ def remove_listener(self, listener: MessageListenerCallback) -> None: self._listeners.pop(listener) async def _read_task_shield(self) -> None: - try: - await self._read_task() - except asyncio.CancelledError: - pass - except EnumeratedError: - raise - except Exception as exc: - log.exception("Exception in read") - raise CanbusCommunicationError( - message="Exception in read", wrapping=[PythonException(exc)] - ) + while True: + try: + await self._read_task() + except (asyncio.CancelledError, StopAsyncIteration): + return + except BaseException as exc: + log.exception("Exception in read") + continue async def _read_task(self) -> None: """Read task.""" From 06502ed27a707381d08a7b7deb9f0fd0266b847c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 6 Jul 2023 14:55:20 -0400 Subject: [PATCH 19/20] lint --- hardware/opentrons_hardware/drivers/can_bus/can_messenger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index 3aa325c46ee..8803859d741 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -350,8 +350,8 @@ async def _read_task_shield(self) -> None: try: await self._read_task() except (asyncio.CancelledError, StopAsyncIteration): - return - except BaseException as exc: + return + except BaseException: log.exception("Exception in read") continue From 38741fb828cb9962a6f8240ef3c9afb0a0b5f954 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 10 Jul 2023 14:24:42 -0400 Subject: [PATCH 20/20] rebase fixups --- .../hardware_control/move_group_runner.py | 123 +++++++++--------- .../test_move_group_runner.py | 4 +- 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/hardware/opentrons_hardware/hardware_control/move_group_runner.py b/hardware/opentrons_hardware/hardware_control/move_group_runner.py index 8e123257058..0b182d1e8c8 100644 --- a/hardware/opentrons_hardware/hardware_control/move_group_runner.py +++ b/hardware/opentrons_hardware/hardware_control/move_group_runner.py @@ -11,7 +11,7 @@ MoveConditionNotMetError, EnumeratedError, MotionFailedError, - PythonException + PythonException, ) from opentrons_hardware.firmware_bindings import ArbitrationId @@ -508,73 +508,74 @@ async def _send_stop_if_necessary( f"Recoverable firmware errors during {group_id}: {self._errors}" ) - async def run(self, can_messenger: CanMessenger) -> _Completions: - """Start each move group after the prior has completed.""" - for group_id in range( - self._start_at_index, self._start_at_index + len(self._moves) - ): - self._event.clear() + async def _run_one_group(self, group_id: int, can_messenger: CanMessenger) -> None: + self._event.clear() - log.debug(f"Executing move group {group_id}.") - self._current_group = group_id - self._start_at_index - error = await can_messenger.ensure_send( - node_id=NodeId.broadcast, - message=ExecuteMoveGroupRequest( - payload=ExecuteMoveGroupRequestPayload( - group_id=UInt8Field(group_id), - # TODO (al, 2021-11-8): The triggers should be populated - # with actual values. - start_trigger=UInt8Field(0), - cancel_trigger=UInt8Field(0), - ) - ), - expected_nodes=self._get_nodes_in_move_group(group_id), + log.debug(f"Executing move group {group_id}.") + self._current_group = group_id - self._start_at_index + error = await can_messenger.ensure_send( + node_id=NodeId.broadcast, + message=ExecuteMoveGroupRequest( + payload=ExecuteMoveGroupRequestPayload( + group_id=UInt8Field(group_id), + # TODO (al, 2021-11-8): The triggers should be populated + # with actual values. + start_trigger=UInt8Field(0), + cancel_trigger=UInt8Field(0), + ) + ), + expected_nodes=self._get_nodes_in_move_group(group_id), + ) + if error != ErrorCode.ok: + log.error(f"received error trying to execute move group: {str(error)}") + + expected_time = max(1.0, self._durations[group_id - self._start_at_index] * 1.1) + full_timeout = max(1.0, self._durations[group_id - self._start_at_index] * 2) + start_time = time.time() + + try: + # The staged timeout handles some times when a move takes a liiiittle extra + await asyncio.wait_for( + self._event.wait(), + full_timeout, ) - if error != ErrorCode.ok: - log.error(f"recieved error trying to execute move group {str(error)}") + duration = time.time() - start_time + await self._send_stop_if_necessary(can_messenger, group_id) - expected_time = max( - 1.0, self._durations[group_id - self._start_at_index] * 1.1 + if duration >= expected_time: + log.warning( + f"Move set {str(group_id)} took longer ({duration} seconds) than expected ({expected_time} seconds)." + ) + except asyncio.TimeoutError: + missing_node_msg = ", ".join( + node.name for node in self._get_nodes_in_move_group(group_id) ) - full_timeout = max( - 1.0, self._durations[group_id - self._start_at_index] * 2 + log.error( + f"Move set {str(group_id)} timed out of max duration {full_timeout}. Expected time: {expected_time}. Missing: {missing_node_msg}" ) - start_time = time.time() - - try: - # TODO: The max here can be removed once can_driver.send() no longer - # returns before the message actually hits the bus. Right now it - # returns when the message is enqueued in the kernel, meaning that - # for short move durations we can see the timeout expiring before - # the execute even gets sent. - await asyncio.wait_for( - self._event.wait(), - full_timeout, - ) - duration = time.time() - start_time - await self._send_stop_if_necessary(can_messenger, group_id) - if duration >= expected_time: - log.warning( - f"Move set {str(group_id)} took longer ({duration} seconds) than expected ({expected_time} seconds)." - ) - except asyncio.TimeoutError: - missing_node_msg = ', '.join(node.name for node in self._get_nodes_in_move_group(group_id)) - log.error( - f"Move set {str(group_id)} timed out of max duration {full_timeout}. Expected time: {expected_time}. Missing: {missing_node_Msg}" - ) + raise MotionFailedError( + message="Command timed out", + detail={ + "missing-nodes": missing_node_msg, + "full-timeout": str(full_timeout), + "expected-time": expected_time, + "elapsed": str(time.time() - start_time), + }, + ) + except EnumeratedError: + log.exception("Cancelling move group scheduler") + raise + except BaseException as e: + log.exception("canceling move group scheduler") + raise PythonException(e) from e - raise MotionFailedError(message='Command timed out', detail={ - 'missing-nodes': missing_node_msg, - 'full-timeout': str(full_timeout), - 'expected-time': expected_time, - 'elapsed': str(time.time() - start_time)}) - except EnumeratedError: - log.exception('Cancelling move group scheduler') - raise - except BaseException as e: - log.exception("canceling move group scheduler") - raise PythonException(e) from e + async def run(self, can_messenger: CanMessenger) -> _Completions: + """Start each move group after the prior has completed.""" + for group_id in range( + self._start_at_index, self._start_at_index + len(self._moves) + ): + await self._run_one_group(group_id, can_messenger) def _reify_queue_iter() -> Iterator[_CompletionPacket]: while not self._completion_queue.empty(): diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py b/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py index 10762e1cf21..f6f1386bd87 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_move_group_runner.py @@ -3,10 +3,10 @@ from typing import List, Any, Tuple from numpy import float64, float32, int32 from mock import AsyncMock, call, MagicMock, patch -import asyncio from opentrons_shared_data.errors.exceptions import ( MoveConditionNotMetError, EnumeratedError, + MotionFailedError, ) from opentrons_hardware.firmware_bindings import ArbitrationId, ArbitrationIdParts @@ -799,7 +799,7 @@ async def test_tip_action_move_runner_fail_receives_one_response( mock_can_messenger.ensure_send.side_effect = mock_sender.mock_ensure_send_failure mock_can_messenger.send.side_effect = mock_sender.mock_send_failure - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(MotionFailedError): await subject.run(can_messenger=mock_can_messenger)