@signalk/streams 4.1.1 → 4.3.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.
@@ -0,0 +1,374 @@
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, glob
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 gpiochip, based on model of Raspberry Pi
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 = "gpiochip0"
48
+ if gpiod_v == 1:
49
+ for c in gpiod.ChipIter():
50
+ if c.label() == "pinctrl-rp1":
51
+ gpio_chip = c.name()
52
+ break
53
+ else:
54
+ for g in glob.glob("/dev/gpiochip*"):
55
+ if gpiod.is_gpiochip_device(g):
56
+ with gpiod.Chip(g) as c:
57
+ info = c.get_info()
58
+ if info.label == "pinctrl-rp1":
59
+ gpio_chip = info.name
60
+ break
61
+ else:
62
+ print("Warning: Use of {} is untested".format(model))
63
+ gpio_chip = "gpiochip0"
64
+
65
+ class st1rx:
66
+ line = None
67
+ pending_e = None
68
+
69
+ def open(self, pin, baud=ST_BAUD, bits=ST_BITS, stop=ST_STOP, invert=ST_INVERT):
70
+ self.baud = baud
71
+ self.bits = bits
72
+ self.stop = stop
73
+ self.invert = invert
74
+
75
+ # calculate timing based on baud rate
76
+ self.fullbit_ns = int(1000000000 / self.baud)
77
+ self.halfbit_ns = int(self.fullbit_ns / 2)
78
+ self.frame_ns = int((1 + self.bits + self.stop) * self.fullbit_ns)
79
+ # ideally we should sample at halfbit_ns, but opto-coupler circuit may have slow rising edge
80
+ # sample at 1/4 bit pos with invert, and 3/4 bit without invert
81
+ self.sample_ns = int(self.halfbit_ns / 2)
82
+ if invert == False:
83
+ self.sample_ns += self.halfbit_ns
84
+
85
+ if gpiod_v == 1:
86
+ # get pin with gpiod v1.x.x
87
+ if self.invert == 0:
88
+ pull = gpiod.LINE_REQ_FLAG_BIAS_PULL_UP
89
+ else:
90
+ pull = gpiod.LINE_REQ_FLAG_BIAS_PULL_DOWN
91
+ chip = gpiod.Chip(gpio_chip)
92
+ self.line = chip.get_line(pin)
93
+ if self.line is None:
94
+ print("Error connecting to pin ", pin)
95
+ return False
96
+ self.line.request(
97
+ consumer="ST1RX",
98
+ type=gpiod.LINE_REQ_EV_BOTH_EDGES,
99
+ flags=pull)
100
+ else:
101
+ # get pin with gpiod v2.x.x
102
+ if self.invert == 0:
103
+ pull = gpiod.line.Bias.PULL_UP
104
+ else:
105
+ pull = gpiod.line.Bias.PULL_DOWN
106
+ self.line = gpiod.request_lines(
107
+ "/dev/" + gpio_chip,
108
+ consumer="ST1RX",
109
+ config={pin: gpiod.LineSettings(edge_detection=gpiod.line.Edge.BOTH, bias=pull)}
110
+ )
111
+
112
+ self.pending_e = None
113
+ return True
114
+
115
+ def close(self):
116
+ if self.line is not None:
117
+ self.line.release()
118
+ self.line = None
119
+
120
+ def read_gpiod1(self):
121
+ l = self.line
122
+ level = 0
123
+ data = 0
124
+ bits = self.bits
125
+ stop = self.stop
126
+ pol = self.invert
127
+
128
+ if self.pending_e is None:
129
+ # wait for new gpio events, timeout after 0.5 seconds
130
+ if l.event_wait(nsec=500000000) == False:
131
+ # no activity, return None
132
+ return
133
+ e = l.event_read()
134
+ else:
135
+ # we got a pending event
136
+ e = self.pending_e
137
+ self.pending_e = None
138
+
139
+ if e.type == e.FALLING_EDGE:
140
+ level = 0^pol
141
+ else:
142
+ level = 1^pol
143
+ e_ns = e.nsec
144
+
145
+ fullbit_ns = self.fullbit_ns
146
+ sample_ns = e_ns + self.sample_ns
147
+ remaining_ns = self.frame_ns
148
+
149
+ b = 0
150
+ sb = False
151
+
152
+ while True:
153
+ # wait for next event
154
+ if l.event_wait(nsec=remaining_ns):
155
+ e = l.event_read()
156
+ e_ns = e.nsec
157
+ if e_ns < sample_ns:
158
+ e_ns += 1000000000
159
+
160
+ # process bits since previous event
161
+ while sample_ns < e_ns:
162
+ if sb == False:
163
+ if level == 0:
164
+ sb = True
165
+ else:
166
+ # not a start bit, return None
167
+ print("not a start bit")
168
+ return
169
+ elif b < bits:
170
+ # store data bits
171
+ data |= level << b
172
+ b += 1
173
+ elif stop > 0:
174
+ # check stop bits
175
+ if level == 1:
176
+ stop -= 1
177
+ else:
178
+ # invalid stop bit
179
+ print("invalid stop bits")
180
+ return
181
+ sample_ns += fullbit_ns
182
+ remaining_ns -= fullbit_ns
183
+
184
+ # new level going forward
185
+ if e.type == e.FALLING_EDGE:
186
+ level = 0^pol
187
+ else:
188
+ level = 1^pol
189
+
190
+ # check if we are done processing this event
191
+ if remaining_ns < fullbit_ns:
192
+ # if so, this event is already start of next frame
193
+ self.pending_e = e
194
+ break
195
+ else:
196
+ # timeout is end of frame
197
+ if level == 0:
198
+ # invalid idle state at end of frame
199
+ print("invalid idle state")
200
+ return
201
+ # add remaining bits to byte
202
+ while b < bits:
203
+ data |= level << b
204
+ b += 1
205
+ stop = 0
206
+ break
207
+
208
+ if stop == 0 and b == bits:
209
+ return data
210
+ else:
211
+ # missing stop or data bits
212
+ print("missing stop or data bits")
213
+ return
214
+
215
+ def read_gpiod2(self):
216
+ l = self.line
217
+ level = 0
218
+ data = 0
219
+ bits = self.bits
220
+ stop = self.stop
221
+ pol = self.invert
222
+
223
+ if self.pending_e is None:
224
+ # wait for new gpio events, timeout after 0.5 seconds
225
+ if l.wait_edge_events(datetime.timedelta(microseconds=500000)) == False:
226
+ # no activity, return None
227
+ return
228
+ e = l.read_edge_events(1)[0]
229
+ else:
230
+ # we got a pending event
231
+ e = self.pending_e
232
+ self.pending_e = None
233
+
234
+ if e.event_type == e.Type.FALLING_EDGE:
235
+ level = 0^pol
236
+ else:
237
+ level = 1^pol
238
+ e_ns = e.timestamp_ns
239
+
240
+ fullbit_ns = self.fullbit_ns
241
+ sample_ns = e_ns + self.sample_ns
242
+ remaining_ns = self.frame_ns
243
+
244
+ b = 0
245
+ sb = False
246
+
247
+ while True:
248
+ # wait for next event
249
+ if l.wait_edge_events(datetime.timedelta(microseconds=remaining_ns/1000)):
250
+ e = l.read_edge_events(1)[0]
251
+ e_ns = e.timestamp_ns
252
+ if e_ns < sample_ns:
253
+ e_ns += 1000000000
254
+
255
+ # process bits since previous event
256
+ while sample_ns < e_ns:
257
+ if sb == False:
258
+ if level == 0:
259
+ sb = True
260
+ else:
261
+ # not a start bit, return None
262
+ print("not a start bit")
263
+ return
264
+ elif b < bits:
265
+ # store data bits
266
+ data |= level << b
267
+ b += 1
268
+ elif stop > 0:
269
+ # check stop bits
270
+ if level == 1:
271
+ stop -= 1
272
+ else:
273
+ # invalid stop bit
274
+ print("invalid stop bits")
275
+ return
276
+ sample_ns += fullbit_ns
277
+ remaining_ns -= fullbit_ns
278
+
279
+ # new level going forward
280
+ if e.event_type == e.Type.FALLING_EDGE:
281
+ level = 0^pol
282
+ else:
283
+ level = 1^pol
284
+
285
+ # check if we are done processing this event
286
+ if remaining_ns < fullbit_ns:
287
+ # if so, this event is already start of next frame
288
+ self.pending_e = e
289
+ break
290
+ else:
291
+ # timeout is end of frame
292
+ if level == 0:
293
+ # invalid idle state at end of frame
294
+ print("invalid idle state")
295
+ return
296
+ # add remaining bits to byte
297
+ while b < bits:
298
+ data |= level << b
299
+ b += 1
300
+ stop = 0
301
+ break
302
+
303
+ if stop == 0 and b == bits:
304
+ return data
305
+ else:
306
+ # missing stop or data bits
307
+ print("missing stop or data bits")
308
+ return
309
+
310
+ def read(self):
311
+ if self.line is None:
312
+ print("Error: no pin connected")
313
+ return
314
+ if gpiod_v == 1:
315
+ return self.read_gpiod1()
316
+ else:
317
+ return self.read_gpiod2()
318
+
319
+ if __name__ == "__main__":
320
+ gpio = ST_PIN
321
+ if len(sys.argv) > 1:
322
+ # Gpio, info as "GPIOnn", from GUI setup. Sensing the seatalk1 (yellow wire)
323
+ gpio = int("".join(filter(str.isdigit, sys.argv[1])))
324
+ pol = ST_INVERT
325
+ if len(sys.argv) > 2:
326
+ # Invert, inverted input from ST1, selected in the GUI
327
+ if sys.argv[2] == "true":
328
+ pol = 1
329
+
330
+ try:
331
+ st = st1rx()
332
+ if st.open(pin=gpio, invert=pol) == False:
333
+ print("Error: Failed to open Seatalk1 pin")
334
+ sys.exit()
335
+
336
+ st_msg = ""
337
+ st_start = False
338
+ while True:
339
+ # read a byte from Seatalk pin
340
+ d = st.read()
341
+ # if error, timeout, or start flag is set
342
+ if d is None or d > 255:
343
+ # output pending seatalk data
344
+ if st_start == True:
345
+ print("$STALK" + st_msg)
346
+ st_start = False
347
+ st_msg = ""
348
+ # if new data
349
+ if d is not None:
350
+ # if start flag is set, start a new msg
351
+ if d > 255:
352
+ st_start = True
353
+ st_msg = ""
354
+ # if a msg is in progress, append byte
355
+ if st_start == True:
356
+ st_msg += ",{:02X}".format(d & 0xff)
357
+ except Exception as e:
358
+ print(e)
359
+ except KeyboardInterrupt:
360
+ pass
361
+ st.close()
362
+ print("exit")
363
+ `
364
+
365
+ function GpiodSeatalk(options) {
366
+ const createDebug = options.createDebug || require('debug')
367
+ Execute.call(this, { debug: createDebug('signalk:streams:gpiod-seatalk') })
368
+ this.options = options
369
+ this.options.command = `python -u -c '${cmd}' ${options.gpio} ${options.gpioInvert} `
370
+ }
371
+
372
+ require('util').inherits(GpiodSeatalk, Execute)
373
+
374
+ module.exports = GpiodSeatalk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/streams",
3
- "version": "4.1.1",
3
+ "version": "4.3.0",
4
4
  "description": "Utilities for handling streams of Signal K data",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- return [new pigpioSeatalk(subOptions)]
421
+ if (subOptions.type === 'gpiod') {
422
+ return [new gpiodSeatalk(subOptions)]
423
+ } else {
424
+ return [new pigpioSeatalk(subOptions)]
425
+ }
421
426
  }