@canboat/canboatjs 3.16.4-beta.1 → 3.17.0-beta.1
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.
- package/dist/canSocket.d.ts +14 -15
- package/dist/canSocket.d.ts.map +1 -1
- package/dist/canSocket.js +43 -54
- package/dist/canSocket.js.map +1 -1
- package/dist/n2kDevice.js +12 -1
- package/dist/n2kDevice.js.map +1 -1
- package/native/canSocket.cpp +190 -68
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/native/canSocket.cpp
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include <napi.h>
|
|
2
|
+
#include <uv.h>
|
|
2
3
|
#include <sys/socket.h>
|
|
3
4
|
#include <sys/ioctl.h>
|
|
4
5
|
#include <net/if.h>
|
|
@@ -9,33 +10,25 @@
|
|
|
9
10
|
#include <cstring>
|
|
10
11
|
#include <cerrno>
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.ThrowAsJavaScriptException();
|
|
18
|
-
return env.Undefined();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
std::string ifname = info[0].As<Napi::String>().Utf8Value();
|
|
22
|
-
|
|
13
|
+
// Open a PF_CAN raw socket bound to the given interface in non-blocking mode.
|
|
14
|
+
// Both reads and writes use non-blocking I/O so the libuv threadpool is never
|
|
15
|
+
// occupied by a blocking syscall — this allows process.exit() to terminate
|
|
16
|
+
// cleanly even when no CAN traffic is flowing.
|
|
17
|
+
static int OpenAndBindCanSocket(const std::string& ifname, std::string& err) {
|
|
23
18
|
int fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
|
|
24
19
|
if (fd < 0) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return env.Undefined();
|
|
20
|
+
err = std::string("socket(PF_CAN): ") + strerror(errno);
|
|
21
|
+
return -1;
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
struct ifreq ifr;
|
|
31
25
|
std::memset(&ifr, 0, sizeof(ifr));
|
|
32
26
|
std::strncpy(ifr.ifr_name, ifname.c_str(), IFNAMSIZ - 1);
|
|
33
27
|
if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
err = std::string("ioctl(SIOCGIFINDEX) for '") + ifname +
|
|
29
|
+
"': " + strerror(errno);
|
|
36
30
|
close(fd);
|
|
37
|
-
|
|
38
|
-
return env.Undefined();
|
|
31
|
+
return -1;
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
struct sockaddr_can addr;
|
|
@@ -43,94 +36,223 @@ static Napi::Value OpenCanSocketImpl(const Napi::CallbackInfo& info, bool nonblo
|
|
|
43
36
|
addr.can_family = AF_CAN;
|
|
44
37
|
addr.can_ifindex = ifr.ifr_ifindex;
|
|
45
38
|
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
|
46
|
-
std::string
|
|
47
|
-
std::string("bind() to '") + ifname + "': " + strerror(errno);
|
|
39
|
+
err = std::string("bind() to '") + ifname + "': " + strerror(errno);
|
|
48
40
|
close(fd);
|
|
49
|
-
|
|
50
|
-
return env.Undefined();
|
|
41
|
+
return -1;
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
std::string err =
|
|
59
|
-
std::string("setsockopt(CAN_RAW_FILTER) for '") + ifname + "': " + strerror(errno);
|
|
60
|
-
close(fd);
|
|
61
|
-
Napi::Error::New(env, err).ThrowAsJavaScriptException();
|
|
62
|
-
return env.Undefined();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// NOTE: CAN_RAW_LOOPBACK is left at its default (enabled). On virtual CAN
|
|
66
|
-
// (vcan) interfaces the kernel uses loopback to distribute frames between
|
|
67
|
-
// sockets bound to the same interface — disabling it here would cause
|
|
68
|
-
// write() to succeed silently while no other socket (including candump or
|
|
69
|
-
// other processes on the bus) ever sees the frame. The empty CAN_RAW_FILTER
|
|
70
|
-
// above already prevents unwanted frames from reaching this socket's
|
|
71
|
-
// receive queue, so loopback has no effect on read behavior.
|
|
72
|
-
|
|
73
|
-
if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
|
|
74
|
-
std::string err =
|
|
75
|
-
std::string("fcntl(O_NONBLOCK) for '") + ifname + "': " + strerror(errno);
|
|
76
|
-
close(fd);
|
|
77
|
-
Napi::Error::New(env, err).ThrowAsJavaScriptException();
|
|
78
|
-
return env.Undefined();
|
|
79
|
-
}
|
|
44
|
+
if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
|
|
45
|
+
err = std::string("fcntl(O_NONBLOCK) for '") + ifname +
|
|
46
|
+
"': " + strerror(errno);
|
|
47
|
+
close(fd);
|
|
48
|
+
return -1;
|
|
80
49
|
}
|
|
81
50
|
|
|
82
|
-
return
|
|
51
|
+
return fd;
|
|
83
52
|
}
|
|
84
53
|
|
|
85
|
-
Napi::Value
|
|
86
|
-
|
|
54
|
+
Napi::Value OpenCanReadSocket(const Napi::CallbackInfo& info) {
|
|
55
|
+
Napi::Env env = info.Env();
|
|
56
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
57
|
+
Napi::TypeError::New(env, "Interface name required")
|
|
58
|
+
.ThrowAsJavaScriptException();
|
|
59
|
+
return env.Undefined();
|
|
60
|
+
}
|
|
61
|
+
std::string ifname = info[0].As<Napi::String>().Utf8Value();
|
|
62
|
+
std::string err;
|
|
63
|
+
int fd = OpenAndBindCanSocket(ifname, err);
|
|
64
|
+
if (fd < 0) {
|
|
65
|
+
Napi::Error::New(env, err).ThrowAsJavaScriptException();
|
|
66
|
+
return env.Undefined();
|
|
67
|
+
}
|
|
68
|
+
return Napi::Number::New(env, fd);
|
|
87
69
|
}
|
|
88
70
|
|
|
89
|
-
Napi::Value
|
|
90
|
-
|
|
71
|
+
Napi::Value OpenCanWriteSocket(const Napi::CallbackInfo& info) {
|
|
72
|
+
Napi::Env env = info.Env();
|
|
73
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
74
|
+
Napi::TypeError::New(env, "Interface name required")
|
|
75
|
+
.ThrowAsJavaScriptException();
|
|
76
|
+
return env.Undefined();
|
|
77
|
+
}
|
|
78
|
+
std::string ifname = info[0].As<Napi::String>().Utf8Value();
|
|
79
|
+
std::string err;
|
|
80
|
+
int fd = OpenAndBindCanSocket(ifname, err);
|
|
81
|
+
if (fd < 0) {
|
|
82
|
+
Napi::Error::New(env, err).ThrowAsJavaScriptException();
|
|
83
|
+
return env.Undefined();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Disable reception on the write-only socket — empty filter array tells
|
|
87
|
+
// the kernel not to deliver incoming frames to this socket's receive
|
|
88
|
+
// buffer, avoiding wasted copies.
|
|
89
|
+
if (setsockopt(fd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0) < 0) {
|
|
90
|
+
std::string msg = std::string("setsockopt(CAN_RAW_FILTER) for '") +
|
|
91
|
+
ifname + "': " + strerror(errno);
|
|
92
|
+
close(fd);
|
|
93
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
94
|
+
return env.Undefined();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// NOTE: CAN_RAW_LOOPBACK is left at its default (enabled). On virtual CAN
|
|
98
|
+
// (vcan) interfaces the kernel uses loopback to distribute frames between
|
|
99
|
+
// sockets bound to the same interface — disabling it would cause write()
|
|
100
|
+
// to succeed silently while no other socket (including candump or peers
|
|
101
|
+
// on the bus) ever sees the frame.
|
|
102
|
+
|
|
103
|
+
return Napi::Number::New(env, fd);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
Napi::Value WriteCanFrame(const Napi::CallbackInfo& info) {
|
|
94
107
|
Napi::Env env = info.Env();
|
|
95
|
-
|
|
96
108
|
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsBuffer()) {
|
|
97
109
|
Napi::TypeError::New(env, "writeCanFrame(fd, buffer) expected")
|
|
98
110
|
.ThrowAsJavaScriptException();
|
|
99
111
|
return env.Undefined();
|
|
100
112
|
}
|
|
101
|
-
|
|
102
113
|
int fd = info[0].As<Napi::Number>().Int32Value();
|
|
103
114
|
Napi::Buffer<uint8_t> buf = info[1].As<Napi::Buffer<uint8_t>>();
|
|
104
|
-
|
|
105
115
|
ssize_t written = write(fd, buf.Data(), buf.Length());
|
|
106
|
-
|
|
107
116
|
if (written < 0) {
|
|
108
117
|
return Napi::Number::New(env, -errno);
|
|
109
118
|
}
|
|
110
|
-
|
|
111
119
|
return Napi::Number::New(env, static_cast<int>(written));
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
|
|
122
|
+
// Drain all currently-readable frames from the non-blocking fd into an
|
|
123
|
+
// array of Buffers, returned to JS. Returns an empty array if no frames
|
|
124
|
+
// are available (EAGAIN). This is called from the JS-side poll callback
|
|
125
|
+
// when the fd becomes readable.
|
|
126
|
+
Napi::Value ReadCanFrames(const Napi::CallbackInfo& info) {
|
|
115
127
|
Napi::Env env = info.Env();
|
|
116
|
-
|
|
117
128
|
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
118
|
-
Napi::TypeError::New(env, "
|
|
129
|
+
Napi::TypeError::New(env, "readCanFrames(fd) expected")
|
|
119
130
|
.ThrowAsJavaScriptException();
|
|
120
131
|
return env.Undefined();
|
|
121
132
|
}
|
|
122
|
-
|
|
123
133
|
int fd = info[0].As<Napi::Number>().Int32Value();
|
|
124
|
-
int rc = shutdown(fd, SHUT_RDWR);
|
|
125
134
|
|
|
126
|
-
|
|
135
|
+
Napi::Array out = Napi::Array::New(env);
|
|
136
|
+
uint32_t idx = 0;
|
|
137
|
+
uint8_t buf[sizeof(struct can_frame)];
|
|
138
|
+
|
|
139
|
+
while (true) {
|
|
140
|
+
ssize_t n = read(fd, buf, sizeof(buf));
|
|
141
|
+
if (n < 0) {
|
|
142
|
+
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
Napi::Error::New(env, std::string("read: ") + strerror(errno))
|
|
146
|
+
.ThrowAsJavaScriptException();
|
|
147
|
+
return env.Undefined();
|
|
148
|
+
}
|
|
149
|
+
if (n == 0) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
out.Set(idx++, Napi::Buffer<uint8_t>::Copy(env, buf, n));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return out;
|
|
127
156
|
}
|
|
128
157
|
|
|
158
|
+
// CanPoller: wraps a uv_poll_t on a CAN read fd, calls a JS callback when
|
|
159
|
+
// frames are readable. Closing the poller is synchronous and prevents any
|
|
160
|
+
// further callbacks; the underlying fd is the caller's responsibility.
|
|
161
|
+
class CanPoller : public Napi::ObjectWrap<CanPoller> {
|
|
162
|
+
public:
|
|
163
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
164
|
+
Napi::Function func = DefineClass(
|
|
165
|
+
env, "CanPoller",
|
|
166
|
+
{InstanceMethod("close", &CanPoller::Close)});
|
|
167
|
+
exports.Set("CanPoller", func);
|
|
168
|
+
return exports;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
CanPoller(const Napi::CallbackInfo& info) : Napi::ObjectWrap<CanPoller>(info) {
|
|
172
|
+
Napi::Env env = info.Env();
|
|
173
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsFunction()) {
|
|
174
|
+
Napi::TypeError::New(env, "new CanPoller(fd, callback)")
|
|
175
|
+
.ThrowAsJavaScriptException();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
fd_ = info[0].As<Napi::Number>().Int32Value();
|
|
179
|
+
callback_.Reset(info[1].As<Napi::Function>(), 1);
|
|
180
|
+
closed_ = false;
|
|
181
|
+
|
|
182
|
+
uv_loop_t* loop = nullptr;
|
|
183
|
+
napi_get_uv_event_loop(env, &loop);
|
|
184
|
+
poll_ = new uv_poll_t;
|
|
185
|
+
poll_->data = this;
|
|
186
|
+
int rc = uv_poll_init(loop, poll_, fd_);
|
|
187
|
+
if (rc < 0) {
|
|
188
|
+
delete poll_;
|
|
189
|
+
poll_ = nullptr;
|
|
190
|
+
Napi::Error::New(env,
|
|
191
|
+
std::string("uv_poll_init: ") + uv_strerror(rc))
|
|
192
|
+
.ThrowAsJavaScriptException();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
rc = uv_poll_start(poll_, UV_READABLE, &CanPoller::OnPoll);
|
|
196
|
+
if (rc < 0) {
|
|
197
|
+
uv_close(reinterpret_cast<uv_handle_t*>(poll_), &CanPoller::OnClose);
|
|
198
|
+
poll_ = nullptr;
|
|
199
|
+
Napi::Error::New(env,
|
|
200
|
+
std::string("uv_poll_start: ") + uv_strerror(rc))
|
|
201
|
+
.ThrowAsJavaScriptException();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
~CanPoller() {
|
|
207
|
+
// poll_ should already be null after Close(); if not, the object was
|
|
208
|
+
// GC'd without explicit close — schedule a libuv close to clean up.
|
|
209
|
+
if (poll_ != nullptr && !closed_) {
|
|
210
|
+
closed_ = true;
|
|
211
|
+
uv_poll_stop(poll_);
|
|
212
|
+
uv_close(reinterpret_cast<uv_handle_t*>(poll_), &CanPoller::OnClose);
|
|
213
|
+
poll_ = nullptr;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private:
|
|
218
|
+
static void OnPoll(uv_poll_t* handle, int status, int events) {
|
|
219
|
+
CanPoller* self = static_cast<CanPoller*>(handle->data);
|
|
220
|
+
if (self->closed_ || status < 0 || !(events & UV_READABLE)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
Napi::Env env = self->callback_.Env();
|
|
224
|
+
Napi::HandleScope scope(env);
|
|
225
|
+
self->callback_.Call({});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static void OnClose(uv_handle_t* handle) {
|
|
229
|
+
delete reinterpret_cast<uv_poll_t*>(handle);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Napi::Value Close(const Napi::CallbackInfo& info) {
|
|
233
|
+
if (!closed_ && poll_ != nullptr) {
|
|
234
|
+
closed_ = true;
|
|
235
|
+
uv_poll_stop(poll_);
|
|
236
|
+
uv_close(reinterpret_cast<uv_handle_t*>(poll_), &CanPoller::OnClose);
|
|
237
|
+
poll_ = nullptr;
|
|
238
|
+
callback_.Reset();
|
|
239
|
+
}
|
|
240
|
+
return info.Env().Undefined();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
int fd_ = -1;
|
|
244
|
+
uv_poll_t* poll_ = nullptr;
|
|
245
|
+
bool closed_ = true;
|
|
246
|
+
Napi::FunctionReference callback_;
|
|
247
|
+
};
|
|
248
|
+
|
|
129
249
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
130
|
-
exports.Set("
|
|
131
|
-
exports.Set("
|
|
250
|
+
exports.Set("openCanReadSocket", Napi::Function::New(env, OpenCanReadSocket));
|
|
251
|
+
exports.Set("openCanWriteSocket",
|
|
252
|
+
Napi::Function::New(env, OpenCanWriteSocket));
|
|
132
253
|
exports.Set("writeCanFrame", Napi::Function::New(env, WriteCanFrame));
|
|
133
|
-
exports.Set("
|
|
254
|
+
exports.Set("readCanFrames", Napi::Function::New(env, ReadCanFrames));
|
|
255
|
+
CanPoller::Init(env, exports);
|
|
134
256
|
return exports;
|
|
135
257
|
}
|
|
136
258
|
|