@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/streams",
3
- "version": "4.1.1",
3
+ "version": "4.2.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
  }