silabs-pycommander-core 1.0.0__py3-none-win_amd64.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.
Files changed (43) hide show
  1. pycommander_core/__init__.py +25 -0
  2. pycommander_core/__main__.py +18 -0
  3. pycommander_core/_ensure_commander.py +114 -0
  4. pycommander_core/_utils.py +37 -0
  5. pycommander_core/_version.py +14 -0
  6. pycommander_core/adapter_base.py +368 -0
  7. pycommander_core/aemstream_base.py +118 -0
  8. pycommander_core/cli.py +41 -0
  9. pycommander_core/commander_base.py +265 -0
  10. pycommander_core/commands/__init__.py +62 -0
  11. pycommander_core/commands/_base.py +208 -0
  12. pycommander_core/commands/adapter.py +206 -0
  13. pycommander_core/commands/aem.py +140 -0
  14. pycommander_core/commands/convert.py +108 -0
  15. pycommander_core/commands/ctune.py +57 -0
  16. pycommander_core/commands/device.py +151 -0
  17. pycommander_core/commands/ebl.py +146 -0
  18. pycommander_core/commands/extflash.py +102 -0
  19. pycommander_core/commands/flash.py +102 -0
  20. pycommander_core/commands/gbl3.py +218 -0
  21. pycommander_core/commands/gbl4.py +151 -0
  22. pycommander_core/commands/littlefs.py +240 -0
  23. pycommander_core/commands/mfg917.py +753 -0
  24. pycommander_core/commands/nvm3.py +264 -0
  25. pycommander_core/commands/ota.py +199 -0
  26. pycommander_core/commands/postbuild.py +45 -0
  27. pycommander_core/commands/readmem.py +52 -0
  28. pycommander_core/commands/rps.py +197 -0
  29. pycommander_core/commands/security.py +742 -0
  30. pycommander_core/commands/serial.py +115 -0
  31. pycommander_core/commands/tokens.py +170 -0
  32. pycommander_core/commands/util.py +237 -0
  33. pycommander_core/commands/vcom.py +51 -0
  34. pycommander_core/commands/verify.py +84 -0
  35. pycommander_core/errors.py +24 -0
  36. pycommander_core/paths.py +28 -0
  37. pycommander_core/runner.py +191 -0
  38. pycommander_core/target.py +1019 -0
  39. pycommander_core/types.py +286 -0
  40. silabs_pycommander_core-1.0.0.dist-info/METADATA +30 -0
  41. silabs_pycommander_core-1.0.0.dist-info/RECORD +43 -0
  42. silabs_pycommander_core-1.0.0.dist-info/WHEEL +5 -0
  43. silabs_pycommander_core-1.0.0.dist-info/licenses/LICENSE.md +8 -0
