pycan-send 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __pycache__/
@@ -0,0 +1,9 @@
1
+ pycan-send, Send can-frames encoded according to a KCD database
2
+ Copyright (c) 2025-present Alexander Becker <nabla.becker@mailbox.org>
3
+
4
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
+
6
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
7
+
8
+ You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
9
+
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycan-send
3
+ Version: 0.0.1
4
+ Summary: Command line tool that quickly lets you send a can frame using the message and signal names encoded in a KCD
5
+ Project-URL: Documentation, https://sr.ht/~laplace/pycan-send#readme
6
+ Project-URL: Issues, https://sr.ht/~laplace/pycan-send/issues
7
+ Project-URL: Source, https://sr.ht/~laplace/pycan-send
8
+ Author-email: Alexander Becker <nabla.becker@mailbox.org>
9
+ License-Expression: GPL-3.0-or-later
10
+ License-File: LICENSE.txt
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: Implementation :: CPython
19
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
20
+ Requires-Python: >=3.8
21
+ Requires-Dist: cantools
22
+ Requires-Dist: click
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pycan-send
26
+
27
+ [![PyPI - Version](https://img.shields.io/pypi/v/pycan-send.svg)](https://pypi.org/project/pycan-send)
28
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pycan-send.svg)](https://pypi.org/project/pycan-send)
29
+
30
+ -----
31
+
32
+ ## Table of Contents
33
+
34
+ - [Installation](#installation)
35
+ - [License](#license)
36
+
37
+ ## Installation
38
+
39
+ ```console
40
+ pip install pycan-send
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```console
46
+ pycan-send path/to/file.kcd can0 -m MessageName -s SignalName Value
47
+ ```
48
+
49
+ `Value` can be numeric or, for signals that define labeled choices in the
50
+ KCD, the label name itself (e.g. `-s Mode Active`).
51
+
52
+ ## Fish tab completion
53
+
54
+ One-shot (current shell only):
55
+
56
+ ```fish
57
+ _PYCAN_SEND_COMPLETE=fish_source pycan-send | source
58
+ ```
59
+
60
+ Persistent:
61
+
62
+ ```fish
63
+ _PYCAN_SEND_COMPLETE=fish_source pycan-send > ~/.config/fish/completions/pycan-send.fish
64
+ ```
65
+
66
+ Once enabled, tab completion works for KCD files, message names (`-m <TAB>`
67
+ after the KCD path), signal names, and choice labels — all pulled live from
68
+ the KCD file given on the command line.
69
+
70
+ ## License
71
+
72
+ `pycan-send` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,48 @@
1
+ # pycan-send
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/pycan-send.svg)](https://pypi.org/project/pycan-send)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pycan-send.svg)](https://pypi.org/project/pycan-send)
5
+
6
+ -----
7
+
8
+ ## Table of Contents
9
+
10
+ - [Installation](#installation)
11
+ - [License](#license)
12
+
13
+ ## Installation
14
+
15
+ ```console
16
+ pip install pycan-send
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```console
22
+ pycan-send path/to/file.kcd can0 -m MessageName -s SignalName Value
23
+ ```
24
+
25
+ `Value` can be numeric or, for signals that define labeled choices in the
26
+ KCD, the label name itself (e.g. `-s Mode Active`).
27
+
28
+ ## Fish tab completion
29
+
30
+ One-shot (current shell only):
31
+
32
+ ```fish
33
+ _PYCAN_SEND_COMPLETE=fish_source pycan-send | source
34
+ ```
35
+
36
+ Persistent:
37
+
38
+ ```fish
39
+ _PYCAN_SEND_COMPLETE=fish_source pycan-send > ~/.config/fish/completions/pycan-send.fish
40
+ ```
41
+
42
+ Once enabled, tab completion works for KCD files, message names (`-m <TAB>`
43
+ after the KCD path), signal names, and choice labels — all pulled live from
44
+ the KCD file given on the command line.
45
+
46
+ ## License
47
+
48
+ `pycan-send` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pycan-send"
7
+ dynamic = ["version"]
8
+ description = 'Command line tool that quickly lets you send a can frame using the message and signal names encoded in a KCD'
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "GPL-3.0-or-later"
12
+ keywords = []
13
+ authors = [
14
+ { name = "Alexander Becker", email = "nabla.becker@mailbox.org" },
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
+ 'click',
29
+ 'cantools',
30
+ ]
31
+
32
+ [project.urls]
33
+ Documentation = "https://sr.ht/~laplace/pycan-send#readme"
34
+ Issues = "https://sr.ht/~laplace/pycan-send/issues"
35
+ Source = "https://sr.ht/~laplace/pycan-send"
36
+
37
+ [project.scripts]
38
+ pycan-send = "pycan_send.main:main"
39
+
40
+ [tool.hatch.version]
41
+ path = "src/pycan_send/__about__.py"
42
+
43
+ [tool.hatch.envs.types]
44
+ extra-dependencies = [
45
+ "mypy>=1.0.0",
46
+ ]
47
+ [tool.hatch.envs.types.scripts]
48
+ check = "mypy --install-types --non-interactive {args:src/pycan_send tests}"
49
+
50
+ [tool.coverage.run]
51
+ source_pkgs = ["pycan_send", "tests"]
52
+ branch = true
53
+ parallel = true
54
+ omit = [
55
+ "src/pycan_send/__about__.py",
56
+ ]
57
+
58
+ [tool.coverage.paths]
59
+ pycan_send = ["src/pycan_send", "*/pycan-send/src/pycan_send"]
60
+ tests = ["tests", "*/pycan-send/tests"]
61
+
62
+ [tool.coverage.report]
63
+ exclude_lines = [
64
+ "no cov",
65
+ "if __name__ == .__main__.:",
66
+ "if TYPE_CHECKING:",
67
+ ]
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2025-present Alexander Becker <nabla.becker@mailbox.org>
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+ __version__ = "0.0.1"
5
+ __author__ = "Alexander Becker"
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025-present Alexander Becker <nabla.becker@mailbox.org>
2
+ #
3
+ # SPDX-License-Identifier: MIT
@@ -0,0 +1,202 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ import cantools
5
+ import can
6
+ from click.shell_completion import CompletionItem
7
+
8
+
9
+ def _kcd_from_ctx(ctx):
10
+ """During shell completion, positional args aren't parsed into
11
+ ctx.params yet — they sit in ctx.args. Check both."""
12
+ kcd = ctx.params.get("kcd_file")
13
+ if kcd:
14
+ return kcd
15
+ for a in ctx.args:
16
+ if a.endswith(".kcd"):
17
+ return a
18
+ return None
19
+
20
+
21
+ def _load_db(ctx):
22
+ kcd = _kcd_from_ctx(ctx)
23
+ if not kcd:
24
+ return None
25
+ kcd = Path(kcd).expanduser()
26
+ try:
27
+ return cantools.database.load_file(str(kcd))
28
+ except Exception:
29
+ return None
30
+
31
+
32
+ def _complete_message(ctx, param, incomplete):
33
+ db = _load_db(ctx)
34
+ if db is None:
35
+ return []
36
+ return [
37
+ CompletionItem(m.name)
38
+ for m in db.messages
39
+ if m.name.startswith(incomplete)
40
+ ]
41
+
42
+
43
+ def _complete_signal(ctx, param, incomplete):
44
+ db = _load_db(ctx)
45
+ if db is None:
46
+ return []
47
+ message = ctx.params.get("message")
48
+ existing = ctx.params.get("signal") or ()
49
+
50
+ # Click invokes shell_complete once per tuple element. Determine which
51
+ # element we're completing by the count of already-parsed pairs vs the
52
+ # raw tokens on the command line.
53
+ # Heuristic: if the last existing tuple is incomplete, we're on value.
54
+ # Simpler: inspect the current incomplete position via len of trailing
55
+ # partial. Click passes only the current token as `incomplete`, so use
56
+ # ctx.params to decide: if #signals * 2 tokens already consumed, we're
57
+ # on a name; otherwise on a value.
58
+ # `existing` is a tuple of fully-parsed (name, value) pairs; Click only
59
+ # adds a pair once both tokens are provided. So when shell_complete
60
+ # fires, we're always completing the NAME of the next pair... unless
61
+ # the user is mid-pair. We can't distinguish from ctx alone, so offer
62
+ # both: signal names (always useful) plus — if the previous token on
63
+ # argv was a known signal name — its choice labels.
64
+ if not message:
65
+ return []
66
+ try:
67
+ db_msg = db.get_message_by_name(message)
68
+ except KeyError:
69
+ return []
70
+
71
+ items = [
72
+ CompletionItem(s.name)
73
+ for s in db_msg.signals
74
+ if s.name.startswith(incomplete)
75
+ ]
76
+
77
+ # Also add choice labels for any signal in the message whose choices
78
+ # start with `incomplete` — this makes value completion work after
79
+ # typing `-s SigName <TAB>`.
80
+ for s in db_msg.signals:
81
+ if not s.choices:
82
+ continue
83
+ for raw_label in s.choices.values():
84
+ label = str(raw_label)
85
+ if label.startswith(incomplete):
86
+ items.append(CompletionItem(label))
87
+ return items
88
+
89
+
90
+ def _complete_kcd(ctx, param, incomplete):
91
+ raw = incomplete or ""
92
+ expanded = Path(raw).expanduser() if raw else Path(".")
93
+ if raw.endswith("/") and expanded.is_dir():
94
+ directory, prefix = expanded, ""
95
+ else:
96
+ directory, prefix = expanded.parent, expanded.name
97
+ if not directory.exists():
98
+ return []
99
+
100
+ # Keep the user-visible form (~/foo, ./foo, foo) rather than
101
+ # replacing the token with an absolute path.
102
+ if raw.startswith("~"):
103
+ home = Path.home()
104
+
105
+ def display(p: Path) -> str:
106
+ try:
107
+ return "~/" + str(p.relative_to(home))
108
+ except ValueError:
109
+ return str(p)
110
+ else:
111
+ def display(p: Path) -> str:
112
+ return str(p)
113
+
114
+ items = []
115
+ for entry in directory.iterdir():
116
+ if not entry.name.startswith(prefix):
117
+ continue
118
+ shown = display(entry)
119
+ if entry.is_dir():
120
+ items.append(CompletionItem(shown + "/", type="plain"))
121
+ elif entry.suffix == ".kcd":
122
+ items.append(CompletionItem(shown))
123
+ return items
124
+
125
+
126
+ def _coerce_signal_value(sig, raw):
127
+ """Accept a numeric string or a choice label and return what cantools
128
+ encode() should receive."""
129
+ try:
130
+ f = float(raw)
131
+ if f.is_integer():
132
+ return int(f)
133
+ return f
134
+ except ValueError:
135
+ pass
136
+ if sig.choices:
137
+ labels = {str(v): k for k, v in sig.choices.items()}
138
+ if raw in labels:
139
+ # cantools encode accepts the label string directly when a
140
+ # signal has choices, so pass it through.
141
+ return raw
142
+ raise click.ClickException(
143
+ f"Value '{raw}' for signal '{sig.name}' is not numeric and is not "
144
+ f"one of the defined labels: "
145
+ f"{', '.join(str(v) for v in sig.choices.values()) if sig.choices else '(none)'}"
146
+ )
147
+
148
+
149
+ @click.command()
150
+ @click.argument("KCD-File", type=click.Path(exists=True, dir_okay=False),
151
+ required=True, nargs=1, shell_complete=_complete_kcd)
152
+ @click.argument("can-interface", type=str, required=True)
153
+ @click.option("-p", "--prefix", required=False, default="0x0", type=str,
154
+ help="A prefixe that is 'ORed' with the message id. This is "
155
+ "useful if there are multiple identical devices on the bus")
156
+ @click.option("-m", "--message", required=False, type=str, multiple=False,
157
+ shell_complete=_complete_message)
158
+ @click.option("-s", "--signal", required=False, type=(str, str), multiple=True,
159
+ shell_complete=_complete_signal)
160
+ def main(kcd_file, can_interface, prefix, message, signal):
161
+ signals = signal
162
+ prefix = int(prefix, 16)
163
+ try:
164
+ can_db = cantools.database.load_file(kcd_file)
165
+ except ValueError as e:
166
+ click.echo(e)
167
+ return
168
+
169
+ try:
170
+ db_msg = can_db.get_message_by_name(message)
171
+ except KeyError:
172
+ click.echo(f"The message '{message}' could not be found")
173
+ click.echo("Available messages are:")
174
+ for msg in can_db.messages:
175
+ click.echo(f" {msg.name}")
176
+ return
177
+
178
+ resolved = {}
179
+ for signame, raw in signals:
180
+ try:
181
+ sig = db_msg.get_signal_by_name(signame)
182
+ except KeyError:
183
+ click.echo(f"The message '{message}' does not contain '{signame}'")
184
+ click.echo(f"Available signals in '{message}' are:")
185
+ for s in db_msg.signals:
186
+ click.echo(f" {s.name}")
187
+ return
188
+ resolved[signame] = _coerce_signal_value(sig, raw)
189
+
190
+ try:
191
+ other_signals = {s.name: 0 for s in db_msg.signals if s.name not in resolved}
192
+ encode_input = resolved | other_signals
193
+ data = db_msg.encode(encode_input)
194
+ except OverflowError as err:
195
+ click.echo(str(err))
196
+ exit(-1)
197
+
198
+ bus = can.interface.Bus(channel=can_interface, interface="socketcan")
199
+ msg = can.Message(arbitration_id=db_msg.frame_id | prefix, data=data,
200
+ is_extended_id=True)
201
+ bus.send(msg)
202
+ bus.shutdown()
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025-present Alexander Becker <nabla.becker@mailbox.org>
2
+ #
3
+ # SPDX-License-Identifier: MIT