esp-test-utils 0.2.0__tar.gz → 0.2.1__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 (112) hide show
  1. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/.gitignore +3 -0
  2. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/.pre-commit-config.yaml +7 -0
  3. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/CHANGELOG.md +9 -0
  4. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/PKG-INFO +3 -1
  5. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esp_test_utils.egg-info/PKG-INFO +3 -1
  6. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esp_test_utils.egg-info/SOURCES.txt +9 -0
  7. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esp_test_utils.egg-info/requires.txt +2 -0
  8. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/dut_base.py +10 -5
  9. esp_test_utils-0.2.1/esptest/db/runners.py +199 -0
  10. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/scripts/downbin.py +20 -8
  11. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/tools/download_bin.py +53 -4
  12. esp_test_utils-0.2.1/esptest/tools/http_download.py +61 -0
  13. esp_test_utils-0.2.1/esptest/utility/gen_esp32part.py +808 -0
  14. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/utility/parse_bin_path.py +125 -44
  15. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/pyproject.toml +7 -0
  16. esp_test_utils-0.2.1/tests/conftest.py +0 -0
  17. esp_test_utils-0.2.1/tests/db/test_db_runners.py +66 -0
  18. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/test_Dut.py +1 -1
  19. esp_test_utils-0.2.1/tests/tools/test_download_file.py +44 -0
  20. esp_test_utils-0.2.1/tests/utility/_files/test-bin.zip +0 -0
  21. esp_test_utils-0.2.1/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +80 -0
  22. esp_test_utils-0.2.1/tests/utility/test_parse_bin_path.py +149 -0
  23. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/.github/.gitkeep +0 -0
  24. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/.github/workflows/pypi-publish.yml +0 -0
  25. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/.gitlab-ci.yml +0 -0
  26. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/CONTRIBUTING.md +0 -0
  27. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/LICENSE +0 -0
  28. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/README.md +0 -0
  29. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/docs/Makefile +0 -0
  30. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/docs/conf.py +0 -0
  31. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/docs/index.rst +0 -0
  32. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/docs/make.bat +0 -0
  33. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esp_test_utils.egg-info/dependency_links.txt +0 -0
  34. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esp_test_utils.egg-info/entry_points.txt +0 -0
  35. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esp_test_utils.egg-info/top_level.txt +0 -0
  36. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/__init__.py +0 -0
  37. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/__main__.py +0 -0
  38. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/__init__.py +0 -0
  39. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/__init__.py +0 -0
  40. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/create_dut.py +0 -0
  41. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/esp_dut.py +0 -0
  42. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/esp_mixin.py +0 -0
  43. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/esp_port.py +0 -0
  44. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/mac_mixin.py +0 -0
  45. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/dut/wrapper.py +0 -0
  46. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/port/__init__.py +0 -0
  47. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/port/base_port.py +0 -0
  48. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/adapter/port/serial_port.py +0 -0
  49. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/all.py +0 -0
  50. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/__init__.py +0 -0
  51. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/compat_typing.py +0 -0
  52. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/data_monitor.py +0 -0
  53. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/decorators.py +0 -0
  54. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/encoding.py +0 -0
  55. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/generator.py +0 -0
  56. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/shell.py +0 -0
  57. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/common/timestamp.py +0 -0
  58. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/config/__init__.py +0 -0
  59. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/config/default_config.py +0 -0
  60. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/config/env_config.py +0 -0
  61. {esp_test_utils-0.2.0/esptest/devices → esp_test_utils-0.2.1/esptest/db}/__init__.py +0 -0
  62. {esp_test_utils-0.2.0/esptest/env → esp_test_utils-0.2.1/esptest/devices}/__init__.py +0 -0
  63. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/devices/attenuator.py +0 -0
  64. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/devices/esp_serial.py +0 -0
  65. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/devices/serial_dut.py +0 -0
  66. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/devices/serial_tools.py +0 -0
  67. {esp_test_utils-0.2.0/esptest/interface → esp_test_utils-0.2.1/esptest/env}/__init__.py +0 -0
  68. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/env/base_env.py +0 -0
  69. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/env/wifi_env.py +0 -0
  70. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/esp_console/__init__.py +0 -0
  71. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/esp_console/wifi_cmd.py +0 -0
  72. {esp_test_utils-0.2.0/esptest/network → esp_test_utils-0.2.1/esptest/interface}/__init__.py +0 -0
  73. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/interface/dut.py +0 -0
  74. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/interface/port.py +0 -0
  75. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/iperf_utility/__init__.py +0 -0
  76. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/iperf_utility/iperf_results.py +0 -0
  77. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/iperf_utility/iperf_test.py +0 -0
  78. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/iperf_utility/iperf_test.test.py +0 -0
  79. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/iperf_utility/line_chart.py +0 -0
  80. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/logger/__init__.py +0 -0
  81. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/logger/logger.py +0 -0
  82. {esp_test_utils-0.2.0/tests → esp_test_utils-0.2.1/esptest/network}/__init__.py +0 -0
  83. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/network/mac.py +0 -0
  84. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/network/netif.py +0 -0
  85. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/network/nic.py +0 -0
  86. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/scripts/list_ports.py +0 -0
  87. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/scripts/monitor.py +0 -0
  88. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/scripts/set_att.py +0 -0
  89. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/tools/copy_bin.py +0 -0
  90. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/esptest/tools/pip_check.py +0 -0
  91. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/example/jap_test.py +0 -0
  92. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/example/restart_test.py +0 -0
  93. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/setup.cfg +0 -0
  94. /esp_test_utils-0.2.0/tests/conftest.py → /esp_test_utils-0.2.1/tests/__init__.py +0 -0
  95. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/basic/test_decorators.py +0 -0
  96. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/basic/test_network.py +0 -0
  97. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
  98. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
  99. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/esp_console/conftest.py +0 -0
  100. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/esp_console/test_WifiCmd.py +0 -0
  101. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
  102. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
  103. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
  104. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
  105. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/iperf_utility/test_chart.py +0 -0
  106. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/iperf_utility/test_iperf_results.py +0 -0
  107. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/iperf_utility/test_iperf_util.py +0 -0
  108. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/test_EnvConfig.py +0 -0
  109. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/test_common.py +0 -0
  110. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/test_import.py +0 -0
  111. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tests/tools/test_pip_check.py +0 -0
  112. {esp_test_utils-0.2.0 → esp_test_utils-0.2.1}/tools/ci/check_dev_version.py +0 -0
