Sfoglia il codice sorgente

Python software example (#106)

Add protobuf/cobs/crc32 framework code and a really basic python example
Scott Bezek 3 anni fa
parent
commit
acfc85f72b

+ 1 - 1
firmware/src/proto_gen/smartknob.pb.c

@@ -1,5 +1,5 @@
 /* Automatically generated nanopb constant definitions */
-/* Generated by nanopb-0.4.7-dev */
+/* Generated by nanopb-0.4.7 */
 
 #include "smartknob.pb.h"
 #if PB_PROTO_HEADER_VERSION != 40

+ 18 - 18
firmware/src/proto_gen/smartknob.pb.h

@@ -1,5 +1,5 @@
 /* Automatically generated nanopb header */
-/* Generated by nanopb-0.4.7-dev */
+/* Generated by nanopb-0.4.7 */
 
 #ifndef PB_PB_SMARTKNOB_PB_H_INCLUDED
 #define PB_PB_SMARTKNOB_PB_H_INCLUDED
@@ -10,10 +10,6 @@
 #endif
 
 /* Struct definitions */
-typedef struct _PB_RequestState {
-    char dummy_field;
-} PB_RequestState;
-
 typedef struct _PB_Ack {
     uint32_t nonce;
 } PB_Ack;
@@ -43,16 +39,6 @@ typedef struct _PB_SmartKnobState {
     PB_SmartKnobConfig config;
 } PB_SmartKnobState;
 
-/* Message TO the Smartknob from the USB host */
-typedef struct _PB_ToSmartknob {
-    uint32_t nonce;
-    pb_size_t which_payload;
-    union {
-        PB_RequestState request_state;
-        PB_SmartKnobConfig smartknob_config;
-    } payload;
-} PB_ToSmartknob;
-
 /* Message FROM the SmartKnob to USB host */
 typedef struct _PB_FromSmartKnob {
     pb_size_t which_payload;
@@ -63,6 +49,20 @@ typedef struct _PB_FromSmartKnob {
     } payload;
 } PB_FromSmartKnob;
 
+typedef struct _PB_RequestState {
+    char dummy_field;
+} PB_RequestState;
+
+/* Message TO the Smartknob from the USB host */
+typedef struct _PB_ToSmartknob {
+    uint32_t nonce;
+    pb_size_t which_payload;
+    union {
+        PB_RequestState request_state;
+        PB_SmartKnobConfig smartknob_config;
+    } payload;
+} PB_ToSmartknob;
+
 
 #ifdef __cplusplus
 extern "C" {
@@ -100,12 +100,12 @@ extern "C" {
 #define PB_SmartKnobState_current_position_tag   1
 #define PB_SmartKnobState_sub_position_unit_tag  2
 #define PB_SmartKnobState_config_tag             3
-#define PB_ToSmartknob_nonce_tag                 1
-#define PB_ToSmartknob_request_state_tag         2
-#define PB_ToSmartknob_smartknob_config_tag      3
 #define PB_FromSmartKnob_ack_tag                 1
 #define PB_FromSmartKnob_log_tag                 2
 #define PB_FromSmartKnob_smartknob_state_tag     3
+#define PB_ToSmartknob_nonce_tag                 1
+#define PB_ToSmartknob_request_state_tag         2
+#define PB_ToSmartknob_smartknob_config_tag      3
 
 /* Struct field encoding specification for nanopb */
 #define PB_FromSmartKnob_FIELDLIST(X, a) \

+ 13 - 0
proto/Pipfile

@@ -0,0 +1,13 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+grpcio-tools = "*"
+protobuf = "==3.20.3"
+
+[dev-packages]
+
+[requires]
+python_version = "3.10"

+ 160 - 0
proto/Pipfile.lock

@@ -0,0 +1,160 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "81cf165543dd8620d62e1e5c86575e5245b7b76980809b0fbd484de2aba002cb"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.10"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "grpcio": {
+            "hashes": [
+                "sha256:094e64236253590d9d4075665c77b329d707b6fca864dd62b144255e199b4f87",
+                "sha256:0dc5354e38e5adf2498312f7241b14c7ce3484eefa0082db4297189dcbe272e6",
+                "sha256:0e1a9e1b4a23808f1132aa35f968cd8e659f60af3ffd6fb00bcf9a65e7db279f",
+                "sha256:0fb93051331acbb75b49a2a0fd9239c6ba9528f6bdc1dd400ad1cb66cf864292",
+                "sha256:16c71740640ba3a882f50b01bf58154681d44b51f09a5728180a8fdc66c67bd5",
+                "sha256:172405ca6bdfedd6054c74c62085946e45ad4d9cec9f3c42b4c9a02546c4c7e9",
+                "sha256:17ec9b13cec4a286b9e606b48191e560ca2f3bbdf3986f91e480a95d1582e1a7",
+                "sha256:22b011674090594f1f3245960ced7386f6af35485a38901f8afee8ad01541dbd",
+                "sha256:24ac1154c4b2ab4a0c5326a76161547e70664cd2c39ba75f00fc8a2170964ea2",
+                "sha256:257478300735ce3c98d65a930bbda3db172bd4e00968ba743e6a1154ea6edf10",
+                "sha256:29cb97d41a4ead83b7bcad23bdb25bdd170b1e2cba16db6d3acbb090bc2de43c",
+                "sha256:2b170eaf51518275c9b6b22ccb59450537c5a8555326fd96ff7391b5dd75303c",
+                "sha256:31bb6bc7ff145e2771c9baf612f4b9ebbc9605ccdc5f3ff3d5553de7fc0e0d79",
+                "sha256:3c2b3842dcf870912da31a503454a33a697392f60c5e2697c91d133130c2c85d",
+                "sha256:3f9b0023c2c92bebd1be72cdfca23004ea748be1813a66d684d49d67d836adde",
+                "sha256:471d39d3370ca923a316d49c8aac66356cea708a11e647e3bdc3d0b5de4f0a40",
+                "sha256:49d680356a975d9c66a678eb2dde192d5dc427a7994fb977363634e781614f7c",
+                "sha256:4c4423ea38a7825b8fed8934d6d9aeebdf646c97e3c608c3b0bcf23616f33877",
+                "sha256:506b9b7a4cede87d7219bfb31014d7b471cfc77157da9e820a737ec1ea4b0663",
+                "sha256:538d981818e49b6ed1e9c8d5e5adf29f71c4e334e7d459bf47e9b7abb3c30e09",
+                "sha256:59dffade859f157bcc55243714d57b286da6ae16469bf1ac0614d281b5f49b67",
+                "sha256:5a6ebcdef0ef12005d56d38be30f5156d1cb3373b52e96f147f4a24b0ddb3a9d",
+                "sha256:5dca372268c6ab6372d37d6b9f9343e7e5b4bc09779f819f9470cd88b2ece3c3",
+                "sha256:6df3b63538c362312bc5fa95fb965069c65c3ea91d7ce78ad9c47cab57226f54",
+                "sha256:6f0b89967ee11f2b654c23b27086d88ad7bf08c0b3c2a280362f28c3698b2896",
+                "sha256:75e29a90dc319f0ad4d87ba6d20083615a00d8276b51512e04ad7452b5c23b04",
+                "sha256:7942b32a291421460d6a07883033e392167d30724aa84987e6956cd15f1a21b9",
+                "sha256:9235dcd5144a83f9ca6f431bd0eccc46b90e2c22fe27b7f7d77cabb2fb515595",
+                "sha256:97d67983189e2e45550eac194d6234fc38b8c3b5396c153821f2d906ed46e0ce",
+                "sha256:9ff42c5620b4e4530609e11afefa4a62ca91fa0abb045a8957e509ef84e54d30",
+                "sha256:a8a0b77e992c64880e6efbe0086fe54dfc0bbd56f72a92d9e48264dcd2a3db98",
+                "sha256:aacb54f7789ede5cbf1d007637f792d3e87f1c9841f57dd51abf89337d1b8472",
+                "sha256:bc59f7ba87972ab236f8669d8ca7400f02a0eadf273ca00e02af64d588046f02",
+                "sha256:cc2bece1737b44d878cc1510ea04469a8073dbbcdd762175168937ae4742dfb3",
+                "sha256:cd3baccea2bc5c38aeb14e5b00167bd4e2373a373a5e4d8d850bd193edad150c",
+                "sha256:dad6533411d033b77f5369eafe87af8583178efd4039c41d7515d3336c53b4f1",
+                "sha256:e223a9793522680beae44671b9ed8f6d25bbe5ddf8887e66aebad5e0686049ef",
+                "sha256:e473525c28251558337b5c1ad3fa969511e42304524a4e404065e165b084c9e4",
+                "sha256:e4ef09f8997c4be5f3504cefa6b5c6cc3cf648274ce3cede84d4342a35d76db6",
+                "sha256:e6dfc2b6567b1c261739b43d9c59d201c1b89e017afd9e684d85aa7a186c9f7a",
+                "sha256:eacad297ea60c72dd280d3353d93fb1dcca952ec11de6bb3c49d12a572ba31dd",
+                "sha256:f1158bccbb919da42544a4d3af5d9296a3358539ffa01018307337365a9a0c64",
+                "sha256:f1fec3abaf274cdb85bf3878167cfde5ad4a4d97c68421afda95174de85ba813",
+                "sha256:f96ace1540223f26fbe7c4ebbf8a98e3929a6aa0290c8033d12526847b291c0f",
+                "sha256:fbdbe9a849854fe484c00823f45b7baab159bdd4a46075302281998cb8719df5"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.51.1"
+        },
+        "grpcio-tools": {
+            "hashes": [
+                "sha256:0119aabd9ceedfdf41b56b9fdc8284dd85a7f589d087f2694d743f346a368556",
+                "sha256:072234859f6069dc43a6be8ad6b7d682f4ba1dc2e2db2ebf5c75f62eee0f6dfb",
+                "sha256:0fb6c1c1e56eb26b224adc028a4204b6ad0f8b292efa28067dff273bbc8b27c4",
+                "sha256:189be2a9b672300ca6845d94016bdacc052fdbe9d1ae9e85344425efae2ff8ef",
+                "sha256:21ff50e321736eba22210bf9b94e05391a9ac345f26e7df16333dc75d63e74fb",
+                "sha256:3c8749dca04a8d302862ceeb1dfbdd071ee13b281395975f24405a347e5baa57",
+                "sha256:4fa4300b1be59b046492ed3c5fdb59760bc6433f44c08f50de900f9552ec7461",
+                "sha256:516eedd5eb7af6326050bc2cfceb3a977b9cc1144f283c43cc4956905285c912",
+                "sha256:51be91b7c7056ff9ee48b1eccd4a2840b0126230803a5e09dfc082a5b16a91c1",
+                "sha256:5410d6b601d1404835e34466bd8aee37213489b36ee1aad2276366e265ff29d4",
+                "sha256:55fdebc73fb580717656b1bafa4f8eca448726a7aa22726a6c0a7895d2f0f088",
+                "sha256:6cc298fbfe584de8876a85355efbcf796dfbcfac5948c9560f5df82e79336e2a",
+                "sha256:6d9753944e5a6b6b78b76ce9d2ae0fe3f748008c1849deb7fadcb64489d6553b",
+                "sha256:70564521e86a0de35ea9ac6daecff10cb46860aec469af65869974807ce8e98b",
+                "sha256:7307dd2408b82ea545ae63502ec03036b025f449568556ea9a056e06129a7a4e",
+                "sha256:80f450272316ca0924545f488c8492649ca3aeb7044d4bf59c426dcdee527f7c",
+                "sha256:84a84d601a238572d049d3108e04fe4c206536e81076d56e623bd525a1b38def",
+                "sha256:8588819b22d0de3aa1951e1991cc3e4b9aa105eecf6e3e24eb0a2fc8ab958b3e",
+                "sha256:8902a035708555cddbd61b5467cea127484362decc52de03f061a1a520fe90cd",
+                "sha256:8a5614251c46da07549e24f417cf989710250385e9d80deeafc53a0ee7df6325",
+                "sha256:8e0d74403484eb77e8df2566a64b8b0b484b5c87903678c381634dd72f252d5e",
+                "sha256:92acc3e10ba2b0dcb90a88ae9fe1cc0ffba6868545207e4ff20ca95284f8e3c9",
+                "sha256:9443f5c30bac449237c3cf99da125f8d6e6c01e17972bc683ee73b75dea95573",
+                "sha256:9771d4d317dca029dfaca7ec9282d8afe731c18bc536ece37fd39b8a974cc331",
+                "sha256:a415fbec67d4ff7efe88794cbe00cf548d0f0a5484cceffe0a0c89d47694c491",
+                "sha256:a43d26714933f23de93ea0bf9c86c66a6ede709b8ca32e357f9e2181703e64ae",
+                "sha256:ace0035766fe01a1b096aa050be9f0a9f98402317e7aeff8bfe55349be32a407",
+                "sha256:ae56f133b05b7e5d780ef7e032dd762adad7f3dc8f64adb43ff5bfabd659f435",
+                "sha256:bdbbe63f6190187de5946891941629912ac8196701ed2253fa91624a397822ec",
+                "sha256:cabc8b0905cedbc3b2b7b2856334fa35cce3d4bc79ae241cacd8cca8940a5c85",
+                "sha256:cb75bac0cd43858cb759ef103fe68f8c540cb58b63dda127e710228fec3007b8",
+                "sha256:d18599ab572b2f15a8f3db49503272d1bb4fcabb4b4d1214ef03aca1816b20a0",
+                "sha256:d18ef2adc05a8ef9e58ac46357f6d4ce7e43e077c7eda0a4425773461f9d0e6e",
+                "sha256:d598ccde6338b2cfbb3124f34c95f03394209013f9b1ed4a5360a736853b1c27",
+                "sha256:d77e8b1613876e0d8fd17709509d4ceba13492816426bd156f7e88a4c47e7158",
+                "sha256:d886a9e052a038642b3af5d18e6f2085d1656d9788e202dc23258cf3a751e7ca",
+                "sha256:d96e96ae7361aa51c9cd9c73b677b51f691f98df6086860fcc3c45852d96b0b0",
+                "sha256:dcaaecdd5e847de5c1d533ea91522bf56c9e6b2dc98cdc0d45f0a1c26e846ea2",
+                "sha256:e0403e095b343431195db1305248b50019ad55d3dd310254431af87e14ef83a2",
+                "sha256:e20d7885a40e68a2bda92908acbabcdf3c14dd386c3845de73ba139e9df1f132",
+                "sha256:e5bb396d63495667d4df42e506eed9d74fc9a51c99c173c04395fe7604c848f1",
+                "sha256:e712a6d00606ad19abdeae852a7e521d6f6d0dcea843708fecf3a38be16a851e",
+                "sha256:e7e7668f89fd598c5469bb58e16bfd12b511d9947ccc75aec94da31f62bc3758",
+                "sha256:f0feb4f2b777fa6377e977faa89c26359d4f31953de15e035505b92f41aa6906",
+                "sha256:f75973a42c710999acd419968bc79f00327e03e855bbe82c6529e003e49af660",
+                "sha256:f766050e491d0b3203b6b85638015f543816a2eb7d089fc04e86e00f6de0e31d"
+            ],
+            "index": "pypi",
+            "version": "==1.48.2"
+        },
+        "protobuf": {
+            "hashes": [
+                "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7",
+                "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c",
+                "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2",
+                "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b",
+                "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050",
+                "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9",
+                "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7",
+                "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454",
+                "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480",
+                "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469",
+                "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c",
+                "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e",
+                "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db",
+                "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905",
+                "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b",
+                "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86",
+                "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4",
+                "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402",
+                "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7",
+                "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4",
+                "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99",
+                "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"
+            ],
+            "index": "pypi",
+            "version": "==3.20.3"
+        },
+        "setuptools": {
+            "hashes": [
+                "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378",
+                "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==67.1.0"
+        }
+    },
+    "develop": {}
+}

+ 20 - 4
proto/generate_protobuf.py

@@ -1,11 +1,12 @@
-#!/usr/bin/env python3
+import os
+import sys
+if __name__ == '__main__':
+    if 'PIPENV_ACTIVE' not in os.environ:
+        sys.exit(f'This script should be run in a Pipenv.\n\nRun it as:\npipenv run python {os.path.basename(__file__)}')
 
 from pathlib import Path
-
-import os
 import shutil
 import subprocess
-import sys
 
 def run():
     SCRIPT_PATH = Path(__file__).absolute().parent
@@ -30,5 +31,20 @@ def run():
     subprocess.check_call(['python3', nanopb_generator_path, '-D', c_generated_output_path] + proto_files, cwd=proto_path)
 
 
+    # Use nanopb's packaged protoc to generate python bindings
+    protoc_path = nanopb_path / 'generator' / 'protoc'
+    python_generated_output_path = REPO_ROOT / 'software' / 'python' / 'proto_gen'
+    python_generated_output_path.mkdir(parents=True, exist_ok=True)
+    subprocess.check_call([protoc_path, '--version'])
+    subprocess.check_call([
+        protoc_path,
+        '--python_out',
+        python_generated_output_path,
+    ] + proto_files, cwd=proto_path)
+
+    # Copy nanopb's compiled options proto
+    shutil.copy2(nanopb_path / 'generator' / 'proto' / 'nanopb_pb2.py', python_generated_output_path)
+
+
 if __name__ == '__main__':
     run()

+ 14 - 0
software/python/Pipfile

@@ -0,0 +1,14 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pyserial = "*"
+cobs = "*"
+protobuf = "==3.20.3"
+
+[dev-packages]
+
+[requires]
+python_version = "3.10"

+ 73 - 0
software/python/Pipfile.lock

@@ -0,0 +1,73 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "a74717fa76c8f068ed910686866d0d3ad0ef124601f1d8cf37cc0592e71d4fee"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.10"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "cobs": {
+            "hashes": [
+                "sha256:3f2e8ef22070c3a95ed998303b81c209dd49aaaebf45173499a1a48b8b17bb6f",
+                "sha256:4625f7648e8b0ea50702cf213dcfcc0a50dc78fc6678b174f1f5e0d6fc3687c1",
+                "sha256:7c4a9e246c11f48e12c1231ad5738ea927f663b7b4ffd1de0be3b1cae5bb3884",
+                "sha256:991fc195f0581e456d3dd21e29df493fe9ca1b772084c26921601e48dbc34bd2",
+                "sha256:b4af64be97f2a7215c3ffcd5cca26e6aa166e49cd90941efab95c76211b6710c",
+                "sha256:c747c4b385f04203e1d2b767d61a69580b58c97eea7a6ed998edd42ddd9fcdc5",
+                "sha256:cb573184d982edf8e7c3c08c753b765790790e7ffdca4c2ca5c258bbd9099f33",
+                "sha256:d0f7e7fcad3020676feef505a0212bcd0c86549d3e4c3680f6a7c77ef599e52b",
+                "sha256:d93b10b5370dc8868960aefb70adb1f73a4e6107b16918307a4aeeefd3edb8f6",
+                "sha256:f68ee5e70ae6be79b424cb270835e864b86320a9fcb8048575443b658f61ee0a"
+            ],
+            "index": "pypi",
+            "version": "==1.2.0"
+        },
+        "protobuf": {
+            "hashes": [
+                "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7",
+                "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c",
+                "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2",
+                "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b",
+                "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050",
+                "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9",
+                "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7",
+                "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454",
+                "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480",
+                "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469",
+                "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c",
+                "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e",
+                "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db",
+                "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905",
+                "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b",
+                "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86",
+                "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4",
+                "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402",
+                "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7",
+                "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4",
+                "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99",
+                "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"
+            ],
+            "index": "pypi",
+            "version": "==3.20.3"
+        },
+        "pyserial": {
+            "hashes": [
+                "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb",
+                "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"
+            ],
+            "index": "pypi",
+            "version": "==3.5"
+        }
+    },
+    "develop": {}
+}

