Skip to content

Commit bfb5d9c

Browse files
committed
Enable multi-channel acquisition
1 parent e2bbd28 commit bfb5d9c

3 files changed

Lines changed: 79 additions & 15 deletions

File tree

README.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ Download the `Arduino IDE <https://www.arduino.cc/en/Main/Software>`__ on your c
4444

4545
You're all set!
4646

47-
An example graph can be found `here <https://github.com/timeflux/timeflux_upsidedownlabs/blob/master/examples/uart.yaml>`__.
47+
An example graph for the default A0 analog port can be found `here <https://github.com/timeflux/timeflux_upsidedownlabs/blob/master/examples/uart.yaml>`__.
4848
You can run it like this:
4949

5050
::
5151

5252
$ conda activate timeflux
53-
$ timeflux -d examples/uart.yaml
53+
$ timeflux -d examples/uart.yaml
54+
55+
For an example of querying multiple ports and naming the channels, see `this graph <https://github.com/timeflux/timeflux_upsidedownlabs/blob/master/examples/channels.yaml>`__.

examples/channels.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
graphs:
2+
- id: demo
3+
nodes:
4+
- id: data
5+
module: timeflux_upsidedownlabs.nodes.driver
6+
class: UpsideDownLabs
7+
params:
8+
rate: 100
9+
channels:
10+
0: ECG
11+
2: EMG1
12+
3: EMG2
13+
100: EEG # will be removed (invalid pin)
14+
- id: display
15+
module: timeflux.nodes.debug
16+
class: Display
17+
- id: ui
18+
module: timeflux_ui.nodes.ui
19+
class: UI
20+
edges:
21+
- source: data
22+
target: ui:data
23+
- source: data
24+
target: display
25+
rate: 10
26+

timeflux_upsidedownlabs/nodes/driver.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class UpsideDownLabs(Node):
1717
e.g. ``COM3`` on Windows; ``/dev/cu.usbmodem14601`` on MacOS;
1818
``/dev/ttyUSB0`` on GNU/Linux.
1919
Default: autodetect
20+
channels (dict): The pin/channel mapping.
21+
Keys are pin numbers and values are channel names.
22+
Default: {1: "signal"}
2023
rate (int): The device rate in Hz.
2124
Default: ``500``.
2225
@@ -26,23 +29,32 @@ class UpsideDownLabs(Node):
2629
2730
"""
2831

29-
def __init__(self, port=None, rate=500):
32+
def __init__(self, port=None, channels=None, rate=500):
3033
if not port:
3134
port = Arduino.AUTODETECT
35+
if not channels:
36+
channels = {1: "signal"}
37+
self.channels = channels
3238
self.rate = rate
33-
self.pin = 0
3439
self.timestamp = 0
35-
self.timestamps = []
36-
self.data = []
3740
self.board = Arduino(port)
3841
self.meta = {"rate": rate}
3942
self._lock = Lock()
4043
self._blink()
41-
self.board.analog[self.pin].register_callback(self._callback)
4244
self.board.samplingOn(1000 / self.rate)
43-
self.board.analog[self.pin].enable_reporting()
45+
for pin in list(self.channels.keys()):
46+
if pin >= len(self.board.analog):
47+
self.logger.warning(f"Removing invalid pin {pin}")
48+
del self.channels[pin]
49+
self._reset_buffer()
50+
self._reset_sample()
51+
for pin, channel in self.channels.items():
52+
# See: https://docs.python-guide.org/writing/gotchas/#late-binding-closures
53+
self.board.analog[pin].register_callback(lambda data, channel=channel : self._callback(data, channel))
54+
self.board.analog[pin].enable_reporting()
4455

4556
def _blink(self):
57+
"""Show a cool led animation"""
4658
pins = [8, 9, 10, 11, 12, 13]
4759
for i in range(10):
4860
for pin in pins:
@@ -51,24 +63,48 @@ def _blink(self):
5163
self.board.digital[pin].write(0)
5264
pins.reverse()
5365

54-
def _callback(self, data):
66+
def _callback(self, data, channel):
5567
"""Acquire and cache data."""
5668
if self.timestamp == 0:
5769
self.timestamp = time.time()
5870
self._lock.acquire()
59-
# print(f"{self.timestamp:.3f}\t{data}")
60-
self.timestamps.append(self.timestamp)
61-
self.data.append(data)
71+
self.sample["received"] += 1
72+
self.sample["data"][channel].append(data)
73+
if not self.sample["timestamp"]:
74+
self.sample["timestamp"] = self.timestamp
75+
if self.sample["received"] == len(self.channels):
76+
self._commit_sample()
77+
self.timestamp += 1 / self.rate
6278
self._lock.release()
63-
self.timestamp += 1 / self.rate
79+
80+
def _reset_buffer(self):
81+
"""Reset the buffer"""
82+
self.timestamps = []
83+
self.data = {}
84+
for channel in self.channels.values():
85+
self.data[channel] = []
86+
87+
def _reset_sample(self):
88+
"""Reset the sample"""
89+
self.sample = {
90+
"timestamp": None,
91+
"data": { channel: [] for channel in self.channels.values() },
92+
"received": 0
93+
}
94+
95+
def _commit_sample(self):
96+
"""Append the sample"""
97+
self.timestamps.append(self.sample["timestamp"])
98+
for channel in self.channels.values():
99+
self.data[channel] += self.sample["data"][channel]
100+
self._reset_sample()
64101

65102
def update(self):
66103
"""Update the node output."""
67104
self._lock.acquire()
68105
index = pd.to_datetime(self.timestamps, unit="s")
69106
self.o.set(self.data, index, meta=self.meta)
70-
self.data = []
71-
self.timestamps = []
107+
self._reset_buffer()
72108
self._lock.release()
73109

74110
def terminate(self):

0 commit comments

Comments
 (0)