pycan-send 0.0.1__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.
- pycan_send/__about__.py +5 -0
- pycan_send/__init__.py +3 -0
- pycan_send/main.py +202 -0
- pycan_send-0.0.1.dist-info/METADATA +72 -0
- pycan_send-0.0.1.dist-info/RECORD +8 -0
- pycan_send-0.0.1.dist-info/WHEEL +4 -0
- pycan_send-0.0.1.dist-info/entry_points.txt +2 -0
- pycan_send-0.0.1.dist-info/licenses/LICENSE.txt +9 -0
pycan_send/__about__.py
ADDED
pycan_send/__init__.py
ADDED
pycan_send/main.py
ADDED
|
@@ -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,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
|
+
[](https://pypi.org/project/pycan-send)
|
|
28
|
+
[](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,8 @@
|
|
|
1
|
+
pycan_send/__about__.py,sha256=VOvIJ7Epjsynw8ZNY3FIAAUEMvBsvyFiQXKKwEmQT8I,183
|
|
2
|
+
pycan_send/__init__.py,sha256=s8AKqKGu2E_AP5LFUerd8Pjj8xpncezzUxr4hqdOars,116
|
|
3
|
+
pycan_send/main.py,sha256=hVoIv8J9qaBKws-SGW3DEEy6li0P45oZ6Q0oj3AN-v8,6787
|
|
4
|
+
pycan_send-0.0.1.dist-info/METADATA,sha256=BNQb3p7w7f2sMI_JY-LrLpHy2UNGTgrcm-agd7Iw4UM,2212
|
|
5
|
+
pycan_send-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
pycan_send-0.0.1.dist-info/entry_points.txt,sha256=EEkxm_H9eenzFqLDt3LHL7Sz00mW24X2d7Fxu33dX9Y,52
|
|
7
|
+
pycan_send-0.0.1.dist-info/licenses/LICENSE.txt,sha256=NjpbyaWzT2JQHqe2mUJftu67l6NfN13kddmgOTVZcQ8,748
|
|
8
|
+
pycan_send-0.0.1.dist-info/RECORD,,
|
|
@@ -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
|
+
|