@signalk/streams 4.1.1 → 4.2.0
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/gpiod-seatalk.js +361 -0
- package/package.json +1 -1
- package/simple.js +6 -1
package/gpiod-seatalk.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adrian Studer
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* This stream receives Seatalk1 data over GPIO on a Raspberry Pi
|
|
19
|
+
* Supports Python libraries gpiod 1.x and 2.x
|
|
20
|
+
* Supports Raspberry Pi OS Bookworm and Bullseye on Raspberry Pi 3, 4 and 5
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const Execute = require('./execute')
|
|
24
|
+
|
|
25
|
+
const cmd = `
|
|
26
|
+
import gpiod, sys, datetime
|
|
27
|
+
|
|
28
|
+
ST_PIN = 20
|
|
29
|
+
|
|
30
|
+
ST_INVERT = 0 # 0=idle high, 1=idle low
|
|
31
|
+
ST_BITS = 9
|
|
32
|
+
ST_STOP = 1
|
|
33
|
+
ST_BAUD = 4800
|
|
34
|
+
|
|
35
|
+
# detect version of gpiod,
|
|
36
|
+
gpiod_v = int(gpiod.__version__.split(".")[0])
|
|
37
|
+
if gpiod_v != 1 and gpiod_v !=2:
|
|
38
|
+
print("Error: gpiod version {} is not supported".format(gpiod.__version__))
|
|
39
|
+
sys.exit()
|
|
40
|
+
|
|
41
|
+
# detect model of Raspberry Pi, tested with Pi 4 and Pi 5 running Bookworm
|
|
42
|
+
with open("/proc/device-tree/model") as f:
|
|
43
|
+
model = f.read()
|
|
44
|
+
if "Pi 4" in model or "Pi 3" in model:
|
|
45
|
+
gpio_chip = "gpiochip0"
|
|
46
|
+
elif "Pi 5" in model:
|
|
47
|
+
gpio_chip = "gpiochip4"
|
|
48
|
+
else:
|
|
49
|
+
print("Warning: Use of {} is untested".format(model))
|
|
50
|
+
gpio_chip = "gpiochip0"
|
|
51
|
+
|
|
52
|
+
class st1rx:
|
|
53
|
+
line = None
|
|
54
|
+
pending_e = None
|
|
55
|
+
|
|
56
|
+
def open(self, pin, baud=ST_BAUD, bits=ST_BITS, stop=ST_STOP, invert=ST_INVERT):
|
|
57
|
+
self.baud = baud
|
|
58
|
+
self.bits = bits
|
|
59
|
+
self.stop = stop
|
|
60
|
+
self.invert = invert
|
|
61
|
+
|
|
62
|
+
# calculate timing based on baud rate
|
|
63
|
+
self.fullbit_ns = int(1000000000 / self.baud)
|
|
64
|
+
self.halfbit_ns = int(self.fullbit_ns / 2)
|
|
65
|
+
self.frame_ns = int((1 + self.bits + self.stop) * self.fullbit_ns)
|
|
66
|
+
# ideally we should sample at halfbit_ns, but opto-coupler circuit may have slow rising edge
|
|
67
|
+
# sample at 1/4 bit pos with invert, and 3/4 bit without invert
|
|
68
|
+
self.sample_ns = int(self.halfbit_ns / 2)
|
|
69
|
+
if invert == False:
|
|
70
|
+
self.sample_ns += self.halfbit_ns
|
|
71
|
+
|
|
72
|
+
if gpiod_v == 1:
|
|
73
|
+
# get pin with gpiod v1.x.x
|
|
74
|
+
if self.invert == 0:
|
|
75
|
+
pull = gpiod.LINE_REQ_FLAG_BIAS_PULL_UP
|
|
76
|
+
else:
|
|
77
|
+
pull = gpiod.LINE_REQ_FLAG_BIAS_PULL_DOWN
|
|
78
|
+
chip = gpiod.Chip(gpio_chip)
|
|
79
|
+
self.line = chip.get_line(pin)
|
|
80
|
+
if self.line is None:
|
|
81
|
+
print("Error connecting to pin ", pin)
|
|
82
|
+
return False
|
|
83
|
+
self.line.request(
|
|
84
|
+
consumer="ST1RX",
|
|
85
|
+
type=gpiod.LINE_REQ_EV_BOTH_EDGES,
|
|
86
|
+
flags=pull)
|
|
87
|
+
else:
|
|
88
|
+
# get pin with gpiod v2.x.x
|
|
89
|
+
if self.invert == 0:
|
|
90
|
+
pull = gpiod.line.Bias.PULL_UP
|
|
91
|
+
else:
|
|
92
|
+
pull = gpiod.line.Bias.PULL_DOWN
|
|
93
|
+
self.line = gpiod.request_lines(
|
|
94
|
+
"/dev/" + gpio_chip,
|
|
95
|
+
consumer="ST1RX",
|
|
96
|
+
config={pin: gpiod.LineSettings(edge_detection=gpiod.line.Edge.BOTH, bias=pull)}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.pending_e = None
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def close(self):
|
|
103
|
+
if self.line is not None:
|
|
104
|
+
self.line.release()
|
|
105
|
+
self.line = None
|
|
106
|
+
|
|
107
|
+
def read_gpiod1(self):
|
|
108
|
+
l = self.line
|
|
109
|
+
level = 0
|
|
110
|
+
data = 0
|
|
111
|
+
bits = self.bits
|
|
112
|
+
stop = self.stop
|
|
113
|
+
pol = self.invert
|
|
114
|
+
|
|
115
|
+
if self.pending_e is None:
|
|
116
|
+
# wait for new gpio events, timeout after 0.5 seconds
|
|
117
|
+
if l.event_wait(nsec=500000000) == False:
|
|
118
|
+
# no activity, return None
|
|
119
|
+
return
|
|
120
|
+
e = l.event_read()
|
|
121
|
+
else:
|
|
122
|
+
# we got a pending event
|
|
123
|
+
e = self.pending_e
|
|
124
|
+
self.pending_e = None
|
|
125
|
+
|
|
126
|
+
if e.type == e.FALLING_EDGE:
|
|
127
|
+
level = 0^pol
|
|
128
|
+
else:
|
|
129
|
+
level = 1^pol
|
|
130
|
+
e_ns = e.nsec
|
|
131
|
+
|
|
132
|
+
fullbit_ns = self.fullbit_ns
|
|
133
|
+
sample_ns = e_ns + self.sample_ns
|
|
134
|
+
remaining_ns = self.frame_ns
|
|
135
|
+
|
|
136
|
+
b = 0
|
|
137
|
+
sb = False
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
# wait for next event
|
|
141
|
+
if l.event_wait(nsec=remaining_ns):
|
|
142
|
+
e = l.event_read()
|
|
143
|
+
e_ns = e.nsec
|
|
144
|
+
if e_ns < sample_ns:
|
|
145
|
+
e_ns += 1000000000
|
|
146
|
+
|
|
147
|
+
# process bits since previous event
|
|
148
|
+
while sample_ns < e_ns:
|
|
149
|
+
if sb == False:
|
|
150
|
+
if level == 0:
|
|
151
|
+
sb = True
|
|
152
|
+
else:
|
|
153
|
+
# not a start bit, return None
|
|
154
|
+
print("not a start bit")
|
|
155
|
+
return
|
|
156
|
+
elif b < bits:
|
|
157
|
+
# store data bits
|
|
158
|
+
data |= level << b
|
|
159
|
+
b += 1
|
|
160
|
+
elif stop > 0:
|
|
161
|
+
# check stop bits
|
|
162
|
+
if level == 1:
|
|
163
|
+
stop -= 1
|
|
164
|
+
else:
|
|
165
|
+
# invalid stop bit
|
|
166
|
+
print("invalid stop bits")
|
|
167
|
+
return
|
|
168
|
+
sample_ns += fullbit_ns
|
|
169
|
+
remaining_ns -= fullbit_ns
|
|
170
|
+
|
|
171
|
+
# new level going forward
|
|
172
|
+
if e.type == e.FALLING_EDGE:
|
|
173
|
+
level = 0^pol
|
|
174
|
+
else:
|
|
175
|
+
level = 1^pol
|
|
176
|
+
|
|
177
|
+
# check if we are done processing this event
|
|
178
|
+
if remaining_ns < fullbit_ns:
|
|
179
|
+
# if so, this event is already start of next frame
|
|
180
|
+
self.pending_e = e
|
|
181
|
+
break
|
|
182
|
+
else:
|
|
183
|
+
# timeout is end of frame
|
|
184
|
+
if level == 0:
|
|
185
|
+
# invalid idle state at end of frame
|
|
186
|
+
print("invalid idle state")
|
|
187
|
+
return
|
|
188
|
+
# add remaining bits to byte
|
|
189
|
+
while b < bits:
|
|
190
|
+
data |= level << b
|
|
191
|
+
b += 1
|
|
192
|
+
stop = 0
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if stop == 0 and b == bits:
|
|
196
|
+
return data
|
|
197
|
+
else:
|
|
198
|
+
# missing stop or data bits
|
|
199
|
+
print("missing stop or data bits")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
def read_gpiod2(self):
|
|
203
|
+
l = self.line
|
|
204
|
+
level = 0
|
|
205
|
+
data = 0
|
|
206
|
+
bits = self.bits
|
|
207
|
+
stop = self.stop
|
|
208
|
+
pol = self.invert
|
|
209
|
+
|
|
210
|
+
if self.pending_e is None:
|
|
211
|
+
# wait for new gpio events, timeout after 0.5 seconds
|
|
212
|
+
if l.wait_edge_events(datetime.timedelta(microseconds=500000)) == False:
|
|
213
|
+
# no activity, return None
|
|
214
|
+
return
|
|
215
|
+
e = l.read_edge_events(1)[0]
|
|
216
|
+
else:
|
|
217
|
+
# we got a pending event
|
|
218
|
+
e = self.pending_e
|
|
219
|
+
self.pending_e = None
|
|
220
|
+
|
|
221
|
+
if e.event_type == e.Type.FALLING_EDGE:
|
|
222
|
+
level = 0^pol
|
|
223
|
+
else:
|
|
224
|
+
level = 1^pol
|
|
225
|
+
e_ns = e.timestamp_ns
|
|
226
|
+
|
|
227
|
+
fullbit_ns = self.fullbit_ns
|
|
228
|
+
sample_ns = e_ns + self.sample_ns
|
|
229
|
+
remaining_ns = self.frame_ns
|
|
230
|
+
|
|
231
|
+
b = 0
|
|
232
|
+
sb = False
|
|
233
|
+
|
|
234
|
+
while True:
|
|
235
|
+
# wait for next event
|
|
236
|
+
if l.wait_edge_events(datetime.timedelta(microseconds=remaining_ns/1000)):
|
|
237
|
+
e = l.read_edge_events(1)[0]
|
|
238
|
+
e_ns = e.timestamp_ns
|
|
239
|
+
if e_ns < sample_ns:
|
|
240
|
+
e_ns += 1000000000
|
|
241
|
+
|
|
242
|
+
# process bits since previous event
|
|
243
|
+
while sample_ns < e_ns:
|
|
244
|
+
if sb == False:
|
|
245
|
+
if level == 0:
|
|
246
|
+
sb = True
|
|
247
|
+
else:
|
|
248
|
+
# not a start bit, return None
|
|
249
|
+
print("not a start bit")
|
|
250
|
+
return
|
|
251
|
+
elif b < bits:
|
|
252
|
+
# store data bits
|
|
253
|
+
data |= level << b
|
|
254
|
+
b += 1
|
|
255
|
+
elif stop > 0:
|
|
256
|
+
# check stop bits
|
|
257
|
+
if level == 1:
|
|
258
|
+
stop -= 1
|
|
259
|
+
else:
|
|
260
|
+
# invalid stop bit
|
|
261
|
+
print("invalid stop bits")
|
|
262
|
+
return
|
|
263
|
+
sample_ns += fullbit_ns
|
|
264
|
+
remaining_ns -= fullbit_ns
|
|
265
|
+
|
|
266
|
+
# new level going forward
|
|
267
|
+
if e.event_type == e.Type.FALLING_EDGE:
|
|
268
|
+
level = 0^pol
|
|
269
|
+
else:
|
|
270
|
+
level = 1^pol
|
|
271
|
+
|
|
272
|
+
# check if we are done processing this event
|
|
273
|
+
if remaining_ns < fullbit_ns:
|
|
274
|
+
# if so, this event is already start of next frame
|
|
275
|
+
self.pending_e = e
|
|
276
|
+
break
|
|
277
|
+
else:
|
|
278
|
+
# timeout is end of frame
|
|
279
|
+
if level == 0:
|
|
280
|
+
# invalid idle state at end of frame
|
|
281
|
+
print("invalid idle state")
|
|
282
|
+
return
|
|
283
|
+
# add remaining bits to byte
|
|
284
|
+
while b < bits:
|
|
285
|
+
data |= level << b
|
|
286
|
+
b += 1
|
|
287
|
+
stop = 0
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
if stop == 0 and b == bits:
|
|
291
|
+
return data
|
|
292
|
+
else:
|
|
293
|
+
# missing stop or data bits
|
|
294
|
+
print("missing stop or data bits")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
def read(self):
|
|
298
|
+
if self.line is None:
|
|
299
|
+
print("Error: no pin connected")
|
|
300
|
+
return
|
|
301
|
+
if gpiod_v == 1:
|
|
302
|
+
return self.read_gpiod1()
|
|
303
|
+
else:
|
|
304
|
+
return self.read_gpiod2()
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
gpio = ST_PIN
|
|
308
|
+
if len(sys.argv) > 1:
|
|
309
|
+
# Gpio, info as "GPIOnn", from GUI setup. Sensing the seatalk1 (yellow wire)
|
|
310
|
+
gpio = int("".join(filter(str.isdigit, sys.argv[1])))
|
|
311
|
+
pol = ST_INVERT
|
|
312
|
+
if len(sys.argv) > 2:
|
|
313
|
+
# Invert, inverted input from ST1, selected in the GUI
|
|
314
|
+
if sys.argv[2] == "true":
|
|
315
|
+
pol = 1
|
|
316
|
+
|
|
317
|
+
st = st1rx()
|
|
318
|
+
if st.open(pin=gpio, invert=pol) == False:
|
|
319
|
+
print("Error: Failed to open Seatalk1 pin")
|
|
320
|
+
sys.exit()
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
st_msg = ""
|
|
324
|
+
st_start = False
|
|
325
|
+
while True:
|
|
326
|
+
# read a byte from Seatalk pin
|
|
327
|
+
d = st.read()
|
|
328
|
+
# if error, timeout, or start flag is set
|
|
329
|
+
if d is None or d > 255:
|
|
330
|
+
# output pending seatalk data
|
|
331
|
+
if st_start == True:
|
|
332
|
+
print("$STALK" + st_msg)
|
|
333
|
+
st_start = False
|
|
334
|
+
st_msg = ""
|
|
335
|
+
# if new data
|
|
336
|
+
if d is not None:
|
|
337
|
+
# if start flag is set, start a new msg
|
|
338
|
+
if d > 255:
|
|
339
|
+
st_start = True
|
|
340
|
+
st_msg = ""
|
|
341
|
+
# if a msg is in progress, append byte
|
|
342
|
+
if st_start == True:
|
|
343
|
+
st_msg += ",{:02X}".format(d & 0xff)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print(e)
|
|
346
|
+
except KeyboardInterrupt:
|
|
347
|
+
pass
|
|
348
|
+
st.close()
|
|
349
|
+
print("exit")
|
|
350
|
+
`
|
|
351
|
+
|
|
352
|
+
function GpiodSeatalk(options) {
|
|
353
|
+
const createDebug = options.createDebug || require('debug')
|
|
354
|
+
Execute.call(this, { debug: createDebug('signalk:streams:gpiod-seatalk') })
|
|
355
|
+
this.options = options
|
|
356
|
+
this.options.command = `python -u -c '${cmd}' ${options.gpio} ${options.gpioInvert} `
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
require('util').inherits(GpiodSeatalk, Execute)
|
|
360
|
+
|
|
361
|
+
module.exports = GpiodSeatalk
|
package/package.json
CHANGED
package/simple.js
CHANGED
|
@@ -23,6 +23,7 @@ const Ydwg02 = require('@canboat/canboatjs').Ydwg02
|
|
|
23
23
|
const W2k01 = require('@canboat/canboatjs').W2k01
|
|
24
24
|
const gpsd = require('./gpsd')
|
|
25
25
|
const pigpioSeatalk = require('./pigpio-seatalk')
|
|
26
|
+
const gpiodSeatalk = require('./gpiod-seatalk')
|
|
26
27
|
|
|
27
28
|
function Simple(options) {
|
|
28
29
|
Transform.call(this, { objectMode: true })
|
|
@@ -417,5 +418,9 @@ function signalKInput(subOptions) {
|
|
|
417
418
|
}
|
|
418
419
|
|
|
419
420
|
function seatalkInput(subOptions) {
|
|
420
|
-
|
|
421
|
+
if (subOptions.type === 'gpiod') {
|
|
422
|
+
return [new gpiodSeatalk(subOptions)]
|
|
423
|
+
} else {
|
|
424
|
+
return [new pigpioSeatalk(subOptions)]
|
|
425
|
+
}
|
|
421
426
|
}
|