├── LICENSE ├── README.md ├── example └── read_mindwave_mobile.py ├── mindwavemobile ├── MindwaveDataPointReader.py ├── MindwaveDataPoints.py ├── MindwaveMobileRawReader.py ├── MindwavePacketPayloadParser.py ├── __init__.py └── tests │ ├── MindwavePacketPayloadParserTest.py │ ├── __init__.py │ └── protocol │ └── protocolSpecificationTest.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Robin Tibor Schirrmeister 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Some scripts to access the data streamed by the **Neurosky Mindwave Mobile** Headset over Bluetooth on Linux. 2 | 3 | Requirements: 4 | * [PyBluez](http://code.google.com/p/pybluez/), see their [documentation](http://code.google.com/p/pybluez/wiki/Documentation) for installation instructions :) 5 | For Ubuntu, installation might work like this: 6 | ``` 7 | sudo apt-get install libbluetooth-dev python-bluetooth 8 | ``` 9 | 10 | 11 | If you want to install the library as a module, do: 12 | ``` 13 | python setup.py install 14 | ``` 15 | from the root folder of the repository. 16 | 17 | Afterwards, you can use it within python like this, with the headset set in pairing mode (http://support.neurosky.com/kb/mindwave-mobile/how-do-i-put-the-mindwave-mobile-into-discovery-mode): 18 | 19 | ```python 20 | from mindwavemobile.MindwaveDataPointReader import MindwaveDataPointReader 21 | mindwaveDataPointReader = MindwaveDataPointReader() 22 | # connect to the mindwave mobile headset... 23 | mindwaveDataPointReader.start() 24 | # read one data point, data point types are specified in MindwaveDataPoints.py' 25 | dataPoint = mindwaveDataPointReader.readNextDataPoint() 26 | print(dataPoint) 27 | ``` 28 | -------------------------------------------------------------------------------- /example/read_mindwave_mobile.py: -------------------------------------------------------------------------------- 1 | import time 2 | import bluetooth 3 | from mindwavemobile.MindwaveDataPoints import RawDataPoint 4 | from mindwavemobile.MindwaveDataPointReader import MindwaveDataPointReader 5 | import textwrap 6 | 7 | if __name__ == '__main__': 8 | mindwaveDataPointReader = MindwaveDataPointReader() 9 | mindwaveDataPointReader.start() 10 | if (mindwaveDataPointReader.isConnected()): 11 | while(True): 12 | dataPoint = mindwaveDataPointReader.readNextDataPoint() 13 | if (not dataPoint.__class__ is RawDataPoint): 14 | print(dataPoint) 15 | else: 16 | print((textwrap.dedent("""\ 17 | Exiting because the program could not connect 18 | to the Mindwave Mobile device.""").replace("\n", " "))) 19 | 20 | -------------------------------------------------------------------------------- /mindwavemobile/MindwaveDataPointReader.py: -------------------------------------------------------------------------------- 1 | from .MindwaveMobileRawReader import MindwaveMobileRawReader 2 | import struct 3 | import collections 4 | 5 | from .MindwavePacketPayloadParser import MindwavePacketPayloadParser 6 | 7 | class MindwaveDataPointReader: 8 | def __init__(self, address=None): 9 | self._mindwaveMobileRawReader = MindwaveMobileRawReader(address=address) 10 | self._dataPointQueue = collections.deque() 11 | 12 | def start(self): 13 | self._mindwaveMobileRawReader.connectToMindWaveMobile() 14 | 15 | def isConnected(self): 16 | return self._mindwaveMobileRawReader.isConnected() 17 | 18 | def readNextDataPoint(self): 19 | if (not self._moreDataPointsInQueue()): 20 | self._putNextDataPointsInQueue() 21 | return self._getDataPointFromQueue() 22 | 23 | def _moreDataPointsInQueue(self): 24 | return len(self._dataPointQueue) > 0 25 | 26 | def _getDataPointFromQueue(self): 27 | return self._dataPointQueue.pop(); 28 | 29 | def _putNextDataPointsInQueue(self): 30 | dataPoints = self._readDataPointsFromOnePacket() 31 | self._dataPointQueue.extend(dataPoints) 32 | 33 | def _readDataPointsFromOnePacket(self): 34 | self._goToStartOfNextPacket() 35 | payloadBytes, checkSum = self._readOnePacket() 36 | if (not self._checkSumIsOk(payloadBytes, checkSum)): 37 | print("checksum of packet was not correct, discarding packet...") 38 | return self._readDataPointsFromOnePacket(); 39 | else: 40 | dataPoints = self._readDataPointsFromPayload(payloadBytes) 41 | self._mindwaveMobileRawReader.clearAlreadyReadBuffer() 42 | return dataPoints; 43 | 44 | def _goToStartOfNextPacket(self): 45 | while(True): 46 | byte = self._mindwaveMobileRawReader.getByte() 47 | if (byte == MindwaveMobileRawReader.START_OF_PACKET_BYTE): # need two of these bytes at the start.. 48 | byte = self._mindwaveMobileRawReader.getByte() 49 | if (byte == MindwaveMobileRawReader.START_OF_PACKET_BYTE): 50 | # now at the start of the packet.. 51 | return; 52 | 53 | def _readOnePacket(self): 54 | payloadLength = self._readPayloadLength(); 55 | payloadBytes, checkSum = self._readPacket(payloadLength); 56 | return payloadBytes, checkSum 57 | 58 | def _readPayloadLength(self): 59 | payloadLength = self._mindwaveMobileRawReader.getByte() 60 | return payloadLength 61 | 62 | def _readPacket(self, payloadLength): 63 | payloadBytes = self._mindwaveMobileRawReader.getBytes(payloadLength) 64 | checkSum = self._mindwaveMobileRawReader.getByte() 65 | return payloadBytes, checkSum 66 | 67 | def _checkSumIsOk(self, payloadBytes, checkSum): 68 | sumOfPayload = sum(payloadBytes) 69 | lastEightBits = sumOfPayload % 256 70 | invertedLastEightBits = self._computeOnesComplement(lastEightBits) #1's complement! 71 | return invertedLastEightBits == checkSum; 72 | 73 | def _computeOnesComplement(self, lastEightBits): 74 | return ~lastEightBits + 256 75 | 76 | def _readDataPointsFromPayload(self, payloadBytes): 77 | payloadParser = MindwavePacketPayloadParser(payloadBytes) 78 | return payloadParser.parseDataPoints(); 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /mindwavemobile/MindwaveDataPoints.py: -------------------------------------------------------------------------------- 1 | 2 | class DataPoint: 3 | def __init__(self, dataValueBytes): 4 | self._dataValueBytes = dataValueBytes 5 | 6 | class UnknownDataPoint(DataPoint): 7 | def __init__(self, dataValueBytes): 8 | DataPoint.__init__(self, dataValueBytes) 9 | self.unknownPoint = self._dataValueBytes[0] 10 | 11 | def __str__(self): 12 | retMsgString = "Unknown OpCode. Value: {}".format(self.unknownPoint) 13 | return retMsgString 14 | 15 | class PoorSignalLevelDataPoint(DataPoint): 16 | def __init__(self, dataValueBytes): 17 | DataPoint.__init__(self, dataValueBytes) 18 | self.amountOfNoise = self._dataValueBytes[0]; 19 | 20 | def headSetHasContactToSkin(self): 21 | return self.amountOfNoise < 200; 22 | 23 | def __str__(self): 24 | poorSignalLevelString = "Poor Signal Level: " + str(self.amountOfNoise) 25 | if (not self.headSetHasContactToSkin()): 26 | poorSignalLevelString += " - NO CONTACT TO SKIN" 27 | return poorSignalLevelString 28 | 29 | class AttentionDataPoint(DataPoint): 30 | def __init__(self, _dataValueBytes): 31 | DataPoint.__init__(self, _dataValueBytes) 32 | self.attentionValue = self._dataValueBytes[0] 33 | 34 | def __str__(self): 35 | return "Attention Level: " + str(self.attentionValue) 36 | 37 | class MeditationDataPoint(DataPoint): 38 | def __init__(self, _dataValueBytes): 39 | DataPoint.__init__(self, _dataValueBytes) 40 | self.meditationValue = self._dataValueBytes[0] 41 | 42 | def __str__(self): 43 | return "Meditation Level: " + str(self.meditationValue) 44 | 45 | class BlinkDataPoint(DataPoint): 46 | def __init__(self, _dataValueBytes): 47 | DataPoint.__init__(self, _dataValueBytes) 48 | self.blinkValue = self._dataValueBytes[0] 49 | 50 | def __str__(self): 51 | return "Blink Level: " + str(self.blinkValue) 52 | 53 | class RawDataPoint(DataPoint): 54 | def __init__(self, dataValueBytes): 55 | DataPoint.__init__(self, dataValueBytes) 56 | self.rawValue = self._readRawValue() 57 | 58 | def _readRawValue(self): 59 | firstByte = self._dataValueBytes[0] 60 | secondByte = self._dataValueBytes[1] 61 | # TODO(check if this is correct iwth soem more tests.. 62 | # and see http://stackoverflow.com/questions/5994307/bitwise-operations-in-python 63 | rawValue = firstByte * 256 + secondByte; 64 | if rawValue >= 32768: 65 | rawValue -= 65536 66 | return rawValue # hope this is correct ;) 67 | 68 | def __str__(self): 69 | return "Raw Value: " + str(self.rawValue) 70 | 71 | class EEGPowersDataPoint(DataPoint): 72 | def __init__(self, dataValueBytes): 73 | DataPoint.__init__(self, dataValueBytes) 74 | self._rememberEEGValues(); 75 | 76 | def _rememberEEGValues(self): 77 | self.delta = self._convertToBigEndianInteger(self._dataValueBytes[0:3]); 78 | self.theta = self._convertToBigEndianInteger(self._dataValueBytes[3:6]); 79 | self.lowAlpha = self._convertToBigEndianInteger(self._dataValueBytes[6:9]); 80 | self.highAlpha = self._convertToBigEndianInteger(self._dataValueBytes[9:12]); 81 | self.lowBeta = self._convertToBigEndianInteger(self._dataValueBytes[12:15]); 82 | self.highBeta = self._convertToBigEndianInteger(self._dataValueBytes[15:18]); 83 | self.lowGamma = self._convertToBigEndianInteger(self._dataValueBytes[18:21]); 84 | self.midGamma = self._convertToBigEndianInteger(self._dataValueBytes[21:24]); 85 | 86 | 87 | def _convertToBigEndianInteger(self, threeBytes): 88 | # TODO(check if this is correct iwth soem more tests.. 89 | # and see http://stackoverflow.com/questions/5994307/bitwise-operations-in-python 90 | # only use first 16 bits of second number, not rest inc ase number is negative, otherwise 91 | # python would take all 1s before this bit... 92 | # same with first number, only take first 8 bits... 93 | bigEndianInteger = (threeBytes[0] << 16) |\ 94 | (((1 << 16) - 1) & (threeBytes[1] << 8)) |\ 95 | ((1 << 8) - 1) & threeBytes[2] 96 | return bigEndianInteger 97 | 98 | def __str__(self): 99 | return """EEG Powers: 100 | delta: {self.delta} 101 | theta: {self.theta} 102 | lowAlpha: {self.lowAlpha} 103 | highAlpha: {self.highAlpha} 104 | lowBeta: {self.lowBeta} 105 | highBeta: {self.highBeta} 106 | lowGamma: {self.lowGamma} 107 | midGamma: {self.midGamma} 108 | """.format(self = self) 109 | -------------------------------------------------------------------------------- /mindwavemobile/MindwaveMobileRawReader.py: -------------------------------------------------------------------------------- 1 | import bluetooth 2 | import time 3 | import textwrap 4 | 5 | 6 | class MindwaveMobileRawReader: 7 | START_OF_PACKET_BYTE = 0xaa; 8 | def __init__(self, address=None): 9 | self._buffer = []; 10 | self._bufferPosition = 0; 11 | self._isConnected = False; 12 | self._mindwaveMobileAddress = address 13 | 14 | def connectToMindWaveMobile(self): 15 | # First discover mindwave mobile address, then connect. 16 | # Headset address of my headset was'9C:B7:0D:72:CD:02'; 17 | # not sure if it really can be different? 18 | # now discovering address because of https://github.com/robintibor/python-mindwave-mobile/issues/4 19 | if (self._mindwaveMobileAddress is None): 20 | self._mindwaveMobileAddress = self._findMindwaveMobileAddress() 21 | if (self._mindwaveMobileAddress is not None): 22 | print ("Discovered Mindwave Mobile...") 23 | self._connectToAddress(self._mindwaveMobileAddress) 24 | else: 25 | self._printErrorDiscoveryMessage() 26 | 27 | def _findMindwaveMobileAddress(self): 28 | nearby_devices = bluetooth.discover_devices(lookup_names = True) 29 | for address, name in nearby_devices: 30 | if (name == "MindWave Mobile"): 31 | return address 32 | return None 33 | 34 | def _connectToAddress(self, mindwaveMobileAddress): 35 | self.mindwaveMobileSocket = bluetooth.BluetoothSocket(bluetooth.RFCOMM) 36 | while (not self._isConnected): 37 | try: 38 | self.mindwaveMobileSocket.connect( 39 | (mindwaveMobileAddress, 1)) 40 | self._isConnected = True 41 | except bluetooth.btcommon.BluetoothError as error: 42 | print("Could not connect: ", error, "; Retrying in 5s...") 43 | time.sleep(5) 44 | 45 | 46 | def isConnected(self): 47 | return self._isConnected 48 | 49 | def _printErrorDiscoveryMessage(self): 50 | print((textwrap.dedent("""\ 51 | Could not discover Mindwave Mobile. Please make sure the 52 | Mindwave Mobile device is in pairing mode and your computer 53 | has bluetooth enabled.""").replace("\n", " "))) 54 | 55 | def _readMoreBytesIntoBuffer(self, amountOfBytes): 56 | newBytes = self._readBytesFromMindwaveMobile(amountOfBytes) 57 | self._buffer += newBytes 58 | 59 | def _readBytesFromMindwaveMobile(self, amountOfBytes): 60 | missingBytes = amountOfBytes 61 | # receivedBytes = "" #py2 62 | receivedBytes = b'' #py3 63 | 64 | # Sometimes the socket will not send all the requested bytes 65 | # on the first request, therefore a loop is necessary... 66 | while(missingBytes > 0): 67 | receivedBytes += self.mindwaveMobileSocket.recv(missingBytes) 68 | missingBytes = amountOfBytes - len(receivedBytes) 69 | return receivedBytes; 70 | 71 | def peekByte(self): 72 | self._ensureMoreBytesCanBeRead(); 73 | return ord(self._buffer[self._bufferPosition]) 74 | 75 | def getByte(self): 76 | self._ensureMoreBytesCanBeRead(100); 77 | return self._getNextByte(); 78 | 79 | def _ensureMoreBytesCanBeRead(self, amountOfBytes): 80 | if (self._bufferSize() <= self._bufferPosition + amountOfBytes): 81 | self._readMoreBytesIntoBuffer(amountOfBytes) 82 | 83 | def _getNextByte(self): 84 | # nextByte = ord(self._buffer[self._bufferPosition]) #py2 85 | nextByte = self._buffer[self._bufferPosition] #py3 86 | self._bufferPosition += 1; 87 | return nextByte; 88 | 89 | def getBytes(self, amountOfBytes): 90 | self._ensureMoreBytesCanBeRead(amountOfBytes); 91 | return self._getNextBytes(amountOfBytes); 92 | 93 | def _getNextBytes(self, amountOfBytes): 94 | # nextBytes = list(map(ord, self._buffer[self._bufferPosition: self._bufferPosition + amountOfBytes])) #py2 95 | nextBytes = list(self._buffer[self._bufferPosition: self._bufferPosition + amountOfBytes]) #py3 96 | self._bufferPosition += amountOfBytes 97 | return nextBytes 98 | 99 | def clearAlreadyReadBuffer(self): 100 | self._buffer = self._buffer[self._bufferPosition : ] 101 | self._bufferPosition = 0; 102 | 103 | def _bufferSize(self): 104 | return len(self._buffer); 105 | 106 | #------------------------------------------------------------------------------ 107 | -------------------------------------------------------------------------------- /mindwavemobile/MindwavePacketPayloadParser.py: -------------------------------------------------------------------------------- 1 | from .MindwaveDataPoints import RawDataPoint, PoorSignalLevelDataPoint,\ 2 | AttentionDataPoint, MeditationDataPoint, BlinkDataPoint, EEGPowersDataPoint,\ 3 | UnknownDataPoint 4 | 5 | EXTENDED_CODE_BYTE = 0x55 6 | 7 | class MindwavePacketPayloadParser: 8 | 9 | def __init__(self, payloadBytes): 10 | self._payloadBytes = payloadBytes 11 | self._payloadIndex = 0 12 | 13 | def parseDataPoints(self): 14 | dataPoints = [] 15 | while (not self._atEndOfPayloadBytes()): 16 | dataPoint = self._parseOneDataPoint() 17 | dataPoints.append(dataPoint) 18 | return dataPoints 19 | 20 | def _atEndOfPayloadBytes(self): 21 | return self._payloadIndex == len(self._payloadBytes) 22 | 23 | def _parseOneDataPoint(self): 24 | dataRowCode = self._extractDataRowCode(); 25 | dataRowValueBytes = self._extractDataRowValueBytes(dataRowCode) 26 | return self._createDataPoint(dataRowCode, dataRowValueBytes) 27 | 28 | def _extractDataRowCode(self): 29 | return self._ignoreExtendedCodeBytesAndGetRowCode() 30 | 31 | def _ignoreExtendedCodeBytesAndGetRowCode(self): 32 | # EXTENDED_CODE_BYTES seem not to be used according to 33 | # http://wearcam.org/ece516/mindset_communications_protocol.pdf 34 | # (August 2012) 35 | # so we ignore them 36 | byte = self._getNextByte() 37 | while (byte == EXTENDED_CODE_BYTE): 38 | byte = self._getNextByte() 39 | dataRowCode = byte 40 | return dataRowCode 41 | 42 | def _getNextByte(self): 43 | nextByte = self._payloadBytes[self._payloadIndex] 44 | self._payloadIndex += 1 45 | return nextByte 46 | 47 | def _getNextBytes(self, amountOfBytes): 48 | nextBytes = self._payloadBytes[self._payloadIndex : self._payloadIndex + amountOfBytes] 49 | self._payloadIndex += amountOfBytes 50 | return nextBytes 51 | 52 | def _extractDataRowValueBytes(self, dataRowCode): 53 | lengthOfValueBytes = self._extractLengthOfValueBytes(dataRowCode) 54 | dataRowValueBytes = self._getNextBytes(lengthOfValueBytes) 55 | return dataRowValueBytes 56 | 57 | def _extractLengthOfValueBytes(self, dataRowCode): 58 | # If code is one of the mysterious initial code values 59 | # return before the extended code check 60 | if dataRowCode == 0xBA or dataRowCode == 0xBC: 61 | return 1 62 | 63 | dataRowHasLengthByte = dataRowCode > 0x7f 64 | if (dataRowHasLengthByte): 65 | return self._getNextByte() 66 | else: 67 | return 1 68 | 69 | def _createDataPoint(self, dataRowCode, dataRowValueBytes): 70 | if (dataRowCode == 0x02): 71 | return PoorSignalLevelDataPoint(dataRowValueBytes) 72 | elif (dataRowCode == 0x04): 73 | return AttentionDataPoint(dataRowValueBytes) 74 | elif (dataRowCode == 0x05): 75 | return MeditationDataPoint(dataRowValueBytes) 76 | elif (dataRowCode == 0x16): 77 | return BlinkDataPoint(dataRowValueBytes) 78 | elif (dataRowCode == 0x80): 79 | return RawDataPoint(dataRowValueBytes) 80 | elif (dataRowCode == 0x83): 81 | return EEGPowersDataPoint(dataRowValueBytes) 82 | elif (dataRowCode == 0xba or dataRowCode == 0xbc): 83 | return UnknownDataPoint(dataRowValueBytes) 84 | else: 85 | assert False 86 | 87 | -------------------------------------------------------------------------------- /mindwavemobile/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robintibor/python-mindwave-mobile/b2bedb56f1e3b249a69ee669378f2e505c495091/mindwavemobile/__init__.py -------------------------------------------------------------------------------- /mindwavemobile/tests/MindwavePacketPayloadParserTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from MindwavePacketPayloadParser import MindwavePacketPayloadParser 3 | from MindwaveDataPoints import RawDataPoint, PoorSignalLevelDataPoint,\ 4 | MeditationDataPoint, AttentionDataPoint, EEGPowersDataPoint, BlinkDataPoint 5 | 6 | 7 | 8 | class ParseRawValueTest(unittest.TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | payload = [0x80, 0x02, 0x60, 0x0] # first raw value 12 | payload.extend([0x80, 0x02, 0x13, 0x12]) # second raw value 13 | payload.extend([0x2, 0x55]) # poor signal level value 14 | payload.extend([0x4, 0x25]) # attention level value 15 | payload.extend([0x5, 0x35]) # meditation level value 16 | payload.extend([0x16, 0x15]) # blink level value 17 | payload.extend([0x83, 0x18, 0x15, 0x13, 0x17, 0x12, 0x11, 0x10, 0x9, 0x8, 0x7]) # 3 eeg powers 18 | payload.extend([0x14, 0x13, 0x17, 0x12, 0x11, 0x10, 0x9, 0x8, 0x5]) # 3 more eeg powers 19 | payload.extend([0xaf, 0x13, 0xbf, 0x0, 0x1, 0x0]) # 2 more eeg powers 20 | cls._payloadParser = MindwavePacketPayloadParser(payload) 21 | cls._dataPoints = cls._payloadParser.parseDataPoints() 22 | 23 | def testReadingRawValueCorrectly(self): 24 | dataPoint = self._dataPoints[0] 25 | self.readingValueCorrectly(dataPoint.__class__, dataPoint.rawValue, \ 26 | RawDataPoint, (0x60 << 8) | 0x0,\ 27 | "should parse correct first raw value") 28 | 29 | def testReadingSecondRawValueCorrectly(self): 30 | dataPoint = self._dataPoints[1] 31 | self.readingValueCorrectly(dataPoint.__class__, dataPoint.rawValue, \ 32 | RawDataPoint, (0x13 << 8) | 0x12,\ 33 | "should parse correct second raw value") 34 | 35 | def testReadingPoorSignalLevelCorrectly(self): 36 | dataPoint = self._dataPoints[2]; 37 | self.readingValueCorrectly(dataPoint.__class__, dataPoint.amountOfNoise, \ 38 | PoorSignalLevelDataPoint, 0x55,\ 39 | "should parse correct noise level") 40 | 41 | def testReadingAttentionLevelCorrectly(self): 42 | dataPoint = self._dataPoints[3]; 43 | self.readingValueCorrectly(dataPoint.__class__, dataPoint.attentionValue, \ 44 | AttentionDataPoint, 0x25,\ 45 | "should parse correct attention level") 46 | 47 | def testReadingMeditationLevelCorrectly(self): 48 | dataPoint = self._dataPoints[4]; 49 | self.readingValueCorrectly(dataPoint.__class__, dataPoint.meditationValue, \ 50 | MeditationDataPoint, 0x35,\ 51 | "should parse correct meditation level") 52 | 53 | def testReadingBlinkLevelCorrectly(self): 54 | dataPoint = self._dataPoints[5]; 55 | self.readingValueCorrectly(dataPoint.__class__, dataPoint.blinkValue, \ 56 | BlinkDataPoint, 0x15,\ 57 | "should parse correct blink level") 58 | 59 | def readingValueCorrectly(self, actualClass, actualValue, expectedClass, expectedValue, msg): 60 | self.assertTrue(actualClass is expectedClass, msg) 61 | self.assertEqual(actualValue, expectedValue, msg) 62 | 63 | def testReadingEEGLevelsCorrectly(self): 64 | dataPoint = self._dataPoints[6]; 65 | self.assertIs(dataPoint.__class__, EEGPowersDataPoint, "eeg powers should be parsed correctly") 66 | self.assertEqual(dataPoint.delta, (0x15 << 16) | (0x13 << 8) | 0x17, "delta should be parsed correctly") 67 | self.assertEqual(dataPoint.theta, (0x12 << 16) | (0x11 << 8) | 0x10, "theta should be parsed correctly") 68 | self.assertEqual(dataPoint.lowAlpha, (0x9 << 16) | (0x8 << 8) | 0x7, "lowAlpha should be parsed correctly") 69 | self.assertEqual(dataPoint.highAlpha, (0x14 << 16) | (0x13 << 8) | 0x17, "highAlpha should be parsed correctly") 70 | self.assertEqual(dataPoint.lowBeta, (0x12 << 16) | (0x11 << 8) | 0x10, "lowBeta should be parsed correctly") 71 | self.assertEqual(dataPoint.highBeta, (0x9 << 16) | (0x8 << 8) | 0x5, "highBeta should be parsed correctly") 72 | self.assertEqual(dataPoint.lowGamma, (0xaf << 16) | (0x13 << 8) | 0xbf, "lowGamma should be parsed correctly") 73 | self.assertEqual(dataPoint.midGamma, (0x0 << 16) | (0x1 << 8) | 0x0, "midGamma should be parsed correctly") 74 | 75 | 76 | if __name__ == '__main__': 77 | unittest.main() -------------------------------------------------------------------------------- /mindwavemobile/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robintibor/python-mindwave-mobile/b2bedb56f1e3b249a69ee669378f2e505c495091/mindwavemobile/tests/__init__.py -------------------------------------------------------------------------------- /mindwavemobile/tests/protocol/protocolSpecificationTest.py: -------------------------------------------------------------------------------- 1 | # This test tries to verify that the neurosky headset sends packets 2 | # according to the specification at http://wearcam.org/ece516/mindset_communications_protocol.pdf :) 3 | # You can run this to check whether the neurosky headset behaves as described in this specification 4 | import bluetooth 5 | import unittest 6 | 7 | mindwaveMobileSocket = bluetooth.BluetoothSocket(bluetooth.RFCOMM) 8 | mindwaveMobileAddress = '9C:B7:0D:72:CD:02' 9 | numberOfPacketsToTest = 300; 10 | 11 | def setUpModule(): 12 | connectToMindwaveHeadset() 13 | 14 | def tearDownModule(): 15 | closeConnectionToMindwaveHeadset(); 16 | 17 | def connectToMindwaveHeadset(): 18 | try: 19 | mindwaveMobileSocket.connect((mindwaveMobileAddress, 1)) 20 | return; 21 | except bluetooth.btcommon.BluetoothError as error: 22 | print("Could not connect: ", error) 23 | 24 | def closeConnectionToMindwaveHeadset(): 25 | mindwaveMobileSocket.close(); 26 | 27 | class ProtocolTest(unittest.TestCase): 28 | def testIncomingBytes(self): 29 | numberOfTestedPackets = 0; 30 | while(numberOfTestedPackets < numberOfPacketsToTest): 31 | self._goToStartOfPacket(); 32 | payloadBytes, checkSum = self._readOnePacket(); 33 | self._checkPacket(payloadBytes, checkSum); 34 | numberOfTestedPackets += 1 35 | 36 | def _goToStartOfPacket(self): 37 | # Need to read TWO sync bytes, then you are at the start of a packet 38 | syncByteCode = 0xaa 39 | maximumBytesBeforePacketSync = 200 # to prevent this test from running forever 40 | bytesRead = 0 41 | while(bytesRead < maximumBytesBeforePacketSync): 42 | receivedByte = self._getByteFromMindwaveHeadset(); 43 | bytesRead += 1 44 | if (receivedByte == syncByteCode): 45 | receivedByte = self._getByteFromMindwaveHeadset(); 46 | bytesRead += 1 47 | if (receivedByte == syncByteCode): 48 | return 49 | self.fail("should find packet sync bytes before 200 bytes are read...") 50 | 51 | def _getByteFromMindwaveHeadset(self): 52 | return ord(mindwaveMobileSocket.recv(1)) 53 | 54 | def _readOnePacket(self): 55 | payloadLength = self._readPayloadLength(); 56 | payloadBytes, checkSum = self._readPacket(payloadLength); 57 | return payloadBytes, checkSum 58 | 59 | def _readPayloadLength(self): 60 | payloadLength = self._getByteFromMindwaveHeadset() 61 | self.assertLess(payloadLength, 170, "payloadLength should be smaller than 170") 62 | return payloadLength 63 | 64 | def _readPacket(self, payloadLength): 65 | payloadBytes = self._getBytesFromMindwaveHeadset(payloadLength) 66 | checkSum = self._getByteFromMindwaveHeadset() 67 | return payloadBytes, checkSum 68 | 69 | def _getBytesFromMindwaveHeadset(self, numberOfBytes): 70 | receivedChars = "" 71 | while (len(receivedChars) < numberOfBytes): 72 | receivedChars += mindwaveMobileSocket.recv(numberOfBytes - len(receivedChars)) 73 | assert len(receivedChars) == numberOfBytes 74 | return list(map(ord, receivedChars)); 75 | 76 | def _checkPacket(self, payloadBytes, checkSum): 77 | self._checkCheckSum(payloadBytes, checkSum) 78 | self._checkPayloadOfPacket(payloadBytes) 79 | 80 | def _checkCheckSum(self, payloadBytes, checkSum): 81 | sumOfPayload = sum(payloadBytes) 82 | lastEightBits = sumOfPayload % 256 83 | invertedLastEightBits = self._computeOnesComplement(lastEightBits) #1's complement! 84 | self.assertEqual(invertedLastEightBits, checkSum, "checksum should match inverted last 8 bits of sum of the payload") 85 | 86 | def _computeOnesComplement(self, lastEightBits): 87 | return ~lastEightBits + 256 88 | 89 | def _checkPayloadOfPacket(self, payloadBytes): 90 | remainingPayloadBytes = payloadBytes 91 | while (len(remainingPayloadBytes) > 0): 92 | dataRowCode, dataValueBytes, remainingPayloadBytes = self._extractDataRow(remainingPayloadBytes) 93 | self._checkDataRow(dataRowCode, dataValueBytes) 94 | 95 | def _extractDataRow(self, payloadBytes): 96 | dataRowCode, dataValueLength, remainingPayloadBytes = self._extractFirstDataType(payloadBytes) 97 | dataBytes = remainingPayloadBytes[0:dataValueLength] 98 | remainingPayloadAfterDataRow = remainingPayloadBytes[dataValueLength:] 99 | return dataRowCode, dataBytes, remainingPayloadAfterDataRow 100 | 101 | def _extractFirstDataType(self, payloadBytes): 102 | numberOfExtendedCodeBytes = self._extractNumberOfExtendedCodeBytes(payloadBytes) 103 | dataRowCode = payloadBytes[numberOfExtendedCodeBytes] 104 | if (dataRowCode > 0x7f): 105 | dataValueLength = payloadBytes[numberOfExtendedCodeBytes + 1]; 106 | # remaining payload starts at the beginning of the data value 107 | # after extended code bytes and row code and length byte! 108 | remainingPayloadBytes = payloadBytes[(numberOfExtendedCodeBytes + 2):] 109 | else: 110 | dataValueLength = 1; 111 | remainingPayloadBytes = payloadBytes[(numberOfExtendedCodeBytes + 1):] 112 | return dataRowCode, dataValueLength, remainingPayloadBytes 113 | 114 | def _extractNumberOfExtendedCodeBytes(self, payloadBytes): 115 | numberOfExtendedCodeBytes = 0 116 | for byte in payloadBytes: 117 | if byte == 0x55: 118 | numberOfExtendedCodeBytes += 1 119 | else: 120 | break; 121 | return numberOfExtendedCodeBytes; 122 | 123 | def _checkDataRow(self, dataRowCode, dataValueBytes): 124 | if (dataRowCode == 0x02): 125 | self.assertEqual(len(dataValueBytes), 1, "poor signal values should have one byte") 126 | elif (dataRowCode == 0x04): 127 | self.assertEqual(len(dataValueBytes), 1, "attention signal values should have one byte") 128 | self.assertLess(dataValueBytes[0], 101, "attention value should be inbetween 0 and 100") 129 | elif (dataRowCode == 0x05): 130 | self.assertEqual(len(dataValueBytes), 1, "meditation signal values should have one byte") 131 | self.assertLess(dataValueBytes[0], 101, "meditation value should be inbetween 0 and 100") 132 | elif (dataRowCode == 0x16): 133 | self.assertEqual(len(dataValueBytes), 1, "blink strength signal values should have one byte") 134 | elif (dataRowCode == 0x80): 135 | self.assertEqual(len(dataValueBytes), 2, "raw values should have two bytes") 136 | elif (dataRowCode == 0x83): 137 | self.assertEqual(len(dataValueBytes), 24, "eeg power values should have 24 bytes") 138 | else: 139 | self.fail("unknown data row code " + str(dataRowCode)); 140 | 141 | if __name__ == '__main__': 142 | unittest.main() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='mindwavemobile', 4 | version='0.200', 5 | description='Small Library to access neurosky mindwave mobile functionality from python', 6 | url='https://github.com/robintibor/python-mindwave-mobile', 7 | author='Robin Tibor Schirrmeister', 8 | author_email='robintibor@gmail.com', 9 | packages=['mindwavemobile'], 10 | install_requires=[ 11 | 'pybluez', 12 | ], 13 | zip_safe=False) 14 | --------------------------------------------------------------------------------