aj090-hw-tools 0.0.4__py3-none-any.whl

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.

@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2025-present Vasencheg <roman.vasilev@nobitlost.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.0.4"
@@ -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)
@@ -0,0 +1,7 @@
1
+ import aj090_hw_tools
2
+
3
+ def script_ep():
4
+ aj090_hw_tools._main()
5
+
6
+ if __name__ == '__main__':
7
+ script_ep()
File without changes
@@ -0,0 +1,4 @@
1
+ from .firmware import firmware
2
+ from .test import test
3
+ from .serial_number import serial
4
+ from .info import info
@@ -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,7 @@
1
+ __all__ = [
2
+ 'serial'
3
+ ]
4
+
5
+
6
+ def serial(argv) -> int:
7
+ pass
@@ -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
@@ -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
+ [![PyPI - Version](https://img.shields.io/pypi/v/aj090-hw-tools.svg)](https://pypi.org/project/aj090-hw-tools)
36
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/aj090-hw-tools.svg)](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,14 @@
1
+ aj090_hw_tools/__about__.py,sha256=o_NUeOopQA1MmtnZ7tQgS86qSaFnvMV7yLl1OCOTTwc,134
2
+ aj090_hw_tools/__init__.py,sha256=pMo4sAdPV2wzQ-ldc_dDPknLijWavwzdzQxpywycAy0,1950
3
+ aj090_hw_tools/__main__.py,sha256=lgLhZKtFbsENp3hWyT0EoqDiKMkTdJ_Z0DBNoEXd0VU,110
4
+ aj090_hw_tools/targets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ aj090_hw_tools/tools/__init__.py,sha256=zPFvS-hx_IdS07KYhYeZ4RmPlEnab6J2Z9bqNjDjOMU,110
6
+ aj090_hw_tools/tools/firmware.py,sha256=8de3f4t4oUeaDkVnUj5yUw-9E4l2yBATm_O-FR3j6H0,10652
7
+ aj090_hw_tools/tools/info.py,sha256=pUp5m7r0z6jKfDtJIHv3qzJx_13PqLtN2kSh9rXCpYk,4524
8
+ aj090_hw_tools/tools/serial_number.py,sha256=l6sYaUdOtAzI4xtdvQ4hOnAkQU9spNvO7sXGJa16KXA,62
9
+ aj090_hw_tools/tools/test.py,sha256=tHbrEULsueOXexEmE3ui8pnzTWJCD-05Fu_rtCo1UnY,8400
10
+ aj090_hw_tools-0.0.4.dist-info/METADATA,sha256=f69_owfCeInqITorlKr0iO2gQ5ADbqwTnLwecYs10LU,1787
11
+ aj090_hw_tools-0.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ aj090_hw_tools-0.0.4.dist-info/entry_points.txt,sha256=1AwKATXzb-NsDJEaS5fMAmQYsOAi2ly5Ey5RJZf4Iis,69
13
+ aj090_hw_tools-0.0.4.dist-info/licenses/LICENSE.txt,sha256=otEZO2jmzBs9ErzZVB3CaAUU7n3-1jC0D2oFZXv8kxs,1096
14
+ aj090_hw_tools-0.0.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aj090-hw-tools = aj090_hw_tools.__main__:script_ep
@@ -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.