aj090-hw-tools 0.0.4__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.
Potentially problematic release.
This version of aj090-hw-tools might be problematic. Click here for more details.
- aj090_hw_tools-0.0.4/.gitignore +174 -0
- aj090_hw_tools-0.0.4/LICENSE.txt +9 -0
- aj090_hw_tools-0.0.4/PKG-INFO +53 -0
- aj090_hw_tools-0.0.4/README.md +21 -0
- aj090_hw_tools-0.0.4/pyproject.toml +79 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/__about__.py +4 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/__init__.py +71 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/__main__.py +7 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/targets/__init__.py +0 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/tools/__init__.py +4 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/tools/firmware.py +294 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/tools/info.py +128 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/tools/serial_number.py +7 -0
- aj090_hw_tools-0.0.4/src/aj090_hw_tools/tools/test.py +224 -0
- aj090_hw_tools-0.0.4/tests/__init__.py +3 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# Ruff stuff:
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# PyPI configuration file
|
|
174
|
+
.pypirc
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Vasencheg <vasencheg@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aj090-hw-tools
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Project-URL: Documentation, https://github.com/Vasencheg/aj090-hw-tools#readme
|
|
5
|
+
Project-URL: Issues, https://github.com/Vasencheg/aj090-hw-tools/issues
|
|
6
|
+
Project-URL: Source, https://github.com/Vasencheg/aj090-hw-tools
|
|
7
|
+
Author-email: Vasencheg <vasencheg@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Requires-Dist: aioconsole>=0.8.1
|
|
21
|
+
Requires-Dist: aiofile>=3.9.0
|
|
22
|
+
Requires-Dist: aiohttp>=3.11.14
|
|
23
|
+
Requires-Dist: colorama>=0.4.6
|
|
24
|
+
Requires-Dist: configargparse>=1.7
|
|
25
|
+
Requires-Dist: esp-idf-nvs-partition-gen==0.1.8
|
|
26
|
+
Requires-Dist: esptool==4.8.1
|
|
27
|
+
Requires-Dist: numpy>=2.2.4
|
|
28
|
+
Requires-Dist: pexpect>=4.9.0
|
|
29
|
+
Requires-Dist: pyserial>=3.5
|
|
30
|
+
Requires-Dist: rich>=14.0.0
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# aj090_hw_tools
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/aj090-hw-tools)
|
|
36
|
+
[](https://pypi.org/project/aj090-hw-tools)
|
|
37
|
+
|
|
38
|
+
-----
|
|
39
|
+
|
|
40
|
+
## Table of Contents
|
|
41
|
+
|
|
42
|
+
- [Installation](#installation)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
pip install aj090-hw-tools
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
`aj090-hw-tools` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# aj090_hw_tools
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/aj090-hw-tools)
|
|
4
|
+
[](https://pypi.org/project/aj090-hw-tools)
|
|
5
|
+
|
|
6
|
+
-----
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [License](#license)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```console
|
|
16
|
+
pip install aj090-hw-tools
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## License
|
|
20
|
+
|
|
21
|
+
`aj090-hw-tools` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aj090-hw-tools"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = ''
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = []
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Vasencheg", email = "vasencheg@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Programming Language :: Python",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
25
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"pyserial>=3.5",
|
|
29
|
+
"esptool==4.8.1",
|
|
30
|
+
"esp_idf_nvs_partition_gen==0.1.8",
|
|
31
|
+
"pexpect>=4.9.0",
|
|
32
|
+
"configargparse>=1.7",
|
|
33
|
+
"aiohttp>=3.11.14",
|
|
34
|
+
"aiofile>=3.9.0",
|
|
35
|
+
"aioconsole>=0.8.1",
|
|
36
|
+
"numpy>=2.2.4",
|
|
37
|
+
"colorama>=0.4.6",
|
|
38
|
+
"rich>=14.0.0"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Documentation = "https://github.com/Vasencheg/aj090-hw-tools#readme"
|
|
43
|
+
Issues = "https://github.com/Vasencheg/aj090-hw-tools/issues"
|
|
44
|
+
Source = "https://github.com/Vasencheg/aj090-hw-tools"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
path = "src/aj090_hw_tools/__about__.py"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.envs.types]
|
|
50
|
+
extra-dependencies = [
|
|
51
|
+
"mypy>=1.0.0",
|
|
52
|
+
]
|
|
53
|
+
[tool.hatch.envs.types.scripts]
|
|
54
|
+
check = "mypy --install-types --non-interactive {args:src/aj090_hw_tools tests}"
|
|
55
|
+
|
|
56
|
+
[tool.coverage.run]
|
|
57
|
+
source_pkgs = ["aj090_hw_tools", "tests"]
|
|
58
|
+
branch = true
|
|
59
|
+
parallel = true
|
|
60
|
+
omit = [
|
|
61
|
+
"src/aj090_hw_tools/__about__.py",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.coverage.paths]
|
|
65
|
+
aj090_hw_tools = ["src/aj090_hw_tools", "*/aj090-hw-tools/src/aj090_hw_tools"]
|
|
66
|
+
tests = ["tests", "*/aj090-hw-tools/tests"]
|
|
67
|
+
|
|
68
|
+
[tool.coverage.report]
|
|
69
|
+
exclude_lines = [
|
|
70
|
+
"no cov",
|
|
71
|
+
"if __name__ == .__main__.:",
|
|
72
|
+
"if TYPE_CHECKING:",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.hatch.envs.default]
|
|
76
|
+
system-packages = true
|
|
77
|
+
|
|
78
|
+
[project.scripts]
|
|
79
|
+
aj090-hw-tools = 'aj090_hw_tools.__main__:script_ep'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import configargparse
|
|
3
|
+
|
|
4
|
+
from .__about__ import __version__
|
|
5
|
+
from .tools import (
|
|
6
|
+
firmware,
|
|
7
|
+
test,
|
|
8
|
+
info
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"_main"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
def _main():
|
|
16
|
+
parser = configargparse.ArgParser(
|
|
17
|
+
description="aj090-hw-tools.py v%s - AJ090 hardware tools"
|
|
18
|
+
% __version__,
|
|
19
|
+
prog="aj090-hw-tools",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--port",
|
|
24
|
+
"-p",
|
|
25
|
+
help="Serial port device",
|
|
26
|
+
env_var="AJ090_HW_PORT",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
parser.add_argument('-c', '--config', is_config_file=True, help='config file path')
|
|
30
|
+
|
|
31
|
+
parser.add_argument('device', type=str, choices=['cell', 'shelf'], help='device')
|
|
32
|
+
|
|
33
|
+
subparsers = parser.add_subparsers(title='Device tools', dest='tools', help='Run aj090-hw-tools {device} {tool} -h for additional help')
|
|
34
|
+
|
|
35
|
+
# Firmware operations
|
|
36
|
+
parser_firmware = subparsers.add_parser('firmware', help='device firmware operations group')
|
|
37
|
+
parser_firmware.set_defaults(func=firmware)
|
|
38
|
+
parser_firmware.add_argument('operation', type=str, choices=['write', 'erase'], help='firmware operations')
|
|
39
|
+
parser_firmware.add_argument('--bin_file', type=str, help='firmware bin file')
|
|
40
|
+
|
|
41
|
+
# Info
|
|
42
|
+
parser_firmware = subparsers.add_parser('info', help='device info')
|
|
43
|
+
parser_firmware.set_defaults(func=info)
|
|
44
|
+
|
|
45
|
+
# Test
|
|
46
|
+
parser_test = subparsers.add_parser('test', help='device tests')
|
|
47
|
+
parser_test.set_defaults(func=test)
|
|
48
|
+
|
|
49
|
+
# Serial
|
|
50
|
+
parser_serial = subparsers.add_parser('serial', help='device serial number operations')
|
|
51
|
+
|
|
52
|
+
# Factory
|
|
53
|
+
# parser_factory = subparsers.add_parser('factory_mode', help='device factory mode control')
|
|
54
|
+
|
|
55
|
+
if len(sys.argv) <= 1:
|
|
56
|
+
parser.print_help()
|
|
57
|
+
sys.exit(-1)
|
|
58
|
+
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
return args.func(args)
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
sys.exit(-1)
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
try:
|
|
68
|
+
sys.exit(_main())
|
|
69
|
+
except Exception as ex:
|
|
70
|
+
print(ex)
|
|
71
|
+
sys.exit(-1)
|
|
File without changes
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import aiohttp
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import esptool
|
|
7
|
+
import numpy as np
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
from aiofile import async_open
|
|
11
|
+
from aioconsole import aprint, ainput
|
|
12
|
+
from contextlib import AbstractAsyncContextManager
|
|
13
|
+
from types import TracebackType
|
|
14
|
+
from typing import Any, Optional, Dict, Type, NamedTuple
|
|
15
|
+
from esptool.cmds import detect_chip
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
'firmware'
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
FIRMWARE_PATTERN = {
|
|
25
|
+
"cell" : r'aj090_cell_controller_firmware_(\w{7}).bin',
|
|
26
|
+
"shelf": r'aj090_shelf_controller_firmware_(\w{7}).bin'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Logger
|
|
30
|
+
FORMAT = '%(name)s:%(levelname)s: %(message)s'
|
|
31
|
+
logging.basicConfig(level=logging.ERROR, format=FORMAT)
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GitHubReleaseManager(AbstractAsyncContextManager):
|
|
36
|
+
"""
|
|
37
|
+
"""
|
|
38
|
+
API_VERSION = '2022-11-28'
|
|
39
|
+
|
|
40
|
+
class GitHubError(Exception):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
repo_owner: str,
|
|
46
|
+
repo: str,
|
|
47
|
+
*,
|
|
48
|
+
token: str
|
|
49
|
+
):
|
|
50
|
+
self._repo_owner: str = repo_owner
|
|
51
|
+
self._repo: str = repo
|
|
52
|
+
self._token: str = token
|
|
53
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
54
|
+
|
|
55
|
+
async def latest_release_info_get(self) -> Dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
curl -L -H "Accept: application/vnd.github+json" \
|
|
58
|
+
-H "Authorization:Bearer github_pat_11BGD5X..." \
|
|
59
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
60
|
+
https://api.github.com/repos/owner/repo/releases/latest
|
|
61
|
+
"""
|
|
62
|
+
async with self._session.get(
|
|
63
|
+
url = f'https://api.github.com/repos/{self._repo_owner}/{self._repo}/releases/latest',
|
|
64
|
+
headers = {
|
|
65
|
+
'Accept': 'application/vnd.github+json',
|
|
66
|
+
}
|
|
67
|
+
) as resp:
|
|
68
|
+
if resp.status == 200:
|
|
69
|
+
return await resp.json()
|
|
70
|
+
else:
|
|
71
|
+
raise self.GitHubError('Release not found')
|
|
72
|
+
|
|
73
|
+
async def asset_download(self, asset_download_url: str, output_file: str) -> int:
|
|
74
|
+
"""
|
|
75
|
+
returns downloaded asset size in bytes
|
|
76
|
+
"""
|
|
77
|
+
async with self._session.get(
|
|
78
|
+
url = asset_download_url,
|
|
79
|
+
headers = {
|
|
80
|
+
'Accept': 'application/octet-stream'
|
|
81
|
+
}
|
|
82
|
+
) as response:
|
|
83
|
+
if response.status == 200:
|
|
84
|
+
async with async_open(output_file, 'wb') as output_file:
|
|
85
|
+
chunk_size = 4096
|
|
86
|
+
asset_size = 0
|
|
87
|
+
async for data in response.content.iter_chunked(chunk_size):
|
|
88
|
+
asset_size += await output_file.write(data)
|
|
89
|
+
return asset_size
|
|
90
|
+
else:
|
|
91
|
+
raise self.GitHubError('Asset download failed')
|
|
92
|
+
|
|
93
|
+
async def __aenter__(self) -> None:
|
|
94
|
+
loop = asyncio.get_running_loop()
|
|
95
|
+
self._session: aiohttp.ClientSession = aiohttp.ClientSession(
|
|
96
|
+
loop = loop,
|
|
97
|
+
headers = {
|
|
98
|
+
'Authorization' : f'Bearer {self._token}',
|
|
99
|
+
'X-GitHub-Api-Version': self.API_VERSION
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
async def __aexit__(
|
|
105
|
+
self,
|
|
106
|
+
exc_type: Optional[Type[BaseException]],
|
|
107
|
+
exc: Optional[BaseException],
|
|
108
|
+
tb: Optional[TracebackType],
|
|
109
|
+
) -> None:
|
|
110
|
+
await self._session.close()
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
class FirmwareInfo(NamedTuple):
|
|
114
|
+
version: str
|
|
115
|
+
project: str
|
|
116
|
+
build_time: str
|
|
117
|
+
build_date: str
|
|
118
|
+
idf: str
|
|
119
|
+
sha: str
|
|
120
|
+
|
|
121
|
+
class Firmware():
|
|
122
|
+
"""
|
|
123
|
+
"""
|
|
124
|
+
def __init__(self, port: Optional[str] = None):
|
|
125
|
+
self._port: str = port if port is not None else esptool.ESPLoader.DEFAULT_PORT
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
async def info(firmware_file: str) -> FirmwareInfo:
|
|
129
|
+
"""
|
|
130
|
+
typedef struct {
|
|
131
|
+
uint32_t magic_word; /*!< Magic word ESP_APP_DESC_MAGIC_WORD */
|
|
132
|
+
uint32_t secure_version; /*!< Secure version */
|
|
133
|
+
uint32_t reserv1[2]; /*!< reserv1 */
|
|
134
|
+
char version[32]; /*!< Application version */
|
|
135
|
+
char project_name[32]; /*!< Project name */
|
|
136
|
+
char time[16]; /*!< Compile time */
|
|
137
|
+
char date[16]; /*!< Compile date*/
|
|
138
|
+
char idf_ver[32]; /*!< Version IDF */
|
|
139
|
+
uint8_t app_elf_sha256[32]; /*!< sha256 of elf file */
|
|
140
|
+
uint16_t min_efuse_blk_rev_full; /*!< Minimal eFuse block revision supported by image, in format: major * 100 + minor */
|
|
141
|
+
uint16_t max_efuse_blk_rev_full; /*!< Maximal eFuse block revision supported by image, in format: major * 100 + minor */
|
|
142
|
+
uint8_t mmu_page_size; /*!< MMU page size in log base 2 format */
|
|
143
|
+
uint8_t reserv3[3]; /*!< reserv3 */
|
|
144
|
+
uint32_t reserv2[18]; /*!< reserv2 */
|
|
145
|
+
} esp_app_desc_t;
|
|
146
|
+
"""
|
|
147
|
+
INFO_OFFSET = 262144 + 32
|
|
148
|
+
|
|
149
|
+
esp_app_desc_t = np.dtype([
|
|
150
|
+
('magic_word', '<u4'),
|
|
151
|
+
('secure_version', '<u4'),
|
|
152
|
+
('reserv1', '<u4', (2,)),
|
|
153
|
+
('version', '<u1', (32,)),
|
|
154
|
+
('project_name', '<u1', (32,)),
|
|
155
|
+
('time', '<u1', (16,)),
|
|
156
|
+
('date', '<u1', (16,)),
|
|
157
|
+
('idf_ver', '<u1', (32,)),
|
|
158
|
+
('app_elf_sha256', '<u1', (32,)),
|
|
159
|
+
('min_efuse_blk_rev_full', '<u2'),
|
|
160
|
+
('max_efuse_blk_rev_full', '<u2'),
|
|
161
|
+
('reserv3', '<u1', (3,)),
|
|
162
|
+
('reserv2', '<u4', (18,)),
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
async with async_open(firmware_file, 'rb') as file:
|
|
166
|
+
file.seek(INFO_OFFSET)
|
|
167
|
+
b = await file.read(esp_app_desc_t.itemsize)
|
|
168
|
+
|
|
169
|
+
app_desc = np.frombuffer(b, esp_app_desc_t)
|
|
170
|
+
sha = ''.join([f'{d:02x}' for d in app_desc['app_elf_sha256'].tobytes()])
|
|
171
|
+
def to_str(nd_array):
|
|
172
|
+
return nd_array.tobytes().decode().rstrip('\x00')
|
|
173
|
+
|
|
174
|
+
return FirmwareInfo(
|
|
175
|
+
version = to_str(app_desc['version']),
|
|
176
|
+
project = to_str(app_desc['project_name']),
|
|
177
|
+
build_time = to_str(app_desc['time']),
|
|
178
|
+
build_date = to_str(app_desc['date']),
|
|
179
|
+
idf = to_str(app_desc['idf_ver']),
|
|
180
|
+
sha = sha
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
async def write(self, firmware: str) -> None:
|
|
184
|
+
def _write():
|
|
185
|
+
with detect_chip(port=self._port, connect_attempts=0) as esp:
|
|
186
|
+
command = ['write_flash', '0', f'{firmware}']
|
|
187
|
+
logger.debug("Using command ", " ".join(command))
|
|
188
|
+
esptool.main(command, esp)
|
|
189
|
+
|
|
190
|
+
loop = asyncio.get_running_loop()
|
|
191
|
+
return await loop.run_in_executor(None, _write)
|
|
192
|
+
|
|
193
|
+
async def erase(self) -> None:
|
|
194
|
+
def _erase():
|
|
195
|
+
with detect_chip(port=self._port, connect_attempts=0) as esp:
|
|
196
|
+
command = ['erase_flash']
|
|
197
|
+
logger.debug("Using command ", " ".join(command))
|
|
198
|
+
esptool.main(command, esp)
|
|
199
|
+
|
|
200
|
+
loop = asyncio.get_running_loop()
|
|
201
|
+
return await loop.run_in_executor(None, _erase)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def firmware_flasher(argv):
|
|
205
|
+
"""
|
|
206
|
+
"""
|
|
207
|
+
# Create FirmwareFlasher
|
|
208
|
+
firmware = Firmware(argv.port)
|
|
209
|
+
|
|
210
|
+
match argv.operation:
|
|
211
|
+
case 'write':
|
|
212
|
+
# Download the firmware from GitHub if the bin_file was not passed directly to the arguments.
|
|
213
|
+
if argv.bin_file is None:
|
|
214
|
+
gh_token = os.getenv('AJ090_FWU_TOKEN', None)
|
|
215
|
+
fw_owner = os.getenv('AJ090_FWU_OWNER', 'Vasencheg-AJPT')
|
|
216
|
+
fw_repo = os.getenv('AJ090_FWU_REPO', 'aj090_firmware')
|
|
217
|
+
|
|
218
|
+
if gh_token is None:
|
|
219
|
+
await aprint('There is no token for accessing the firmware repository!!!')
|
|
220
|
+
return -1
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
async with GitHubReleaseManager(fw_owner, fw_repo, token=gh_token) as github:
|
|
224
|
+
info = await github.latest_release_info_get()
|
|
225
|
+
assets = info['assets']
|
|
226
|
+
commit_sha = info['target_commitish']
|
|
227
|
+
result = list(filter(lambda i: re.match(FIRMWARE_PATTERN[argv.device], i['name']), assets))
|
|
228
|
+
# validate result
|
|
229
|
+
if len(result):
|
|
230
|
+
firmware_info = {
|
|
231
|
+
'version': commit_sha[:7],
|
|
232
|
+
'url' : result[0]['url'],
|
|
233
|
+
'name' : result[0]['name']
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fwu_file = str(Path(tempfile.gettempdir()).joinpath(firmware_info['name']))
|
|
237
|
+
|
|
238
|
+
if not os.path.exists(fwu_file):
|
|
239
|
+
await aprint('The latest firmware version is being downloaded...')
|
|
240
|
+
await github.asset_download(firmware_info['url'], fwu_file)
|
|
241
|
+
else:
|
|
242
|
+
await aprint('A cached firmware file is being used...')
|
|
243
|
+
|
|
244
|
+
bin_file = fwu_file
|
|
245
|
+
else:
|
|
246
|
+
await aprint('No firmware found')
|
|
247
|
+
return -1
|
|
248
|
+
except GitHubReleaseManager.GitHubError as err:
|
|
249
|
+
await aprint(f'An error occured: {err}')
|
|
250
|
+
# TODO: search for cached firmwares and flash it
|
|
251
|
+
return -1
|
|
252
|
+
else:
|
|
253
|
+
await aprint('A local firmware file is being used...')
|
|
254
|
+
bin_file = argv.bin_file
|
|
255
|
+
|
|
256
|
+
# Get firmware info
|
|
257
|
+
fw_info = await Firmware.info(bin_file)
|
|
258
|
+
|
|
259
|
+
await aprint(
|
|
260
|
+
"""Firmware information:
|
|
261
|
+
version : {version}
|
|
262
|
+
build time : {build_time}
|
|
263
|
+
build date : {build_date}
|
|
264
|
+
SHA : {sha}
|
|
265
|
+
""".format(version=fw_info.version, build_time=fw_info.build_time, build_date=fw_info.build_date, sha=fw_info.sha)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
operation = firmware.write(bin_file)
|
|
269
|
+
|
|
270
|
+
case 'erase':
|
|
271
|
+
operation = firmware.erase()
|
|
272
|
+
|
|
273
|
+
case _:
|
|
274
|
+
raise Exception('Unsupported flash operation')
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
await ainput('Press Any key to continue or Ctrl^C to abort operation: ')
|
|
278
|
+
await operation
|
|
279
|
+
except asyncio.CancelledError:
|
|
280
|
+
await aprint('\r\n')
|
|
281
|
+
await aprint('Operation aborted')
|
|
282
|
+
operation.close()
|
|
283
|
+
return -1
|
|
284
|
+
except Exception as ex:
|
|
285
|
+
await aprint(f'An unexpected exception occured: {ex}')
|
|
286
|
+
return -1
|
|
287
|
+
else:
|
|
288
|
+
await aprint('DONE!')
|
|
289
|
+
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def firmware(argv) -> int:
|
|
294
|
+
return asyncio.run(firmware_flasher(argv))
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import pexpect
|
|
4
|
+
import pexpect.spawnbase
|
|
5
|
+
import time
|
|
6
|
+
import esptool
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from pexpect.fdpexpect import fdspawn
|
|
11
|
+
from typing import Optional, Type, NamedTuple
|
|
12
|
+
from esptool.cmds import detect_chip
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'info'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# Logger
|
|
19
|
+
FORMAT = '%(name)s:%(levelname)s: %(message)s'
|
|
20
|
+
logging.basicConfig(level=logging.ERROR, format=FORMAT)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Rich console
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
DUT = Type[pexpect.spawnbase.SpawnBase]
|
|
27
|
+
|
|
28
|
+
class GatherInfoError(Exception):
|
|
29
|
+
def __init__(self, message):
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self._message : str = message
|
|
32
|
+
|
|
33
|
+
class DeviceInfo(NamedTuple):
|
|
34
|
+
""" Base device information """
|
|
35
|
+
app_ver: str
|
|
36
|
+
compile_time: str
|
|
37
|
+
app_sha: str
|
|
38
|
+
hw_ver: str
|
|
39
|
+
mac:str
|
|
40
|
+
sn: str
|
|
41
|
+
|
|
42
|
+
async def board_type_get(dut: DUT) -> Optional[str]:
|
|
43
|
+
result = await dut.expect(['MAIN: Starting (?:\w+ )?(CELL|SHELF) CONTROLLER application', pexpect.TIMEOUT], timeout=5.0, async_=True)
|
|
44
|
+
if result:
|
|
45
|
+
raise GatherInfoError('Unknown type of board or device has not been flashed yet')
|
|
46
|
+
board_type = dut.match.group(1).decode('utf-8').lower()
|
|
47
|
+
return board_type.lower()
|
|
48
|
+
|
|
49
|
+
async def device_info_get(dut: DUT) -> Optional[DeviceInfo]:
|
|
50
|
+
# I (00:00:01.100) DEVICE_INFO:
|
|
51
|
+
# app version: b366ae1
|
|
52
|
+
# compile time: 13:00:08
|
|
53
|
+
# sha256: 3c050c5d0ea4aea33643eba2b7e840c607af710176e4387b3b22f183c920f09e
|
|
54
|
+
# hw version: 1.0
|
|
55
|
+
# MAC: 64:E8:33:48:EF:C4
|
|
56
|
+
# serial: AJ090_SC_XX_XXXXXXXX
|
|
57
|
+
MATCH_PATTERN = 'DEVICE_INFO:\s+app version:\s+(\w{7})\s+compile time:\s+(\d{2}:\d{2}:\d{2})\s+sha256:\s+(\w{64})\s+hw version:\s+(\d{1}\.\d{1})\s+MAC:\s+(\w{2}:\w{2}:\w{2}:\w{2}:\w{2}:\w{2})\s+serial:\s+(AJ090_(?:SC|CC)_\w+)'
|
|
58
|
+
result = await dut.expect([MATCH_PATTERN, pexpect.TIMEOUT], timeout=5.0, async_=True)
|
|
59
|
+
if result:
|
|
60
|
+
raise GatherInfoError('Failed to collect information about the device')
|
|
61
|
+
device_info = DeviceInfo(
|
|
62
|
+
app_ver = dut.match.group(1).decode('utf-8').lower(),
|
|
63
|
+
compile_time = dut.match.group(2).decode('utf-8').lower(),
|
|
64
|
+
app_sha = dut.match.group(3).decode('utf-8').lower(),
|
|
65
|
+
hw_ver = dut.match.group(4).decode('utf-8').lower(),
|
|
66
|
+
mac = dut.match.group(5).decode('utf-8').lower(),
|
|
67
|
+
sn = dut.match.group(6).decode('utf-8').lower()
|
|
68
|
+
)
|
|
69
|
+
return device_info
|
|
70
|
+
|
|
71
|
+
async def gather_info(device, argv) -> int:
|
|
72
|
+
device.hard_reset()
|
|
73
|
+
|
|
74
|
+
# NOTE: we are working with an already open port in another place (ESPLoader) !!!
|
|
75
|
+
dut: DUT = fdspawn(device._port, timeout=180)
|
|
76
|
+
last_error = 0
|
|
77
|
+
try:
|
|
78
|
+
# get board type
|
|
79
|
+
board_type = await board_type_get(dut)
|
|
80
|
+
|
|
81
|
+
# get device info
|
|
82
|
+
dev_info = await device_info_get(dut)
|
|
83
|
+
|
|
84
|
+
table = Table(title=f"{board_type.capitalize()} controller information", show_lines=True)
|
|
85
|
+
|
|
86
|
+
table.add_column("Info", justify="left", style="cyan", no_wrap=True)
|
|
87
|
+
table.add_column("Value", justify="left", style="green")
|
|
88
|
+
|
|
89
|
+
table.add_row('Application version', dev_info.app_ver)
|
|
90
|
+
table.add_row('Application SHA', dev_info.app_sha)
|
|
91
|
+
table.add_row('Hardware version', dev_info.hw_ver)
|
|
92
|
+
table.add_row('MAC', dev_info.mac)
|
|
93
|
+
table.add_row('Serial number', dev_info.sn)
|
|
94
|
+
|
|
95
|
+
console.print(table)
|
|
96
|
+
|
|
97
|
+
except GatherInfoError as err:
|
|
98
|
+
console.print(err, style="bold red")
|
|
99
|
+
last_error = -1
|
|
100
|
+
except pexpect.TIMEOUT:
|
|
101
|
+
console.print('Waiting time exceeded', style="bold red")
|
|
102
|
+
last_error = -1
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
dut.close()
|
|
106
|
+
except OSError as err:
|
|
107
|
+
logger.debug(f'OSError: {err}')
|
|
108
|
+
|
|
109
|
+
return last_error
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def info(argv) -> int:
|
|
113
|
+
port = argv.port if argv.port is not None else esptool.ESPLoader.DEFAULT_PORT
|
|
114
|
+
connects = 10 # NOTE: the workaround to the issue "Could not open /dev/tty..., the port is busy or doesn't exist"
|
|
115
|
+
for _ in range(connects):
|
|
116
|
+
try:
|
|
117
|
+
with detect_chip(port=port, connect_attempts=0) as device:
|
|
118
|
+
return asyncio.run(gather_info(device, argv))
|
|
119
|
+
except OSError:
|
|
120
|
+
# NOTE: we are trying to close an already closed port (in device_test()),
|
|
121
|
+
# thus an OSError occurs (invalid file descriptor)
|
|
122
|
+
return 0
|
|
123
|
+
except esptool.util.FatalError as err:
|
|
124
|
+
logger.debug(err)
|
|
125
|
+
time.sleep(1.0)
|
|
126
|
+
|
|
127
|
+
console.print("Can't connect to the device", style="bold red")
|
|
128
|
+
return -1
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import pexpect
|
|
4
|
+
import pexpect.spawnbase
|
|
5
|
+
import serial
|
|
6
|
+
import serial.rs485
|
|
7
|
+
import serial.tools.list_ports_linux
|
|
8
|
+
import time
|
|
9
|
+
import esptool
|
|
10
|
+
|
|
11
|
+
from aioconsole import aprint
|
|
12
|
+
from colorama import Fore, Style
|
|
13
|
+
from pexpect.fdpexpect import fdspawn
|
|
14
|
+
from typing import Optional, Type, NamedTuple
|
|
15
|
+
from esptool.cmds import detect_chip
|
|
16
|
+
|
|
17
|
+
DEFAULT_BAUDRATE = 115200
|
|
18
|
+
DEFAULT_SERIAL_TIMEOUT_IN_SECONDS = 2
|
|
19
|
+
|
|
20
|
+
# Logger
|
|
21
|
+
FORMAT = '%(name)s:%(levelname)s: %(message)s'
|
|
22
|
+
logging.basicConfig(level=logging.ERROR, format=FORMAT)
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
DUT = Type[pexpect.spawnbase.SpawnBase]
|
|
26
|
+
|
|
27
|
+
class BootInfo(NamedTuple):
|
|
28
|
+
idf_version: str
|
|
29
|
+
boot_offset: str
|
|
30
|
+
app_version: str
|
|
31
|
+
|
|
32
|
+
class DeviceInfo(NamedTuple):
|
|
33
|
+
""" Base device information """
|
|
34
|
+
app_ver: str
|
|
35
|
+
compile_time: str
|
|
36
|
+
app_sha: str
|
|
37
|
+
hw_ver: str
|
|
38
|
+
mac:str
|
|
39
|
+
sn: str
|
|
40
|
+
|
|
41
|
+
def br(text) -> str:
|
|
42
|
+
return Style.BRIGHT + f'{text}' + Style.RESET_ALL
|
|
43
|
+
|
|
44
|
+
def yellow(text) -> str:
|
|
45
|
+
return Fore.YELLOW + f'{text}' + Style.RESET_ALL
|
|
46
|
+
|
|
47
|
+
def red(text) -> str:
|
|
48
|
+
return Style.BRIGHT + Fore.RED + f'{text}' + Style.RESET_ALL
|
|
49
|
+
|
|
50
|
+
def green(text) -> str:
|
|
51
|
+
return Style.BRIGHT + Fore.GREEN + f'{text}' + Style.RESET_ALL
|
|
52
|
+
|
|
53
|
+
def open_serial_port(port):
|
|
54
|
+
serInstance = serial.Serial(port, DEFAULT_BAUDRATE, timeout=DEFAULT_SERIAL_TIMEOUT_IN_SECONDS)
|
|
55
|
+
return serInstance
|
|
56
|
+
|
|
57
|
+
class TestError(Exception):
|
|
58
|
+
def __init__(self, message, expected_output: str = None, before: str = None):
|
|
59
|
+
super().__init__(message)
|
|
60
|
+
self._message : str = message
|
|
61
|
+
self._expected: Optional[str] = expected_output
|
|
62
|
+
self._before : Optional[str] = before
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def before(self) -> Optional[str]:
|
|
66
|
+
return self._before
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
if self._expected is not None:
|
|
70
|
+
return f'{red(self._message)}:\r\n\t{br("Expected output")}: {self._expected}'
|
|
71
|
+
else:
|
|
72
|
+
return f'{red(self._message)}'
|
|
73
|
+
|
|
74
|
+
async def do_test(dut: DUT, unit:str, expected: str, timeout: float = 5.0) -> int:
|
|
75
|
+
result = await dut.expect([expected, pexpect.TIMEOUT], timeout=timeout, async_=True)
|
|
76
|
+
if result:
|
|
77
|
+
raise TestError(f'{unit} test error', expected, dut.before)
|
|
78
|
+
else:
|
|
79
|
+
await aprint(f'{unit} test: {green("OK")}')
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
async def device_boot_test(dut: DUT) -> BootInfo:
|
|
84
|
+
boot_expect = [
|
|
85
|
+
'boot: ESP-IDF v(\d\.\d|\d\.\d+-dirty) 2nd stage bootloader',
|
|
86
|
+
'boot: Loaded app from partition at offset (0x\d{5,})',
|
|
87
|
+
'app_init: App version:\s+(\w{7})'
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
await do_test(dut, '2nd stage bootloader', boot_expect[0])
|
|
91
|
+
idf_version = dut.match.group(1).decode('utf-8').lower()
|
|
92
|
+
await do_test(dut, 'Load app', boot_expect[1])
|
|
93
|
+
boot_offset = dut.match.group(1).decode('utf-8')
|
|
94
|
+
await do_test(dut, 'App version', boot_expect[2])
|
|
95
|
+
app_version = dut.match.group(1).decode('utf-8').lower()
|
|
96
|
+
|
|
97
|
+
boot_info = BootInfo(
|
|
98
|
+
idf_version = idf_version,
|
|
99
|
+
app_version = app_version,
|
|
100
|
+
boot_offset = boot_offset
|
|
101
|
+
)
|
|
102
|
+
logger.debug(f'Device boot info: {boot_info}')
|
|
103
|
+
|
|
104
|
+
return boot_info
|
|
105
|
+
|
|
106
|
+
async def common_modules_init_test(dut: DUT) -> int:
|
|
107
|
+
expected_outputs = [
|
|
108
|
+
('Firmware manager', 'FW_MANAGER: Firmware manager has been successfully initialized'),
|
|
109
|
+
('File storage', 'STORAGE: Storage manager has been successfully initialized'),
|
|
110
|
+
('Configuration manager', 'CFG_MANAGER: Configuration manager has been successfully initialized'),
|
|
111
|
+
('Board', 'BOARD: Board has been successfully initialized')
|
|
112
|
+
]
|
|
113
|
+
for unit, output in expected_outputs:
|
|
114
|
+
await do_test(dut, unit, output)
|
|
115
|
+
|
|
116
|
+
async def board_type_test(dut: DUT) -> str:
|
|
117
|
+
await do_test(dut, 'Board type', 'MAIN: Starting (?:\w+ )?(CELL|SHELF) CONTROLLER application')
|
|
118
|
+
board_type = dut.match.group(1).decode('utf-8').lower()
|
|
119
|
+
return board_type.lower()
|
|
120
|
+
|
|
121
|
+
async def device_info_test(dut: DUT) -> DeviceInfo:
|
|
122
|
+
# I (00:00:01.100) DEVICE_INFO:
|
|
123
|
+
# app version: b366ae1
|
|
124
|
+
# compile time: 13:00:08
|
|
125
|
+
# sha256: 3c050c5d0ea4aea33643eba2b7e840c607af710176e4387b3b22f183c920f09e
|
|
126
|
+
# hw version: 1.0
|
|
127
|
+
# MAC: 64:E8:33:48:EF:C4
|
|
128
|
+
# serial: AJ090_SC_XX_XXXXXXXX
|
|
129
|
+
MATCH_PATTERN = 'DEVICE_INFO:\s+app version:\s+(\w{7})\s+compile time:\s+(\d{2}:\d{2}:\d{2})\s+sha256:\s+(\w{64})\s+hw version:\s+(\d{1}\.\d{1})\s+MAC:\s+(\w{2}:\w{2}:\w{2}:\w{2}:\w{2}:\w{2})\s+serial:\s+(AJ090_(?:SC|CC)_\w+)'
|
|
130
|
+
await do_test(dut, 'Device info', MATCH_PATTERN)
|
|
131
|
+
device_info = DeviceInfo(
|
|
132
|
+
app_ver = dut.match.group(1).decode('utf-8').lower(),
|
|
133
|
+
compile_time = dut.match.group(2).decode('utf-8').lower(),
|
|
134
|
+
app_sha = dut.match.group(3).decode('utf-8').lower(),
|
|
135
|
+
hw_ver = dut.match.group(4).decode('utf-8').lower(),
|
|
136
|
+
mac = dut.match.group(5).decode('utf-8').lower(),
|
|
137
|
+
sn = dut.match.group(6).decode('utf-8').lower()
|
|
138
|
+
)
|
|
139
|
+
return device_info
|
|
140
|
+
|
|
141
|
+
async def cell_test(dut: DUT, app_version: str) -> int:
|
|
142
|
+
await do_test(dut, 'Cells initialization', 'CELL_CONTROLLER: initialize cells of the (\w{6,9}) board')
|
|
143
|
+
cell_type = dut.match.group(1).decode('utf-8').lower()
|
|
144
|
+
await do_test(dut, 'Application', 'MAIN: The application has been successfully initialized')
|
|
145
|
+
match cell_type:
|
|
146
|
+
case 'retail':
|
|
147
|
+
await do_test(dut, 'Cells#0 measurements', 'CELL_CONTROLLER: cell#0 measure result', timeout=20)
|
|
148
|
+
await do_test(dut, 'Cells#1 measurements', 'CELL_CONTROLLER: cell#1 measure result', timeout=20)
|
|
149
|
+
case 'wholesale':
|
|
150
|
+
await do_test(dut, 'Cell measurements', 'CELL_CONTROLLER: cell#0 measure result', timeout=20)
|
|
151
|
+
case _:
|
|
152
|
+
raise TestError(f'Unknown cell type: {cell_type}')
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
async def shelf_test(dut: DUT, app_version: str) -> int:
|
|
156
|
+
await do_test(dut, 'Main task start', 'SHELF_CONTROLLER: Shelf controller main task started')
|
|
157
|
+
await do_test(dut, 'Environment sensor initialization', 'ENV_SENSOR: sensor created')
|
|
158
|
+
await do_test(dut, 'Application', 'MAIN: The application has been successfully initialized')
|
|
159
|
+
await do_test(dut, 'Environment sensor measurements', 'SHELF_CONTROLLER: Temperature: (\d{1,3}[.,]\d+); humudity: (\d{1,2}[.,]\d+)')
|
|
160
|
+
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
async def device_test(device, argv):
|
|
164
|
+
device.hard_reset()
|
|
165
|
+
|
|
166
|
+
# NOTE: we are working with an already open port in another place (ESPLoader) !!!
|
|
167
|
+
dut: DUT = fdspawn(device._port, timeout=180)
|
|
168
|
+
last_error = 0
|
|
169
|
+
try:
|
|
170
|
+
boot_info = await device_boot_test(dut)
|
|
171
|
+
await aprint(yellow(f'Device boot info: {boot_info}'))
|
|
172
|
+
await common_modules_init_test(dut)
|
|
173
|
+
|
|
174
|
+
board_type = await board_type_test(dut)
|
|
175
|
+
if argv.device != board_type:
|
|
176
|
+
raise TestError(f'Wrong board type. Got: {board_type.upper()}', f'{argv.device.upper()}')
|
|
177
|
+
|
|
178
|
+
device_info = await device_info_test(dut)
|
|
179
|
+
await aprint(yellow(f'Device info: {device_info}'))
|
|
180
|
+
|
|
181
|
+
match argv.device:
|
|
182
|
+
case 'cell':
|
|
183
|
+
last_error = await cell_test(dut, device_info.app_ver)
|
|
184
|
+
case 'shelf':
|
|
185
|
+
last_error = await shelf_test(dut, device_info.app_ver)
|
|
186
|
+
|
|
187
|
+
case _:
|
|
188
|
+
raise Exception('Unsupported device')
|
|
189
|
+
except TestError as err:
|
|
190
|
+
await aprint(err)
|
|
191
|
+
last_error = -1
|
|
192
|
+
except pexpect.TIMEOUT:
|
|
193
|
+
await aprint('Waiting time exceeded')
|
|
194
|
+
last_error = -1
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
dut.close()
|
|
198
|
+
except OSError as err:
|
|
199
|
+
logger.debug(f'OSError: {err}')
|
|
200
|
+
|
|
201
|
+
if last_error:
|
|
202
|
+
await aprint(red('FAILED'))
|
|
203
|
+
else:
|
|
204
|
+
await aprint(green('PASSED'))
|
|
205
|
+
|
|
206
|
+
return last_error
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test(argv) -> int:
|
|
210
|
+
port = argv.port if argv.port is not None else esptool.ESPLoader.DEFAULT_PORT
|
|
211
|
+
connects = 10 # NOTE: the workaround to the issue "Could not open /dev/tty..., the port is busy or doesn't exist"
|
|
212
|
+
for _ in range(connects):
|
|
213
|
+
try:
|
|
214
|
+
with detect_chip(port=port, connect_attempts=0) as device:
|
|
215
|
+
return asyncio.run(device_test(device, argv))
|
|
216
|
+
except OSError:
|
|
217
|
+
# NOTE: we are trying to close an already closed port (in device_test()),
|
|
218
|
+
# thus an OSError occurs (invalid file descriptor)
|
|
219
|
+
return 0
|
|
220
|
+
except esptool.util.FatalError as err:
|
|
221
|
+
logger.debug(err)
|
|
222
|
+
time.sleep(1.0)
|
|
223
|
+
print("Can't connect to the device")
|
|
224
|
+
return -1
|