#-- # Copyright (C) 2016 Wolfgang Hotwagner # # This file is part of the mindwave gem # # This mindwave gem is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This mindwave gem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this mindwave gem; if not, write to the # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301 USA #++ require "mindwave/version" require 'serialport' require 'bindata' require 'logger' # This module provides access to the Mindwave-Headset module Mindwave # The Mindwave::Headset-class gives access to the Mindwave-Headset. # It's written for the Mindwave-Headset only, but most of the code # should work for Mindwave-Mobile too. # # To use the callback-methods, just inherit from this class and # override the Callback-Methods with your own code. class Headset # ----------------------- # :section: Request Codes # ----------------------- # Connection Requests CONNECT = 0xc0 # Disconnect Request DISCONNECT = 0xc1 # Autoconnect Request AUTOCONNECT = 0xc2 # ----------------------- # :section: Headset Status Codes # ----------------------- # Headset connected HEADSET_CONNECTED = 0xd0 # Headset not found HEADSET_NOTFOUND = 0xd1 # Headset disconnected HEADSET_DISCONNECTED = 0xd2 # Request denied REQUEST_DENIED = 0xd3 # Dongle is in standby mode DONGLE_STANDBY = 0xd4 # ----------------------- # :section: Control Codes # ----------------------- # Start of a new data-set(packet) SYNC = 0xaa # Extended codes EXCODE = 0x55 # ----------------------- # :section: Single-Byte-Codes # ----------------------- # 0-255(zero is good). 200 means no-skin-contact POOR_SIGNAL = 0x02 # Heartrate HEART_RATE = 0x03 # Attention # @see #eSenseStr ATTENTION = 0x04 # Meditation # @see #eSenseStr MEDITATION = 0x05 # Not available in Mindwave and Mindwave Mobile BIT8_RAW = 0x06 # Not available in Mindwave and Mindwave Mobile RAW_MARKER = 0x07 # ----------------------- # :section: Multi-Byte-Codes # ----------------------- # Raw Wave output RAW_WAVE = 0x80 # EEG-Power EEG_POWER = 0x81 # ASIC-EEG-POWER-INT ASIC_EEG_POWER = 0x83 # RRinterval RRINTERVAL = 0x86 # @!attribute headsetid # @return [Integer] headset id # @!attribute device # @return [String] dongle device(like /dev/ttyUSB0) # @!attribute rate # @return [Integer] baud-rate of the device # @!attribute log # @return [Logger] logger instance attr_accessor :headsetid, :device, :rate, :log # @!attribute [r] attention # stores the current attention-value # @!attribute [r] meditation # stores the current meditation-value # @!attribute [r] asic # stores the current asic-value # @!attribute [r] poor # stores the current poor-value # @!attribute [r] headsetstatus # stores the current headsetstatus-value # @!attribute [r] heart # stores the current heart-value # @!attribute [r] runner # @see #stop attr_reader :attention, :meditation, :asic, :poor, :headsetstatus, :heart, :runner # If connectserial is true, then this constructor opens a serial connection # and automatically connects to the headset # # @param [Integer] headsetid it's on the sticker in the battery-case # @param [String] device tty-device # @param [Integer] rate baud-rate # @param [Logger] log (logger-instance) def initialize(headsetid=nil,device='/dev/ttyUSB0', connectserial=true,rate=115200, log=Logger.new(STDOUT)) @headsetid=headsetid @device=device @rate=rate @log=log @log.level = Logger::FATAL @headsetstatus = 0 @runner = true if connectserial serial_open connect(@headsetid) end end # connects the Mindwave-headset(not needed with Mindwave-Mobile) # (Mindwave only) # # @param [Integer] headsetid it's on the sticker in the battery-case # TODO: implement connection with headsetid def connect(headsetid=nil) if not headsetid.nil? @headsetid = headsetid end if @headsetid.nil? autoconnect() return end cmd = BinData::Uint8be.new(Mindwave::Headset::CONNECT) cmd.write(@conn) end # This method creates an infinite loop # and reads out all data from the headset using # the open serial-line. def run tmpbyte = 0; @runner = true while @runner log.debug("<<< START RECORD >>>") tmpbyte = logreadbyte # 0xaa indicates the first start of a packet if tmpbyte != Mindwave::Headset::SYNC log.info(sprintf("LOST: %x\n",tmpbyte)) next else tmpbyte = logreadbyte() # a second 0xaa verifies the start of a packet if tmpbyte != Mindwave::Headset::SYNC log.info(sprintf("LOST: %x\n",tmpbyte)) next end end while true # read out the length of the packet plength = logreadbyte() if(plength != 170) break end end if(plength > 170) next end log.info(sprintf("Header-Length: %d",plength)) payload = Array.new(plength) checksum = 0 # read out payload (0..plength-1).each do |n| payload[n] = logreadbyte() # ..and add it to the checksum checksum += payload[n] end # weird checksum calculations checksum &= 0xff checksum = ~checksum & 0xff # read checksum-packet c = logreadbyte() # compare checksum-packet with the calculated checksum if( c != checksum) log.info(sprintf("Checksum Error: %x - %x\n",c,checksum)) else # so finally parse the payload of our packet parse_payload(payload) end end end # this method parses the payload of a data-row, parses the values and invokes the callback methods # @param [Array] payload Array with the payload def parse_payload(payload) if not payload.instance_of?Array or payload.nil? or payload.length < 2 raise "Invalid Argument" end log.info("####### PARSE PAYLOAD #########") extcodelevel = 0 # parse the first code and it's payload code = payload[0] pl = payload[1,payload.length-1] if code == Mindwave::Headset::EXCODE extcodelevel += 1 # iterate through the payload-array (1..payload.length).each do |n| # if there is an excode, increment the level if payload[n] == Mindwave::Headset::EXCODE extcodelevel += 1 else # ..otherwise parse the next code and it's payload code = payload[n] pl = payload[n+1,payload.length-(n+1)] break end end end # some debugging output log.info(sprintf("extcodelevel: %x",extcodelevel)) log.info(sprintf("Code: %x",code)) log.debug(sprintf("Length: %d",pl.length)) pl.each do |n| log.debug(sprintf("payload: Hex: %x Dec: %d",n,n)) end # SINGLE-BYTE-CODES if code < Mindwave::Headset::RAW_WAVE or code >= Mindwave::Headset::HEADSET_CONNECTED sbpayload = pl[0] codestr = "" case code when Mindwave::Headset::HEADSET_CONNECTED codestr = "Headset connected" @headsetstatus = code when Mindwave::Headset::HEADSET_NOTFOUND codestr = "Headset not found" @headsetstatus = code when Mindwave::Headset::HEADSET_DISCONNECTED codestr = "Headset disconnected" @headsetstatus = code when Mindwave::Headset::REQUEST_DENIED codestr = "Request denied" @headsetstatus = code when Mindwave::Headset::DONGLE_STANDBY codestr = "Dongle standby" @headsetstatus = code when Mindwave::Headset::POOR_SIGNAL codestr = "Poor Signal" @poor = sbpayload poorCall(@poor) when Mindwave::Headset::HEART_RATE codestr = "Heart Rate" @heart = sbpayload heartCall(@heart) when Mindwave::Headset::ATTENTION codestr = "Attention" @attention = sbpayload attentionCall(@attention) when Mindwave::Headset::MEDITATION codestr = "Meditation" @meditation = sbpayload meditationCall(@meditation) ## THIS METHODS ARE NOT AVAILABLE FOR MINDWAVE(MOBILE) when Mindwave::Headset::BIT8_RAW codestr = "8Bit Raw" when Mindwave::Headset::RAW_MARKER codestr = "Raw Marker" # EOF NOT AVAILABLE else codestr = "Unknown" end log.debug(sprintf("SINGLEBYTE-PAYLOAD: Code: %s Hex: %x - Dec: %d",codestr,sbpayload,sbpayload)) # Re-Parse the rest of the payload if pl.length > 2 payload = pl[1,pl.length-1] # recursive call of parse_payload for the next data-rows parse_payload(payload) end # MULTI-BYTE-CODES else codestr = "" plength = pl[0] mpl = pl[1,plength] case code when Mindwave::Headset::RAW_WAVE codestr = "RAW_WAVE Code detected" rawCall(convertRaw(mpl[0],mpl[1])) when Mindwave::Headset::EEG_POWER codestr = "EEG Power" when Mindwave::Headset::ASIC_EEG_POWER codestr = "ASIC EEG POWER" @asic = mpl asicCall(@asic) when Mindwave::Headset::RRINTERVAL codestr = "RRINTERVAL" else codestr = "Unknown" end # Fetch the Multi-Payload log.info(sprintf("Multibyte-Code: %s",codestr)) log.info(sprintf("Multibyte-Payload-Length: %d",pl[0])) mpl.each() do |n| log.debug(sprintf("MULTIBYTE-PAYLOAD: Hex: %x - Dec: %d",n,n)) end # Re-Parse the rest of the payload if pl.length-1 > plength payload = pl[mpl.length+1,pl.length-mpl.length] # recursive call of parse_payload for the next data-rows parse_payload(payload) end end end # this method parses the raw ASIC values and returns the values of each # of the wave types # # @param [Integer] asic value # # @returns [Array] Array of: delta,theta,lowAlpha,highAlpha,lowBeta,highBeta,lowGamma,midGamma def parseASIC(asic) # assign #{asic} to the array 'a' a = "#{asic}" # strip off square brackets a = a.delete! '[]' # convert to array of integers a = a.split(",").map(&:to_i) # define wave values delta = convertToBigEndianInteger(a[0..3]) theta = convertToBigEndianInteger(a[3..6]) lowAlpha = convertToBigEndianInteger(a[6..9]) highAlpha = convertToBigEndianInteger(a[9..12]) lowBeta = convertToBigEndianInteger(a[12..15]) highBeta = convertToBigEndianInteger(a[15..18]) lowGamma = convertToBigEndianInteger(a[18..21]) midGamma = convertToBigEndianInteger(a[21..24]) # stuff wave values in array asicArray = [delta,theta,lowAlpha,highAlpha,lowBeta,highBeta,lowGamma,midGamma] # return array of wave values return asicArray end # this method sends a byte to the serial connection # (Mindwave only) # # @param [Integer] hexbyte byte to send def sendbyte(hexbyte) cmd = BinData::Uint8be.new(hexbyte) cmd.write(@conn) end # This method connects to the headset automatically without knowing the device-id # (Mindwave only) def autoconnect cmd = BinData::Uint8be.new(Mindwave::Headset::AUTOCONNECT) cmd.write(@conn) end # this method disconnects a connection between headset and dongle # (Mindwave only) def disconnect cmd = BinData::Uint8be.new(Mindwave::Headset::DISCONNECT) cmd.write(@conn) end # this method opens a serial connection to the device def serial_open @conn = SerialPort.new(@device,@rate) end # this method closes a serial connection to the device def serial_close @conn.close end # this method disconnects the headset and closes the serial line def close disconnect serial_close end # this method stops the run-method def stop @runner = false end # -------------------------- # :section: Callback Methods # -------------------------- # this method is called when the poor-value is parsed # override this method to implement your own clode # # @param [Integer] poor poor-value def poorCall(poor) if poor == 200 log.info("No skin-contact detected") end end # this method is called when the attention-value is parsed # override this method to implement your own code # # @param [Integer] attention attention-value def attentionCall(attention) str = eSenseStr(attention) log.info("ATTENTION #{attention} #{str}") end # this method is called when the meditation-value is parsed # override this method to implement your own code # # @param [Integer] meditation meditation-value def meditationCall(meditation) str = eSenseStr(meditation) log.info("MEDITATION #{meditation} #{str}") end # this method is called when the heart-rate-value is parsed # override this method to implement your own code # # @param [Integer] heart heart-value def heartCall(heart) log.info("HEART RATE #{heart}") end # this method is called when the raw-wave-value is parsed # override this method to implement your own code # # @param [Integer] rawvalue raw-wave-value def rawCall(rawvalue) log.debug("Converted Raw-Value: #{rawvalue}") end ## # this method is called when the asic-value is parsed # override this method to implement your own code # # @param [Integer] asic asic-value # def asicCall(asic) log.debug("ASIC Value: #{asic}") end private # reads out a byte from the serial-line and # logs the byte using "debug" def logreadbyte begin ret = @conn.readbyte rescue EOFError log.fatal("EOFError") # But Ignore it with FF ret = 0x00 end log.debug(sprintf("HEX: %x DEC: %d",ret,ret)) return ret end # this method converts the numeric eSense-value of attention or meditation # to a string # # @param [Integer] value eSense-value # @returns [String] string-value # = eSense Values(Attention and Meditation) # * 1-20 = strongly lowered # * 20-40 = reduced # * 40-60 = neutral # * 60-80 = slightly elevated # * 80-100 = elevated def eSenseStr(value) result = case value when 0..20 then "Strongly lowered" when 21..40 then "Reduced" when 41..60 then "Neutral" when 61..80 then "Slightly elevated" when 81..100 then "Elevated" else "Unknown" end return result end # converts a raw-wave-data-packet of 2 bytes to a single value # # @param [Integer] rawval1 first byte-packet of the raw-wave-code # @param [Integer] rawval2 second byte-packet of the raw-wave-code # # @return [Integer] single value generated from the 2 bytes def convertRaw(rawval1,rawval2) raw = rawval1*256 + rawval2 if raw >= 32768 raw = raw - 65536 end return raw end # converts a raw ASIC power packet of three bytes to a single value # # @param [Integer] threeBytes[0] first byte-packet of the ASIC wave code # @param [Integer] threeBytes[1] second byte-packet of the ASIC wave code # @param [Integer] threeBytes[2] third byte-packet of the ASIC wave code # # @return [Integer] single value generated from the 3 bytes def convertToBigEndianInteger(threeBytes) # see MindwaveDataPoints.py at # https://github.com/robintibor/python-mindwave-mobile # bigEndianInteger = (threeBytes[0] << 16) |\ (((1 << 16) - 1) & (threeBytes[1] << 8)) |\ ((1 << 8) -1) & threeBytes[2] return bigEndianInteger end end end