+ 5 - 0
software/python/proto_gen/README

@@ -0,0 +1,5 @@
+The files in this directory are auto-generated by the protobuf compiler.
+
+They're checked into the repo to avoid the need to always re-compile them.
+
+They can be regenerated by running `<repo_root>/proto/generate_protobuf.py`

File diff suppressed because it is too large
+ 23 - 0
software/python/proto_gen/nanopb_pb2.py


+ 101 - 0
software/python/proto_gen/smartknob_pb2.py

@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: smartknob.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+import nanopb_pb2 as nanopb__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fsmartknob.proto\x12\x02PB\x1a\x0cnanopb.proto\"y\n\rFromSmartKnob\x12\x16\n\x03\x61\x63k\x18\x01 \x01(\x0b\x32\x07.PB.AckH\x00\x12\x16\n\x03log\x18\x02 \x01(\x0b\x32\x07.PB.LogH\x00\x12-\n\x0fsmartknob_state\x18\x03 \x01(\x0b\x32\x12.PB.SmartKnobStateH\x00\x42\t\n\x07payload\"\x14\n\x03\x41\x63k\x12\r\n\x05nonce\x18\x01 \x01(\r\"\x1a\n\x03Log\x12\x13\n\x03msg\x18\x01 \x01(\tB\x06\x92?\x03p\xff\x01\"j\n\x0eSmartKnobState\x12\x18\n\x10\x63urrent_position\x18\x01 \x01(\x05\x12\x19\n\x11sub_position_unit\x18\x02 \x01(\x02\x12#\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\x13.PB.SmartKnobConfig\"\x83\x01\n\x0bToSmartknob\x12\r\n\x05nonce\x18\x01 \x01(\r\x12)\n\rrequest_state\x18\x02 \x01(\x0b\x32\x10.PB.RequestStateH\x00\x12/\n\x10smartknob_config\x18\x03 \x01(\x0b\x32\x13.PB.SmartKnobConfigH\x00\x42\t\n\x07payload\"\x8f\x02\n\x0fSmartKnobConfig\x12\x10\n\x08position\x18\x01 \x01(\x05\x12\x14\n\x0cmin_position\x18\x02 \x01(\x05\x12\x14\n\x0cmax_position\x18\x03 \x01(\x05\x12\x1e\n\x16position_width_radians\x18\x04 \x01(\x02\x12\x1c\n\x14\x64\x65tent_strength_unit\x18\x05 \x01(\x02\x12\x1d\n\x15\x65ndstop_strength_unit\x18\x06 \x01(\x02\x12\x12\n\nsnap_point\x18\x07 \x01(\x02\x12\x13\n\x04text\x18\x08 \x01(\tB\x05\x92?\x02p2\x12\x1f\n\x10\x64\x65tent_positions\x18\t \x03(\x05\x42\x05\x92?\x02\x10\x05\x12\x17\n\x0fsnap_point_bias\x18\n \x01(\x02\"\x0e\n\x0cRequestStateb\x06proto3')
+
+
+
+_FROMSMARTKNOB = DESCRIPTOR.message_types_by_name['FromSmartKnob']
+_ACK = DESCRIPTOR.message_types_by_name['Ack']
+_LOG = DESCRIPTOR.message_types_by_name['Log']
+_SMARTKNOBSTATE = DESCRIPTOR.message_types_by_name['SmartKnobState']
+_TOSMARTKNOB = DESCRIPTOR.message_types_by_name['ToSmartknob']
+_SMARTKNOBCONFIG = DESCRIPTOR.message_types_by_name['SmartKnobConfig']
+_REQUESTSTATE = DESCRIPTOR.message_types_by_name['RequestState']
+FromSmartKnob = _reflection.GeneratedProtocolMessageType('FromSmartKnob', (_message.Message,), {
+  'DESCRIPTOR' : _FROMSMARTKNOB,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.FromSmartKnob)
+  })
+_sym_db.RegisterMessage(FromSmartKnob)
+
+Ack = _reflection.GeneratedProtocolMessageType('Ack', (_message.Message,), {
+  'DESCRIPTOR' : _ACK,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.Ack)
+  })
+_sym_db.RegisterMessage(Ack)
+
+Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), {
+  'DESCRIPTOR' : _LOG,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.Log)
+  })
+_sym_db.RegisterMessage(Log)
+
+SmartKnobState = _reflection.GeneratedProtocolMessageType('SmartKnobState', (_message.Message,), {
+  'DESCRIPTOR' : _SMARTKNOBSTATE,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.SmartKnobState)
+  })
+_sym_db.RegisterMessage(SmartKnobState)
+
+ToSmartknob = _reflection.GeneratedProtocolMessageType('ToSmartknob', (_message.Message,), {
+  'DESCRIPTOR' : _TOSMARTKNOB,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.ToSmartknob)
+  })
+_sym_db.RegisterMessage(ToSmartknob)
+
+SmartKnobConfig = _reflection.GeneratedProtocolMessageType('SmartKnobConfig', (_message.Message,), {
+  'DESCRIPTOR' : _SMARTKNOBCONFIG,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.SmartKnobConfig)
+  })
+_sym_db.RegisterMessage(SmartKnobConfig)
+
+RequestState = _reflection.GeneratedProtocolMessageType('RequestState', (_message.Message,), {
+  'DESCRIPTOR' : _REQUESTSTATE,
+  '__module__' : 'smartknob_pb2'
+  # @@protoc_insertion_point(class_scope:PB.RequestState)
+  })
+_sym_db.RegisterMessage(RequestState)
+
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _LOG.fields_by_name['msg']._options = None
+  _LOG.fields_by_name['msg']._serialized_options = b'\222?\003p\377\001'
+  _SMARTKNOBCONFIG.fields_by_name['text']._options = None
+  _SMARTKNOBCONFIG.fields_by_name['text']._serialized_options = b'\222?\002p2'
+  _SMARTKNOBCONFIG.fields_by_name['detent_positions']._options = None
+  _SMARTKNOBCONFIG.fields_by_name['detent_positions']._serialized_options = b'\222?\002\020\005'
+  _FROMSMARTKNOB._serialized_start=37
+  _FROMSMARTKNOB._serialized_end=158
+  _ACK._serialized_start=160
+  _ACK._serialized_end=180
+  _LOG._serialized_start=182
+  _LOG._serialized_end=208
+  _SMARTKNOBSTATE._serialized_start=210
+  _SMARTKNOBSTATE._serialized_end=316
+  _TOSMARTKNOB._serialized_start=319
+  _TOSMARTKNOB._serialized_end=450
+  _SMARTKNOBCONFIG._serialized_start=453
+  _SMARTKNOBCONFIG._serialized_end=724
+  _REQUESTSTATE._serialized_start=726
+  _REQUESTSTATE._serialized_end=740
+# @@protoc_insertion_point(module_scope)

