diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a5842..ca1ac06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,19 @@ * Refactored library for pip * Moved plugins folder into openbci dir so plugins can be imported when installed with pip + +## Beta 0 + +* Adds high speed for Daisy over WiFi - now all boards are supported! + +## Alpha 1 + +* Adds high speed for Ganglion over WiFi + +## Alpha 0 + +* Adds high speed for Cyton over WiFi + # v0.1 ## dev diff --git a/openbci/utils/parse.py b/openbci/utils/parse.py index fe629f6..b0c5664 100644 --- a/openbci/utils/parse.py +++ b/openbci/utils/parse.py @@ -179,6 +179,57 @@ def transform_raw_data_packet_to_sample(self, raw_data): return sample + def make_daisy_sample_object_wifi(self, lower_sample_object, upper_sample_object): + """ + /** + * @description Used to make one sample object from two sample objects. The sample number of the new daisy sample will + * be the upperSampleObject's sample number divded by 2. This allows us to preserve consecutive sample numbers that + * flip over at 127 instead of 255 for an 8 channel. The daisySampleObject will also have one `channelData` array + * with 16 elements inside it, with the lowerSampleObject in the lower indices and the upperSampleObject in the + * upper set of indices. The auxData from both channels shall be captured in an object called `auxData` which + * contains two arrays referenced by keys `lower` and `upper` for the `lowerSampleObject` and `upperSampleObject`, + * respectively. The timestamps shall be averaged and moved into an object called `timestamp`. Further, the + * un-averaged timestamps from the `lowerSampleObject` and `upperSampleObject` shall be placed into an object called + * `_timestamps` which shall contain two keys `lower` and `upper` which contain the original timestamps for their + * respective sampleObjects. + * @param lowerSampleObject {Object} - Lower 8 channels with odd sample number + * @param upperSampleObject {Object} - Upper 8 channels with even sample number + * @returns {Object} - The new merged daisy sample object + */ + """ + daisy_sample_object = OpenBCISample() + + if lower_sample_object.channel_data is not None: + daisy_sample_object.channel_data = lower_sample_object.channel_data + upper_sample_object.channel_data + + daisy_sample_object.sample_number = upper_sample_object.sample_number + daisy_sample_object.id = daisy_sample_object.sample_number + + daisy_sample_object.aux_data = { + 'lower': lower_sample_object.aux_data, + 'upper': upper_sample_object.aux_data + } + + if lower_sample_object.timestamp: + daisy_sample_object.timestamp = lower_sample_object.timestamp + + daisy_sample_object.stop_byte = lower_sample_object.stop_byte + + daisy_sample_object._timestamps = { + 'lower': lower_sample_object.timestamp, + 'upper': upper_sample_object.timestamp + } + + if lower_sample_object.accel_data is not None: + if lower_sample_object.accel_data[0] > 0 or lower_sample_object.accel_data[1] > 0 or lower_sample_object.accel_data[2] > 0: + daisy_sample_object.accel_data = lower_sample_object.accel_data + else: + daisy_sample_object.accel_data = upper_sample_object.accel_data + + daisy_sample_object.valid = True + + return daisy_sample_object + """ /** * @description Used transform raw data packets into fully qualified packets @@ -279,4 +330,6 @@ def __init__(self, self.sample_number = sample_number self.start_byte = start_byte self.stop_byte = stop_byte + self.timestamp = 0 + self._timestamps = {} self.valid = valid diff --git a/openbci/wifi.py b/openbci/wifi.py index abc286f..7cf1d18 100644 --- a/openbci/wifi.py +++ b/openbci/wifi.py @@ -28,7 +28,7 @@ def handle_sample(sample): import requests import xmltodict -from openbci.utils import k, ParseRaw, ssdp +from openbci.utils import k, ParseRaw, OpenBCISample, ssdp SAMPLE_RATE = 0 # Hz @@ -56,14 +56,17 @@ class OpenBCIWiFi(object): """ def __init__(self, ip_address=None, shield_name=None, sample_rate=None, log=True, timeout=3, - max_packets_to_skip=20, latency=10000, high_speed=True, ssdp_attempts=5): + max_packets_to_skip=20, latency=10000, high_speed=True, ssdp_attempts=5, + num_channels=8): # these one are used + self.daisy = False self.high_speed = high_speed self.impedance = False self.ip_address = ip_address self.latency = latency self.log = log # print_incoming_text needs log self.max_packets_to_skip = max_packets_to_skip + self.num_channles = num_channels self.sample_rate = sample_rate self.shield_name = shield_name self.ssdp_attempts = ssdp_attempts @@ -157,8 +160,14 @@ def connect(self): gains = None if self.board_type == k.BOARD_CYTON: gains = [24, 24, 24, 24, 24, 24, 24, 24] + self.daisy = False + elif self.board_type == k.BOARD_DAISY: + gains = [24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24] + self.daisy = True elif self.board_type == k.BOARD_GANGLION: gains = [51, 51, 51, 51] + self.daisy = False + self.local_wifi_server.set_daisy(daisy=self.daisy) self.local_wifi_server.set_parser(ParseRaw(gains=gains, board_type=self.board_type)) if self.high_speed: @@ -444,7 +453,7 @@ def warn(self, text): # log how many packets where sent succesfully in between warnings if self.log_packet_count: logging.info('Data packets received:' + str(self.log_packet_count)) - self.log_packet_count = 0; + self.log_packet_count = 0 logging.warning(text) print("Warning: %s" % text) @@ -472,11 +481,14 @@ def reconnect(self): class WiFiShieldHandler(asyncore.dispatcher_with_send): - def __init__(self, sock, callback=None, high_speed=True, parser=None): + def __init__(self, sock, callback=None, high_speed=True, + parser=None, daisy=False): asyncore.dispatcher_with_send.__init__(self, sock) - self.high_speed = high_speed self.callback = callback + self.daisy = daisy + self.high_speed = high_speed + self.last_odd_sample = OpenBCISample() self.parser = parser if parser is not None else ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24]) def handle_read(self): @@ -490,8 +502,22 @@ def handle_read(self): samples = self.parser.transform_raw_data_packets_to_sample(raw_data_packets=raw_data_packets) for sample in samples: - if self.callback is not None: - self.callback(sample) + # if a daisy module is attached, wait to concatenate two samples (main board + daisy) + # before passing it to callback + if self.daisy: + # odd sample: daisy sample, save for later + if ~sample.sample_number % 2: + self.last_odd_sample = sample + # even sample: concatenate and send if last sample was the fist part, otherwise drop the packet + elif sample.sample_number - 1 == self.last_odd_sample.sample_number: + # the aux data will be the average between the two samples, as the channel + # samples themselves have been averaged by the board + daisy_sample = self.parser.make_daisy_sample_object_wifi(self.last_odd_sample, sample) + if self.callback is not None: + self.callback(daisy_sample) + else: + if self.callback is not None: + self.callback(sample) else: try: @@ -516,11 +542,12 @@ def handle_read(self): class WiFiShieldServer(asyncore.dispatcher): - def __init__(self, host, port, callback=None, gains=None, high_speed=True): + def __init__(self, host, port, callback=None, gains=None, high_speed=True, daisy=False): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind((host, port)) + self.daisy = daisy self.listen(5) self.callback = None self.handler = None @@ -532,13 +559,19 @@ def handle_accept(self): if pair is not None: sock, addr = pair print 'Incoming connection from %s' % repr(addr) - self.handler = WiFiShieldHandler(sock, self.callback, high_speed=self.high_speed, parser=self.parser) + self.handler = WiFiShieldHandler(sock, self.callback, high_speed=self.high_speed, + parser=self.parser, daisy=self.daisy) def set_callback(self, callback): self.callback = callback if self.handler is not None: self.handler.callback = callback + def set_daisy(self, daisy): + self.daisy = daisy + if self.handler is not None: + self.handler.daisy = daisy + def set_gains(self, gains): self.parser.set_ads1299_scale_factors(gains) diff --git a/scripts/stream_data_wifi.py b/scripts/stream_data_wifi.py index e581b37..159f56b 100644 --- a/scripts/stream_data_wifi.py +++ b/scripts/stream_data_wifi.py @@ -4,7 +4,8 @@ def printData(sample): - print(sample) + print(sample.sample_number) + print(sample.channel_data) if __name__ == '__main__': diff --git a/tests/test_parse.py b/tests/test_parse.py index 69e383e..773e504 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -2,6 +2,7 @@ import mock from openbci.utils import (k, + OpenBCISample, ParseRaw, sample_packet, sample_packet_standard_raw_aux, @@ -245,6 +246,70 @@ def test_transform_raw_data_packets_to_sample(self): for i in range(len(samples)): self.assertEqual(samples[i].sample_number, i) + def test_make_daisy_sample_object_wifi(self): + parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24]) + # Make the lower sample(channels 1 - 8) + lower_sample_object = OpenBCISample(sample_number=1) + lower_sample_object.channel_data = [1, 2, 3, 4, 5, 6, 7, 8] + lower_sample_object.aux_data = [0, 1, 2] + lower_sample_object.timestamp = 4 + lower_sample_object.accel_data = [0, 0, 0] + # Make the upper sample(channels 9 - 16) + upper_sample_object = OpenBCISample(sample_number=2) + upper_sample_object.channel_data = [9, 10, 11, 12, 13, 14, 15, 16] + upper_sample_object.accel_data = [0, 1, 2] + upper_sample_object.aux_data = [3, 4, 5] + upper_sample_object.timestamp = 8 + + daisy_sample_object = parser.make_daisy_sample_object_wifi(lower_sample_object, upper_sample_object); + + # should have valid object true + self.assertTrue(daisy_sample_object.valid) + + # should make a channelData array 16 elements long + self.assertEqual(len(daisy_sample_object.channel_data), k.NUMBER_OF_CHANNELS_DAISY) + + # should make a channelData array with lower array in front of upper array + for i in range(16): + self.assertEqual(daisy_sample_object.channel_data[i], i + 1) + + self.assertEqual(daisy_sample_object.id, daisy_sample_object.sample_number) + self.assertEqual(daisy_sample_object.sample_number, daisy_sample_object.sample_number) + + # should put the aux packets in an object + self.assertIsNotNone(daisy_sample_object.aux_data['lower']) + self.assertIsNotNone(daisy_sample_object.aux_data['upper']) + + # should put the aux packets in an object in the right order + for i in range(3): + self.assertEqual(daisy_sample_object.aux_data['lower'][i], i) + self.assertEqual(daisy_sample_object.aux_data['upper'][i], i + 3) + + # should take the lower timestamp + self.assertEqual(daisy_sample_object.timestamp, lower_sample_object.timestamp) + + # should take the lower stopByte + self.assertEqual(daisy_sample_object.stop_byte, lower_sample_object.stop_byte) + + # should place the old timestamps in an object + self.assertEqual(daisy_sample_object._timestamps['lower'], lower_sample_object.timestamp) + self.assertEqual(daisy_sample_object._timestamps['upper'], upper_sample_object.timestamp) + + # should store an accelerometer value if present + self.assertIsNotNone(daisy_sample_object.accel_data) + self.assertListEqual(daisy_sample_object.accel_data, [0, 1, 2]) + + lower_sample = OpenBCISample(sample_number=1) + lower_sample.accel_data = [0, 1, 2] + upper_sample = OpenBCISample(sample_number=2) + upper_sample.accel_data = [0, 0, 0] + + # Call the function under test + daisy_sample = parser.make_daisy_sample_object_wifi(lower_sample, upper_sample) + + self.assertIsNotNone(daisy_sample.accel_data) + self.assertListEqual(daisy_sample.accel_data, [0, 1, 2]) + if __name__ == '__main__': main()