@@ -14,10 +14,13 @@ gl-codequality.json
14
14
  /dist/
15
15
  *.egg-info/
16
16
 
17
+ # tests
17
18
  /log/
18
19
  /logs/
19
20
  dut_logs/
20
21
  dut.log
22
+ tests/utility/_files/test-bin
23
+
21
24
 
22
25
  # docs
23
26
  /docs/*.rst
@@ -35,6 +35,7 @@ repos:
35
35
  - 'types-psutil'
36
36
  - 'types-PyYAML'
37
37
  - 'typing_extensions'
38
+ - 'sqlalchemy'
38
39
 
39
40
  - repo: https://github.com/espressif/conventional-precommit-linter
40
41
  rev: v1.10.0
@@ -48,3 +49,9 @@ repos:
48
49
  - id: codespell
49
50
  args: ["--write-changes"]
50
51
  additional_dependencies: [tomli]
52
+
53
+ exclude: |
54
+ (
55
+ ^docs/.*
56
+ | gen_esp32part\.py$
57
+ )
@@ -1,3 +1,12 @@
1
+ ## v0.2.1 (2025-09-24)
2
+
3
+
4
+ - feat(utility): support to flash bin to encrypted device
5
+ - feat: add runners database
6
+ - feat: add default gen part tool
7
+ - feat(utility): support to check flash enc from bin path
8
+ - fix: get baud from bin path sdkconfig file
9
+
1
10
  ## v0.2.0 (2025-07-16)
2
11
 
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -228,7 +228,9 @@ Requires-Dist: pyserial
228
228
  Requires-Dist: PyYAML
229
229
  Requires-Dist: pexpect
230
230
  Requires-Dist: pyusb
231
+ Requires-Dist: esptool
231
232
  Requires-Dist: packaging
233
+ Requires-Dist: sqlalchemy
232
234
  Requires-Dist: typing_extensions; python_version < "3.11"
233
235
  Provides-Extra: idfci
234
236
  Requires-Dist: pyecharts; extra == "idfci"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -228,7 +228,9 @@ Requires-Dist: pyserial
228
228
  Requires-Dist: PyYAML
229
229
  Requires-Dist: pexpect
230
230
  Requires-Dist: pyusb
231
+ Requires-Dist: esptool
231
232
  Requires-Dist: packaging
233
+ Requires-Dist: sqlalchemy
232
234
  Requires-Dist: typing_extensions; python_version < "3.11"
233
235
  Provides-Extra: idfci
234
236
  Requires-Dist: pyecharts; extra == "idfci"
@@ -44,6 +44,8 @@ esptest/common/timestamp.py
44
44
  esptest/config/__init__.py
45
45
  esptest/config/default_config.py
46
46
  esptest/config/env_config.py
47
+ esptest/db/__init__.py
48
+ esptest/db/runners.py
47
49
  esptest/devices/__init__.py
48
50
  esptest/devices/attenuator.py
49
51
  esptest/devices/esp_serial.py
@@ -74,7 +76,9 @@ esptest/scripts/monitor.py
74
76
  esptest/scripts/set_att.py
75
77
  esptest/tools/copy_bin.py
76
78
  esptest/tools/download_bin.py
79
+ esptest/tools/http_download.py
77
80
  esptest/tools/pip_check.py
81
+ esptest/utility/gen_esp32part.py
78
82
  esptest/utility/parse_bin_path.py
79
83
  example/jap_test.py
80
84
  example/restart_test.py
@@ -86,6 +90,7 @@ tests/test_common.py
86
90
  tests/test_import.py
87
91
  tests/basic/test_decorators.py
88
92
  tests/basic/test_network.py
93
+ tests/db/test_db_runners.py
89
94
  tests/esp_console/conftest.py
90
95
  tests/esp_console/test_WifiCmd.py
91
96
  tests/esp_console/_files/wifi_cmd_connected_1.log
@@ -97,5 +102,9 @@ tests/iperf_utility/_files/dut_iperf_rx1.log
97
102
  tests/iperf_utility/_files/dut_iperf_rx2.log
98
103
  tests/iperf_utility/_files/pc_iperf_rx.log
99
104
  tests/iperf_utility/_files/pc_iperf_rx2.log
105
+ tests/tools/test_download_file.py
100
106
  tests/tools/test_pip_check.py
107
+ tests/utility/test_parse_bin_path.py
108
+ tests/utility/_files/test-bin.zip
109
+ tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig
101
110
  tools/ci/check_dev_version.py
@@ -3,7 +3,9 @@ pyserial
3
3
  PyYAML
4
4
  pexpect
5
5
  pyusb
6
+ esptool
6
7
  packaging
8
+ sqlalchemy
7
9
 
8
10
  [:python_version < "3.11"]
9
11
  typing_extensions
@@ -27,7 +27,8 @@ DEFAULT_SERIAL_CONFIGS = {'timeout': 0.005}
27
27
  class DutConfig:
28
28
  name: str = '' # default = dut name / port name
29
29
  device: str = '' # log serial device, eg: '/dev/ttyUSB0', 'COM3', etc.
30
- baudrate: int = 0 # 0: get from bin path or 115200
30
+ baudrate: int = 0 # console baudrate, 0: get from bin path or 115200
31
+ baudrate_from_bin_path: bool = True # always get baudrate from bin path if bin_path is set
31
32
  serial_configs: t.Optional[t.Dict[str, t.Any]] = None # serial configs, eg: {'bytesize': 8, 'timeout': 0.1}
32
33
  # capabilities
33
34
  support_esptool: bool = False # esp port or serial port
@@ -71,10 +72,14 @@ class DutConfig:
71
72
  # bin_path and get variables from bin path
72
73
  if self.bin_path:
73
74
  self.bin_path = Path(self.bin_path).expanduser().resolve()
74
- self.esptool_stub = ParseBinPath(self.bin_path).stub
75
- self.esptool_chip = ParseBinPath(self.bin_path).chip
76
- if not self.baudrate:
77
- self.baudrate = get_baud_from_bin_path(self.bin_path) or 115200
75
+ parsed_bin = ParseBinPath(self.bin_path)
76
+ self.esptool_stub = parsed_bin.stub
77
+ self.esptool_chip = parsed_bin.chip
78
+ if self.baudrate_from_bin_path:
79
+ # baudrate from bin path is much reliable than the specified one for uart0
80
+ self.baudrate = get_baud_from_bin_path(self.bin_path) or self.baudrate or 115200
81
+ if not self.baudrate:
82
+ self.baudrate = 115200 # set default baudrate to 115200
78
83
  # download device
79
84
  if not self.download_device:
80
85
  self.download_device = self.device
@@ -0,0 +1,199 @@
1
+ import contextlib
2
+ import csv
3
+ from typing import Any, Dict, Generator, List, Optional
4
+
5
+ from sqlalchemy import JSON, Engine, ForeignKey, Integer, String
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship
7
+
8
+
9
+ class Base(DeclarativeBase):
10
+ pass
11
+
12
+
13
+ class User(Base):
14
+ __tablename__ = 'user'
15
+
16
+ id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
17
+ name: Mapped[str] = mapped_column(String(50), unique=True)
18
+ email: Mapped[Optional[str]] = mapped_column(String(50))
19
+ runners: Mapped[List['Runner']] = relationship(back_populates='user')
20
+
21
+
22
+ class Runner(Base):
23
+ __tablename__ = 'runner'
24
+
25
+ id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
26
+ mac: Mapped[str] = mapped_column(String(50), unique=True)
27
+ ip: Mapped[str] = mapped_column(String(100), unique=True)
28
+ vlan: Mapped[Optional[int]] = mapped_column(Integer)
29
+ switch_info: Mapped[Optional[str]] = mapped_column(String) # ip-port
30
+ name: Mapped[str] = mapped_column(String(50))
31
+ owner: Mapped[Optional[str]] = mapped_column(ForeignKey('user.name'), nullable=True)
32
+ tags: Mapped[Optional[List[str]]] = mapped_column(JSON) # only PostgreSQL supports ARRAY(String)
33
+ raw_data: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON)
34
+ description: Mapped[Optional[str]] = mapped_column(String)
35
+ # Link to user table
36
+ user: Mapped[User] = relationship(back_populates='runners')
37
+
38
+ def __str__(self) -> str:
39
+ tags_str = ','.join(self.tags) if self.tags else ''
40
+ return (
41
+ f'Runner('
42
+ f'id={self.id},'
43
+ f'mac={self.mac},'
44
+ f'ip={self.ip},'
45
+ f'vlan={self.vlan},'
46
+ f'switch_info={self.switch_info},'
47
+ f'name={self.name},'
48
+ f'owner={self.owner},'
49
+ f'tags="{tags_str}",'
50
+ f'raw_data={self.raw_data},'
51
+ f'description="{self.description}"'
52
+ ')'
53
+ )
54
+
55
+
56
+ class RunnerDB:
57
+ def __init__(self, engine: Engine) -> None:
58
+ self.engine = engine
59
+ self._session: Optional[Session] = None
60
+ self.__init_db()
61
+
62
+ def __init_db(self) -> None:
63
+ Base.metadata.create_all(self.engine)
64
+
65
+ @contextlib.contextmanager
66
+ def open_session(self, *args: Any, **kwargs: Any) -> Generator['DBSession', None, None]:
67
+ session = DBSession(self, *args, **kwargs)
68
+ yield session
69
+ try:
70
+ session.commit()
71
+ except:
72
+ session.rollback()
73
+ raise
74
+ finally:
75
+ session.close()
76
+
77
+ def to_csv(self, csv_file: str) -> None:
78
+ """
79
+ Export the runners to a CSV file.
80
+
81
+ Args:
82
+ csv_file: The path to the CSV file.
83
+ session: The session to use. If None, use the default session.
84
+ """
85
+ with self.open_session() as session:
86
+ runners = session.all_runners()
87
+ fields = [c.name for c in Runner.__table__.columns]
88
+ with open(csv_file, 'w', encoding='utf-8') as f:
89
+ writer = csv.writer(f)
90
+ writer.writerow(fields)
91
+ for runner in runners:
92
+ writer.writerow([getattr(runner, f) for f in fields])
93
+
94
+
95
+ class DBSession(Session):
96
+ def __init__(self, runner_db: RunnerDB, *args: Any, **kwargs: Any) -> None:
97
+ self.runner_db = runner_db
98
+ super().__init__(self.runner_db.engine, *args, **kwargs)
99
+
100
+ def all_runners(self) -> List[Runner]:
101
+ return self.query(Runner).outerjoin(Runner.user).all()
102
+
103
+ def all_users(self) -> List[User]:
104
+ return self.query(User).outerjoin(User.runners).all()
105
+
106
+ def get_runners_by_mac(self, mac: str) -> List[Runner]:
107
+ return self.query(Runner).filter(Runner.mac == mac).all()
108
+
109
+ def get_runners_by_ip(self, ip: str) -> List[Runner]:
110
+ return self.query(Runner).filter(Runner.ip == ip).all()
111
+
112
+ def get_users_by_name(self, name: str) -> List[User]:
113
+ return self.query(User).outerjoin(Runner.user).filter(User.name == name).all()
114
+
115
+ def get_users_by_email(self, email: str) -> List[User]:
116
+ return self.query(User).outerjoin(Runner.user).filter(User.email == email).all()
117
+
118
+ def remove_runner(self, runner: Runner) -> None:
119
+ """
120
+ Remove a runner.
121
+
122
+ Args:
123
+ runner: The runner to remove.
124
+ session: The session to use. If None, use the default session.
125
+ """
126
+ # session.merge(runner)
127
+ self.delete(runner)
128
+ self.flush()
129
+
130
+ def add_or_update_runner(self, runner: Runner) -> int:
131
+ """
132
+ Add or update a runner.
133
+
134
+ Args:
135
+ runner: The runner to add or update.
136
+ session: The session to use. If None, use the default session.
137
+ """
138
+
139
+ if runner.id is not None:
140
+ self.merge(runner)
141
+ else:
142
+ if self.get_runners_by_mac(runner.mac) or self.get_runners_by_ip(runner.ip):
143
+ raise ValueError(f'Runner with mac {runner.mac} or ip {runner.ip} already exists')
144
+ self.add(runner)
145
+ self.flush()
146
+ return runner.id
147
+
148
+ def add_or_update_user(self, user: User) -> int:
149
+ """
150
+ Add or update an user.
151
+
152
+ Args:
153
+ user: The user to add or update.
154
+ session: The session to use. If None, use the default session.
155
+ """
156
+ if user.id is not None:
157
+ self.merge(user)
158
+ else:
159
+ if self.get_users_by_name(user.name):
160
+ raise ValueError(f'User with name {user.name} already exists')
161
+ self.add(user)
162
+ self.flush()
163
+ return user.id
164
+
165
+
166
+ if __name__ == '__main__':
167
+ from sqlalchemy import create_engine
168
+
169
+ engine1 = create_engine('sqlite:///test.db')
170
+ # engine = create_engine("mysql+pymysql://user:pass@localhost/testdb")
171
+ db1 = RunnerDB(engine1)
172
+ import logging
173
+
174
+ logging.basicConfig(level=logging.DEBUG)
175
+
176
+ with db1.open_session() as session1:
177
+ for i in range(5):
178
+ runner1 = Runner(
179
+ # id=i+1,
180
+ mac=f'11:22:33:44:55:6{i}',
181
+ ip=f'192.168.1.{i}',
182
+ owner=f'test{i // 2}',
183
+ name=f'runner00{i}',
184
+ description=f'test runner 00{i} update 2',
185
+ tags=['esp32', 'generic', 'eco4'],
186
+ raw_data={'envs': {'esp32': [f'wifi_iperf_{i}']}, 'os': 'linux'},
187
+ )
188
+ session1.add_or_update_runner(runner1)
189
+ new_user = User(
190
+ # id=1,
191
+ name='test0'
192
+ )
193
+ session1.add_or_update_user(new_user)
194
+ session1.commit()
195
+ users = session1.query(User).outerjoin(User.runners).all()
196
+ print(users)
197
+ all_runners = session1.all_runners()
198
+ print(','.join([str(r) for r in all_runners]))
199
+ db1.to_csv('runners.csv')
@@ -2,26 +2,34 @@ import argparse
2
2
  import logging
3
3
  import os
4
4
  import re
5
+ import sys
5
6
 
6
7
  from esptest.devices.serial_tools import get_all_serial_ports
7
- from esptest.tools.download_bin import download_bin_to_ports
8
+ from esptest.tools.download_bin import bin_path_to_dir, download_bin_to_ports
8
9
 
9
10
 
10
11
  def main() -> None:
11
- parser = argparse.ArgumentParser(description='Download bin')
12
+ usage_string = '%(prog)s [bin_path] [options]'
13
+ parser = argparse.ArgumentParser(description='Download bin', usage=usage_string)
12
14
  parser.add_argument('bin_path', type=str, nargs='?', help='esp bin path, default ./build')
13
15
  parser.add_argument('-p', '--ports', type=str, nargs='*', help='download port list')
14
16
  parser.add_argument(
15
17
  '--range', type=str, help='port list from range (linux), eg: "0-10" equals to "-p ttyUSB0 ttyUSB1 ... ttyUSB10"'
16
18
  )
17
- parser.add_argument('--all', action='store_true', help='download to all serial ports.')
18
- parser.add_argument('--erase-nvs', type=bool, default=True, help='use --erase-nvs=n to skip erase nvs')
19
- parser.add_argument('--max-workers', type=int, default=True, help='max download threads')
19
+ parser.add_argument(
20
+ '--all', action='store_true', help='download to all serial ports, ignored if "-p/--ports" is specified.'
21
+ )
22
+ parser.add_argument('--no-erase-nvs', dest='erase_nvs', action='store_false', help='skip erase nvs')
23
+ parser.add_argument('--max-workers', type=int, default=0, help='max download threads')
20
24
  args = parser.parse_args()
21
25
 
22
26
  bin_path = args.bin_path or './build'
23
27
  if not os.path.isdir(bin_path):
24
- raise ValueError(f'Can not find bin_path: {bin_path}')
28
+ try:
29
+ bin_path = bin_path_to_dir(bin_path)
30
+ except Exception as e: # pylint: disable=broad-except
31
+ logging.exception(f'Invalid bin path {bin_path} : {str(e)}')
32
+ sys.exit(1)
25
33
 
26
34
  ports = []
27
35
  if args.ports:
@@ -37,9 +45,13 @@ def main() -> None:
37
45
  ports = [os.getenv('ESPPORT') or '/dev/ttyUSB0']
38
46
  assert isinstance(ports, list)
39
47
 
40
- logging.critical(f'Download {bin_path} to {ports}')
41
- download_bin_to_ports(bin_path, ports, args.erase_nvs, args.max_workers)
48
+ try:
49
+ download_bin_to_ports(bin_path, ports, args.erase_nvs, args.max_workers)
50
+ except RuntimeError as e:
51
+ logging.error(str(e))
52
+ sys.exit(1)
42
53
 
43
54
 
44
55
  if __name__ == '__main__':
56
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
45
57
  main()
@@ -1,13 +1,18 @@
1
1
  import asyncio
2
2
  import concurrent
3
3
  import concurrent.futures
4
+ import os
5
+ import re
4
6
  import subprocess
7
+ import tempfile
8
+ import zipfile
5
9
  from asyncio.events import AbstractEventLoop
6
10
  from functools import lru_cache, partial
7
11
 
8
12
  import esptest.common.compat_typing as t
9
13
  from esptest.devices.serial_tools import compute_serial_port
10
14
  from esptest.logger import get_logger
15
+ from esptest.tools.http_download import download_file
11
16
  from esptest.utility.parse_bin_path import ParseBinPath
12
17
 
13
18
  logger = get_logger('download_bin')
@@ -18,6 +23,32 @@ def _get_bin_parser(bin_path: str, parttool: str) -> ParseBinPath:
18
23
  return ParseBinPath(bin_path, parttool)
19
24
 
20
25
 
26
+ @lru_cache()
27
+ def _tmp_dir() -> str:
28
+ return tempfile.mkdtemp()
29
+
30
+
31
+ @lru_cache()
32
+ def bin_path_to_dir(bin_path: str) -> str:
33
+ bin_hash = hash(bin_path)
34
+ bin_hash_name = os.path.basename(bin_path)
35
+ if bin_path.startswith('http'):
36
+ assert bin_path.endswith('.zip') # for now only support zip from url
37
+ new_bin_path = os.path.join(_tmp_dir(), f'{bin_hash}', bin_hash_name)
38
+ os.makedirs(os.path.dirname(new_bin_path), exist_ok=True)
39
+ download_file(bin_path, new_bin_path)
40
+ bin_path = new_bin_path
41
+ if bin_path.endswith('.zip'):
42
+ new_bin_path = os.path.join(_tmp_dir(), f'{bin_hash}', bin_hash_name.removesuffix('.zip'))
43
+ os.makedirs(new_bin_path, exist_ok=True)
44
+ with zipfile.ZipFile(bin_path, 'r') as zip_ref:
45
+ zip_ref.extractall(new_bin_path)
46
+ bin_path = new_bin_path
47
+ if 'partition_table' not in os.listdir(bin_path):
48
+ logger.warning('Can not find partition_table from bin_path, maybe invalid!')
49
+ return bin_path
50
+
51
+
21
52
  def _filter_esptool_log(log: str) -> str:
22
53
  lines = log.splitlines(keepends=True)
23
54
  new_log = ''
@@ -36,6 +67,7 @@ def _filter_esptool_log(log: str) -> str:
36
67
  class DownBinTool:
37
68
  # RETRY_CNT = 2
38
69
  DEFAULT_BAUD_LIST = [921600, 460800]
70
+ FLASH_CRYPT_CNT_PATTERN = re.compile(r'(?:FLASH_CRYPT_CNT|SPI_BOOT_CRYPT_CNT).*\(0b([01]+)')
39
71
 
40
72
  def __init__(
41
73
  self,
@@ -54,11 +86,29 @@ class DownBinTool:
54
86
  else:
55
87
  self.baud_list = baud
56
88
  self.esptool = esptool or 'python -m esptool'
89
+ self.espefuse = self.esptool.replace('esptool', 'espefuse')
57
90
  self.erase_nvs = erase_nvs
58
91
  self.bin_parser = _get_bin_parser(bin_path, parttool)
59
92
  self.force_no_stub = force_no_stub
60
93
 
94
+ def check_flash_encrypted(self, efuse_summary: str) -> bool:
95
+ match = self.FLASH_CRYPT_CNT_PATTERN.search(efuse_summary)
96
+ if match:
97
+ return match.group(1).count('1') % 2 == 1
98
+ return False
99
+
61
100
  def download(self) -> None:
101
+ efuse_cmd = self.espefuse.split()
102
+ try:
103
+ summary = subprocess.check_output(
104
+ efuse_cmd + ['--port', self.port, 'summary'], stderr=subprocess.STDOUT, text=True
105
+ )
106
+ except subprocess.CalledProcessError as err:
107
+ logger.error(err.output)
108
+ raise RuntimeError(f'Failed to get efuse information from {self.port}') from err
109
+
110
+ enc_indicator = ' [encrypted]' if self.check_flash_encrypted(summary) else ''
111
+
62
112
  download_log = ''
63
113
  for baud in self.baud_list:
64
114
  args = self.esptool.split()
@@ -66,19 +116,18 @@ class DownBinTool:
66
116
  args += ['--no-stub']
67
117
  args += ['-p', self.port]
68
118
  args += ['-b', f'{baud}']
69
- args += self.bin_parser.flash_bin_args(erase_nvs=self.erase_nvs)
119
+ args += self.bin_parser.flash_bin_args(erase_nvs=self.erase_nvs, encrypted=bool(enc_indicator))
70
120
 
71
- logger.critical(f'Downloading {self.port}@{baud}: {self.bin_path}')
121
+ logger.info(f'Downloading {self.port}@{baud}{enc_indicator}: {self.bin_path}')
72
122
  # get return code rather than check
73
123
  ret = subprocess.run(args, capture_output=True, text=True, check=False)
74
124
  if ret.returncode == 0:
75
125
  return # succeed
76
126
  # failed
77
- download_log = f'esptool cmd failed ({ret.returncode}): ' + ' '.join(args)
127
+ download_log += f'esptool cmd failed ({ret.returncode}): ' + ' '.join(args)
78
128
  download_log += f'\nDownload failed: [{self.port}@{baud}]\n'
79
129
  esptool_msg = ret.stdout + ret.stderr
80
130
  download_log += f'esptool output: {_filter_esptool_log(esptool_msg)}'
81
- logger.debug(download_log)
82
131
  logger.error(download_log)
83
132
  raise RuntimeError(f'Failed to download Bin to {self.port}')
84
133
 
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import urllib.request
5
+ from typing import Optional
6
+
7
+
8
+ def _progress(downloaded: int, total_size: int) -> None:
9
+ if total_size > 0:
10
+ percent = min(downloaded / total_size * 100, 100)
11
+ bar_len = 50
12
+ filled_len = int(bar_len * downloaded // total_size)
13
+ progress_bar = '█' * filled_len + '-' * (bar_len - filled_len)
14
+ sys.stdout.write(f'\r[{progress_bar}] {percent:6.1f}%')
15
+ sys.stdout.flush()
16
+ else:
17
+ # show downloaded size if no total_size
18
+ sys.stdout.write(f'\rDownloaded {downloaded} bytes')
19
+ sys.stdout.flush()
20
+
21
+
22
+ def download_file(url: str, local_filename: str, timeout: Optional[float] = None, progress: bool = True) -> None:
23
+ """
24
+ Download a file from a URL.
25
+
26
+ The optional *timeout* parameter specifies a timeout in seconds for
27
+ blocking operations like the connection attempt (if not specified, the
28
+ global default timeout setting will be used). This only works for HTTP,
29
+ HTTPS and FTP connections.
30
+
31
+ Args:
32
+ url: The URL of the file to download.
33
+ local_filename: The local filename to save the downloaded file.
34
+ progress: Whether to show the download progress.
35
+ """
36
+ if os.path.exists(local_filename):
37
+ os.remove(local_filename)
38
+ try:
39
+ logging.info(f'Downloading {url} -> {local_filename}')
40
+ with urllib.request.urlopen(url, timeout=timeout) as response, open(local_filename, 'wb') as out_file:
41
+ total_length = int(response.getheader('Content-Length') or '0')
42
+ downloaded = 0
43
+ block_size = 8192
44
+ while True:
45
+ chunk = response.read(block_size)
46
+ if not chunk:
47
+ break
48
+ out_file.write(chunk)
49
+ downloaded += len(chunk)
50
+ if progress:
51
+ _progress(downloaded, total_length)
52
+ if progress:
53
+ sys.stdout.write('\n')
54
+ sys.stdout.flush()
55
+ if total_length and total_length != downloaded:
56
+ logging.error(f'Download {url} failed! maybe url timeout.')
57
+ raise OSError(f'Download {url} failed: total_length {total_length} != downloaded {downloaded}')
58
+ logging.info('Download complete!')
59
+ except OSError as e:
60
+ logging.error(f'Download {url} failed: {str(e)}')
61
+ raise e