+ 46 - 0
software/python/simple_example.py

@@ -0,0 +1,46 @@
+import os
+import sys
+if __name__ == '__main__':
+    if 'PIPENV_ACTIVE' not in os.environ:
+        sys.exit(f'This script should be run in a Pipenv.\n\nRun it as:\npipenv run python {os.path.basename(__file__)}')
+
+# Place imports below this line
+import logging
+import math
+
+from smartknob_io import (
+    ask_for_serial_port,
+    smartknob_context
+)
+from proto_gen import smartknob_pb2
+
+def _run_example():
+    logging.basicConfig(level=logging.INFO)
+
+    p = ask_for_serial_port()
+    with smartknob_context(p) as s:
+        last_state = smartknob_pb2.SmartKnobState()
+        def log_state(message):
+            nonlocal last_state
+            if last_state.config.SerializeToString(deterministic=True) != message.config.SerializeToString(deterministic=True):
+                logging.info('State: ' + str(message))
+                last_state = message
+        s.add_handler('smartknob_state', log_state)
+        s.request_state()
+
+        # Run forever, set config when enter is pressed
+        while True:
+            input()
+            config = smartknob_pb2.SmartKnobConfig()
+            config.position = 0
+            config.min_position = 0
+            config.max_position = 5
+            config.position_width_radians = math.radians(10)
+            config.detent_strength_unit = 1
+            config.endstop_strength_unit = 1
+            config.snap_point = 1.1
+            config.text = "From Python!"
+            s.set_config(config)
+
+if __name__ == '__main__':
+    _run_example()

