whotwagner/mindwave

View on GitHub
lib/mindwave.rb

Summary

Maintainability
D
2 days
Test Coverage
#--
# Copyright (C) 2016 Wolfgang Hotwagner <code@feedyourhead.at>       
#                                                                
# 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<Integer>] 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