@@ -0,0 +1,25 @@
1
+ """
2
+ License
3
+ Copyright 2026 Silicon Laboratories Inc. www.silabs.com
4
+ *******************************************************************************
5
+ The licensor of this software is Silicon Laboratories Inc. Your use of this
6
+ software is governed by the terms of Silicon Labs Master Software License
7
+ Agreement (MSLA) available at
8
+ www.silabs.com/about-us/legal/master-software-license-agreement. This
9
+ software is distributed to you in Source Code format and is governed by the
10
+ sections of the MSLA applicable to Source Code.
11
+ *******************************************************************************
12
+ """
13
+
14
+ # This is the entry point for the PyCommander package, when you do `import pycommander`
15
+ from .commander_base import CommanderBase
16
+ from .adapter_base import AdapterBase
17
+ from .target import Target
18
+ from ._version import __version__
19
+
20
+ __all__ = [
21
+ "CommanderBase",
22
+ "AdapterBase",
23
+ "Target",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,18 @@
1
+ """
2
+ License
3
+ Copyright 2026 Silicon Laboratories Inc. www.silabs.com
4
+ *******************************************************************************
5
+ The licensor of this software is Silicon Laboratories Inc. Your use of this
6
+ software is governed by the terms of Silicon Labs Master Software License
7
+ Agreement (MSLA) available at
8
+ www.silabs.com/about-us/legal/master-software-license-agreement. This
9
+ software is distributed to you in Source Code format and is governed by the
10
+ sections of the MSLA applicable to Source Code.
11
+ *******************************************************************************
12
+ """
13
+
14
+ # Entry point for `pycommander` command and `python3 -m pycommander`
15
+ from .cli import main
16
+
17
+ if __name__ == "__main__":
18
+ raise SystemExit(main())
@@ -0,0 +1,114 @@
1
+ """
2
+ License
3
+ Copyright 2026 Silicon Laboratories Inc. www.silabs.com
4
+ *******************************************************************************
5
+ The licensor of this software is Silicon Laboratories Inc. Your use of this
6
+ software is governed by the terms of Silicon Labs Master Software License
7
+ Agreement (MSLA) available at
8
+ www.silabs.com/about-us/legal/master-software-license-agreement. This
9
+ software is distributed to you in Source Code format and is governed by the
10
+ sections of the MSLA applicable to Source Code.
11
+ *******************************************************************************
12
+ """
13
+
14
+ # Ensure that the Commander executable is present
15
+ # This entails checking if the executable is present where it should be, and if not, extracting it from the
16
+ # archive embedded in the package.
17
+ import sys
18
+ import hashlib
19
+ import subprocess
20
+ import shutil
21
+
22
+ import importlib.resources as ir
23
+
24
+ from pathlib import Path
25
+ from platformdirs import user_cache_dir
26
+
27
+ from .paths import EXECUTABLE_PATH_CLI, EXECUTABLE_PATH_GUI
28
+
29
+ def ensure_commander(cli: bool = True) -> Path:
30
+ if cli:
31
+ package_name = "pycommander_cli._archive"
32
+ executable_path = EXECUTABLE_PATH_CLI
33
+ else:
34
+ package_name = "pycommander_gui._archive"
35
+ executable_path = EXECUTABLE_PATH_GUI
36
+
37
+ cache_dir = Path(user_cache_dir(f"pycommander", "silabs"))
38
+ archive_path = Path(_find_executable_archive_resource(package_name))
39
+ archive_hash = _compute_hash_of_file(archive_path)
40
+
41
+ target_dir = cache_dir / archive_hash
42
+
43
+ if not target_dir.exists():
44
+ target_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ _extract_commander(archive_path, target_dir)
47
+
48
+ return Path(target_dir / executable_path)
49
+
50
+ def _extract_commander(zip_file_path: Path, destination: Path) -> None:
51
+ # Remove the existing executable and stamp files, if they exist.
52
+ if destination.exists():
53
+ shutil.rmtree(destination, ignore_errors=True)
54
+
55
+ # Find the filename of the executable in the archive
56
+ with ir.as_file(zip_file_path) as zip_file:
57
+ if sys.platform == "darwin":
58
+ _extract_commander_macos(zip_file, destination)
59
+ elif sys.platform == "linux":
60
+ _extract_commander_linux(zip_file, destination)
61
+ elif sys.platform == "win32":
62
+ _extract_commander_windows(zip_file, destination)
63
+ else:
64
+ raise ValueError(f"Unsupported platform: {sys.platform}")
65
+
66
+ def _extract_commander_macos(zip_file: Path, destination: Path) -> None:
67
+ if not zip_file.exists():
68
+ raise FileNotFoundError(f"Executable archive not found: {zip_file}")
69
+
70
+ destination.mkdir(parents=True, exist_ok=True)
71
+ subprocess.run(
72
+ ["ditto", "-xk", str(zip_file), str(destination)],
73
+ check=True
74
+ )
75
+
76
+ def _extract_commander_linux(zip_file: Path, destination: Path) -> None:
77
+ if not zip_file.exists():
78
+ raise FileNotFoundError(f"Executable archive not found: {zip_file}")
79
+
80
+ destination.mkdir(parents=True, exist_ok=True)
81
+ subprocess.run(
82
+ ["tar", "-xjf", str(zip_file), "-C", str(destination)],
83
+ check=True
84
+ )
85
+
86
+ def _extract_commander_windows(zip_file: Path, destination: Path) -> None:
87
+ if not zip_file.exists():
88
+ raise FileNotFoundError(f"Executable archive not found: {zip_file}")
89
+
90
+ destination.mkdir(parents=True, exist_ok=True)
91
+ subprocess.run(
92
+ ["powershell", "-Command", "Expand-Archive", f"-Path {str(zip_file)}", f"-DestinationPath {str(destination)}"],
93
+ check=True
94
+ )
95
+
96
+ def _compute_hash_of_file(path: Path) -> str:
97
+ hash = hashlib.sha256()
98
+ hash.update(path.read_bytes())
99
+ return hash.hexdigest()[:16]
100
+
101
+ def _find_executable_archive_resource(package: str) -> Path:
102
+ root = ir.files(package)
103
+ candidates : list[Path] = []
104
+ for entry in root.iterdir():
105
+ if not entry.is_file():
106
+ continue
107
+ name = entry.name
108
+ if name.endswith(".zip") or name.endswith(".tar.bz"):
109
+ candidates.append(root / name)
110
+
111
+ if len(candidates) == 0:
112
+ raise FileNotFoundError(f"No executable archive found in {root}")
113
+
114
+ return candidates[0]
@@ -0,0 +1,37 @@
1
+ """
2
+ License
3
+ Copyright 2026 Silicon Laboratories Inc. www.silabs.com
4
+ *******************************************************************************
5
+ The licensor of this software is Silicon Laboratories Inc. Your use of this
6
+ software is governed by the terms of Silicon Labs Master Software License
7
+ Agreement (MSLA) available at
8
+ www.silabs.com/about-us/legal/master-software-license-agreement. This
9
+ software is distributed to you in Source Code format and is governed by the
10
+ sections of the MSLA applicable to Source Code.
11
+ *******************************************************************************
12
+ """
13
+
14
+ def sanitize_args(args: list[str | int | float | None]) -> list[str]:
15
+ # NOTE: We do NOT want to split any args that contain whitespace into multiple args.
16
+ # There might be filenames or other arguments that have whitespace in them.
17
+
18
+ sanitized_args : list[str] = []
19
+ for arg in args:
20
+ # Remove any empty or None elements
21
+ if arg is None:
22
+ continue
23
+
24
+ # Stringify any non-string elements
25
+ if not isinstance(arg, str):
26
+ arg = str(arg)
27
+
28
+ # Remove any whitespace-only elements
29
+ if arg.strip() == "":
30
+ continue
31
+
32
+ # Trim leading and trailing whitespace
33
+ arg = arg.strip()
34
+
35
+ sanitized_args.append(arg)
36
+
37
+ return sanitized_args
@@ -0,0 +1,14 @@
1
+ """
2
+ License
3
+ Copyright 2026 Silicon Laboratories Inc. www.silabs.com
4
+ *******************************************************************************
5
+ The licensor of this software is Silicon Laboratories Inc. Your use of this
6
+ software is governed by the terms of Silicon Labs Master Software License
7
+ Agreement (MSLA) available at
8
+ www.silabs.com/about-us/legal/master-software-license-agreement. This
9
+ software is distributed to you in Source Code format and is governed by the
10
+ sections of the MSLA applicable to Source Code.
11
+ *******************************************************************************
12
+ """
13
+
14
+ __version__ = "1.0.0"
@@ -0,0 +1,368 @@
1
+ """
2
+ License
3
+ Copyright 2026 Silicon Laboratories Inc. www.silabs.com
4
+ *******************************************************************************
5
+ The licensor of this software is Silicon Laboratories Inc. Your use of this
6
+ software is governed by the terms of Silicon Labs Master Software License
7
+ Agreement (MSLA) available at
8
+ www.silabs.com/about-us/legal/master-software-license-agreement. This
9
+ software is distributed to you in Source Code format and is governed by the
10
+ sections of the MSLA applicable to Source Code.
11
+ *******************************************************************************
12
+ """
13
+
14
+ from pathlib import Path
15
+
16
+ from . import types
17
+
18
+ from .commander_base import CommanderBase
19
+ from .target import Target
20
+
21
+ class AdapterBase:
22
+ """High-level interface for interacting with a Silicon Labs adapter (kit/debugger).
23
+
24
+ The convenience methods on this class (adapter info, reset, supply voltage, VCOM
25
+ configuration, and energy analysis) rely on features that are only present on
26
+ Silicon Labs adapters, and are not available when using a generic J-Link adapter.
27
+ """
28
+
29
+ def __init__(self,
30
+ serial_number: str | None = None,
31
+ ip_address: str | None = None,
32
+ serial_port: str | None = None,
33
+ target_device: str | None = None,
34
+ debug_speed: int | None = None,
35
+ debug_tif: str | None = None,
36
+ debug_irpre: int | None = None,
37
+ debug_drpre: int | None = None,
38
+ target: Target | None = None,
39
+ commander: CommanderBase | None = None):
40
+
41
+ if commander is None:
42
+ raise ValueError("commander must be provided")
43
+
44
+ self._commander : CommanderBase = commander
45
+ self.target : Target | None = target
46
+
47
+ def info(self) -> types.AdapterInfo | None:
48
+ """Get information about the adapter. Only available on Silicon Labs adapters.
49
+
50
+ Returns:
51
+ An AdapterInfo object containing the information about the adapter, or None if the information could not be retrieved.
52
+ """
53
+
54
+ result : dict = self._commander.adapter.probe()
55
+
56
+ if not result["success"]:
57
+ return None
58
+
59
+ if "board_lists" not in result["result"]:
60
+ return None
61
+
62
+ board_list : list[types.AdapterBoardInfo] = []
63
+ for board in result["result"]["board_lists"]:
64
+ board_list.append(types.AdapterBoardInfo(
65
+ name=board.get("name", None),
66
+ part_number=board.get("part_number", None),
67
+ serial_number=board.get("serial_number", None),
68
+ target_device=board.get("target_device", None),
69
+ ))
70
+
71
+ if "firmware_info" not in result["result"]:
72
+ return None
73
+
74
+ fw_info = types.AdapterFwInfo(
75
+ current_version=result["result"]["firmware_info"].get("fw_version", None),
76
+ latest_version=result["result"]["firmware_info"].get("new_fw_version", None),
77
+ upgrade_available=result["result"]["firmware_info"].get("upgrade_available", None),
78
+ )
79
+
80
+ if "kit_info" not in result["result"]:
81
+ return None
82
+
83
+ adapter_info = types.AdapterInfo(
84
+ board_list=board_list,
85
+ fw_info=fw_info,
86
+ jlink_serial_number=result["result"]["kit_info"].get("j_link_serial", None),
87
+ vcom_port=result["result"]["kit_info"].get("vcom_port", None),
88
+ vcom_supported=result["result"]["kit_info"].get("vcom_supported", None),
89
+ ip_supported=result["result"]["kit_info"].get("ip_supported", None),
90
+ ip_address=result["result"]["kit_info"].get("ip_address", None),
91
+ mac_address=result["result"]["kit_info"].get("mac_address", None),
92
+ nickname=result["result"]["kit_info"].get("nickname", None),
93
+ kit_name=result["result"]["kit_info"].get("kit_name", None),
94
+ kit_part_number=result["result"]["kit_info"].get("kit_part_number", None),
95
+ aem_supported=result["result"]["kit_info"].get("aem_supported", None),
96
+ debug_mode=result["result"]["kit_info"].get("debug_mode", None),
97
+ debug_part=result["result"]["kit_info"].get("debug_part", None),
98
+ )
99
+
100
+ return adapter_info
101
+
102
+ def reset(self) -> bool:
103
+ """Reset the adapter. Only available on Silicon Labs adapters.
104
+
105
+ Returns:
106
+ True if the adapter was reset successfully, False otherwise.
107
+ """
108
+ result : dict = self._commander.adapter.reset()
109
+ return result["success"]
110
+
111
+ def upgradeFirmware(self, filename: Path | None = None) -> types.AdapterFwUpgradeResult | None:
112
+ """Upgrade the firmware of the adapter. Only available on Silicon Labs adapters.
113
+
114
+ Args:
115
+ filename (Path | None): The path to the firmware package to upgrade (*.emz). If not provided, Commander's bundled firmware will be used; in this case, the upgrade will only be performed if the bundled version is newer than the installed version.
116
+
117
+ Returns:
118
+ An AdapterFwUpgradeResult object containing the result of the firmware upgrade, or None if the firmware upgrade could not be performed.
119
+ """
120
+
121
+ current_version : str | None = self.__get_current_firmware_version()
122
+
123
+ args = []
124
+ if filename is None or str(filename) == "":
125
+ # Check if a firmware upgrade is available first
126
+ result : dict = self._commander.adapter.fwupgradecheck()
127
+ if not result["success"]:
128
+ return None
129
+
130
+ if not result["result"].get("upgrade_available", False):
131
+ return types.AdapterFwUpgradeResult(
132
+ package_was_installed=False,
133
+ currently_installed_version=current_version,
134
+ )
135
+ else:
136
+ args.append(str(filename))
137
+
138
+ result : dict = self._commander.adapter.fwupgrade(*args)
139
+ if not result["success"]:
140
+ return None
141
+
142
+ new_version : str | None = self.__get_current_firmware_version()
143
+
144
+ return types.AdapterFwUpgradeResult(
145
+ package_was_installed=True,
146
+ currently_installed_version=new_version,
147
+ )
148
+
149
+ def getVoltage(self) -> types.AdapterVoltageInfo | None:
150
+ """Get the supply voltage for the target device. Only available on Silicon Labs adapters.
151
+
152
+ Returns:
153
+ A dictionary of rail indices and AdapterVoltageInfo objects containing the supply voltage
154
+ information for each rail, or None if the supply voltage information could not be retrieved.
155
+ If no supply voltage information is available for a rail, the rail index will not be present in the dictionary.
156
+ """
157
+ result : dict = self._commander.adapter.voltage()
158
+
159
+ if not result["success"]:
160
+ return None
161
+
162
+ voltage_info : types.AdapterVoltageInfo = types.AdapterVoltageInfo(rails=[])
163
+ for rail_info in result["result"]["voltages"]:
164
+ voltage_info.rails.append(types.AdapterRailInfo(
165
+ rail_index=rail_info.get("rail_index", None),
166
+ configured_voltage_v=rail_info.get("configured_voltage_v", None),
167
+ measured_voltage_v=rail_info.get("measured_voltage_v", None),
168
+ rail_powered=rail_info.get("rail_powered", None),
169
+ ))
170
+
171
+ return voltage_info
172
+
173
+ def setVoltage(self, voltage: float, calibrate: bool = True) -> bool:
174
+ """Set the supply voltage for the target device. Only available on Silicon Labs adapters.
175
+
176
+ Args:
177
+ voltage (float): Supply voltage to set.
178
+ calibrate (bool): If True, automatically calibrate if voltage has changed.
179
+
180
+ Returns:
181
+ True if the supply voltage was set successfully, False otherwise.
182
+ """
183
+ result : dict = self._commander.adapter.voltage(voltage=voltage, calibrate=calibrate)
184
+ return result["success"]
185
+
186
+ def setVcomConfig(self, baudrate: int, handshake: types.VcomHandshake, store: bool = False) -> bool:
187
+ """Set the VCOM configuration for the adapter. Only available on Silicon Labs adapters.
188
+
189
+ Args:
190
+ baudrate (int): VCOM baudrate.
191
+ handshake (VcomHandshake): VCOM handshake.
192
+ store (bool): Store the VCOM configuration.
193
+
194
+ Returns:
195
+ True if the VCOM configuration was set successfully, False otherwise.
196
+ """
197
+ if handshake not in types.VcomHandshake.__members__.values():
198
+ raise ValueError(f"Invalid handshake: {handshake}")
199
+
200
+ result : dict = self._commander.vcom.config(baudrate=baudrate, handshake=handshake.value, store=store)
201
+ return result["success"]
202
+
203
+ def analyzeEnergyUsage(self, duration_s: float, get_distribution: bool = False, cluster_states: bool = False, detect_period: bool = False) -> types.AemAnalysisResult | None:
204
+ """Analyze the energy usage of the target device. Only available on Silicon Labs adapters.
205
+
206
+ Args:
207
+ duration_s: Duration of the measurement in seconds.
208
+ get_distribution: If True, get the distribution of measurements.
209
+ cluster_states: If True, cluster the states of the measurements.
210
+ detect_period: If True, detect the period of the measurements.
211
+
212
+ Returns:
213
+ An AemAnalysisResult object containing the analysis results, or None if the analysis could not be performed.
214
+ """
215
+
216
+ if not get_distribution and not cluster_states and not detect_period:
217
+ raise ValueError("Distribution, cluster states, and period detection cannot all be False.")
218
+
219
+ result : dict = self._commander.aem.analyze(windowlength_ms=duration_s * 1000, get_distribution=get_distribution, cluster=cluster_states, find_period=detect_period)
220
+ if not result["success"]:
221
+ return None
222
+
223
+ aem_analysis_result : types.AemAnalysisResult | None = AdapterBase.__parse_aem_analyze_output(result)
224
+ return aem_analysis_result
225
+
226
+ #########################################################
227
+
228
+ @staticmethod
229
+ def __parse_aem_analyze_output(result: dict) -> types.AemAnalysisResult | None:
230
+ if "result" not in result:
231
+ return None
232
+
233
+ aem_distribution : types.AemDistribution | None = None
234
+ aem_clustering : types.AemClustering | None = None
235
+ aem_period_detection : types.AemPeriodDetection | None = None
236
+ aem_signal_characteristics : types.AemSignalCharacteristics | None = None
237
+
238
+ dist_dict : dict | None = result["result"].get("distribution", None)
239
+ if dist_dict:
240
+ aem_distribution = types.AemDistribution()
241
+ aem_distribution.bins = []
242
+ for bin in dist_dict.get("bins", {}):
243
+ aem_distribution.bins.append(types.AemDistributionBin(
244
+ average_current = bin.get("average_current", None),
245
+ bin_max = bin.get("bin_max", None),
246
+ bin_min = bin.get("bin_min", None),
247
+ current_unit = bin.get("current_unit", None),
248
+ num_samples = bin.get("num_samples", None),
249
+ percentage = bin.get("percentage", None),
250
+ standard_deviation = bin.get("standard_deviation", None),
251
+ time = bin.get("time", None),
252
+ time_unit = bin.get("time_unit", None),
253
+ ))
254
+
255
+ aem_distribution.configuration = types.AemDistributionConfiguration(
256
+ bins = dist_dict.get("configuration", {}).get("bins", None),
257
+ logarithmic = dist_dict.get("configuration", {}).get("logarithmic", None),
258
+ )
259
+
260
+ aem_distribution.summary = types.AemDistributionSummary(
261
+ max_current = dist_dict.get("summary", {}).get("max_current", None),
262
+ min_current = dist_dict.get("summary", {}).get("min_current", None),
263
+ total_duration_ms = dist_dict.get("summary", {}).get("total_duration_ms", None),
264
+ total_samples = dist_dict.get("summary", {}).get("total_samples", None),
265
+ unit = dist_dict.get("summary", {}).get("unit", None),
266
+ )
267
+
268
+ aem_distribution.type = dist_dict.get("type", None)
269
+
270
+ cluster_dict : dict | None = result["result"].get("clustering", None)
271
+ if cluster_dict:
272
+ aem_clustering = types.AemClustering()
273
+ aem_clustering.blocks = []
274
+ for block in cluster_dict.get("blocks", {}):
275
+ aem_clustering.blocks.append(types.AemClusterBlock(
276
+ duration_ms = block.get("duration_ms", None),
277
+ end_ms = block.get("end_ms", None),
278
+ level_mA = block.get("level_mA", None),
279
+ max_mA = block.get("max_mA", None),
280
+ min_mA = block.get("min_mA", None),
281
+ range_mA = block.get("range_mA", None),
282
+ samples = block.get("samples", None),
283
+ start_ms = block.get("start_ms", None),
284
+ ))
285
+
286
+ aem_clustering.configuration = types.AemClusterConfiguration(
287
+ false_alarm_probability = cluster_dict.get("configuration", {}).get("false_alarm_probability", None),
288
+ max_points = cluster_dict.get("configuration", {}).get("max_points", None),
289
+ min_segment_ms = cluster_dict.get("configuration", {}).get("min_segment_ms", None),
290
+ )
291
+
292
+ aem_clustering.method = cluster_dict.get("method", None)
293
+ aem_clustering.total_blocks = cluster_dict.get("total_blocks", None)
294
+ aem_clustering.type = cluster_dict.get("type", None)
295
+ aem_clustering.unique_states = cluster_dict.get("unique_states", None)
296
+
297
+ period_dict : dict | None = result["result"].get("period_detection", None)
298
+ if period_dict:
299
+ aem_period_detection = types.AemPeriodDetection()
300
+ aem_period_detection.configuration = types.AemPeriodDetectionConfiguration(
301
+ max_period_ms = period_dict.get("configuration", {}).get("max_period_ms", None),
302
+ min_period_ms = period_dict.get("configuration", {}).get("min_period_ms", None),
303
+ )
304
+
305
+ aem_period_detection.result = types.AemPeriodDetectionResult()
306
+ aem_period_detection.result.confidence = period_dict.get("result", {}).get("confidence", None)
307
+ aem_period_detection.result.frequency_hz = period_dict.get("result", {}).get("frequency_hz", None)
308
+
309
+ aem_period_detection.result.interval_summary = types.AemPeriodDetectionIntervalSummary(
310
+ average_mean_current_ma = period_dict.get("result", {}).get("interval_summary", {}).get("average_mean_current_ma", None),
311
+ average_peak_current_ma = period_dict.get("result", {}).get("interval_summary", {}).get("average_peak_current_ma", None),
312
+ max_period_ms = period_dict.get("result", {}).get("interval_summary", {}).get("max_period_ms", None),
313
+ min_period_ms = period_dict.get("result", {}).get("interval_summary", {}).get("min_period_ms", None),
314
+ )
315
+
316
+ aem_period_detection.result.intervals = []
317
+ for interval in period_dict.get("result", {}).get("intervals", {}):
318
+ aem_period_detection.result.intervals.append(types.AemPeriodDetectionInterval(
319
+ cycle = interval.get("cycle", None),
320
+ end_index = interval.get("end_index", None),
321
+ end_ms = interval.get("end_ms", None),
322
+ mean_current_ma = interval.get("mean_current_ma", None),
323
+ peak_current_ma = interval.get("peak_current_ma", None),
324
+ period_ms = interval.get("period_ms", None),
325
+ start_index = interval.get("start_index", None),
326
+ start_ms = interval.get("start_ms", None),
327
+ ))
328
+
329
+ aem_period_detection.result.is_periodic = period_dict.get("result", {}).get("is_periodic", None)
330
+ aem_period_detection.result.jitter_relative = period_dict.get("result", {}).get("jitter_relative", None)
331
+ aem_period_detection.result.method = period_dict.get("result", {}).get("method", None)
332
+ aem_period_detection.result.num_cycles = period_dict.get("result", {}).get("num_cycles", None)
333
+ aem_period_detection.result.period_ms = period_dict.get("result", {}).get("period_ms", None)
334
+ aem_period_detection.result.method_results = []
335
+ for method_result in period_dict.get("result", {}).get("method_results", {}):
336
+ aem_period_detection.result.method_results.append(types.AemPeriodDetectionMethodResult(
337
+ method = method_result.get("method", None),
338
+ detected = method_result.get("detected", None),
339
+ confidence = method_result.get("confidence", None),
340
+ period_ms = method_result.get("period_ms", None),
341
+ relative_error = method_result.get("relative_error", None),
342
+ ))
343
+
344
+ aem_period_detection.type = period_dict.get("type", None)
345
+
346
+ signal_char_dict : dict | None = result["result"].get("signal_characteristics", None)
347
+ if signal_char_dict:
348
+ aem_signal_characteristics = types.AemSignalCharacteristics(
349
+ average_voltage_v = signal_char_dict.get("average_voltage_V", None),
350
+ dynamic_range_ratio = signal_char_dict.get("dynamic_range_ratio", None),
351
+ estimated_states = signal_char_dict.get("estimated_states", None),
352
+ max_current_ma = signal_char_dict.get("max_current_mA", None),
353
+ min_current_ma = signal_char_dict.get("min_current_mA", None),
354
+ noise_level_mad_sigma_ma = signal_char_dict.get("noise_level_mad_sigma_mA", None),
355
+ )
356
+
357
+ return types.AemAnalysisResult(
358
+ distribution = aem_distribution,
359
+ clustering = aem_clustering,
360
+ period_detection = aem_period_detection,
361
+ signal_characteristics = aem_signal_characteristics,
362
+ )
363
+
364
+ def __get_current_firmware_version(self) -> str | None:
365
+ result : dict = self._commander.adapter.probe()
366
+ if not result["success"]:
367
+ return None
368
+ return result["result"].get("firmware_info", {}).get("fw_version", None)