mpflash 1.0.1__py3-none-any.whl → 1.0.2__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.
Files changed (52) hide show
  1. mpflash/add_firmware.py +98 -98
  2. mpflash/ask_input.py +236 -236
  3. mpflash/basicgit.py +284 -284
  4. mpflash/bootloader/__init__.py +2 -2
  5. mpflash/bootloader/activate.py +60 -60
  6. mpflash/bootloader/detect.py +82 -82
  7. mpflash/bootloader/manual.py +101 -101
  8. mpflash/bootloader/micropython.py +12 -12
  9. mpflash/bootloader/touch1200.py +36 -36
  10. mpflash/cli_download.py +129 -129
  11. mpflash/cli_flash.py +224 -216
  12. mpflash/cli_group.py +111 -111
  13. mpflash/cli_list.py +87 -87
  14. mpflash/cli_main.py +39 -39
  15. mpflash/common.py +210 -177
  16. mpflash/config.py +44 -44
  17. mpflash/connected.py +104 -77
  18. mpflash/download.py +364 -364
  19. mpflash/downloaded.py +130 -130
  20. mpflash/errors.py +9 -9
  21. mpflash/flash/__init__.py +55 -55
  22. mpflash/flash/esp.py +59 -59
  23. mpflash/flash/stm32.py +19 -19
  24. mpflash/flash/stm32_dfu.py +104 -104
  25. mpflash/flash/uf2/__init__.py +88 -88
  26. mpflash/flash/uf2/boardid.py +15 -15
  27. mpflash/flash/uf2/linux.py +136 -130
  28. mpflash/flash/uf2/macos.py +42 -42
  29. mpflash/flash/uf2/uf2disk.py +12 -12
  30. mpflash/flash/uf2/windows.py +43 -43
  31. mpflash/flash/worklist.py +170 -170
  32. mpflash/list.py +106 -106
  33. mpflash/logger.py +41 -41
  34. mpflash/mpboard_id/__init__.py +93 -93
  35. mpflash/mpboard_id/add_boards.py +251 -251
  36. mpflash/mpboard_id/board.py +37 -37
  37. mpflash/mpboard_id/board_id.py +86 -86
  38. mpflash/mpboard_id/store.py +43 -43
  39. mpflash/mpremoteboard/__init__.py +266 -266
  40. mpflash/mpremoteboard/mpy_fw_info.py +141 -141
  41. mpflash/mpremoteboard/runner.py +140 -140
  42. mpflash/vendor/click_aliases.py +91 -91
  43. mpflash/vendor/dfu.py +165 -165
  44. mpflash/vendor/pydfu.py +605 -605
  45. mpflash/vendor/readme.md +2 -2
  46. mpflash/versions.py +135 -135
  47. {mpflash-1.0.1.dist-info → mpflash-1.0.2.dist-info}/LICENSE +20 -20
  48. {mpflash-1.0.1.dist-info → mpflash-1.0.2.dist-info}/METADATA +1 -1
  49. mpflash-1.0.2.dist-info/RECORD +53 -0
  50. mpflash-1.0.1.dist-info/RECORD +0 -53
  51. {mpflash-1.0.1.dist-info → mpflash-1.0.2.dist-info}/WHEEL +0 -0
  52. {mpflash-1.0.1.dist-info → mpflash-1.0.2.dist-info}/entry_points.txt +0 -0
