n2k2ip 0.1.0__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.
- n2k2ip-0.1.0/LICENSE +21 -0
- n2k2ip-0.1.0/PKG-INFO +176 -0
- n2k2ip-0.1.0/README.md +149 -0
- n2k2ip-0.1.0/n2k2ip.egg-info/PKG-INFO +176 -0
- n2k2ip-0.1.0/n2k2ip.egg-info/SOURCES.txt +9 -0
- n2k2ip-0.1.0/n2k2ip.egg-info/dependency_links.txt +1 -0
- n2k2ip-0.1.0/n2k2ip.egg-info/entry_points.txt +2 -0
- n2k2ip-0.1.0/n2k2ip.egg-info/top_level.txt +1 -0
- n2k2ip-0.1.0/n2k2ip.py +259 -0
- n2k2ip-0.1.0/pyproject.toml +43 -0
- n2k2ip-0.1.0/setup.cfg +4 -0
n2k2ip-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ghotihook
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
n2k2ip-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: n2k2ip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bridge an NMEA 2000 (SocketCAN) bus to TCP clients as Yacht Devices RAW (YDRAW)
|
|
5
|
+
Author: ghotihook
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ghotihook/n2k2ip
|
|
8
|
+
Project-URL: Repository, https://github.com/ghotihook/n2k2ip
|
|
9
|
+
Project-URL: Issues, https://github.com/ghotihook/n2k2ip/issues
|
|
10
|
+
Keywords: nmea2000,nmea,socketcan,can,ydraw,yacht,marine,gateway
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: No Input/Output (Daemon)
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Communications
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# n2k2ip
|
|
29
|
+
|
|
30
|
+
Bridge an NMEA 2000 (SocketCAN) bus onto the network: read raw CAN frames from a
|
|
31
|
+
SocketCAN interface and stream them to TCP clients as **Yacht Devices RAW (YDRAW)**
|
|
32
|
+
text — one line per CAN frame. A software equivalent of a Yacht Devices Wi-Fi gateway
|
|
33
|
+
(YDWG-02) in *RAW* mode.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
NMEA2000 bus ──(can0)──▶ n2k2ip ──(tcp/1457)──▶ many clients
|
|
37
|
+
(plotters, loggers, OpenCPN, …)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why it exists / design
|
|
41
|
+
|
|
42
|
+
The data is **live**, and stale data is worthless. Everything here serves that:
|
|
43
|
+
|
|
44
|
+
- **Drop-old, never back up.** Each client has a bounded outbound queue. When a client
|
|
45
|
+
can't keep up (a slow or stalled Wi-Fi link), the *oldest* whole lines are dropped so
|
|
46
|
+
it always receives recent frames instead of a growing backlog. Maximum staleness is
|
|
47
|
+
bounded by the `MAX_QUEUE` constant (100 lines) in `n2k2ip.py`.
|
|
48
|
+
- **One slow client can't hurt the others.** All writes are non-blocking; a stalled or
|
|
49
|
+
dead client never blocks the CAN reader or the other clients.
|
|
50
|
+
- **Low latency by default.** `TCP_NODELAY` is set on every client, so each line hits
|
|
51
|
+
the wire immediately (no Nagle batching). Over Wi-Fi this matters — measured median
|
|
52
|
+
latency for this kind of small-message stream is a few ms with NODELAY vs tens of ms
|
|
53
|
+
without.
|
|
54
|
+
- **Lightweight.** Single-threaded, event-driven (`selectors`) — no threads, no
|
|
55
|
+
busy-poll, no dependencies. Pure Python standard library.
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Linux with SocketCAN (a `can0`-style interface)
|
|
60
|
+
- Python **3.9+** — **no third-party packages** (stdlib only)
|
|
61
|
+
- A **synchronised host clock** — YDRAW timestamps are taken from the host's UTC
|
|
62
|
+
clock, so make sure NTP is running and disciplined (e.g. `systemd-timesyncd` or
|
|
63
|
+
`chrony`). On a headless/boat box without a network, fit an RTC or expect the
|
|
64
|
+
clock — and therefore the timestamps — to be wrong until time is acquired.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
On Raspberry Pi OS (and other Debian 12+ systems) the system Python is
|
|
69
|
+
"externally managed", so use **pipx** — it drops the `n2k2ip` command on your
|
|
70
|
+
`PATH` in its own isolated environment and sidesteps `--break-system-packages`:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
sudo apt install pipx # once, if you don't have it
|
|
74
|
+
pipx install n2k2ip
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
There are no third-party dependencies, so plain `pip install n2k2ip` also works
|
|
78
|
+
if you'd rather (add `--break-system-packages`, or install into a virtualenv).
|
|
79
|
+
|
|
80
|
+
Because n2k2ip is a single stdlib-only file, you can also skip installing
|
|
81
|
+
entirely and run it straight from a checkout:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
python3 n2k2ip.py --channel can0 --port 1457
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick start
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
# bring the CAN interface up at the NMEA2000 bitrate
|
|
91
|
+
sudo ip link set can0 up type can bitrate 250000
|
|
92
|
+
|
|
93
|
+
# serve YDRAW on tcp/1457
|
|
94
|
+
n2k2ip --channel can0 --port 1457
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Connect and watch:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
nc <host> 1457
|
|
101
|
+
# 09:46:57.556 R 0DF50B16 00 E6 05 00 00 FF 7F FF
|
|
102
|
+
# 09:46:57.556 R 00FA8C3E 4C 08 C2 24 7E E8 61 4B
|
|
103
|
+
# ...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Usage
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
n2k2ip [--channel can0] [--port 1457] [--log-level INFO]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
(or `python3 n2k2ip.py …` when running from a checkout)
|
|
113
|
+
|
|
114
|
+
| option | default | meaning |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `--channel` | `can0` | SocketCAN interface to read |
|
|
117
|
+
| `--port` | `1457` | TCP port to serve YDRAW on |
|
|
118
|
+
| `--log-level` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
|
119
|
+
|
|
120
|
+
Per-client buffer depth is the `MAX_QUEUE` constant in `n2k2ip.py` (100 lines). When a
|
|
121
|
+
client falls behind, the oldest lines are dropped so it never lags more than ~100
|
|
122
|
+
frames — roughly 0.2–0.5 s of backlog on a busy bus. Raise it only if you'd rather
|
|
123
|
+
tolerate longer stalls than skip frames.
|
|
124
|
+
|
|
125
|
+
## Output format (YDRAW)
|
|
126
|
+
|
|
127
|
+
Each line is one CAN frame:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
HH:MM:SS.mmm R <8-hex CAN-ID> <space-separated hex data bytes>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Lines are terminated with `\r\n`. Direction is always `R` (received). The
|
|
134
|
+
timestamp is the host clock in **UTC** (`HH:MM:SS.mmm`). The YDRAW spec calls for
|
|
135
|
+
UTC sourced from the NMEA 2000 bus when available; we don't decode the bus, so we
|
|
136
|
+
use the host's UTC clock instead — accurate as long as the host clock is synced
|
|
137
|
+
(e.g. NTP).
|
|
138
|
+
|
|
139
|
+
**Multi-frame (fast-packet) messages are passed through, not reassembled** — and that
|
|
140
|
+
is correct for YDRAW, which is a per-CAN-frame format. Each fast-packet frame is sent
|
|
141
|
+
as its own line with its sequence/frame-counter byte intact; reassembling them into a
|
|
142
|
+
logical PGN is the *consumer's* job (e.g. canboat `analyzer`, OpenCPN, a YDRAW
|
|
143
|
+
decoder). Over a lossy link this means a lost frame loses its whole fast-packet PGN —
|
|
144
|
+
inherent to the format, same as a hardware gateway. For that reason **TCP is preferred
|
|
145
|
+
over UDP/broadcast on Wi-Fi**, where reliability and low jitter matter most.
|
|
146
|
+
|
|
147
|
+
## Run as a service (systemd)
|
|
148
|
+
|
|
149
|
+
Install it globally with pipx, then drop in the shipped unit:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
sudo apt install pipx # once, if you don't have it
|
|
153
|
+
sudo pipx install --global n2k2ip # -> /usr/local/bin/n2k2ip
|
|
154
|
+
sudo cp n2k2ip.service /etc/systemd/system/
|
|
155
|
+
# edit the unit if your channel/port differ, or if can0 is brought up elsewhere
|
|
156
|
+
sudo systemctl daemon-reload
|
|
157
|
+
sudo systemctl enable --now n2k2ip.service
|
|
158
|
+
journalctl -u n2k2ip.service -f
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Update later with `sudo pipx upgrade --global n2k2ip`. If you'd rather not
|
|
162
|
+
pip-install at all, the single stdlib-only file can just be copied and run
|
|
163
|
+
directly — see the comment header in `n2k2ip.service`.
|
|
164
|
+
|
|
165
|
+
## Behaviour notes
|
|
166
|
+
|
|
167
|
+
- **CAN reconnect.** If the interface goes down or the controller goes bus-off, the
|
|
168
|
+
reader logs it and reopens the socket with backoff — appropriate for a boat bus that
|
|
169
|
+
may power-cycle.
|
|
170
|
+
- **Error/remote frames** are skipped; only data frames are forwarded.
|
|
171
|
+
- **No filtering.** Every data frame on the bus is forwarded. Add a SocketCAN filter in
|
|
172
|
+
`_open_can` if you need to restrict PGNs.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
n2k2ip-0.1.0/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# n2k2ip
|
|
2
|
+
|
|
3
|
+
Bridge an NMEA 2000 (SocketCAN) bus onto the network: read raw CAN frames from a
|
|
4
|
+
SocketCAN interface and stream them to TCP clients as **Yacht Devices RAW (YDRAW)**
|
|
5
|
+
text — one line per CAN frame. A software equivalent of a Yacht Devices Wi-Fi gateway
|
|
6
|
+
(YDWG-02) in *RAW* mode.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
NMEA2000 bus ──(can0)──▶ n2k2ip ──(tcp/1457)──▶ many clients
|
|
10
|
+
(plotters, loggers, OpenCPN, …)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Why it exists / design
|
|
14
|
+
|
|
15
|
+
The data is **live**, and stale data is worthless. Everything here serves that:
|
|
16
|
+
|
|
17
|
+
- **Drop-old, never back up.** Each client has a bounded outbound queue. When a client
|
|
18
|
+
can't keep up (a slow or stalled Wi-Fi link), the *oldest* whole lines are dropped so
|
|
19
|
+
it always receives recent frames instead of a growing backlog. Maximum staleness is
|
|
20
|
+
bounded by the `MAX_QUEUE` constant (100 lines) in `n2k2ip.py`.
|
|
21
|
+
- **One slow client can't hurt the others.** All writes are non-blocking; a stalled or
|
|
22
|
+
dead client never blocks the CAN reader or the other clients.
|
|
23
|
+
- **Low latency by default.** `TCP_NODELAY` is set on every client, so each line hits
|
|
24
|
+
the wire immediately (no Nagle batching). Over Wi-Fi this matters — measured median
|
|
25
|
+
latency for this kind of small-message stream is a few ms with NODELAY vs tens of ms
|
|
26
|
+
without.
|
|
27
|
+
- **Lightweight.** Single-threaded, event-driven (`selectors`) — no threads, no
|
|
28
|
+
busy-poll, no dependencies. Pure Python standard library.
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Linux with SocketCAN (a `can0`-style interface)
|
|
33
|
+
- Python **3.9+** — **no third-party packages** (stdlib only)
|
|
34
|
+
- A **synchronised host clock** — YDRAW timestamps are taken from the host's UTC
|
|
35
|
+
clock, so make sure NTP is running and disciplined (e.g. `systemd-timesyncd` or
|
|
36
|
+
`chrony`). On a headless/boat box without a network, fit an RTC or expect the
|
|
37
|
+
clock — and therefore the timestamps — to be wrong until time is acquired.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
On Raspberry Pi OS (and other Debian 12+ systems) the system Python is
|
|
42
|
+
"externally managed", so use **pipx** — it drops the `n2k2ip` command on your
|
|
43
|
+
`PATH` in its own isolated environment and sidesteps `--break-system-packages`:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
sudo apt install pipx # once, if you don't have it
|
|
47
|
+
pipx install n2k2ip
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
There are no third-party dependencies, so plain `pip install n2k2ip` also works
|
|
51
|
+
if you'd rather (add `--break-system-packages`, or install into a virtualenv).
|
|
52
|
+
|
|
53
|
+
Because n2k2ip is a single stdlib-only file, you can also skip installing
|
|
54
|
+
entirely and run it straight from a checkout:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
python3 n2k2ip.py --channel can0 --port 1457
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# bring the CAN interface up at the NMEA2000 bitrate
|
|
64
|
+
sudo ip link set can0 up type can bitrate 250000
|
|
65
|
+
|
|
66
|
+
# serve YDRAW on tcp/1457
|
|
67
|
+
n2k2ip --channel can0 --port 1457
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Connect and watch:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
nc <host> 1457
|
|
74
|
+
# 09:46:57.556 R 0DF50B16 00 E6 05 00 00 FF 7F FF
|
|
75
|
+
# 09:46:57.556 R 00FA8C3E 4C 08 C2 24 7E E8 61 4B
|
|
76
|
+
# ...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
n2k2ip [--channel can0] [--port 1457] [--log-level INFO]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
(or `python3 n2k2ip.py …` when running from a checkout)
|
|
86
|
+
|
|
87
|
+
| option | default | meaning |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `--channel` | `can0` | SocketCAN interface to read |
|
|
90
|
+
| `--port` | `1457` | TCP port to serve YDRAW on |
|
|
91
|
+
| `--log-level` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
|
92
|
+
|
|
93
|
+
Per-client buffer depth is the `MAX_QUEUE` constant in `n2k2ip.py` (100 lines). When a
|
|
94
|
+
client falls behind, the oldest lines are dropped so it never lags more than ~100
|
|
95
|
+
frames — roughly 0.2–0.5 s of backlog on a busy bus. Raise it only if you'd rather
|
|
96
|
+
tolerate longer stalls than skip frames.
|
|
97
|
+
|
|
98
|
+
## Output format (YDRAW)
|
|
99
|
+
|
|
100
|
+
Each line is one CAN frame:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
HH:MM:SS.mmm R <8-hex CAN-ID> <space-separated hex data bytes>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Lines are terminated with `\r\n`. Direction is always `R` (received). The
|
|
107
|
+
timestamp is the host clock in **UTC** (`HH:MM:SS.mmm`). The YDRAW spec calls for
|
|
108
|
+
UTC sourced from the NMEA 2000 bus when available; we don't decode the bus, so we
|
|
109
|
+
use the host's UTC clock instead — accurate as long as the host clock is synced
|
|
110
|
+
(e.g. NTP).
|
|
111
|
+
|
|
112
|
+
**Multi-frame (fast-packet) messages are passed through, not reassembled** — and that
|
|
113
|
+
is correct for YDRAW, which is a per-CAN-frame format. Each fast-packet frame is sent
|
|
114
|
+
as its own line with its sequence/frame-counter byte intact; reassembling them into a
|
|
115
|
+
logical PGN is the *consumer's* job (e.g. canboat `analyzer`, OpenCPN, a YDRAW
|
|
116
|
+
decoder). Over a lossy link this means a lost frame loses its whole fast-packet PGN —
|
|
117
|
+
inherent to the format, same as a hardware gateway. For that reason **TCP is preferred
|
|
118
|
+
over UDP/broadcast on Wi-Fi**, where reliability and low jitter matter most.
|
|
119
|
+
|
|
120
|
+
## Run as a service (systemd)
|
|
121
|
+
|
|
122
|
+
Install it globally with pipx, then drop in the shipped unit:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
sudo apt install pipx # once, if you don't have it
|
|
126
|
+
sudo pipx install --global n2k2ip # -> /usr/local/bin/n2k2ip
|
|
127
|
+
sudo cp n2k2ip.service /etc/systemd/system/
|
|
128
|
+
# edit the unit if your channel/port differ, or if can0 is brought up elsewhere
|
|
129
|
+
sudo systemctl daemon-reload
|
|
130
|
+
sudo systemctl enable --now n2k2ip.service
|
|
131
|
+
journalctl -u n2k2ip.service -f
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Update later with `sudo pipx upgrade --global n2k2ip`. If you'd rather not
|
|
135
|
+
pip-install at all, the single stdlib-only file can just be copied and run
|
|
136
|
+
directly — see the comment header in `n2k2ip.service`.
|
|
137
|
+
|
|
138
|
+
## Behaviour notes
|
|
139
|
+
|
|
140
|
+
- **CAN reconnect.** If the interface goes down or the controller goes bus-off, the
|
|
141
|
+
reader logs it and reopens the socket with backoff — appropriate for a boat bus that
|
|
142
|
+
may power-cycle.
|
|
143
|
+
- **Error/remote frames** are skipped; only data frames are forwarded.
|
|
144
|
+
- **No filtering.** Every data frame on the bus is forwarded. Add a SocketCAN filter in
|
|
145
|
+
`_open_can` if you need to restrict PGNs.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: n2k2ip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bridge an NMEA 2000 (SocketCAN) bus to TCP clients as Yacht Devices RAW (YDRAW)
|
|
5
|
+
Author: ghotihook
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ghotihook/n2k2ip
|
|
8
|
+
Project-URL: Repository, https://github.com/ghotihook/n2k2ip
|
|
9
|
+
Project-URL: Issues, https://github.com/ghotihook/n2k2ip/issues
|
|
10
|
+
Keywords: nmea2000,nmea,socketcan,can,ydraw,yacht,marine,gateway
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: No Input/Output (Daemon)
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Communications
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# n2k2ip
|
|
29
|
+
|
|
30
|
+
Bridge an NMEA 2000 (SocketCAN) bus onto the network: read raw CAN frames from a
|
|
31
|
+
SocketCAN interface and stream them to TCP clients as **Yacht Devices RAW (YDRAW)**
|
|
32
|
+
text — one line per CAN frame. A software equivalent of a Yacht Devices Wi-Fi gateway
|
|
33
|
+
(YDWG-02) in *RAW* mode.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
NMEA2000 bus ──(can0)──▶ n2k2ip ──(tcp/1457)──▶ many clients
|
|
37
|
+
(plotters, loggers, OpenCPN, …)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why it exists / design
|
|
41
|
+
|
|
42
|
+
The data is **live**, and stale data is worthless. Everything here serves that:
|
|
43
|
+
|
|
44
|
+
- **Drop-old, never back up.** Each client has a bounded outbound queue. When a client
|
|
45
|
+
can't keep up (a slow or stalled Wi-Fi link), the *oldest* whole lines are dropped so
|
|
46
|
+
it always receives recent frames instead of a growing backlog. Maximum staleness is
|
|
47
|
+
bounded by the `MAX_QUEUE` constant (100 lines) in `n2k2ip.py`.
|
|
48
|
+
- **One slow client can't hurt the others.** All writes are non-blocking; a stalled or
|
|
49
|
+
dead client never blocks the CAN reader or the other clients.
|
|
50
|
+
- **Low latency by default.** `TCP_NODELAY` is set on every client, so each line hits
|
|
51
|
+
the wire immediately (no Nagle batching). Over Wi-Fi this matters — measured median
|
|
52
|
+
latency for this kind of small-message stream is a few ms with NODELAY vs tens of ms
|
|
53
|
+
without.
|
|
54
|
+
- **Lightweight.** Single-threaded, event-driven (`selectors`) — no threads, no
|
|
55
|
+
busy-poll, no dependencies. Pure Python standard library.
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Linux with SocketCAN (a `can0`-style interface)
|
|
60
|
+
- Python **3.9+** — **no third-party packages** (stdlib only)
|
|
61
|
+
- A **synchronised host clock** — YDRAW timestamps are taken from the host's UTC
|
|
62
|
+
clock, so make sure NTP is running and disciplined (e.g. `systemd-timesyncd` or
|
|
63
|
+
`chrony`). On a headless/boat box without a network, fit an RTC or expect the
|
|
64
|
+
clock — and therefore the timestamps — to be wrong until time is acquired.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
On Raspberry Pi OS (and other Debian 12+ systems) the system Python is
|
|
69
|
+
"externally managed", so use **pipx** — it drops the `n2k2ip` command on your
|
|
70
|
+
`PATH` in its own isolated environment and sidesteps `--break-system-packages`:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
sudo apt install pipx # once, if you don't have it
|
|
74
|
+
pipx install n2k2ip
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
There are no third-party dependencies, so plain `pip install n2k2ip` also works
|
|
78
|
+
if you'd rather (add `--break-system-packages`, or install into a virtualenv).
|
|
79
|
+
|
|
80
|
+
Because n2k2ip is a single stdlib-only file, you can also skip installing
|
|
81
|
+
entirely and run it straight from a checkout:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
python3 n2k2ip.py --channel can0 --port 1457
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick start
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
# bring the CAN interface up at the NMEA2000 bitrate
|
|
91
|
+
sudo ip link set can0 up type can bitrate 250000
|
|
92
|
+
|
|
93
|
+
# serve YDRAW on tcp/1457
|
|
94
|
+
n2k2ip --channel can0 --port 1457
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Connect and watch:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
nc <host> 1457
|
|
101
|
+
# 09:46:57.556 R 0DF50B16 00 E6 05 00 00 FF 7F FF
|
|
102
|
+
# 09:46:57.556 R 00FA8C3E 4C 08 C2 24 7E E8 61 4B
|
|
103
|
+
# ...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Usage
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
n2k2ip [--channel can0] [--port 1457] [--log-level INFO]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
(or `python3 n2k2ip.py …` when running from a checkout)
|
|
113
|
+
|
|
114
|
+
| option | default | meaning |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `--channel` | `can0` | SocketCAN interface to read |
|
|
117
|
+
| `--port` | `1457` | TCP port to serve YDRAW on |
|
|
118
|
+
| `--log-level` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
|
119
|
+
|
|
120
|
+
Per-client buffer depth is the `MAX_QUEUE` constant in `n2k2ip.py` (100 lines). When a
|
|
121
|
+
client falls behind, the oldest lines are dropped so it never lags more than ~100
|
|
122
|
+
frames — roughly 0.2–0.5 s of backlog on a busy bus. Raise it only if you'd rather
|
|
123
|
+
tolerate longer stalls than skip frames.
|
|
124
|
+
|
|
125
|
+
## Output format (YDRAW)
|
|
126
|
+
|
|
127
|
+
Each line is one CAN frame:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
HH:MM:SS.mmm R <8-hex CAN-ID> <space-separated hex data bytes>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Lines are terminated with `\r\n`. Direction is always `R` (received). The
|
|
134
|
+
timestamp is the host clock in **UTC** (`HH:MM:SS.mmm`). The YDRAW spec calls for
|
|
135
|
+
UTC sourced from the NMEA 2000 bus when available; we don't decode the bus, so we
|
|
136
|
+
use the host's UTC clock instead — accurate as long as the host clock is synced
|
|
137
|
+
(e.g. NTP).
|
|
138
|
+
|
|
139
|
+
**Multi-frame (fast-packet) messages are passed through, not reassembled** — and that
|
|
140
|
+
is correct for YDRAW, which is a per-CAN-frame format. Each fast-packet frame is sent
|
|
141
|
+
as its own line with its sequence/frame-counter byte intact; reassembling them into a
|
|
142
|
+
logical PGN is the *consumer's* job (e.g. canboat `analyzer`, OpenCPN, a YDRAW
|
|
143
|
+
decoder). Over a lossy link this means a lost frame loses its whole fast-packet PGN —
|
|
144
|
+
inherent to the format, same as a hardware gateway. For that reason **TCP is preferred
|
|
145
|
+
over UDP/broadcast on Wi-Fi**, where reliability and low jitter matter most.
|
|
146
|
+
|
|
147
|
+
## Run as a service (systemd)
|
|
148
|
+
|
|
149
|
+
Install it globally with pipx, then drop in the shipped unit:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
sudo apt install pipx # once, if you don't have it
|
|
153
|
+
sudo pipx install --global n2k2ip # -> /usr/local/bin/n2k2ip
|
|
154
|
+
sudo cp n2k2ip.service /etc/systemd/system/
|
|
155
|
+
# edit the unit if your channel/port differ, or if can0 is brought up elsewhere
|
|
156
|
+
sudo systemctl daemon-reload
|
|
157
|
+
sudo systemctl enable --now n2k2ip.service
|
|
158
|
+
journalctl -u n2k2ip.service -f
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Update later with `sudo pipx upgrade --global n2k2ip`. If you'd rather not
|
|
162
|
+
pip-install at all, the single stdlib-only file can just be copied and run
|
|
163
|
+
directly — see the comment header in `n2k2ip.service`.
|
|
164
|
+
|
|
165
|
+
## Behaviour notes
|
|
166
|
+
|
|
167
|
+
- **CAN reconnect.** If the interface goes down or the controller goes bus-off, the
|
|
168
|
+
reader logs it and reopens the socket with backoff — appropriate for a boat bus that
|
|
169
|
+
may power-cycle.
|
|
170
|
+
- **Error/remote frames** are skipped; only data frames are forwarded.
|
|
171
|
+
- **No filtering.** Every data frame on the bus is forwarded. Add a SocketCAN filter in
|
|
172
|
+
`_open_can` if you need to restrict PGNs.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
n2k2ip
|
n2k2ip-0.1.0/n2k2ip.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""n2k2ip — bridge an NMEA2000 (SocketCAN) bus to TCP clients as Yacht Devices RAW.
|
|
3
|
+
|
|
4
|
+
Reads raw CAN frames from a SocketCAN channel and fans each one out, as a single
|
|
5
|
+
YDRAW line, to every connected TCP client.
|
|
6
|
+
|
|
7
|
+
Designed for *live* data, where stale data is worthless:
|
|
8
|
+
- Each client has a bounded outbound queue. If a client can't keep up (a slow or
|
|
9
|
+
stalled wifi link), the OLDEST whole lines are dropped so it always receives
|
|
10
|
+
recent frames, never a growing backlog. Freshness is bounded by MAX_QUEUE.
|
|
11
|
+
- Writes are non-blocking, so one slow/dead client never stalls the CAN reader or
|
|
12
|
+
the other clients.
|
|
13
|
+
- TCP_NODELAY on every client: each line hits the wire immediately (no Nagle).
|
|
14
|
+
|
|
15
|
+
Single-threaded, event-driven (selectors) — no threads, no busy-poll.
|
|
16
|
+
|
|
17
|
+
python3 n2k2ip.py --channel can0 --port 1457
|
|
18
|
+
"""
|
|
19
|
+
import argparse
|
|
20
|
+
import logging
|
|
21
|
+
import selectors
|
|
22
|
+
import socket
|
|
23
|
+
import struct
|
|
24
|
+
import time
|
|
25
|
+
from collections import deque
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
# struct can_frame (Linux): u32 can_id, u8 dlc, 3 pad bytes, 8 data bytes
|
|
30
|
+
CAN_FRAME = struct.Struct("=IB3x8s")
|
|
31
|
+
CAN_RTR_FLAG = 0x40000000 # remote-transmission request
|
|
32
|
+
CAN_ERR_FLAG = 0x20000000 # error frame
|
|
33
|
+
CAN_EFF_MASK = 0x1FFFFFFF # 29-bit extended identifier
|
|
34
|
+
|
|
35
|
+
# Max whole lines buffered per client before the oldest are dropped. Bounds how
|
|
36
|
+
# stale a slow client's data can get; favours freshness over completeness.
|
|
37
|
+
MAX_QUEUE = 100
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger("n2k2ip")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def frame_to_ydraw(can_id: int, data: bytes) -> bytes:
|
|
43
|
+
"""Render a CAN frame as one YDRAW line: 'HH:MM:SS.mmm R <8-hex id> <hex bytes>'."""
|
|
44
|
+
now = time.time()
|
|
45
|
+
stamp = time.strftime("%H:%M:%S", time.gmtime(now)) + ".%03d" % int((now % 1) * 1000)
|
|
46
|
+
body = data.hex(" ").upper()
|
|
47
|
+
line = "%s R %08X" % (stamp, can_id)
|
|
48
|
+
return ((line + " " + body) if body else line).encode("ascii") + b"\r\n"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Client:
|
|
52
|
+
__slots__ = ("sock", "addr", "buf", "dropped")
|
|
53
|
+
|
|
54
|
+
def __init__(self, sock, addr):
|
|
55
|
+
self.sock = sock
|
|
56
|
+
self.addr = "%s:%d" % addr
|
|
57
|
+
self.buf = deque() # pending whole YDRAW lines (bytes); each is atomic
|
|
58
|
+
self.dropped = 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CanDown(Exception):
|
|
62
|
+
"""The CAN socket errored (interface down / bus-off) and must be reopened."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Gateway:
|
|
66
|
+
def __init__(self, channel: str, port: int):
|
|
67
|
+
self.channel = channel
|
|
68
|
+
self.max_queue = MAX_QUEUE
|
|
69
|
+
self.clients: set[Client] = set()
|
|
70
|
+
self.sel = selectors.DefaultSelector()
|
|
71
|
+
self.frames = 0
|
|
72
|
+
|
|
73
|
+
self.listen = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
74
|
+
self.listen.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
75
|
+
self.listen.bind(("", port))
|
|
76
|
+
self.listen.listen(16)
|
|
77
|
+
self.listen.setblocking(False)
|
|
78
|
+
self.sel.register(self.listen, selectors.EVENT_READ, "listen")
|
|
79
|
+
|
|
80
|
+
self.can = self._open_can()
|
|
81
|
+
self.sel.register(self.can, selectors.EVENT_READ, "can")
|
|
82
|
+
log.info("listening on tcp/%d, reading %s", port, channel)
|
|
83
|
+
|
|
84
|
+
# ── CAN ────────────────────────────────────────────────────────────────────
|
|
85
|
+
def _open_can(self):
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
s = socket.socket(socket.PF_CAN, socket.SOCK_RAW, socket.CAN_RAW)
|
|
89
|
+
s.bind((self.channel,))
|
|
90
|
+
s.setblocking(False)
|
|
91
|
+
return s
|
|
92
|
+
except OSError as e:
|
|
93
|
+
log.warning("%s not available (%s) — retrying; bring it up: "
|
|
94
|
+
"sudo ip link set %s up type can bitrate 250000",
|
|
95
|
+
self.channel, e, self.channel)
|
|
96
|
+
time.sleep(1.0)
|
|
97
|
+
|
|
98
|
+
def _drain_can(self) -> list:
|
|
99
|
+
"""Read every frame currently available; return their YDRAW lines."""
|
|
100
|
+
lines = []
|
|
101
|
+
while True:
|
|
102
|
+
try:
|
|
103
|
+
raw = self.can.recv(16)
|
|
104
|
+
except BlockingIOError:
|
|
105
|
+
break
|
|
106
|
+
except OSError as e:
|
|
107
|
+
raise CanDown(str(e))
|
|
108
|
+
if len(raw) < 16:
|
|
109
|
+
continue
|
|
110
|
+
can_id, dlc, payload = CAN_FRAME.unpack(raw)
|
|
111
|
+
if can_id & (CAN_ERR_FLAG | CAN_RTR_FLAG):
|
|
112
|
+
continue # skip error/remote frames
|
|
113
|
+
lines.append(frame_to_ydraw(can_id & CAN_EFF_MASK, payload[:dlc]))
|
|
114
|
+
self.frames += len(lines)
|
|
115
|
+
return lines
|
|
116
|
+
|
|
117
|
+
# ── clients ────────────────────────────────────────────────────────────────
|
|
118
|
+
def _accept(self):
|
|
119
|
+
try:
|
|
120
|
+
sock, addr = self.listen.accept()
|
|
121
|
+
except OSError:
|
|
122
|
+
return
|
|
123
|
+
sock.setblocking(False)
|
|
124
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
125
|
+
c = Client(sock, addr)
|
|
126
|
+
self.clients.add(c)
|
|
127
|
+
self.sel.register(sock, selectors.EVENT_READ, c)
|
|
128
|
+
log.info("client %s connected (%d total)", c.addr, len(self.clients))
|
|
129
|
+
|
|
130
|
+
def _drop(self, c: Client, reason: str):
|
|
131
|
+
if c not in self.clients:
|
|
132
|
+
return # idempotent: already dropped this batch
|
|
133
|
+
self.clients.discard(c)
|
|
134
|
+
try:
|
|
135
|
+
self.sel.unregister(c.sock)
|
|
136
|
+
except (KeyError, ValueError):
|
|
137
|
+
pass # never registered / fd already closed
|
|
138
|
+
try:
|
|
139
|
+
c.sock.close()
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
log.info("client %s gone: %s (dropped %d lines; %d total)",
|
|
143
|
+
c.addr, reason, c.dropped, len(self.clients))
|
|
144
|
+
|
|
145
|
+
def _flush(self, c: Client) -> bool:
|
|
146
|
+
"""Send as much of the client's queue as the socket accepts without blocking.
|
|
147
|
+
Returns True if fully drained. Raises OSError if the client is dead."""
|
|
148
|
+
while c.buf:
|
|
149
|
+
chunk = c.buf[0]
|
|
150
|
+
try:
|
|
151
|
+
n = c.sock.send(chunk)
|
|
152
|
+
except BlockingIOError:
|
|
153
|
+
return False
|
|
154
|
+
if n < len(chunk):
|
|
155
|
+
c.buf[0] = chunk[n:]
|
|
156
|
+
return False
|
|
157
|
+
c.buf.popleft()
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
def _want_write(self, c: Client):
|
|
161
|
+
ev = selectors.EVENT_READ | (selectors.EVENT_WRITE if c.buf else 0)
|
|
162
|
+
try:
|
|
163
|
+
self.sel.modify(c.sock, ev, c)
|
|
164
|
+
except (KeyError, ValueError):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def _fan_out(self, lines: list):
|
|
168
|
+
for c in list(self.clients):
|
|
169
|
+
c.buf.extend(lines)
|
|
170
|
+
# Bound staleness: keep only the most recent max_queue lines.
|
|
171
|
+
excess = len(c.buf) - self.max_queue
|
|
172
|
+
for _ in range(excess if excess > 0 else 0):
|
|
173
|
+
c.buf.popleft()
|
|
174
|
+
c.dropped += 1
|
|
175
|
+
try:
|
|
176
|
+
self._flush(c)
|
|
177
|
+
except OSError as e:
|
|
178
|
+
self._drop(c, str(e))
|
|
179
|
+
continue
|
|
180
|
+
self._want_write(c)
|
|
181
|
+
|
|
182
|
+
def _service(self, c: Client, mask):
|
|
183
|
+
if mask & selectors.EVENT_READ:
|
|
184
|
+
# We don't expect input; a readable client means data or a closed peer.
|
|
185
|
+
try:
|
|
186
|
+
if not c.sock.recv(4096):
|
|
187
|
+
self._drop(c, "closed by peer")
|
|
188
|
+
return
|
|
189
|
+
except OSError as e:
|
|
190
|
+
self._drop(c, str(e))
|
|
191
|
+
return
|
|
192
|
+
if mask & selectors.EVENT_WRITE:
|
|
193
|
+
try:
|
|
194
|
+
self._flush(c)
|
|
195
|
+
except OSError as e:
|
|
196
|
+
self._drop(c, str(e))
|
|
197
|
+
return
|
|
198
|
+
self._want_write(c)
|
|
199
|
+
|
|
200
|
+
# ── main loop ────────────────────────────────────────────────────────────────
|
|
201
|
+
def run(self):
|
|
202
|
+
last_stat = time.monotonic()
|
|
203
|
+
while True:
|
|
204
|
+
for key, mask in self.sel.select(timeout=5.0):
|
|
205
|
+
tag = key.data
|
|
206
|
+
if tag == "can":
|
|
207
|
+
try:
|
|
208
|
+
self._fan_out(self._drain_can())
|
|
209
|
+
except CanDown as e:
|
|
210
|
+
log.error("CAN error (%s) — reopening %s", e, self.channel)
|
|
211
|
+
self.sel.unregister(self.can)
|
|
212
|
+
self.can.close()
|
|
213
|
+
self.can = self._open_can()
|
|
214
|
+
self.sel.register(self.can, selectors.EVENT_READ, "can")
|
|
215
|
+
elif tag == "listen":
|
|
216
|
+
self._accept()
|
|
217
|
+
elif tag in self.clients:
|
|
218
|
+
self._service(tag, mask)
|
|
219
|
+
# else: a stale event for a client already dropped earlier in this
|
|
220
|
+
# same select() batch — ignore it.
|
|
221
|
+
|
|
222
|
+
now = time.monotonic()
|
|
223
|
+
if now - last_stat >= 30:
|
|
224
|
+
log.info("stats: %d clients, %d frames forwarded", len(self.clients),
|
|
225
|
+
self.frames)
|
|
226
|
+
last_stat = now
|
|
227
|
+
|
|
228
|
+
def close(self):
|
|
229
|
+
for c in list(self.clients):
|
|
230
|
+
c.sock.close()
|
|
231
|
+
self.listen.close()
|
|
232
|
+
self.can.close()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main():
|
|
236
|
+
ap = argparse.ArgumentParser(description=__doc__,
|
|
237
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
238
|
+
ap.add_argument("--channel", default="can0", help="SocketCAN interface (default: can0)")
|
|
239
|
+
ap.add_argument("--port", type=int, default=1457,
|
|
240
|
+
help="TCP port to serve YDRAW on (default: 1457)")
|
|
241
|
+
ap.add_argument("--log-level", default="INFO",
|
|
242
|
+
choices=("DEBUG", "INFO", "WARNING", "ERROR"))
|
|
243
|
+
ap.add_argument("--version", action="version",
|
|
244
|
+
version=f"n2k2ip {__version__}")
|
|
245
|
+
args = ap.parse_args()
|
|
246
|
+
|
|
247
|
+
logging.basicConfig(level=getattr(logging, args.log_level),
|
|
248
|
+
format="%(asctime)s [n2k2ip] %(levelname)s %(message)s")
|
|
249
|
+
gw = Gateway(args.channel, args.port)
|
|
250
|
+
try:
|
|
251
|
+
gw.run()
|
|
252
|
+
except KeyboardInterrupt:
|
|
253
|
+
log.info("stopping")
|
|
254
|
+
finally:
|
|
255
|
+
gw.close()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "n2k2ip"
|
|
7
|
+
description = "Bridge an NMEA 2000 (SocketCAN) bus to TCP clients as Yacht Devices RAW (YDRAW)"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "ghotihook" }]
|
|
13
|
+
keywords = ["nmea2000", "nmea", "socketcan", "can", "ydraw", "yacht", "marine", "gateway"]
|
|
14
|
+
dynamic = ["version"]
|
|
15
|
+
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: No Input/Output (Daemon)",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Communications",
|
|
28
|
+
"Topic :: System :: Networking",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/ghotihook/n2k2ip"
|
|
33
|
+
Repository = "https://github.com/ghotihook/n2k2ip"
|
|
34
|
+
Issues = "https://github.com/ghotihook/n2k2ip/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
n2k2ip = "n2k2ip:main"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
py-modules = ["n2k2ip"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.dynamic]
|
|
43
|
+
version = { attr = "n2k2ip.__version__" }
|
n2k2ip-0.1.0/setup.cfg
ADDED