com.github.mrstampy.esp.neurosky.MultiConnectionThinkGearSocket.java Source code

Java tutorial

Introduction

Here is the source code for com.github.mrstampy.esp.neurosky.MultiConnectionThinkGearSocket.java

Source

/*
 * ESP-ThinkGear Copyright (C) 2014 Burton Alexander
 * 
 * This program 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 program 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 program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * 
 */

/*
 * 
 * This provides a simple socket connector to the NeuroSky MindWave ThinkGear connector.
 * For more info visit http://crea.tion.to/processing/thinkgear-java-socket
 * 
 * No warranty or any stuffs like that.
 * 
 * Have fun!
 * Andreas Borg
 * borg@elevated.to
 * 
 * 
 * (c) 2010
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA  02111-1307  USA
 * 
 * @author      Andreas Borg, borg@elevated.to
 * @modified   June, 2011
 * @version      1.0
 * 
 * 
 * This library is following the same design as the one developed by Jorge C. S. Cardoso for the MindSet device.
 * The MindWave device can communicate to a socket over JSON instead of the serial port. That makes it easier and tidier
 * to talk between the device and Java. For instructions on how to use the callback listeners please refer to
 * 
 * http://jorgecardoso.eu/processing/MindSetProcessing/
 * 
 * 
 * Data is passed back to the application via the following callback methods:
 * 
 * 
 * public void attentionEvent(int attentionLevel)
 * Returns the current attention level [0, 100].
 * Values in [1, 20] are considered strongly lowered.
 * Values in [20, 40] are considered reduced levels.
 * Values in [40, 60] are considered neutral.
 * Values in [60, 80] are considered slightly elevated.
 * Values in [80, 100] are considered elevated.
 * 
 * public void meditationEvent(int meditationLevel)
 * Returns the current meditation level [0, 100].
 * The interpretation of the values is the same as for the attentionLevel.
 * 
 * 
 * public void poorSignalEvent(int signalLevel)
 * Returns the signal level [0, 200]. The greater the value, the more noise is detected in the signal.
 * 200 is a special value  that means that the ThinkGear contacts are not touching the skin.
 * 
 * 
 * public void eegEvent(int delta, int theta, int low_alpha, int high_alpha, int low_beta, int high_beta, int low_gamma, int mid_gamma) </code><br>
 * Returns the EEG data. The values have no units.
 * 
 * 
 * 
 * public void rawEvent(int [])
 * Returns the the current 512 raw signal samples [-32768, 32767]. 
 * 
 * 
 */
package com.github.mrstampy.esp.neurosky;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

import javolution.util.FastList;

import org.apache.mina.core.service.IoHandler;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rx.Observable;
import rx.Scheduler;
import rx.Scheduler.Inner;
import rx.Scheduler.Recurse;
import rx.Subscription;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import com.github.mrstampy.esp.multiconnectionsocket.AbstractMultiConnectionSocket;
import com.github.mrstampy.esp.multiconnectionsocket.ConnectionEvent.State;
import com.github.mrstampy.esp.multiconnectionsocket.EspChannel;
import com.github.mrstampy.esp.multiconnectionsocket.MultiConnectionSocketException;
import com.github.mrstampy.esp.multiconnectionsocket.event.AbstractMultiConnectionEvent;
import com.github.mrstampy.esp.neurosky.event.AbstractThinkGearEvent;
import com.github.mrstampy.esp.neurosky.event.BlinkStrengthThinkGearEvent;
import com.github.mrstampy.esp.neurosky.event.EEGPowerThinkGearEvent;
import com.github.mrstampy.esp.neurosky.event.ESenseThinkGearEvent;
import com.github.mrstampy.esp.neurosky.event.EventType;
import com.github.mrstampy.esp.neurosky.event.PoorSignalLevelThinkGearEvent;
import com.github.mrstampy.esp.neurosky.event.RawEEGThinkGearEvent;
import com.github.mrstampy.esp.neurosky.subscription.ThinkGearEventListener;
import com.github.mrstampy.esp.neurosky.subscription.ThinkGearSocketConnector;

