esplink 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. esplink-1.0.2/.gitignore +227 -0
  2. esplink-1.0.2/.python-version +1 -0
  3. esplink-1.0.2/PKG-INFO +52 -0
  4. esplink-1.0.2/README.md +27 -0
  5. esplink-1.0.2/esplink/__init__.py +88 -0
  6. esplink-1.0.2/esplink/_log.py +73 -0
  7. esplink-1.0.2/esplink/aio.py +225 -0
  8. esplink-1.0.2/esplink/analog.py +82 -0
  9. esplink-1.0.2/esplink/ble.py +250 -0
  10. esplink-1.0.2/esplink/bridge.py +1374 -0
  11. esplink-1.0.2/esplink/camera.py +90 -0
  12. esplink-1.0.2/esplink/can.py +120 -0
  13. esplink-1.0.2/esplink/cli.py +235 -0
  14. esplink-1.0.2/esplink/compat/__init__.py +0 -0
  15. esplink-1.0.2/esplink/compat/blinka.py +210 -0
  16. esplink-1.0.2/esplink/compat/gpiozero.py +233 -0
  17. esplink-1.0.2/esplink/compat/luma.py +94 -0
  18. esplink-1.0.2/esplink/compat/rpi_gpio.py +133 -0
  19. esplink-1.0.2/esplink/compat/smbus.py +63 -0
  20. esplink-1.0.2/esplink/constants.py +404 -0
  21. esplink-1.0.2/esplink/drivers/README.md +76 -0
  22. esplink-1.0.2/esplink/drivers/__init__.py +156 -0
  23. esplink-1.0.2/esplink/drivers/ads1115.py +113 -0
  24. esplink-1.0.2/esplink/drivers/bh1750.py +85 -0
  25. esplink-1.0.2/esplink/drivers/bme280.py +220 -0
  26. esplink-1.0.2/esplink/drivers/dht.py +79 -0
  27. esplink-1.0.2/esplink/drivers/ds18b20.py +68 -0
  28. esplink-1.0.2/esplink/drivers/ds3231.py +114 -0
  29. esplink-1.0.2/esplink/drivers/hcsr04.py +38 -0
  30. esplink-1.0.2/esplink/drivers/inmp441.py +67 -0
  31. esplink-1.0.2/esplink/drivers/ir.py +90 -0
  32. esplink-1.0.2/esplink/drivers/max98357.py +65 -0
  33. esplink-1.0.2/esplink/drivers/mcp3202.py +52 -0
  34. esplink-1.0.2/esplink/drivers/mcp3208.py +53 -0
  35. esplink-1.0.2/esplink/drivers/motor.py +143 -0
  36. esplink-1.0.2/esplink/drivers/mpu6050.py +112 -0
  37. esplink-1.0.2/esplink/drivers/neopixel.py +83 -0
  38. esplink-1.0.2/esplink/drivers/nrf24.py +287 -0
  39. esplink-1.0.2/esplink/drivers/oled.py +187 -0
  40. esplink-1.0.2/esplink/drivers/pca9685.py +133 -0
  41. esplink-1.0.2/esplink/drivers/pcf8574.py +61 -0
  42. esplink-1.0.2/esplink/drivers/stepper.py +93 -0
  43. esplink-1.0.2/esplink/drivers/sx126x.py +282 -0
  44. esplink-1.0.2/esplink/drivers/sx127x.py +238 -0
  45. esplink-1.0.2/esplink/drivers/sx128x.py +285 -0
  46. esplink-1.0.2/esplink/errors.py +71 -0
  47. esplink-1.0.2/esplink/espnow.py +170 -0
  48. esplink-1.0.2/esplink/eth.py +106 -0
  49. esplink-1.0.2/esplink/firmware/__init__.py +10 -0
  50. esplink-1.0.2/esplink/firmware/bridge.esp32.huge_app.bin +0 -0
  51. esplink-1.0.2/esplink/flash.py +202 -0
  52. esplink-1.0.2/esplink/fs.py +196 -0
  53. esplink-1.0.2/esplink/gpio.py +154 -0
  54. esplink-1.0.2/esplink/i2c.py +102 -0
  55. esplink-1.0.2/esplink/i2s.py +90 -0
  56. esplink-1.0.2/esplink/manager.py +194 -0
  57. esplink-1.0.2/esplink/mcp/__init__.py +19 -0
  58. esplink-1.0.2/esplink/mcp/__main__.py +5 -0
  59. esplink-1.0.2/esplink/mcp/common.py +131 -0
  60. esplink-1.0.2/esplink/mcp/server.py +197 -0
  61. esplink-1.0.2/esplink/mcp/tools.py +392 -0
  62. esplink-1.0.2/esplink/mcpwm.py +47 -0
  63. esplink-1.0.2/esplink/net.py +341 -0
  64. esplink-1.0.2/esplink/nvs.py +87 -0
  65. esplink-1.0.2/esplink/onewire.py +96 -0
  66. esplink-1.0.2/esplink/ota.py +64 -0
  67. esplink-1.0.2/esplink/protocol.py +202 -0
  68. esplink-1.0.2/esplink/pwm.py +56 -0
  69. esplink-1.0.2/esplink/rmt.py +123 -0
  70. esplink-1.0.2/esplink/spi.py +51 -0
  71. esplink-1.0.2/esplink/transports/__init__.py +33 -0
  72. esplink-1.0.2/esplink/transports/ble.py +282 -0
  73. esplink-1.0.2/esplink/transports/mock.py +46 -0
  74. esplink-1.0.2/esplink/transports/serial.py +90 -0
  75. esplink-1.0.2/esplink/transports/tcp.py +206 -0
  76. esplink-1.0.2/esplink/uart.py +130 -0
  77. esplink-1.0.2/esplink/watch.py +232 -0
  78. esplink-1.0.2/esplink/wifi.py +212 -0
  79. esplink-1.0.2/examples/README.md +141 -0
  80. esplink-1.0.2/examples/basics/adc_dac.py +24 -0
  81. esplink-1.0.2/examples/basics/async_fanout.py +50 -0
  82. esplink-1.0.2/examples/basics/blink.py +15 -0
  83. esplink-1.0.2/examples/basics/button_interrupt.py +14 -0
  84. esplink-1.0.2/examples/basics/i2c_scan.py +10 -0
  85. esplink-1.0.2/examples/basics/mcp3208_benchmark.py +74 -0
  86. esplink-1.0.2/examples/basics/pwm_servo.py +28 -0
  87. esplink-1.0.2/examples/basics/rtos_concurrency.py +198 -0
  88. esplink-1.0.2/examples/basics/shared_connection.py +52 -0
  89. esplink-1.0.2/examples/basics/spi_transfer.py +11 -0
  90. esplink-1.0.2/examples/basics/touch_read.py +28 -0
  91. esplink-1.0.2/examples/basics/uart_loopback.py +27 -0
  92. esplink-1.0.2/examples/basics/watch_threshold.py +110 -0
  93. esplink-1.0.2/examples/compat/adafruit/bme280.py +24 -0
  94. esplink-1.0.2/examples/compat/gpiozero/led_button.py +32 -0
  95. esplink-1.0.2/examples/compat/luma/oled.py +54 -0
  96. esplink-1.0.2/examples/compat/rpi_gpio/blink.py +20 -0
  97. esplink-1.0.2/examples/compat/smbus/read_register.py +33 -0
  98. esplink-1.0.2/examples/devices/can_dump.py +21 -0
  99. esplink-1.0.2/examples/devices/custom_driver.py +53 -0
  100. esplink-1.0.2/examples/devices/dht_read.py +17 -0
  101. esplink-1.0.2/examples/devices/ds18b20_temp.py +20 -0
  102. esplink-1.0.2/examples/devices/hcsr04_ping.py +17 -0
  103. esplink-1.0.2/examples/devices/i2s_record.py +20 -0
  104. esplink-1.0.2/examples/devices/ir_remote.py +22 -0
  105. esplink-1.0.2/examples/devices/neopixel_rainbow.py +25 -0
  106. esplink-1.0.2/examples/devices/stepper_move.py +21 -0
  107. esplink-1.0.2/examples/displays/ble_oled.py +41 -0
  108. esplink-1.0.2/examples/displays/oled_ssd1306.py +41 -0
  109. esplink-1.0.2/examples/network/many_boards.py +53 -0
  110. esplink-1.0.2/examples/network/tcp_through_bridge.py +28 -0
  111. esplink-1.0.2/examples/network/udp_through_bridge.py +23 -0
  112. esplink-1.0.2/examples/network/wifi_ap.py +28 -0
  113. esplink-1.0.2/examples/network/wifi_link.py +67 -0
  114. esplink-1.0.2/examples/network/wifi_scan.py +9 -0
  115. esplink-1.0.2/examples/pyproject.toml +17 -0
  116. esplink-1.0.2/examples/system/deep_sleep.py +34 -0
  117. esplink-1.0.2/examples/system/fs_logger.py +19 -0
  118. esplink-1.0.2/examples/system/mcp_server.py +47 -0
  119. esplink-1.0.2/examples/system/multi_client.py +93 -0
  120. esplink-1.0.2/examples/system/nvs_counter.py +8 -0
  121. esplink-1.0.2/examples/system/ota_update.py +25 -0
  122. esplink-1.0.2/examples/system/power_profiles.py +37 -0
  123. esplink-1.0.2/examples/uv.lock +2462 -0
  124. esplink-1.0.2/examples/wireless/ble_blink.py +31 -0
  125. esplink-1.0.2/examples/wireless/ble_scan.py +8 -0
  126. esplink-1.0.2/examples/wireless/coex_all_radios.py +47 -0
  127. esplink-1.0.2/examples/wireless/espnow_broadcast.py +62 -0
  128. esplink-1.0.2/examples/wireless/espnow_pair.py +54 -0
  129. esplink-1.0.2/examples/wireless/espnow_pingpong.py +62 -0
  130. esplink-1.0.2/examples/wireless/multi_device.py +39 -0
  131. esplink-1.0.2/examples/wireless/stress_test.py +423 -0
  132. esplink-1.0.2/examples/wireless/usb_vs_ble_benchmark.py +122 -0
  133. esplink-1.0.2/pyproject.toml +49 -0
  134. esplink-1.0.2/tests/conftest.py +26 -0
  135. esplink-1.0.2/tests/fake_firmware.py +749 -0
  136. esplink-1.0.2/tests/hw_stress.py +360 -0
  137. esplink-1.0.2/tests/stress_single_com3.py +105 -0
  138. esplink-1.0.2/tests/test_ads1115.py +94 -0
  139. esplink-1.0.2/tests/test_aio.py +70 -0
  140. esplink-1.0.2/tests/test_auth.py +74 -0
  141. esplink-1.0.2/tests/test_bh1750.py +67 -0
  142. esplink-1.0.2/tests/test_bme280.py +210 -0
  143. esplink-1.0.2/tests/test_bridge.py +319 -0
  144. esplink-1.0.2/tests/test_can.py +65 -0
  145. esplink-1.0.2/tests/test_compat_blinka.py +73 -0
  146. esplink-1.0.2/tests/test_compat_gpiozero.py +66 -0
  147. esplink-1.0.2/tests/test_compat_smbus.py +43 -0
  148. esplink-1.0.2/tests/test_concurrency.py +97 -0
  149. esplink-1.0.2/tests/test_contract_sync.py +133 -0
  150. esplink-1.0.2/tests/test_dht.py +68 -0
  151. esplink-1.0.2/tests/test_drivers.py +83 -0
  152. esplink-1.0.2/tests/test_ds18b20.py +44 -0
  153. esplink-1.0.2/tests/test_ds3231.py +79 -0
  154. esplink-1.0.2/tests/test_espnow.py +191 -0
  155. esplink-1.0.2/tests/test_eth_cam.py +45 -0
  156. esplink-1.0.2/tests/test_fs.py +64 -0
  157. esplink-1.0.2/tests/test_gpio_status.py +38 -0
  158. esplink-1.0.2/tests/test_hcsr04.py +21 -0
  159. esplink-1.0.2/tests/test_i2s.py +32 -0
  160. esplink-1.0.2/tests/test_inmp441.py +62 -0
  161. esplink-1.0.2/tests/test_ir.py +59 -0
  162. esplink-1.0.2/tests/test_luma_compat.py +35 -0
  163. esplink-1.0.2/tests/test_manager.py +128 -0
  164. esplink-1.0.2/tests/test_max98357.py +60 -0
  165. esplink-1.0.2/tests/test_mcp.py +252 -0
  166. esplink-1.0.2/tests/test_mcp3202.py +64 -0
  167. esplink-1.0.2/tests/test_mcp3208.py +62 -0
  168. esplink-1.0.2/tests/test_mcpwm.py +25 -0
  169. esplink-1.0.2/tests/test_motor.py +160 -0
  170. esplink-1.0.2/tests/test_mpu6050.py +107 -0
  171. esplink-1.0.2/tests/test_multi.py +393 -0
  172. esplink-1.0.2/tests/test_neopixel.py +48 -0
  173. esplink-1.0.2/tests/test_net.py +94 -0
  174. esplink-1.0.2/tests/test_nrf24.py +159 -0
  175. esplink-1.0.2/tests/test_nvs.py +44 -0
  176. esplink-1.0.2/tests/test_oled.py +171 -0
  177. esplink-1.0.2/tests/test_onewire.py +48 -0
  178. esplink-1.0.2/tests/test_ota.py +47 -0
  179. esplink-1.0.2/tests/test_pca9685.py +127 -0
  180. esplink-1.0.2/tests/test_pcf8574.py +64 -0
  181. esplink-1.0.2/tests/test_protocol.py +102 -0
  182. esplink-1.0.2/tests/test_rmt.py +76 -0
  183. esplink-1.0.2/tests/test_sleep.py +23 -0
  184. esplink-1.0.2/tests/test_stepper.py +53 -0
  185. esplink-1.0.2/tests/test_subapis.py +25 -0
  186. esplink-1.0.2/tests/test_sx126x.py +220 -0
  187. esplink-1.0.2/tests/test_sx127x.py +204 -0
  188. esplink-1.0.2/tests/test_sx128x.py +194 -0
  189. esplink-1.0.2/tests/test_watch.py +148 -0
  190. esplink-1.0.2/tests/test_wifi_link.py +170 -0
  191. esplink-1.0.2/uv.lock +2250 -0