+ 258 - 0
software/python/smartknob_io.py

@@ -0,0 +1,258 @@
+
+if __name__ == '__main__':
+    import sys
+    sys.exit('This is a library file to be imported into your own python scripts. It doesn\'t do anything if run directly')
+
+
+from cobs import cobs
+from collections import (
+    defaultdict,
+)
+from contextlib import contextmanager
+from enum import Enum
+import logging
+import os
+from queue import (
+    Empty,
+    Full,
+    Queue,
+)
+from random import randint
+import serial
+import serial.tools.list_ports
+import sys
+from threading import (
+    Thread,
+    Lock,
+)
+import time
+import zlib
+
+software_root = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(os.path.join(software_root, 'proto_gen'))
+
+from proto_gen import smartknob_pb2
+
+SMARTKNOB_BAUD = 921600
+
+
+class Smartknob(object):
+    RETRY_TIMEOUT = 0.25
+
+    def __init__(self, serial_instance):
+        self._serial = serial_instance
+        self._logger = logging.getLogger('smartknob')
+        self._out_q = Queue()
+        self._ack_q = Queue()
+        self._next_nonce = randint(0, 255)
+        self._run = True
+
+        self._lock = Lock()
+        self._message_handlers = defaultdict(list)
+
+    def _read_loop(self):
+        self._logger.debug('Read loop started')
+        buffer = b''
+        while True:
+            buffer += self._serial.read_until(b'\0')
+            if not self._run:
+                return
+
+            if not len(buffer):
+                continue
+
+            if not buffer.endswith(b'\0'):
+                continue
+            
+            self._process_frame(buffer[:-1])
+            buffer = b''
+
+    def _process_frame(self, frame):
+        try:
+            decoded = cobs.decode(frame)
+        except cobs.DecodeError:
+            self._logger.debug(f'Failed decode ({len(frame)} bytes)')
+            self._logger.debug(frame)
+            return
+
+        if len(decoded) < 4:
+            return
+
+        payload = decoded[:-4]
+        expected_crc = zlib.crc32(payload) & 0xffffffff
+        provided_crc = (decoded[-1] << 24) \
+                        | (decoded[-2] << 16) \
+                        | (decoded[-3] << 8) \
+                        | decoded[-4]
+        
+        if expected_crc != provided_crc:
+            self._logger.debug(f'Bad CRC. expected={hex(expected_crc)}, actual={hex(provided_crc)}')
+            return
+
+        message = smartknob_pb2.FromSmartKnob()
+        message.ParseFromString(payload)
+        self._logger.debug(message)
+
+        payload_type = message.WhichOneof('payload')
+
+        # If this is an ack, notify the write thread
+        if payload_type == 'ack':
+            nonce = message.ack.nonce
+            self._ack_q.put(nonce)
+
+        with self._lock:
+            for handler in self._message_handlers[payload_type] + self._message_handlers[None]:
+                try:
+                    handler(getattr(message, payload_type))
+                except:
+                    self._logger.warning(f'Unhandled exception in message handler ({payload_type})', exc_info=True)
+    
+    def _write_loop(self):
+        self._logger.debug('Write loop started')
+        while True:
+            data = self._out_q.get()
+            # Check for shutdown
+            if not self._run:
+                self._logger.debug('Write loop exiting @ _out_q')
+                return
+            (nonce, encoded_message) = data
+
+            next_retry = 0
+            while True:
+                if time.time() >= next_retry:
+                    if next_retry > 0:
+                        self._logger.debug('Retry write...')
+                    self._serial.write(encoded_message)
+                    self._serial.write(b'\0')
+                    next_retry = time.time() + Smartknob.RETRY_TIMEOUT
+                
+                try:
+                    latest_ack_nonce = self._ack_q.get(timeout=next_retry - time.time())
+                except Empty:
+                    latest_ack_nonce = None
+
+                # Check for shutdown
+                if not self._run:
+                    self._logger.debug('Write loop exiting @ _ack_q')
+                    return
+
+                if latest_ack_nonce == nonce:
+                    break
+                else:
+                    self._logger.debug(f'Got unexpected nonce: {latest_ack_nonce}')
+    
+    def _enqueue_message(self, message):
+        nonce = self._next_nonce
+        self._next_nonce += 1
+
+        message.nonce = nonce
+
+        payload = bytearray(message.SerializeToString())
+
+        crc = zlib.crc32(payload) & 0xffffffff
+        payload.append(crc & 0xff)
+        payload.append((crc >> 8) & 0xff)
+        payload.append((crc >> 16) & 0xff)
+        payload.append((crc >> 24) & 0xff)
+
+        encoded_message = cobs.encode(payload)
+
+        self._out_q.put((nonce, encoded_message))
+
+        approx_q_length = self._out_q.qsize()
+        self._logger.debug(f'Out q length: {approx_q_length}')
+        if approx_q_length > 10:
+            self._logger.warning(f'Output queue length is high! ({approx_q_length}) Is the smartknob still connected and functional?')
+
+    def set_config(self, config):
+        message = smartknob_pb2.ToSmartknob()
+        message.smartknob_config.CopyFrom(config)
+        self._enqueue_message(message)
+
+    def start(self):
+        self.read_thread = Thread(target=self._read_loop)
+        self.write_thread = Thread(target=self._write_loop)
+        self.read_thread.start()
+        self.write_thread.start()
+    
+    def shutdown(self):
+        self._logger.info('Shutting down...')
+        self._run = False
+        self.read_thread.join()
+        self._logger.debug('Read thread terminated')
+        self._out_q.put(None)
+        self._ack_q.put(None)
+        self.write_thread.join()
+        self._logger.debug('Write thread terminated')
+    
+    def add_handler(self, message_type, handler):
+        with self._lock:
+            self._message_handlers[message_type].append(handler)
+        return lambda: self._remove_handler(message_type, handler)
+
+    def _remove_handler(self, message_type, handler):
+        with self._lock:
+            self._message_handlers[message_type].remove(handler)
+
+    def request_state(self):
+        message = smartknob_pb2.ToSmartknob()
+        message.request_state.SetInParent()
+        self._enqueue_message(message)
+
+    def hard_reset(self):
+        self._serial.setRTS(True)
+        self._serial.setDTR(False)
+        time.sleep(0.2)
+        self._serial.setDTR(True)
+        time.sleep(0.2)
+
+
+@contextmanager
+def smartknob_context(serial_port, default_logging=True, wait_for_comms=True):
+    with serial.Serial(serial_port, SMARTKNOB_BAUD, timeout=1.0) as ser:
+        s = Smartknob(ser)
+        s.start()
+
+        if default_logging:
+            s.add_handler('log', lambda msg: s._logger.info(f'From smartknob: {msg.msg}'))
+
+        if wait_for_comms:
+            s._logger.info('Connecting to smartknob...')
+            q = Queue(1)
+            def startup_handler(message):
+                try:
+                    q.put_nowait(None)
+                except Full:
+                    pass
+            unregister = s.add_handler('smartknob_state', startup_handler)
+
+            s.request_state()
+            q.get()
+            unregister()
+            s._logger.info('Connected!')
+
+        try:
+            yield s
+        finally:
+            s.shutdown()
+
+
+def ask_for_serial_port():
+    """
+    Helper function to ask which port to use via stdin
+    """
+    print('Available ports:')
+    ports = sorted(
+        filter(
+            lambda p: p.description != 'n/a',
+            serial.tools.list_ports.comports(),
+        ),
+        key=lambda p: p.device,
+    )
+    for i, port in enumerate(ports):
+        print('[{: 2}] {} - {}'.format(i, port.device, port.description))
+    print()
+    value = input('Use which port? ')
+    port_index = int(value)
+    assert 0 <= port_index < len(ports)
+    return ports[port_index].device

+ 1 - 1
thirdparty/nanopb

@@ -1 +1 @@
-Subproject commit 80f9d5bcbc0da72c95d3411b642c109a14298d75
+Subproject commit b97aa657a706d3ba4a9a6ccca7043c9d6fe41cba

Some files were not shown because too many files changed in this diff