/**
 * This class connects to the socket described in the ThinkGear socket protocol
 * documentation. The events received are converted into strongly typed events
 * located in the net.sourceforge.entrainer.neurosky.event package and are
 * queued for transport. An Apache MINA socket acceptor is bound to the
 * broadcaster port defined in {@link ThinkGearSocketConnector} and the events
 * are read from the queue and sent to subscribers connected using the
 * {@link ThinkGearSocketConnector} class. <br>
 * <br>
 * Broadcasting is turned off by default. To enable set the System property
 * 'broadcast.messages' to true ie. java -Dbroadcast.messages=true ...
 * 
 * @author burton
 */
public class MultiConnectionThinkGearSocket extends AbstractMultiConnectionSocket<String>
        implements ThinkGearConstants {
    private static final Logger log = LoggerFactory.getLogger(MultiConnectionThinkGearSocket.class);

    private String host = LOCAL_HOST;

    private Socket neuroSocket;
    private PrintWriter out;
    private BufferedReader stdIn;

    private SubscriptionHandlerAdapter subscriptionHandlerAdapter;

    private List<ThinkGearEventListener> listeners = new FastList<ThinkGearEventListener>();
    private ReentrantReadWriteLock listenerLock = new ReentrantReadWriteLock(true);
    private ReadLock readLock = listenerLock.readLock();
    private WriteLock writeLock = listenerLock.writeLock();

    private boolean rawData;

    private boolean canSendNeuroskyMessages;

    private SampleBuffer sampleBuffer = new SampleBuffer();
    private Scheduler scheduler = Schedulers.executor(Executors.newScheduledThreadPool(3));
    private Subscription subscription;

    private EspChannel thinkGearChannel = new EspChannel(1, "ThinkGear Raw Signal Channel");

    /**
     * Connects to the ThinkGear socket on the local host. The system property
     * 'broadcast.messages' is used to enable/disable broadcasting for
     * {@link ThinkGearSocketConnector}s, and the system property
     * 'send.neurosky.messages' is used to enable/disable remote
     * {@link ThinkGearSocketConnector}s sending messages to the Neurosky device.
     * 
     * @throws IOException
     */
    public MultiConnectionThinkGearSocket() throws IOException {
        this(LOCAL_HOST, Boolean.getBoolean(BROADCAST_MESSAGES), Boolean.getBoolean(SEND_NEUROSKY_MESSAGES));
    }

    /**
     * Connects to the ThinkGear socket on the specified host. The system property
     * 'broadcast.messages' is used to enable/disable broadcasting for
     * {@link ThinkGearSocketConnector}s, and the system property
     * 'send.neurosky.messages' is used to enable/disable remote
     * {@link ThinkGearSocketConnector}s sending messages to the Neurosky device.
     * 
     * @param host
     * @throws IOException
     */
    public MultiConnectionThinkGearSocket(String host) throws IOException {
        this(host, Boolean.getBoolean(BROADCAST_MESSAGES), Boolean.getBoolean(SEND_NEUROSKY_MESSAGES));
    }

    /**
     * Connects to the ThinkGear socket on the specified host, allowing
     * programmatic control of broadcasting for {@link ThinkGearSocketConnector}s,
     * and the system property 'send.neurosky.messages' is used to enable/disable
     * remote {@link ThinkGearSocketConnector}s sending messages to the Neurosky
     * device.
     * 
     * @param host
     * @param broadcasting
     * @throws IOException
     */
    public MultiConnectionThinkGearSocket(String host, boolean broadcasting) throws IOException {
        this(host, broadcasting, Boolean.getBoolean(SEND_NEUROSKY_MESSAGES));
    }

    /**
     * Connects to the ThinkGear socket on the specified host, allowing
     * programmatic control of broadcasting for {@link ThinkGearSocketConnector}s,
     * and of enabling/disabling remote {@link ThinkGearSocketConnector}s sending
     * messages to the Neurosky device.
     * 
     * @param host
     * @param broadcasting
     * @param canSendNeuroskyMessages
     * @throws IOException
     */
    public MultiConnectionThinkGearSocket(String host, boolean broadcasting, boolean canSendNeuroskyMessages)
            throws IOException {
        this(host, broadcasting, canSendNeuroskyMessages, Boolean.getBoolean(RAW_DATA_KEY));
    }

    /**
     * Connects to the ThinkGear socket on the specified host, allowing
     * programmatic control of broadcasting for {@link ThinkGearSocketConnector}s,
     * of enabling/disabling remote {@link ThinkGearSocketConnector}s sending
     * messages to the Neurosky device and of the raw data acquisition from the
     * Neurosky device.
     * 
     * @param host
     * @param broadcasting
     * @param canSendNeuroskyMessages
     * @throws IOException
     */
    public MultiConnectionThinkGearSocket(String host, boolean broadcasting, boolean canSendNeuroskyMessages,
            boolean rawData) throws IOException {
        super(broadcasting);

        this.host = host;
        this.rawData = rawData;
        this.canSendNeuroskyMessages = canSendNeuroskyMessages;

        addConnectionEventListener(sampleBuffer);

        log.info("MultiConnectonThinkGearSocket is {} events", broadcasting ? "broadcasting" : "not broadcasting");
    }

    /**
     * Adds the specified {@link ThinkGearEventListener} to the
     * {@link MultiConnectionThinkGearSocket} directly. Use this method to receive
     * events in preference to {@link ThinkGearSocketConnector} if running in the
     * same JVM as the {@link MultiConnectionThinkGearSocket}.
     * 
     * @param listener
     */
    public void addListener(ThinkGearEventListener listener) {
        writeLock.lock();
        try {
            listeners.add(listener);
        } finally {
            writeLock.unlock();
        }
    }

    public void removeListener(ThinkGearEventListener listener) {
        writeLock.lock();
        try {
            listeners.remove(listener);
        } finally {
            writeLock.unlock();
        }
    }

    public void clearListeners() {
        writeLock.lock();
        try {
            listeners.clear();
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * When invoked the tuning functionality of the {@link SampleBuffer} will be
     * activated. The tuning process takes ~ 10 seconds, during which the number
     * of samples will be counted and used to resize the buffer to more closely
     * represent 1 seconds' worth of data.
     */
    public void tune() {
        if (!isConnected()) {
            log.warn("Must be connected to the Nia to tune");
            return;
        }

        log.info("Tuning sample buffer");

        sampleBuffer.tune();

        scheduler.schedule(new Action1<Scheduler.Inner>() {

            @Override
            public void call(Inner t1) {
                sampleBuffer.stopTuning();
            }
        }, 10, TimeUnit.SECONDS);
    }

    @Override
    public int getNumChannels() {
        return 1;
    }

    @Override
    public List<EspChannel> getChannels() {
        return Collections.nCopies(1, thinkGearChannel);
    }

    @Override
    public EspChannel getChannel(int channelNumber) {
        return thinkGearChannel;
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sourceforge.entrainer.neurosky.MultiConnectionSocket#start()
     */
    @Override
    protected void startImpl() throws MultiConnectionSocketException {
        closeSocket();

        try {
            createSocketConnection();
        } catch (MultiConnectionSocketException e) {
            closeSocket();
            throw e;
        }

        sendFormats();

        startReadThread();

        if (isRawData())
            startSampleCollection();
    }

    private void startSampleCollection() {
        ThinkGearDSPValues values = ThinkGearDSPValues.getInstance();

        long pause = values.getSampleRate() * 2;
        long snooze = values.getSampleRateSleepTime();
        TimeUnit tu = values.getSampleRateUnits();

        subscription = scheduler.schedulePeriodically(new Action1<Scheduler.Inner>() {

            @Override
            public void call(Inner t1) {
                processSnapshot(sampleBuffer.getSnapshot());
            }
        }, pause, snooze, tu);
    }

    protected void processSnapshot(double[] snapshot) {
        eventPerformed(new RawEEGThinkGearEvent(snapshot));
    }

    private void eventPerformed(AbstractThinkGearEvent event) {
        Observable.just(event).subscribe(new Action1<AbstractThinkGearEvent>() {

            @Override
            public void call(AbstractThinkGearEvent t1) {
                notifyListeners(t1);
                if (canBroadcast())
                    subscriptionHandlerAdapter.sendMultiConnectionEvent(t1);
            }
        });
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sourceforge.entrainer.neurosky.MultiConnectionSocket#stop()
     */
    @Override
    protected void stopImpl() {
        try {
            if (subscription != null)
                subscription.unsubscribe();
            closeSocket();
        } catch (Throwable e) {
            log.error("Unexpected exception on stop", e);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sourceforge.entrainer.neurosky.MultiConnectionSocket#isConnected()
     */
    @Override
    public boolean isConnected() {
        return neuroSocket != null && neuroSocket.isConnected();
    }

    /**
     * Sends the specified message to the ThinkGear socket.
     * 
     * @param msg
     */
    public void sendMessage(String msg) {
        if (!isConnected()) {
            log.error("Cannot send message {} to NeuroSky, not connected", msg);
            return;
        }

        log.info("Sending message to NeuroSky: {}", msg);
        out.println(msg);
    }

    /**
     * Returns true if {@link ThinkGearSocketConnector}s can send messages to the
     * Neurosky device.
     * 
     * @return
     * @see SubscriptionHandlerAdapter
     * @see ThinkGearSocketConnector
     */
    public boolean canSendNeuroskyMessages() {
        return canBroadcast() && subscriptionHandlerAdapter.canSendNeuroskyMessages();
    }

    /**
     * Returns true if the device is to acquire raw data.
     */
    public boolean isRawData() {
        return rawData;
    }

    /**
     * Enable/disable raw data acquisition after the next connect to the ThinkGear
     * socket.
     */
    public void setRawData(boolean rawData) {
        this.rawData = rawData;
    }

    private void createSocketConnection() throws MultiConnectionSocketException {
        try {
            neuroSocket = new Socket(host, NEUROSOCKET_PORT);
            out = new PrintWriter(neuroSocket.getOutputStream(), true);
            stdIn = new BufferedReader(new InputStreamReader(neuroSocket.getInputStream()));
        } catch (Throwable e) {
            log.error("Could not connect to ThinkGearSocket", e);
            throw new MultiConnectionSocketException(e);
        }
    }

    private void closeSocket() throws MultiConnectionSocketException {
        if (neuroSocket == null)
            return;

        try {
            neuroSocket.close();
            out.close();
            stdIn.close();

            neuroSocket = null;
            out = null;
            stdIn = null;
        } catch (Throwable e) {
            throw new MultiConnectionSocketException(e);
        }
    }

    private void sendFormats() {
        scheduler.schedule(new Action1<Scheduler.Inner>() {

            @Override
            public void call(Inner t1) {
                JSONObject format = new JSONObject();

                try {
                    format.put("enableRawOutput", rawData);
                    format.put("format", "Json");
                } catch (JSONException e) {
                    log.error("Could not create formats object", e);
                }

                sendMessage(format.toString());
            }
        });
    }

    private void startReadThread() {
        scheduler.scheduleRecursive(new Action1<Scheduler.Recurse>() {

            @Override
            public void call(Recurse t1) {
                String userInput;
                try {
                    if (!neuroSocket.isConnected() || (userInput = stdIn.readLine()) == null)
                        return;

                    String[] packets = userInput.split("/\r/");
                    for (int s = 0; s < packets.length; s++) {
                        if (((String) packets[s]).indexOf("{") > -1) {
                            publishMessage(packets[s]);
                        }
                    }

                    t1.schedule();
                } catch (Exception e) {
                    log.error("Unexpected exception", e);
                    if (isConnected())
                        notifyConnectionEventListeners(State.ERROR_STOPPED);
                }
            }
        });
    }

    @Override
    protected void parseMessage(String message) {
        try {
            log.trace("Processing message {}", message);

            int first = message.indexOf("{");
            int last = message.lastIndexOf("}");

            if (first == -1 || last == -1) {
                log.info("Cannot parse message {}", message);
                return;
            }

            if (first > 0)
                message = message.substring(first);
            if (last < message.length())
                message = message.substring(0, last + 1);

            parsePacket(new JSONObject(message));
        } catch (Throwable e) {
            log.error("Could not process message {}", e, message);
        }
    }

    @SuppressWarnings("rawtypes")
    private void parsePacket(JSONObject jsonObject) throws JSONException {
        Iterator itr = jsonObject.keys();
        while (itr.hasNext()) {

            Object e = itr.next();
            String key = e.toString();

            EventType type = EventType.valueOf(key);

            AbstractThinkGearEvent event = null;

            switch (type) {
            case blinkStrength:
                event = getBlinkStrengthThinkGearEvent(jsonObject);
                break;
            case eSense:
                JSONObject esense = jsonObject.getJSONObject(EventType.eSense.name());
                event = getESenseThinkGearEvent(esense);
                break;
            case eegPower:
                JSONObject eegPower = jsonObject.getJSONObject(EventType.eegPower.name());
                event = getEegPowerThinkGearEvent(eegPower);
                break;
            case poorSignalLevel:
                event = getPoorSignalLevelThinkGearEvent(jsonObject);
                break;
            case rawEeg:
                sampleBuffer.addSample(getDouble(jsonObject, EventType.rawEeg.name()));
                break;
            default:
                break;
            }

            if (event != null)
                eventPerformed(event);
        }
    }

    public void notifyListeners(AbstractMultiConnectionEvent<EventType> event) {
        readLock.lock();
        try {
            if (listeners.isEmpty())
                return;

            for (ThinkGearEventListener listener : listeners) {
                listener.thinkGearEventPerformed(event);
            }
        } finally {
            readLock.unlock();
        }
    }

    private PoorSignalLevelThinkGearEvent getPoorSignalLevelThinkGearEvent(JSONObject jsonObject) {
        PoorSignalLevelThinkGearEvent event = new PoorSignalLevelThinkGearEvent();

        event.setPoorSignalLevel(getInt(jsonObject, EventType.poorSignalLevel.name()));

        return event;
    }

    private EEGPowerThinkGearEvent getEegPowerThinkGearEvent(JSONObject jsonObject) {
        EEGPowerThinkGearEvent event = new EEGPowerThinkGearEvent();

        event.setDelta(getDouble(jsonObject, DELTA));
        event.setHighAlpha(getDouble(jsonObject, HIGH_ALPHA));
        event.setHighBeta(getDouble(jsonObject, HIGH_BETA));
        event.setHighGamma(getDouble(jsonObject, HIGH_GAMMA));
        event.setLowAlpha(getDouble(jsonObject, LOW_ALPHA));
        event.setLowBeta(getDouble(jsonObject, LOW_BETA));
        event.setLowGamma(getDouble(jsonObject, LOW_GAMMA));
        event.setTheta(getDouble(jsonObject, THETA));

        return event;
    }

    private BlinkStrengthThinkGearEvent getBlinkStrengthThinkGearEvent(JSONObject jsonObject) {
        BlinkStrengthThinkGearEvent event = new BlinkStrengthThinkGearEvent();

        event.setBlinkStrength(getInt(jsonObject, EventType.blinkStrength.name()));

        return event;
    }

    private ESenseThinkGearEvent getESenseThinkGearEvent(JSONObject jsonObject) {
        ESenseThinkGearEvent event = new ESenseThinkGearEvent();

        event.setAttention(getInt(jsonObject, ATTENTION));
        event.setMeditation(getInt(jsonObject, MEDITATION));

        return event;
    }

    private int getInt(JSONObject jsonObject, String key) {
        try {
            return jsonObject.getInt(key);
        } catch (JSONException e) {
            log.error("Could not extract {} from JSON object {}", e, key, jsonObject);
        }

        return -1;
    }

    private double getDouble(JSONObject jsonObject, String key) {
        try {
            return jsonObject.getDouble(key);
        } catch (JSONException e) {
            log.error("Could not extract {} from JSON object {}", e, key, jsonObject);
        }

        return -1;
    }

    @Override
    protected IoHandler getHandlerAdapter() {
        subscriptionHandlerAdapter = new SubscriptionHandlerAdapter(this, canSendNeuroskyMessages);
        return subscriptionHandlerAdapter;
    }
}