@@ -0,0 +1,227 @@
1
+ # Written by python/examples/devices/i2s_record.py when you run it.
2
+ python/examples/devices/recording.wav
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[codz]
7
+ *$py.class
8
+
9
+ # Prebuilt firmware image bundled into the wheel — regenerated by
10
+ # tools/build_firmware.py (locally / in CI) before the package is built.
11
+ python/esplink/firmware/*.bin
12
+ # Isolated arduino-cli build scratch for tools/build_firmware.py (ELF/map/bins).
13
+ tools/.fw-build/
14
+
15
+ # C extensions
16
+ *.so
17
+
18
+ # Distribution / packaging
19
+ .Python
20
+ build/
21
+ develop-eggs/
22
+ dist/
23
+ downloads/
24
+ eggs/
25
+ .eggs/
26
+ lib/
27
+ lib64/
28
+ parts/
29
+ sdist/
30
+ var/
31
+ wheels/
32
+ share/python-wheels/
33
+ *.egg-info/
34
+ .installed.cfg
35
+ *.egg
36
+ MANIFEST
37
+
38
+ # PyInstaller
39
+ # Usually these files are written by a python script from a template
40
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
41
+ *.manifest
42
+ *.spec
43
+
44
+ # Installer logs
45
+ pip-log.txt
46
+ pip-delete-this-directory.txt
47
+
48
+ # Unit test / coverage reports
49
+ htmlcov/
50
+ .tox/
51
+ .nox/
52
+ .coverage
53
+ .coverage.*
54
+ .cache
55
+ nosetests.xml
56
+ coverage.xml
57
+ *.cover
58
+ *.py.cover
59
+ .hypothesis/
60
+ .pytest_cache/
61
+ cover/
62
+
63
+ # Translations
64
+ *.mo
65
+ *.pot
66
+
67
+ # Django stuff:
68
+ *.log
69
+ local_settings.py
70
+ db.sqlite3
71
+ db.sqlite3-journal
72
+
73
+ # Flask stuff:
74
+ instance/
75
+ .webassets-cache
76
+
77
+ # Scrapy stuff:
78
+ .scrapy
79
+
80
+ # Sphinx documentation
81
+ docs/_build/
82
+
83
+ # PyBuilder
84
+ .pybuilder/
85
+ target/
86
+
87
+ # Jupyter Notebook
88
+ .ipynb_checkpoints
89
+
90
+ # IPython
91
+ profile_default/
92
+ ipython_config.py
93
+
94
+ # pyenv
95
+ # For a library or package, you might want to ignore these files since the code is
96
+ # intended to run in multiple environments; otherwise, check them in:
97
+ # .python-version
98
+
99
+ # pipenv
100
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
101
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
102
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
103
+ # install all needed dependencies.
104
+ # Pipfile.lock
105
+
106
+ # UV
107
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # uv.lock
111
+
112
+ # poetry
113
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
114
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
115
+ # commonly ignored for libraries.
116
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
117
+ # poetry.lock
118
+ # poetry.toml
119
+
120
+ # pdm
121
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
122
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
123
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
124
+ # pdm.lock
125
+ # pdm.toml
126
+ .pdm-python
127
+ .pdm-build/
128
+
129
+ # pixi
130
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
131
+ # pixi.lock
132
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
133
+ # in the .venv directory. It is recommended not to include this directory in version control.
134
+ .pixi
135
+
136
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
137
+ __pypackages__/
138
+
139
+ # Celery stuff
140
+ celerybeat-schedule
141
+ celerybeat.pid
142
+
143
+ # Redis
144
+ *.rdb
145
+ *.aof
146
+ *.pid
147
+
148
+ # RabbitMQ
149
+ mnesia/
150
+ rabbitmq/
151
+ rabbitmq-data/
152
+
153
+ # ActiveMQ
154
+ activemq-data/
155
+
156
+ # SageMath parsed files
157
+ *.sage.py
158
+
159
+ # Environments
160
+ .env
161
+ .envrc
162
+ .venv
163
+ env/
164
+ venv/
165
+ ENV/
166
+ env.bak/
167
+ venv.bak/
168
+
169
+ # Spyder project settings
170
+ .spyderproject
171
+ .spyproject
172
+
173
+ # Rope project settings
174
+ .ropeproject
175
+
176
+ # mkdocs documentation
177
+ /site
178
+
179
+ # mypy
180
+ .mypy_cache/
181
+ .dmypy.json
182
+ dmypy.json
183
+
184
+ # Pyre type checker
185
+ .pyre/
186
+
187
+ # pytype static type analyzer
188
+ .pytype/
189
+
190
+ # Cython debug symbols
191
+ cython_debug/
192
+
193
+ # PyCharm
194
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
195
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
196
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
197
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
198
+ # .idea/
199
+
200
+ # Abstra
201
+ # Abstra is an AI-powered process automation framework.
202
+ # Ignore directories containing user credentials, local state, and settings.
203
+ # Learn more at https://abstra.io/docs
204
+ .abstra/
205
+
206
+ # Visual Studio Code
207
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
208
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
209
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
210
+ # you could uncomment the following to ignore the entire vscode folder
211
+ # .vscode/
212
+ # Temporary file for partial code execution
213
+ tempCodeRunnerFile.py
214
+
215
+ # Ruff stuff:
216
+ .ruff_cache/
217
+
218
+ # PyPI configuration file
219
+ .pypirc
220
+
221
+ # Marimo
222
+ marimo/_static/
223
+ marimo/_lsp/
224
+ __marimo__/
225
+
226
+ # Streamlit
227
+ .streamlit/secrets.toml
@@ -0,0 +1 @@
1
+ 3.14
esplink-1.0.2/PKG-INFO ADDED
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.5
2
+ Name: esplink
3
+ Version: 1.0.2
4
+ Summary: Control every ESP32 peripheral from Python over USB serial or Bluetooth — GPIO, ADC, DAC, PWM, touch, I2C, SPI, UART, Wi-Fi sockets, BLE, ESP-NOW, RMT, 1-Wire, CAN, I2S, files, NVS, OTA
5
+ Project-URL: Homepage, https://github.com/HamzaYslmn/esplink
6
+ Author: HamzaYslmn
7
+ Keywords: ble,bridge,esp32,espnow,firmata,gpio,i2c,raspberry-pi,serial,spi
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: bleak>=3.0.2
10
+ Requires-Dist: pyserial>=3.5
11
+ Provides-Extra: all
12
+ Requires-Dist: bleak>=0.22; extra == 'all'
13
+ Requires-Dist: esptool>=5; extra == 'all'
14
+ Requires-Dist: fastmcp>=2.3; extra == 'all'
15
+ Requires-Dist: pillow>=10; extra == 'all'
16
+ Provides-Extra: ble
17
+ Requires-Dist: bleak>=0.22; extra == 'ble'
18
+ Provides-Extra: flash
19
+ Requires-Dist: esptool>=5; extra == 'flash'
20
+ Provides-Extra: mcp
21
+ Requires-Dist: fastmcp>=2.3; extra == 'mcp'
22
+ Provides-Extra: oled
23
+ Requires-Dist: pillow>=10; extra == 'oled'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # esplink
27
+
28
+ Control every ESP32 peripheral from Python over USB serial — GPIO, PWM, ADC,
29
+ DAC, touch, I2C, SPI, UART, Wi-Fi (with TCP/UDP sockets through the ESP32
30
+ radio) and BLE. Flash the bridge firmware once, then it's all Python.
31
+
32
+ ```python
33
+ from esplink import Bridge
34
+
35
+ with Bridge() as esp: # USB first, then Bluetooth, then Wi-Fi
36
+ esp.gpio.mode(2, "output")
37
+ esp.gpio.write(2, 1)
38
+ print(esp.adc.read_mv(34), "mV")
39
+ esp.i2c.init(sda=21, scl=22)
40
+ print(esp.i2c.scan())
41
+ esp.wifi.connect("ssid", "password")
42
+ status, body = esp.net.http_get("http://example.com/")
43
+ ```
44
+
45
+ - On-device rules react without the host in the loop
46
+ (`esp.watch.add("adc", pin=34, above=3200, do=("pwm", 13, 0))`), and
47
+ `esp.radio_off()` silences Wi-Fi + Bluetooth entirely for jitter-sensitive
48
+ realtime work (frees ~110 KB heap, unlocks the ADC2 pins).
49
+ - Firmware (flash once with Arduino IDE) and full docs:
50
+ **<https://github.com/HamzaYslmn/esplink>**
51
+ - Works on Raspberry Pi OS, Linux, Windows, macOS (Python ≥ 3.11, pyserial).
52
+ - `esplink` CLI: connection info; `esplink ports`: list candidate ports.
@@ -0,0 +1,27 @@
1
+ # esplink
2
+
3
+ Control every ESP32 peripheral from Python over USB serial — GPIO, PWM, ADC,
4
+ DAC, touch, I2C, SPI, UART, Wi-Fi (with TCP/UDP sockets through the ESP32
5
+ radio) and BLE. Flash the bridge firmware once, then it's all Python.
6
+
7
+ ```python
8
+ from esplink import Bridge
9
+
10
+ with Bridge() as esp: # USB first, then Bluetooth, then Wi-Fi
11
+ esp.gpio.mode(2, "output")
12
+ esp.gpio.write(2, 1)
13
+ print(esp.adc.read_mv(34), "mV")
14
+ esp.i2c.init(sda=21, scl=22)
15
+ print(esp.i2c.scan())
16
+ esp.wifi.connect("ssid", "password")
17
+ status, body = esp.net.http_get("http://example.com/")
18
+ ```
19
+
20
+ - On-device rules react without the host in the loop
21
+ (`esp.watch.add("adc", pin=34, above=3200, do=("pwm", 13, 0))`), and
22
+ `esp.radio_off()` silences Wi-Fi + Bluetooth entirely for jitter-sensitive
23
+ realtime work (frees ~110 KB heap, unlocks the ADC2 pins).
24
+ - Firmware (flash once with Arduino IDE) and full docs:
25
+ **<https://github.com/HamzaYslmn/esplink>**
26
+ - Works on Raspberry Pi OS, Linux, Windows, macOS (Python ≥ 3.11, pyserial).
27
+ - `esplink` CLI: connection info; `esplink ports`: list candidate ports.
@@ -0,0 +1,88 @@
1
+ """esplink — control every ESP32 peripheral from Python over USB or Bluetooth.
2
+
3
+ Flash the firmware once (Arduino library "esplink"), then:
4
+
5
+ from esplink import Bridge
6
+
7
+ with Bridge() as esp: # USB first, then Bluetooth, then Wi-Fi
8
+ esp.gpio.mode(2, "output")
9
+ esp.gpio.write(2, 1)
10
+ print(esp.adc.read(34))
11
+ print(esp.i2c.scan())
12
+ esp.wifi.connect("ssid", "password")
13
+ sock = esp.net.tcp_connect("example.com", 80) # TCP through the ESP32 radio
14
+
15
+ esp.nvs.set("runs", 1) # on-board key/value storage
16
+ esp.fs.mount("littlefs") # on-board files
17
+ esp.can.begin(tx=21, rx=22) # CAN bus
18
+ esp.ota.flash("new.bin") # update the firmware over the link
19
+
20
+ # device drivers in pure Python (RMT / 1-Wire / I2C primitives), all under
21
+ # esplink.drivers: dht, oled, neopixel, ds18b20, hcsr04, ir, stepper, ...
22
+ # ...or write/register your own — esp.<name>(...) — see esplink.drivers
23
+ # and docs/DRIVERS.md ("bring your own driver").
24
+ """
25
+ from .bridge import Bridge, BridgeSet, Info
26
+ from .constants import Cap, ChipModel, Status
27
+ from .drivers import driver_names, register_driver
28
+ from .manager import BridgeManager, connect, disconnect_all
29
+ from .errors import (
30
+ AuthError,
31
+ BridgeError,
32
+ BridgeTimeoutError,
33
+ NoDeviceError,
34
+ ProtocolError,
35
+ RemoteError,
36
+ UnsupportedError,
37
+ )
38
+ from .transports import find_ports, find_wifi_devices
39
+ from .gpio import EdgeEvent
40
+ from .watch import WatchEvent
41
+
42
+
43
+ def find_ble_devices(timeout: float = 5.0):
44
+ """Scan for bridges advertising over Bluetooth (needs the [ble] extra)."""
45
+ from .transports.ble import find_ble_devices as _scan
46
+
47
+ return _scan(timeout)
48
+
49
+
50
+ def __getattr__(name):
51
+ # Lazy: importing AsyncBridge pulls in asyncio, so defer it until first use
52
+ # rather than charging every sync/USB-only script for the import.
53
+ if name == "AsyncBridge":
54
+ from .aio import AsyncBridge
55
+
56
+ return AsyncBridge
57
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
58
+
59
+ __version__ = "1.0.1"
60
+
61
+ # Grouped by role, not alphabetised: this list is the shape of the public API.
62
+ __all__ = [
63
+ "Bridge",
64
+ "AsyncBridge",
65
+ "BridgeSet",
66
+ "BridgeManager",
67
+ "connect",
68
+ "disconnect_all",
69
+ "register_driver",
70
+ "driver_names",
71
+ "find_ble_devices",
72
+ "find_wifi_devices",
73
+ "Info",
74
+ "Cap",
75
+ "ChipModel",
76
+ "Status",
77
+ "AuthError",
78
+ "BridgeError",
79
+ "BridgeTimeoutError",
80
+ "NoDeviceError",
81
+ "ProtocolError",
82
+ "RemoteError",
83
+ "UnsupportedError",
84
+ "find_ports",
85
+ "EdgeEvent",
86
+ "WatchEvent",
87
+ "__version__",
88
+ ]
@@ -0,0 +1,73 @@
1
+ """Library logger: "[esplink]" prefix + uvicorn-style colored level prefix.
2
+
3
+ Mirrors uvicorn.logging.DefaultFormatter (same colors, same ``LEVEL:``
4
+ padding) without depending on uvicorn: colors auto-enable only when stderr
5
+ is a terminal that supports ANSI, and NO_COLOR is honored.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ import sys
12
+
13
+ # uvicorn's palette: DEBUG cyan, INFO green, WARNING yellow, ERROR red,
14
+ # CRITICAL bright red.
15
+ _LEVEL_COLORS = {
16
+ logging.DEBUG: 36,
17
+ logging.INFO: 32,
18
+ logging.WARNING: 33,
19
+ logging.ERROR: 31,
20
+ logging.CRITICAL: 91,
21
+ }
22
+
23
+
24
+ def _supports_color(stream) -> bool:
25
+ if os.environ.get("NO_COLOR"):
26
+ return False
27
+ if not getattr(stream, "isatty", lambda: False)():
28
+ return False
29
+ if sys.platform == "win32":
30
+ # Windows Terminal and VS Code already have VT processing enabled.
31
+ # For the classic Windows console, try to enable it here.
32
+ # If the call fails, ANSI escape sequences are not supported.
33
+ import ctypes
34
+
35
+ kernel32 = ctypes.windll.kernel32
36
+ handle = kernel32.GetStdHandle(-12) # STD_ERROR_HANDLE
37
+ mode = ctypes.c_uint32()
38
+ if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
39
+ return False
40
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
41
+ return bool(kernel32.SetConsoleMode(
42
+ handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
43
+ return True
44
+
45
+
46
+ class _Formatter(logging.Formatter):
47
+ def __init__(self, use_colors: bool):
48
+ super().__init__("[esplink] %(levelprefix)s %(message)s")
49
+ self.use_colors = use_colors
50
+
51
+ def format(self, record: logging.LogRecord) -> str:
52
+ pad = " " * max(0, 8 - len(record.levelname)) # right-pad level name to 8 chars, matching uvicorn's alignment
53
+ if self.use_colors:
54
+ color = _LEVEL_COLORS.get(record.levelno, 0)
55
+ record.levelprefix = (
56
+ f"\x1b[{color}m{record.levelname}\x1b[0m:{pad}")
57
+ else:
58
+ record.levelprefix = f"{record.levelname}:{pad}"
59
+ return super().format(record)
60
+
61
+
62
+ log = logging.getLogger("esplink")
63
+ if not log.handlers:
64
+ if os.environ.get("ESPLINK_DEBUG"):
65
+ # Enable frame-level tracing (every request and response with command
66
+ # names) without modifying the script: ESPLINK_DEBUG=1 python app.py
67
+ log.setLevel(logging.DEBUG)
68
+ elif log.level == logging.NOTSET: # respect a level the app set first
69
+ log.setLevel(logging.INFO)
70
+ _handler = logging.StreamHandler(sys.stderr)
71
+ _handler.setFormatter(_Formatter(_supports_color(sys.stderr)))
72
+ log.addHandler(_handler)
73
+ log.propagate = False
@@ -0,0 +1,225 @@
1
+ """asyncio facade: ``await`` every bridge call and fan out concurrent I/O.
2
+
3
+ The ESP32 firmware is *already* concurrent — requests carry a sequence number,
4
+ many can be in flight at once, and a slow call on one task never stalls the
5
+ fast lane (see ``examples/basics/rtos_concurrency.py``). ``AsyncBridge`` simply
6
+ exposes that concurrency to ``asyncio`` so you can ``await`` bridge calls and
7
+ run them together with ``asyncio.gather``, instead of juggling threads yourself:
8
+
9
+ import asyncio
10
+ from esplink import AsyncBridge
11
+
12
+ async def main():
13
+ async with AsyncBridge(ble=False) as esp:
14
+ await esp.gpio.mode(2, "output")
15
+ await esp.gpio.write(2, 1)
16
+ # both reads are issued together and overlap on the wire:
17
+ t, h = await asyncio.gather(esp.adc.read(34), esp.adc.read(35))
18
+ print(t, h)
19
+
20
+ asyncio.run(main())
21
+
22
+ Edge interrupts (``gpio.watch``) become an async stream — no callback thread:
23
+
24
+ async with AsyncBridge() as esp:
25
+ await esp.gpio.mode(4, "input_pullup")
26
+ await esp.gpio.watch(4, edge="falling", debounce_ms=30)
27
+ async for ev in esp.gpio_events():
28
+ print("pressed", ev.pin, ev.millis)
29
+
30
+ How it works (and what to expect): the firmware speaks a synchronous
31
+ request/response protocol and the underlying ``Bridge`` is fully thread-safe,
32
+ so each awaited call runs the matching ``Bridge`` method in a worker thread
33
+ (``asyncio.to_thread``). Calls awaited together genuinely overlap on the link
34
+ because ``Bridge`` pipelines concurrent requests and correlates replies by
35
+ sequence number — ``await`` here is about *the host not blocking*, not about
36
+ moving work onto the ESP32 (the firmware's task split already does that).
37
+
38
+ For tight inner loops or fire-and-forget streaming (e.g. pushing OLED frames),
39
+ the plain synchronous ``Bridge`` is still the right tool — there is nothing to
40
+ gain from awaiting each byte, and a thread per call would only add overhead.
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import asyncio
45
+ import functools
46
+
47
+ from . import constants as C
48
+ from .bridge import Bridge
49
+ from .gpio import EdgeEvent
50
+ from .watch import WatchEvent
51
+
52
+
53
+ def _to_coro(fn):
54
+ """Wrap a blocking bridge callable so it runs in a thread and can be awaited."""
55
+ @functools.wraps(fn)
56
+ async def _call(*args, **kwargs):
57
+ return await asyncio.to_thread(fn, *args, **kwargs)
58
+ return _call
59
+
60
+
61
+ def _async_attr(attr):
62
+ """Adapt a bound bridge attribute for async use: callables become coroutines
63
+ that run in a worker thread; plain values (info, caps, max_write, ...) pass
64
+ through. One uniform rule — nothing has to enumerate which calls touch the
65
+ wire, so a new Bridge method is awaitable automatically."""
66
+ return _to_coro(attr) if callable(attr) else attr
67
+
68
+
69
+ class _AsyncSubApi:
70
+ """Proxies a sync sub-API (``esp.gpio``, ``esp.adc``, ...): every method
71
+ becomes awaitable; plain attributes (e.g. ``i2c.max_write``) pass through."""
72
+
73
+ def __init__(self, target):
74
+ self._target = target
75
+
76
+ def __getattr__(self, name):
77
+ if name.startswith("_"): # don't expose the sub-API's internals
78
+ raise AttributeError(name)
79
+ return _async_attr(getattr(self._target, name))
80
+
81
+ def __dir__(self):
82
+ return dir(self._target)
83
+
84
+
85
+ class _EventStream:
86
+ """Async iterator over a firmware event, fed from the reader thread.
87
+
88
+ Registered with ``Bridge.on_event``; each event payload is handed to the
89
+ asyncio loop via ``call_soon_threadsafe`` and surfaces through ``async for``.
90
+ """
91
+
92
+ def __init__(self, bridge: Bridge, cmd: int, loop, parse=None, maxsize: int = 0):
93
+ self._bridge = bridge
94
+ self._cmd = cmd
95
+ self._loop = loop
96
+ self._parse = parse
97
+ self._queue: asyncio.Queue = asyncio.Queue(maxsize)
98
+ bridge.on_event(cmd, self._on_event)
99
+
100
+ def _on_event(self, payload: bytes) -> None:
101
+ item = self._parse(payload) if self._parse else payload
102
+ if item is None: # parser rejected a malformed/short payload
103
+ return
104
+ # Hop from the reader thread onto the asyncio loop; drop if the queue is
105
+ # full (a bounded maxsize gives back-pressure without blocking the reader).
106
+ self._loop.call_soon_threadsafe(self._offer, item)
107
+
108
+ def _offer(self, item) -> None:
109
+ try:
110
+ self._queue.put_nowait(item)
111
+ except asyncio.QueueFull:
112
+ pass
113
+
114
+ def __aiter__(self):
115
+ return self
116
+
117
+ async def __anext__(self):
118
+ return await self._queue.get()
119
+
120
+ async def aclose(self) -> None:
121
+ """Stop the stream and unregister it from the bridge (call when done, or
122
+ just break out of the ``async for`` and let it be garbage-collected)."""
123
+ self._bridge.off_event(self._cmd, self._on_event)
124
+
125
+
126
+ class AsyncBridge:
127
+ """``asyncio`` wrapper around :class:`Bridge`.
128
+
129
+ Accepts the same arguments as ``Bridge``. Connection happens off the event
130
+ loop, so use it as an async context manager (or ``await .connect()``):
131
+
132
+ >>> async with AsyncBridge(ble=False) as esp:
133
+ ... await esp.gpio.write(2, 1)
134
+
135
+ The wrapped synchronous bridge is available as ``.bridge`` for anything not
136
+ surfaced here (events, ``on_event``, device drivers).
137
+ """
138
+
139
+ def __init__(self, *args, **kwargs):
140
+ # Bridge() connects (and blocks) in its constructor, so defer it. Args
141
+ # are passed straight through, so AsyncBridge("relays") selects a board
142
+ # exactly the way Bridge("relays") does.
143
+ self._args, self._kwargs = args, kwargs
144
+ self._bridge = None
145
+ self._owned = True # we opened it, so we close it
146
+
147
+ @classmethod
148
+ def wrap(cls, bridge: Bridge) -> AsyncBridge:
149
+ """Wrap an already-connected Bridge for ``await`` use, sharing its link.
150
+
151
+ Lets an async app reuse the one shared connection instead of opening a
152
+ second: ``esp = AsyncBridge.wrap(esplink.connect(ble=False))``.
153
+ ``close()`` is then a no-op for the wrapper — whoever owns the Bridge
154
+ (e.g. the BridgeManager behind connect()) closes it.
155
+ """
156
+ self = cls.__new__(cls)
157
+ self._args, self._kwargs = (), {}
158
+ self._bridge = bridge
159
+ self._owned = False
160
+ return self
161
+
162
+ @property
163
+ def bridge(self) -> Bridge:
164
+ """The underlying synchronous :class:`Bridge` (for events, ``on_event``,
165
+ device drivers, or any method not surfaced here). Raises if not yet
166
+ connected."""
167
+ if self._bridge is None:
168
+ raise RuntimeError(
169
+ "AsyncBridge is not connected — use `async with AsyncBridge(...)` "
170
+ "or `await bridge.connect()`"
171
+ )
172
+ return self._bridge
173
+
174
+ async def connect(self) -> AsyncBridge:
175
+ """Open the link off the event loop (Bridge() blocks while connecting).
176
+ Usually you don't call this directly — use ``async with AsyncBridge():``."""
177
+ if self._bridge is None:
178
+ self._bridge = await asyncio.to_thread(Bridge, *self._args,
179
+ **self._kwargs)
180
+ return self
181
+
182
+ async def close(self) -> None:
183
+ """Close the link off the event loop. No-op for a wrapper created with
184
+ :meth:`wrap` (whoever owns the underlying Bridge closes it)."""
185
+ if self._bridge is not None and self._owned:
186
+ await asyncio.to_thread(self._bridge.close)
187
+ self._bridge = None
188
+
189
+ async def __aenter__(self) -> AsyncBridge:
190
+ return await self.connect()
191
+
192
+ async def __aexit__(self, *exc) -> None:
193
+ await self.close()
194
+
195
+ def events(self, cmd: int, *, parse=None, maxsize: int = 0) -> _EventStream:
196
+ """``async for`` over a raw firmware event ``cmd`` (payloads are bytes
197
+ unless ``parse`` is given). See ``gpio_events()`` for the GPIO edge case."""
198
+ return _EventStream(self.bridge, cmd, asyncio.get_running_loop(),
199
+ parse=parse, maxsize=maxsize)
200
+
201
+ def gpio_events(self, *, maxsize: int = 0) -> _EventStream:
202
+ """``async for ev in esp.gpio_events()`` yields :class:`EdgeEvent`s from
203
+ every pin armed with ``await esp.gpio.watch(pin, ...)``."""
204
+ return self.events(C.GPIO_EDGE_EVT, parse=EdgeEvent.parse, maxsize=maxsize)
205
+
206
+ def watch_events(self, *, maxsize: int = 0) -> _EventStream:
207
+ """``async for ev in esp.watch_events()`` yields :class:`WatchEvent`s
208
+ from every rule defined with ``await esp.watch.add(...)`` — the polled,
209
+ non-interrupt, threshold-based events."""
210
+ return self.events(C.WATCH_EVT, parse=WatchEvent.parse, maxsize=maxsize)
211
+
212
+ def __getattr__(self, name):
213
+ if name.startswith("_"): # never proxy private/dunder lookups
214
+ raise AttributeError(name)
215
+ attr = getattr(self.bridge, name)
216
+ if name in Bridge._SUBAPIS:
217
+ proxy = _AsyncSubApi(attr)
218
+ setattr(self, name, proxy) # cache: this name skips __getattr__ next time
219
+ return proxy
220
+ return _async_attr(attr)
221
+
222
+ def __dir__(self):
223
+ extra = (*Bridge._SUBAPIS, "bridge", "connect", "close",
224
+ "events", "gpio_events", "watch_events")
225
+ return [*super().__dir__(), *extra]