mpflash/cli_list.py CHANGED
@@ -1,87 +1,87 @@
1
- import json
2
- from typing import List
3
-
4
- import rich_click as click
5
- from rich import print
6
-
7
- from .cli_group import cli
8
- from .connected import list_mcus
9
- from .list import show_mcus
10
- from .logger import make_quiet
11
-
12
-
13
- @cli.command(
14
- "list",
15
- help="List the connected MCU boards. alias: devs",
16
- aliases=["devs"],
17
- )
18
- @click.option(
19
- "--json",
20
- "-j",
21
- "as_json",
22
- is_flag=True,
23
- default=False,
24
- show_default=True,
25
- help="""Output in json format""",
26
- )
27
- @click.option(
28
- "--serial",
29
- "--serial-port",
30
- "-s",
31
- "serial",
32
- default=["*"],
33
- multiple=True,
34
- show_default=True,
35
- help="Serial port(s) (or globs) to list. ",
36
- metavar="SERIALPORT",
37
- )
38
- @click.option(
39
- "--ignore",
40
- "-i",
41
- is_eager=True,
42
- help="Serial port(s) (or globs) to ignore. Defaults to MPFLASH_IGNORE.",
43
- multiple=True,
44
- default=[],
45
- envvar="MPFLASH_IGNORE",
46
- show_default=True,
47
- metavar="SERIALPORT",
48
- )
49
- @click.option(
50
- "--bluetooth/--no-bluetooth",
51
- "-b/-nb",
52
- is_flag=True,
53
- default=False,
54
- show_default=True,
55
- help="""Include bluetooth ports in the list""",
56
- )
57
- @click.option(
58
- "--progress/--no-progress",
59
- # "-p/-np", -p is already used for --port
60
- "progress",
61
- is_flag=True,
62
- default=True,
63
- show_default=True,
64
- help="""Show progress""",
65
- )
66
- def cli_list_mcus(serial: List[str], ignore: List[str], bluetooth: bool, as_json: bool, progress: bool = True) -> int:
67
- """List the connected MCU boards, and output in a nice table or json."""
68
- serial = list(serial)
69
- ignore = list(ignore)
70
- if as_json:
71
- # avoid noise in json output
72
- make_quiet()
73
- # TODO? Ask user to select a serialport if [?] is given ?
74
-
75
- conn_mcus = list_mcus(ignore=ignore, include=serial, bluetooth=bluetooth)
76
- # ignore boards that have the [micropython-stubber] ignore flag set
77
- conn_mcus = [item for item in conn_mcus if not (item.toml.get("mpflash", {}).get("ignore", False))]
78
- if as_json:
79
- # remove the path and firmware attibutes from the json output as they are always empty
80
- for mcu in conn_mcus:
81
- del mcu.path
82
- del mcu.firmware
83
- print(json.dumps([mcu.__dict__ for mcu in conn_mcus], indent=4))
84
- progress = False
85
- if progress:
86
- show_mcus(conn_mcus, refresh=False)
87
- return 0 if conn_mcus else 1
1
+ import json
2
+ from typing import List
3
+
4
+ import rich_click as click
5
+ from rich import print
6
+
7
+ from .cli_group import cli
8
+ from .connected import list_mcus
9
+ from .list import show_mcus
10
+ from .logger import make_quiet
11
+
12
+
13
+ @cli.command(
14
+ "list",
15
+ help="List the connected MCU boards. alias: devs",
16
+ aliases=["devs"],
17
+ )
18
+ @click.option(
19
+ "--json",
20
+ "-j",
21
+ "as_json",
22
+ is_flag=True,
23
+ default=False,
24
+ show_default=True,
25
+ help="""Output in json format""",
26
+ )
27
+ @click.option(
28
+ "--serial",
29
+ "--serial-port",
30
+ "-s",
31
+ "serial",
32
+ default=["*"],
33
+ multiple=True,
34
+ show_default=True,
35
+ help="Serial port(s) (or globs) to list. ",
36
+ metavar="SERIALPORT",
37
+ )
38
+ @click.option(
39
+ "--ignore",
40
+ "-i",
41
+ is_eager=True,
42
+ help="Serial port(s) (or globs) to ignore. Defaults to MPFLASH_IGNORE.",
43
+ multiple=True,
44
+ default=[],
45
+ envvar="MPFLASH_IGNORE",
46
+ show_default=True,
47
+ metavar="SERIALPORT",
48
+ )
49
+ @click.option(
50
+ "--bluetooth/--no-bluetooth",
51
+ "-b/-nb",
52
+ is_flag=True,
53
+ default=False,
54
+ show_default=True,
55
+ help="""Include bluetooth ports in the list""",
56
+ )
57
+ @click.option(
58
+ "--progress/--no-progress",
59
+ # "-p/-np", -p is already used for --port
60
+ "progress",
61
+ is_flag=True,
62
+ default=True,
63
+ show_default=True,
64
+ help="""Show progress""",
65
+ )
66
+ def cli_list_mcus(serial: List[str], ignore: List[str], bluetooth: bool, as_json: bool, progress: bool = True) -> int:
67
+ """List the connected MCU boards, and output in a nice table or json."""
68
+ serial = list(serial)
69
+ ignore = list(ignore)
70
+ if as_json:
71
+ # avoid noise in json output
72
+ make_quiet()
73
+ # TODO? Ask user to select a serialport if [?] is given ?
74
+
75
+ conn_mcus = list_mcus(ignore=ignore, include=serial, bluetooth=bluetooth)
76
+ # ignore boards that have the [micropython-stubber] ignore flag set
77
+ conn_mcus = [item for item in conn_mcus if not (item.toml.get("mpflash", {}).get("ignore", False))]
78
+ if as_json:
79
+ # remove the path and firmware attibutes from the json output as they are always empty
80
+ for mcu in conn_mcus:
81
+ del mcu.path
82
+ del mcu.firmware
83
+ print(json.dumps([mcu.__dict__ for mcu in conn_mcus], indent=4))
84
+ progress = False
85
+ if progress:
86
+ show_mcus(conn_mcus, refresh=False)
87
+ return 0 if conn_mcus else 1
mpflash/cli_main.py CHANGED
@@ -1,39 +1,39 @@
1
- """mpflash is a CLI to download and flash MicroPython firmware to various boards."""
2
-
3
- import os
4
-
5
- import click.exceptions as click_exceptions
6
- from loguru import logger as log
7
-
8
- from .cli_download import cli_download
9
- from .cli_flash import cli_flash_board
10
- from .cli_group import cli
11
- from .cli_list import cli_list_mcus
12
-
13
-
14
- def mpflash():
15
- cli.add_command(cli_list_mcus)
16
- cli.add_command(cli_download)
17
- cli.add_command(cli_flash_board)
18
-
19
- # cli(auto_envvar_prefix="MPFLASH")
20
- if False and os.environ.get("COMPUTERNAME").startswith("JOSVERL"):
21
- # intentional less error suppression on dev machine
22
- result = cli(standalone_mode=False)
23
- else:
24
- try:
25
- result = cli(standalone_mode=True)
26
- exit(result)
27
- except AttributeError as e:
28
- log.error(f"Error: {e}")
29
- exit(-1)
30
- except click_exceptions.ClickException as e:
31
- log.error(f"Error: {e}")
32
- exit(-2)
33
- except click_exceptions.Abort as e:
34
- # Aborted - Ctrl-C
35
- exit(-3)
36
-
37
-
38
- if __name__ == "__main__":
39
- mpflash()
1
+ """mpflash is a CLI to download and flash MicroPython firmware to various boards."""
2
+
3
+ import os
4
+
5
+ import click.exceptions as click_exceptions
6
+ from loguru import logger as log
7
+
8
+ from .cli_download import cli_download
9
+ from .cli_flash import cli_flash_board
10
+ from .cli_group import cli
11
+ from .cli_list import cli_list_mcus
12
+
13
+
14
+ def mpflash():
15
+ cli.add_command(cli_list_mcus)
16
+ cli.add_command(cli_download)
17
+ cli.add_command(cli_flash_board)
18
+
19
+ # cli(auto_envvar_prefix="MPFLASH")
20
+ if False and os.environ.get("COMPUTERNAME").startswith("JOSVERL"):
21
+ # intentional less error suppression on dev machine
22
+ result = cli(standalone_mode=False)
23
+ else:
24
+ try:
25
+ result = cli(standalone_mode=True)
26
+ exit(result)
27
+ except AttributeError as e:
28
+ log.error(f"Error: {e}")
29
+ exit(-1)
30
+ except click_exceptions.ClickException as e:
31
+ log.error(f"Error: {e}")
32
+ exit(-2)
33
+ except click_exceptions.Abort as e:
34
+ # Aborted - Ctrl-C
35
+ exit(-3)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ mpflash()
mpflash/common.py CHANGED
@@ -1,177 +1,210 @@
1
- import fnmatch
2
- import os
3
- import sys
4
- from dataclasses import dataclass, field
5
- from enum import Enum
6
- from pathlib import Path
7
- from typing import List, Optional, Union
8
-
9
- from github import Auth, Github
10
- from serial.tools import list_ports
11
- from serial.tools.list_ports_common import ListPortInfo
12
-
13
- from .logger import log
14
-
15
- # from mpflash.mpremoteboard import MPRemoteBoard
16
-
17
- PORT_FWTYPES = {
18
- "stm32": [".dfu"], # need .dfu for pydfu.py - .hex for cube cli/GUI
19
- "esp32": [".bin"],
20
- "esp8266": [".bin"],
21
- "rp2": [".uf2"],
22
- "samd": [".uf2"],
23
- # below this not yet implemented / tested
24
- "mimxrt": [".hex"],
25
- "nrf": [".uf2"],
26
- "renesas-ra": [".hex"],
27
- }
28
-
29
- # Token with no permissions to avoid throttling
30
- # https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#getting-a-higher-rate-limit
31
- PAT_NO_ACCESS = (
32
- "github_pat"
33
- + "_11AAHPVFQ0qAkDnSUaMKSp"
34
- + "_ZkDl5NRRwBsUN6EYg9ahp1Dvj4FDDONnXVgimxC2EtpY7Q7BUKBoQ0Jq72X"
35
- )
36
- PAT = os.environ.get("GITHUB_TOKEN") or PAT_NO_ACCESS
37
- GH_CLIENT = Github(auth=Auth.Token(PAT))
38
-
39
-
40
- @dataclass
41
- class FWInfo:
42
- """
43
- Downloaded Firmware information
44
- is somewhat related to the BOARD class in the mpboard_id module
45
- """
46
-
47
- port: str # MicroPython port
48
- board: str # MicroPython board
49
- filename: str = field(default="") # relative filename of the firmware image
50
- firmware: str = field(default="") # url or path to original firmware image
51
- variant: str = field(default="") # MicroPython variant
52
- preview: bool = field(default=False) # True if the firmware is a preview version
53
- version: str = field(default="") # MicroPython version (NO v prefix)
54
- url: str = field(default="") # url to the firmware image download folder
55
- build: str = field(default="0") # The build = number of commits since the last release
56
- ext: str = field(default="") # the file extension of the firmware
57
- family: str = field(default="micropython") # The family of the firmware
58
- custom: bool = field(default=False) # True if the firmware is a custom build
59
- description: str = field(default="") # Description used by this firmware (custom only)
60
-
61
- def to_dict(self) -> dict:
62
- """Convert the object to a dictionary"""
63
- return self.__dict__
64
-
65
- @classmethod
66
- def from_dict(cls, data: dict) -> "FWInfo":
67
- """Create a FWInfo object from a dictionary"""
68
- # add missing keys
69
- if "ext" not in data:
70
- data["ext"] = Path(data["firmware"]).suffix
71
- if "family" not in data:
72
- data["family"] = "micropython"
73
- return cls(**data)
74
-
75
-
76
- @dataclass
77
- class Params:
78
- """Common parameters for downloading and flashing firmware"""
79
-
80
- ports: List[str] = field(default_factory=list)
81
- boards: List[str] = field(default_factory=list)
82
- versions: List[str] = field(default_factory=list)
83
- fw_folder: Path = Path()
84
- serial: List[str] = field(default_factory=list)
85
- ignore: List[str] = field(default_factory=list)
86
-
87
-
88
- @dataclass
89
- class DownloadParams(Params):
90
- """Parameters for downloading firmware"""
91
-
92
- clean: bool = False
93
- force: bool = False
94
-
95
-
96
- class BootloaderMethod(Enum):
97
- AUTO = "auto"
98
- MANUAL = "manual"
99
- MPY = "mpy"
100
- TOUCH_1200 = "touch1200"
101
- NONE = "none"
102
-
103
-
104
- @dataclass
105
- class FlashParams(Params):
106
- """Parameters for flashing a board"""
107
-
108
- erase: bool = True
109
- bootloader: BootloaderMethod = BootloaderMethod.NONE
110
- cpu: str = ""
111
-
112
- def __post_init__(self):
113
- if isinstance(self.bootloader, str):
114
- self.bootloader = BootloaderMethod(self.bootloader)
115
-
116
-
117
- ParamType = Union[DownloadParams, FlashParams]
118
-
119
-
120
- def filtered_comports(
121
- ignore: Optional[List[str]] = None,
122
- include: Optional[List[str]] = None,
123
- bluetooth: bool = False,
124
- ) -> List[ListPortInfo]: # sourcery skip: assign-if-exp
125
- """
126
- Get a list of filtered comports using the include and ignore lists.
127
- both can be globs (e.g. COM*) or exact port names (e.g. COM1)
128
- """
129
- if not ignore:
130
- ignore = []
131
- elif not isinstance(ignore, list): # type: ignore
132
- ignore = list(ignore)
133
- if not include:
134
- include = ["*"]
135
- elif not isinstance(include, list): # type: ignore
136
- include = list(include)
137
-
138
- # remove ports that are to be ignored
139
- log.trace(f"{include=}, {ignore=}, {bluetooth=}")
140
- # use p.location to filter out the bogus ports on newer Linux kernels
141
- comports = [
142
- p
143
- for p in list_ports.comports()
144
- if p.location and not any(fnmatch.fnmatch(p.device, i) for i in ignore)
145
- ]
146
- log.trace(f"comports: {[p.device for p in comports]}")
147
- # remove bluetooth ports
148
-
149
- if include != ["*"]:
150
- # if there are explicit ports to include, add them to the list
151
- explicit = [
152
- p for p in list_ports.comports() if any(fnmatch.fnmatch(p.device, i) for i in include)
153
- ]
154
- log.trace(f"explicit: {[p.device for p in explicit]}")
155
- if ignore == []:
156
- # if nothing to ignore, just use the explicit list as a sinple sane default
157
- comports = explicit
158
- else:
159
- # if there are ports to ignore, add the explicit list to the filtered list
160
- comports = list(set(explicit) | set(comports))
161
- if not bluetooth:
162
- # filter out bluetooth ports
163
- comports = [p for p in comports if "bluetooth" not in p.description.lower()]
164
- comports = [p for p in comports if "BTHENUM" not in p.hwid]
165
- if sys.platform == "darwin":
166
- comports = [p for p in comports if ".Bluetooth" not in p.device]
167
- log.trace(f"no Bluetooth: {[p.device for p in comports]}")
168
- log.debug(f"filtered_comports: {[p.device for p in comports]}")
169
- # sort
170
- if sys.platform == "win32":
171
- # Windows sort of comports by number - but fallback to device name
172
- return sorted(
173
- comports,
174
- key=lambda x: int(x.device.split()[0][3:]) if x.device.split()[0][3:].isdigit() else x,
175
- )
176
- # sort by device name
177
- return sorted(comports, key=lambda x: x.device)
1
+ import fnmatch
2
+ import glob
3
+ import os
4
+ import platform
5
+ import sys
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import List, Optional, Union
10
+
11
+ from github import Auth, Github
12
+ from serial.tools import list_ports
13
+ from serial.tools.list_ports_common import ListPortInfo
14
+
15
+ from .logger import log
16
+
17
+ # from mpflash.mpremoteboard import MPRemoteBoard
18
+
19
+ PORT_FWTYPES = {
20
+ "stm32": [".dfu"], # need .dfu for pydfu.py - .hex for cube cli/GUI
21
+ "esp32": [".bin"],
22
+ "esp8266": [".bin"],
23
+ "rp2": [".uf2"],
24
+ "samd": [".uf2"],
25
+ # below this not yet implemented / tested
26
+ "mimxrt": [".hex"],
27
+ "nrf": [".uf2"],
28
+ "renesas-ra": [".hex"],
29
+ }
30
+
31
+ # Token with no permissions to avoid throttling
32
+ # https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#getting-a-higher-rate-limit
33
+ PAT_NO_ACCESS = (
34
+ "github_pat"
35
+ + "_11AAHPVFQ0qAkDnSUaMKSp"
36
+ + "_ZkDl5NRRwBsUN6EYg9ahp1Dvj4FDDONnXVgimxC2EtpY7Q7BUKBoQ0Jq72X"
37
+ )
38
+ PAT = os.environ.get("GITHUB_TOKEN") or PAT_NO_ACCESS
39
+ GH_CLIENT = Github(auth=Auth.Token(PAT))
40
+
41
+
42
+ @dataclass
43
+ class FWInfo:
44
+ """
45
+ Downloaded Firmware information
46
+ is somewhat related to the BOARD class in the mpboard_id module
47
+ """
48
+
49
+ port: str # MicroPython port
50
+ board: str # MicroPython board
51
+ filename: str = field(default="") # relative filename of the firmware image
52
+ firmware: str = field(default="") # url or path to original firmware image
53
+ variant: str = field(default="") # MicroPython variant
54
+ preview: bool = field(default=False) # True if the firmware is a preview version
55
+ version: str = field(default="") # MicroPython version (NO v prefix)
56
+ url: str = field(default="") # url to the firmware image download folder
57
+ build: str = field(default="0") # The build = number of commits since the last release
58
+ ext: str = field(default="") # the file extension of the firmware
59
+ family: str = field(default="micropython") # The family of the firmware
60
+ custom: bool = field(default=False) # True if the firmware is a custom build
61
+ description: str = field(default="") # Description used by this firmware (custom only)
62
+
63
+ def to_dict(self) -> dict:
64
+ """Convert the object to a dictionary"""
65
+ return self.__dict__
66
+
67
+ @classmethod
68
+ def from_dict(cls, data: dict) -> "FWInfo":
69
+ """Create a FWInfo object from a dictionary"""
70
+ # add missing keys
71
+ if "ext" not in data:
72
+ data["ext"] = Path(data["firmware"]).suffix
73
+ if "family" not in data:
74
+ data["family"] = "micropython"
75
+ return cls(**data)
76
+
77
+
78
+ @dataclass
79
+ class Params:
80
+ """Common parameters for downloading and flashing firmware"""
81
+
82
+ ports: List[str] = field(default_factory=list)
83
+ boards: List[str] = field(default_factory=list)
84
+ versions: List[str] = field(default_factory=list)
85
+ fw_folder: Path = Path()
86
+ serial: List[str] = field(default_factory=list)
87
+ ignore: List[str] = field(default_factory=list)
88
+ bluetooth: bool = False
89
+
90
+
91
+ @dataclass
92
+ class DownloadParams(Params):
93
+ """Parameters for downloading firmware"""
94
+
95
+ clean: bool = False
96
+ force: bool = False
97
+
98
+
99
+ class BootloaderMethod(Enum):
100
+ AUTO = "auto"
101
+ MANUAL = "manual"
102
+ MPY = "mpy"
103
+ TOUCH_1200 = "touch1200"
104
+ NONE = "none"
105
+
106
+
107
+ @dataclass
108
+ class FlashParams(Params):
109
+ """Parameters for flashing a board"""
110
+
111
+ erase: bool = True
112
+ bootloader: BootloaderMethod = BootloaderMethod.NONE
113
+ cpu: str = ""
114
+
115
+ def __post_init__(self):
116
+ if isinstance(self.bootloader, str):
117
+ self.bootloader = BootloaderMethod(self.bootloader)
118
+
119
+
120
+ ParamType = Union[DownloadParams, FlashParams]
121
+
122
+
123
+ def filtered_comports(
124
+ ignore: Optional[List[str]] = None,
125
+ include: Optional[List[str]] = None,
126
+ bluetooth: bool = False,
127
+ ) -> List[ListPortInfo]: # sourcery skip: assign-if-exp
128
+ """
129
+ Get a list of filtered comports using the include and ignore lists.
130
+ both can be globs (e.g. COM*) or exact port names (e.g. COM1)
131
+ """
132
+ if not ignore:
133
+ ignore = []
134
+ elif not isinstance(ignore, list): # type: ignore
135
+ ignore = list(ignore)
136
+ if not include:
137
+ include = ["*"]
138
+ elif not isinstance(include, list): # type: ignore
139
+ include = list(include)
140
+
141
+ # remove ports that are to be ignored
142
+ log.trace(f"{include=}, {ignore=}, {bluetooth=}")
143
+
144
+ comports = [
145
+ p for p in list_ports.comports() if not any(fnmatch.fnmatch(p.device, i) for i in ignore)
146
+ ]
147
+ if platform.system() == "Linux":
148
+ # use p.location to filter out the bogus ports on newer Linux kernels
149
+ # filter out the bogus ports on newer Linux kernels
150
+ comports = [p for p in comports if p.location]
151
+
152
+ log.trace(f"comports: {[p.device for p in comports]}")
153
+ # remove bluetooth ports
154
+
155
+ if include != ["*"]:
156
+ # if there are explicit ports to include, add them to the list
157
+ explicit = [
158
+ p for p in list_ports.comports() if any(fnmatch.fnmatch(p.device, i) for i in include)
159
+ ]
160
+ log.trace(f"explicit: {[p.device for p in explicit]}")
161
+ if ignore == []:
162
+ # if nothing to ignore, just use the explicit list as a sinple sane default
163
+ comports = explicit
164
+ else:
165
+ # if there are ports to ignore, add the explicit list to the filtered list
166
+ comports = list(set(explicit) | set(comports))
167
+ if not bluetooth:
168
+ # filter out bluetooth ports
169
+ comports = [p for p in comports if "bluetooth" not in p.description.lower()]
170
+ comports = [p for p in comports if "BTHENUM" not in p.hwid]
171
+ if sys.platform == "darwin":
172
+ comports = [p for p in comports if ".Bluetooth" not in p.device]
173
+ log.trace(f"no Bluetooth: {[p.device for p in comports]}")
174
+ log.debug(f"filtered_comports: {[p.device for p in comports]}")
175
+ # sort
176
+ if sys.platform == "win32":
177
+ # Windows sort of comports by number - but fallback to device name
178
+ return sorted(
179
+ comports,
180
+ key=lambda x: int(x.device.split()[0][3:]) if x.device.split()[0][3:].isdigit() else x,
181
+ )
182
+ # sort by device name
183
+ return sorted(comports, key=lambda x: x.device)
184
+
185
+
186
+ def find_serial_by_path(target_port: str):
187
+ """Find the symbolic link path of a serial port by its device path."""
188
+ # sourcery skip: use-next
189
+
190
+ if os.name == "nt":
191
+ return None
192
+ # List all available serial ports
193
+ available_ports = list_ports.comports()
194
+ # Filter to get the device path of the target port
195
+ target_device_path = None
196
+ for port in available_ports:
197
+ if port.device == target_port:
198
+ target_device_path = port.device
199
+ break
200
+
201
+ if not target_device_path:
202
+ return None # Target port not found among available ports
203
+
204
+ # Search for all symbolic links in /dev/serial/by-path/
205
+ for symlink in glob.glob("/dev/serial/by-path/*"):
206
+ # Resolve the symbolic link to its target
207
+ if os.path.realpath(symlink) == target_device_path:
208
+ return symlink # Return the matching symlink path
209
+
210
+ return None # Return None if no match is found