hemokit 0.5.1 → 0.6.0
raw patch · 9 files changed
+372/−115 lines, 9 filesdep +network-simpledep ~hidapidep ~time
Dependencies added: network-simple
Dependency ranges changed: hidapi, time
Files
- README.md +79/−0
- apps/Dump.hs +76/−22
- apps/DumpConduit.hs +76/−26
- apps/SocketUtils.hs +34/−0
- apps/WebsocketUtils.hs +18/−31
- hemokit.cabal +55/−27
- src/Hemokit.hs +6/−6
- src/Hemokit/Conduit.hs +25/−2
- src/Hemokit/Start.hs +3/−1
+ README.md view
@@ -0,0 +1,79 @@+hemokit+=======++Haskell library and tool suite for the [Emotiv](http://emotiv.com) Epoc EEG, inspired by the [Emokit](https://github.com/openyou/emokit) code.++It currently only works on Linux and Windows - patches for other platforms are welcome, they should be trivial.+++Download+--------++You can download [pre-built binaries here](https://github.com/nh2/hemokit/releases), or build it yourself via [`cabal install hemokit`](http://hackage.haskell.org/package/hemokit).+++Library Features+----------------++* device discovery via [hidapi](https://github.com/vahokif/haskell-hidapi)+* decryption of the raw data (one-to-one port from from Emokit)+* convenient access to sensor values, gyro, qualities, battery, and raw data+++Programs+--------++Hemokit comes with example programs to++* `hemokit-dump` print out the current EEG data+* `hemokit-mouse` move the cursor using the gyro++Note that we have to use `sudo` in most of the cases because the HIDAP-hidraw implementation reads directly from a device file.+++hemokit-dump - Examples+-----------------------++*hemokit-dump* can print EEG data, format it as JSON, serve it via Websockets, and read from real devices and dump files.+++* Output EEG *cumulative state* for an automatically found device:++ ```bash+ sudo hemokit-dump+ ```++* Select one of many connected EEGs by serial number:++ ```bash+ sudo hemokit-dump --serial SN...GM+ ```++* Output only the data the device sends (no cumulative state), and format the output as JSON:++ ```bash+ sudo hemokit-dump --mode packets --json+ ```++* Instead of from a real device, read data recorded to a file, and serve it via JSON over a Websockets server on port `1234`:++ ```bash+ sudo cat /dev/hidraw1 > encrypted.dump # Dump data to a file+ sudo hemokit-dump --from-file encrypted.dump --serial SN...GM --serve 0.0.0.0:1234 --json+ ```++ Here you **have** to specify the serial since HIDAPI is not used to obtain it automatically.++* Output decrypted raw data to stdout:++ ```bash+ sudo hemokit-dump --mode raw+ ```++* Both print the data from the EEG **and** store the original data for later use:++ ```bash+ sudo cat /dev/hidraw1 | tee >(hemokit-dump --from-file - --serial SN...GM --json) > encrypted.dump+ ```++ We use `tee` and shell process substitution to duplicate the data stream, and tell *hemokit-dump* to read from `-` (stdin).
apps/Dump.hs view
@@ -8,12 +8,17 @@ import Data.Aeson (ToJSON (..), encode) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy.Char8 as BSL8+import qualified Data.ByteString.Lazy as BSL+import qualified Data.ByteString.Lazy.Builder as Builder+import qualified Data.ByteString.Lazy.Builder.ASCII as Builder import qualified Data.ByteString.Base64 as Base64 import Data.Function (fix) import Data.IORef import Data.List import Data.List.Split (splitOn) import Data.Time.Clock+import Data.Monoid+import qualified Data.Vector as V import Options.Applicative hiding (action) import System.IO import Text.Read@@ -23,7 +28,8 @@ import Hemokit.Start import Hemokit.Internal.Utils (withJustM)-import WebsocketUtils (makeJsonOrShowWSServer, JsonShowable (..))+import SocketUtils (makeTCPServer)+import WebsocketUtils (makeWSServer) -- | Arguments for the EEG dump application.@@ -32,15 +38,23 @@ , mode :: DumpMode -- ^ What to dump. , realtime :: Bool -- ^ In case fromFile is used, throttle to 128 Hz. , listDevices :: Bool -- ^ Do not do anything, print available devices.- , json :: Bool -- ^ Whether to format the output as JSON.- , serve :: Maybe (String, Int) -- ^ Serve via websockets on host:port.+ , format :: OutputFormat -- ^ How to print the output.+ , serve :: Maybe ServeMethod -- ^ Serve via TCP or websockets on host:port. } -- | Whether to dump raw data, hardware-sent packages, cumulative states, -- or measurements of device-computer latency.-data DumpMode = Raw | Packets | State | Measure deriving (Eq, Show)+data DumpMode = Raw | Packets | State | Measure deriving (Eq, Ord, Show) +-- | In what format to print the output.+-- `Default` is raw bytes to stdout for `Raw` mode and `show` for everything else.+data OutputFormat = Default | Json | Spaced deriving (Eq, Ord, Show) +-- | Whether to serve via plain TCP or Websockets (hostname and port).+data ServeMethod+ = TCP String Int+ | Websocket String Int+ -- | Parser for `DumpArgs`. dumpArgsParser :: Parser DumpArgs dumpArgsParser = DumpArgs@@ -55,14 +69,16 @@ <*> switch ( long "list" <> help "Show all available Emotiv devices and exit" )- <*> switch- ( long "json"- <> help "Format output as JSON" )+ <*> nullOption+ ( long "format"+ <> reader parseOutputFormat <> value Default+ <> help "Format output as Haskell value, JSON or space-separated" ) <*> (optional . nullOption) ( long "serve" <> metavar "HOST:PORT" <> eitherReader parseHostPort- <> help ("Serve output via websockets, e.g. 127.0.0.1:1234 " ++- "(port 1234, only localhost) or 0.0.0.0:1234 (all interfaces)") )+ <> help ("Serve output via a TCP server, e.g. 127.0.0.1:1234 " +++ "(port 1234, only localhost) or 0.0.0.0:1234 (all interfaces). " +++ "Use 'ws://' before the host to serve via websockets") ) where -- TODO https://github.com/pcapriotti/optparse-applicative/issues/48 eitherReader str2either = reader (either fail return . str2either)@@ -77,30 +93,55 @@ "measure" -> return Measure _ -> fail "Mode is not valid. Must be 'raw', 'packets', or 'state'." +-- | `OutputFormat` command line parser.+parseOutputFormat :: Monad m => String -> m OutputFormat+parseOutputFormat s = case s of+ "default"-> return Default+ "json" -> return Json+ "spaced" -> return Spaced+ _ -> fail "Format is not valid. Must be 'default', 'json', or 'spaced'." + -- | Parses host and port from a string like "0.0.0.0:1234".-parseHostPort :: String -> Either String (String, Int)-parseHostPort hostPort = case readMaybe portStr of+parseHostPort :: String -> Either String ServeMethod+parseHostPort hostPortWs = case readMaybe portStr of Nothing -> Left $ show portStr ++ " is not a valid port number"- Just p -> Right (host, p)+ Just p -> Right $ if ws then Websocket host p+ else TCP host p where+ (ws, hostPort) = case stripPrefix "ws://" hostPortWs of+ Just rest -> (True, rest)+ Nothing -> (False, hostPortWs)+ (host, portStr) = splitLast ":" hostPort splitLast :: String -> String -> (String, String) splitLast sep s = let sp = splitOn sep s -- splitOn never returns [] in (intercalate sep (init sp), last sp) +whitespaceFormat :: EmotivState -> BSL.ByteString+whitespaceFormat EmotivState{ counter, battery, gyroX, gyroY, sensors, qualities }+ = Builder.toLazyByteString . mconcat+ . intersperse (Builder.char8 ' ') . map Builder.intDec $ ints+ where+ ints = [ counter, battery, gyroX, gyroY ] ++ V.toList sensors ++ V.toList qualities + main :: IO () main = do DumpArgs{ emotivArgs , mode , realtime , listDevices- , json+ , format , serve } <- parseArgs "Dumps Emotiv data" dumpArgsParser + -- Catch invalid mode/format combinations immediately+ -- (so that we don't block first and error afterwards, see `formatOutput`).+ when (format == Spaced && mode /= State) $+ error $ "cannot space-format in " ++ show mode ++ " mode"+ if listDevices -- Only list devices then getEmotivDevices >>= putStrLn . ("Available devices:\n" ++) . ppShow else do@@ -112,12 +153,20 @@ Left err -> error err Right device -> do + let formatOutput x = case format of+ Default -> BSL8.pack (show x)+ Json -> encode x+ Spaced -> error "hemokit-dump BUG: formatOutput/spaced not caught early"+ -- Print to stdout or serve via websockets? Show the datatype or format via JSON? -- `output` accepts anything that's JSON-formattable and showable (wrapped in JsonShowable). output <- case serve of -- TODO use Data.ByteString.Lazy.UTF8.fromString instead of BSL8 to prevent unicode errors- Nothing -> return (BSL8.putStrLn . if json then encode else BSL8.pack . show)- Just (host, port) -> makeJsonOrShowWSServer host port json+ Nothing -> return (\x -> BSL8.putStrLn x >> hFlush stdout)+ Just (Websocket host port) -> do sendFn <- makeWSServer host port+ return sendFn+ Just (TCP host port) -> do sendFn <- makeTCPServer host port+ return sendFn -- For --mode measure: See how long a 0-128 cycle takes timeRef <- newIORef =<< getCurrentTime@@ -132,27 +181,32 @@ moreInput <- case mode of Packets -> readEmotiv device `withJustM` \(_, packet) ->- output (JsonShowable packet)+ output $ formatOutput packet State -> readEmotiv device `withJustM` \(state, _) ->- output (JsonShowable state)+ case format of+ Spaced -> output $ whitespaceFormat state+ _ -> output $ formatOutput state Raw -> readEmotivRaw device `withJustM` \rawBytes -> do- if json then output (JsonShowable rawBytes)- else BS.putStr (emotivRawDataBytes rawBytes)- hFlush stdout -- flush so that consuming apps immediately get it+ case format of+ Default -> -- raw data stdout; flush so that consuming apps immediately get it+ BS.putStr (emotivRawDataBytes rawBytes) >> hFlush stdout + _ -> -- use EmotivRawData newtype for base64 encoding+ output $ formatOutput rawBytes+ Measure -> readEmotivRaw device `withJustM` \_ -> do -- When a full cycle is done, print how long it took. count <- readIORef countRef modifyIORef' countRef (+1) when (count == 128) $ do cycleTime <- diffUTCTime <$> getCurrentTime <*> readIORef timeRef- output . JsonShowable $ toDoule cycleTime+ output . formatOutput $ toDouble cycleTime writeIORef countRef 0 writeIORef timeRef =<< getCurrentTime where- toDoule x = fromRational (toRational x) :: Double+ toDouble x = fromRational (toRational x) :: Double -- When realtime is on, throttle the reading to 1/129 (a real
apps/DumpConduit.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE NamedFieldPuns, ExistentialQuantification #-}+{-# LANGUAGE NamedFieldPuns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where@@ -8,7 +8,10 @@ import Control.Monad.IO.Class import Data.Aeson (ToJSON (..), encode) import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy.Char8 as BSL8+import qualified Data.ByteString.Lazy.Builder as Builder+import qualified Data.ByteString.Lazy.Builder.ASCII as Builder import qualified Data.ByteString.Base64 as Base64 import Data.Conduit import qualified Data.Conduit.List as CL@@ -17,6 +20,8 @@ import Data.List import Data.List.Split (splitOn) import Data.Time.Clock+import Data.Monoid+import qualified Data.Vector as V import Options.Applicative hiding (action) import System.IO import Text.Read@@ -33,15 +38,23 @@ , mode :: DumpMode -- ^ What to dump. , realtime :: Bool -- ^ In case fromFile is used, throttle to 128 Hz. , listDevices :: Bool -- ^ Do not do anything, print available devices.- , json :: Bool -- ^ Whether to format the output as JSON.- , serve :: Maybe (String, Int) -- ^ Serve via websockets on host:port.+ , format :: OutputFormat -- ^ How to print the output.+ , serve :: Maybe ServeMethod -- ^ Serve via TCP or websockets on host:port. } -- | Whether to dump raw data, hardware-sent packages, cumulative states, -- or measurements of device-computer latency.-data DumpMode = Raw | Packets | State | Measure deriving (Eq, Show)+data DumpMode = Raw | Packets | State | Measure deriving (Eq, Ord, Show) +-- | In what format to print the output.+-- `Default` is raw bytes to stdout for `Raw` mode and `show` for everything else.+data OutputFormat = Default | Json | Spaced deriving (Eq, Ord, Show) +-- | Whether to serve via plain TCP or Websockets (hostname and port).+data ServeMethod+ = TCP String Int+ | Websocket String Int+ -- | Parser for `DumpArgs`. dumpArgsParser :: Parser DumpArgs dumpArgsParser = DumpArgs@@ -56,14 +69,16 @@ <*> switch ( long "list" <> help "Show all available Emotiv devices and exit" )- <*> switch- ( long "json"- <> help "Format output as JSON" )+ <*> nullOption+ ( long "format"+ <> reader parseOutputFormat <> value Default+ <> help "Format output as Haskell value, JSON or space-separated" ) <*> (optional . nullOption) ( long "serve" <> metavar "HOST:PORT" <> eitherReader parseHostPort- <> help ("Serve output via websockets, e.g. 127.0.0.1:1234 " ++- "(port 1234, only localhost) or 0.0.0.0:1234 (all interfaces)") )+ <> help ("Serve output via a TCP server, e.g. 127.0.0.1:1234 " +++ "(port 1234, only localhost) or 0.0.0.0:1234 (all interfaces). " +++ "Use 'ws://' before the host to serve via websockets") ) where -- TODO https://github.com/pcapriotti/optparse-applicative/issues/48 eitherReader str2either = reader (either fail return . str2either)@@ -78,30 +93,55 @@ "measure" -> return Measure _ -> fail "Mode is not valid. Must be 'raw', 'packets', or 'state'." +-- | `OutputFormat` command line parser.+parseOutputFormat :: Monad m => String -> m OutputFormat+parseOutputFormat s = case s of+ "default"-> return Default+ "json" -> return Json+ "spaced" -> return Spaced+ _ -> fail "Format is not valid. Must be 'default', 'json', or 'spaced'." + -- | Parses host and port from a string like "0.0.0.0:1234".-parseHostPort :: String -> Either String (String, Int)-parseHostPort hostPort = case readMaybe portStr of+parseHostPort :: String -> Either String ServeMethod+parseHostPort hostPortWs = case readMaybe portStr of Nothing -> Left $ show portStr ++ " is not a valid port number"- Just p -> Right (host, p)+ Just p -> Right $ if ws then Websocket host p+ else TCP host p where+ (ws, hostPort) = case stripPrefix "ws://" hostPortWs of+ Just rest -> (True, rest)+ Nothing -> (False, hostPortWs)+ (host, portStr) = splitLast ":" hostPort splitLast :: String -> String -> (String, String) splitLast sep s = let sp = splitOn sep s -- splitOn never returns [] in (intercalate sep (init sp), last sp) +whitespaceFormat :: EmotivState -> BSL.ByteString+whitespaceFormat EmotivState{ counter, battery, gyroX, gyroY, sensors, qualities }+ = Builder.toLazyByteString . mconcat+ . intersperse (Builder.char8 ' ') . map Builder.intDec $ ints+ where+ ints = [ counter, battery, gyroX, gyroY ] ++ V.toList sensors ++ V.toList qualities + main :: IO () main = do DumpArgs{ emotivArgs , mode , realtime , listDevices- , json+ , format , serve } <- parseArgs "Dumps Emotiv data" dumpArgsParser + -- Catch invalid mode/format combinations immediately+ -- (so that we don't block first and error afterwards, see `formatOutput`).+ when (format == Spaced && mode /= State) $+ error $ "cannot space-format in " ++ show mode ++ " mode"+ if listDevices -- Only list devices then getEmotivDevices >>= putStrLn . ("Available devices:\n" ++) . ppShow else do@@ -113,27 +153,37 @@ Left err -> error err Right device -> do - -- Print to stdout or serve via websockets? Show the datatype or format via JSON?- let outputSink :: (ToJSON i, Show i) => Sink i IO ()+ let -- Show the datatype or format via JSON?+ formatConduit :: (ToJSON i, Show i) => Conduit i IO BSL.ByteString+ formatConduit = case format of+ Default -> CL.map (BSL8.pack . show)+ Json -> CL.map encode+ Spaced -> error "hemokit-dump BUG: formatOutput/spaced not caught early"++ -- Print to stdout or serve via websockets? outputSink = case serve of- Nothing | json -> asJson =$ CL.mapM_ BSL8.putStrLn- | otherwise -> CL.mapM_ print- Just (host, port) | json -> asJson =$ websocketSink host port- | otherwise -> websocketSink host port- where- asJson = CL.map encode+ Nothing -> CL.mapM_ BSL8.putStrLn+ Just (Websocket host port) -> websocketSink host port+ Just (TCP host port) -> tcpSink host port + -- Prints raw bytes to stdout+ rawBytesSink = CL.mapM_ (putStrBsFlush . emotivRawDataBytes)+ throttled = if realtime then ($= throttle) else id -- Output accumulative state, device-sent packet, or raw data? case mode of- Packets -> throttled (emotivPackets device) $$ outputSink+ Packets -> throttled (emotivPackets device) $$ formatConduit =$ outputSink - State -> throttled (emotivStates device) $$ outputSink+ State -> throttled (emotivStates device) $$ case format of+ Spaced -> CL.map whitespaceFormat =$ outputSink+ _ -> formatConduit =$ outputSink - Raw -> throttled (rawSource device) $$ if json then outputSink- else CL.mapM_ (putStrBsFlush . emotivRawDataBytes)- Measure -> throttled (rawSource device) $= measureConduit $$ outputSink+ Raw -> throttled (rawSource device) $$ case format of+ Default -> rawBytesSink+ _ -> formatConduit =$ outputSink -- use EmotivRawData newtype for base64 encoding++ Measure -> throttled (rawSource device) $= measureConduit $$ formatConduit =$ outputSink where putStrBsFlush bs = BS.putStr bs >> hFlush stdout
+ apps/SocketUtils.hs view
@@ -0,0 +1,34 @@+module SocketUtils+ ( tcpServerFromChan+ , makeTCPServerChan+ , makeTCPServer+ ) where++import Control.Concurrent+import Control.Monad+import qualified Data.ByteString.Lazy as BSL+import Network.Simple.TCP+++-- | A TCP server that serves ByteStrings from a Chan.+tcpServerFromChan :: Chan BSL.ByteString -> (Socket, SockAddr) -> IO ()+tcpServerFromChan chan = \(sock, _remoteAddr) -> do+ c <- dupChan chan+ forever (readChan c >>= send sock . BSL.toStrict)+++-- | Creates and starts (forking) a ByteString-serving TCP server.+-- Returns a Chan from which the server will read.+makeTCPServerChan :: String -> Int -> IO (Chan BSL.ByteString)+makeTCPServerChan host port = do+ chan <- newChan+ _ <- forkIO $ withSocketsDo $ serve (Host host) (show port) (tcpServerFromChan chan)+ return chan+++-- | Creates and starts (forking) a ByteString-serving TCP server.+-- Returns a function that serves the contents.+makeTCPServer :: String -> Int -> IO (BSL.ByteString -> IO ())+makeTCPServer host port = do+ chan <- makeTCPServerChan host port+ return (writeChan chan)
apps/WebsocketUtils.hs view
@@ -1,48 +1,35 @@-{-# LANGUAGE ExistentialQuantification #-}- module WebsocketUtils- ( jsonWSServerFromChan- , makeJsonWSServer- , makeJsonOrShowWSServer- , JsonShowable (..)+ ( wsServerFromChan+ , makeWSServerChan+ , makeWSServer ) where import Control.Concurrent import Control.Monad-import Data.Aeson (ToJSON(..), encode)+import qualified Data.ByteString.Lazy as BSL import Network.WebSockets --- | A websocket server that serves JSON from a Chan.-jsonWSServerFromChan :: (ToJSON a) => Chan a -> PendingConnection -> IO ()-jsonWSServerFromChan chan = \req -> do+-- | A websocket server that serves ByteStrings from a Chan.+wsServerFromChan :: Chan BSL.ByteString -> PendingConnection -> IO ()+wsServerFromChan chan = \req -> do conn <- acceptRequest req c <- dupChan chan- forever (readChan c >>= sendTextData conn . encode)+ forever (readChan c >>= sendTextData conn) --- | Creates and starts (forking) a JSON-serving websocket server.+-- | Creates and starts (forking) a ByteString-serving websocket server. -- Returns a Chan from which the server will read.-makeJsonWSServer :: (ToJSON a) => String -> Int -> IO (Chan a)-makeJsonWSServer host port = do+makeWSServerChan :: String -> Int -> IO (Chan BSL.ByteString)+makeWSServerChan host port = do chan <- newChan- _ <- forkIO $ runServer host port (jsonWSServerFromChan chan)+ _ <- forkIO $ runServer host port (wsServerFromChan chan) return chan --- | Creates a server that can serve things either as text or as JSON.--- If `json` is true, inputs to the returned function will be sent as JSON,--- otherwise they will be `show`n and then JSON-formatted as a String.-makeJsonOrShowWSServer :: (Show a, ToJSON a) => String -> Int -> Bool -> IO (a -> IO ())-makeJsonOrShowWSServer host port json =- if json then do chan <- makeJsonWSServer host port- return (writeChan chan)- else do chan <- makeJsonWSServer host port- return (writeChan chan . show)----- | Something that can be shown and formatted as JSON.-data JsonShowable = forall a . (Show a, ToJSON a) => JsonShowable a--instance Show JsonShowable where show (JsonShowable x) = show x-instance ToJSON JsonShowable where toJSON (JsonShowable x) = toJSON x+-- | Creates and starts (forking) a ByteString-serving websocket server.+-- Returns a function that serves the contents.+makeWSServer :: String -> Int -> IO (BSL.ByteString -> IO ())+makeWSServer host port = do+ chan <- makeWSServerChan host port+ return (writeChan chan)
hemokit.cabal view
@@ -1,5 +1,5 @@ name: hemokit-version: 0.5.1+version: 0.6.0 license: MIT copyright: 2013 Niklas Hambüchen <mail@nh2.me>, Patrick Chilton <chpatrick@gmail.com> author: Niklas Hambüchen <mail@nh2.me>, Patrick Chilton <chpatrick@gmail.com>@@ -36,11 +36,24 @@ in both their plain form and as JSON, and optionally serve any of this via Websockets. +extra-source-files:+ README.md+ source-repository head type: git location: git://github.com/nh2/hemokit.git +flag fft+ description: Enable apps that use FFT. Needs fftw installed.++flag headmap+ description: Build the headmap. Needs GUI libraries installed.++flag mouse+ description: Build the app that controls the mouse with the gyroscope. Needs Xorg and related libraries installed (so it only works on Linux for now).++ library exposed-modules: Hemokit@@ -55,8 +68,9 @@ , conduit >= 1 , deepseq >= 1.2 , deepseq-generics >= 0.1- , hidapi >= 0.1.1+ , hidapi >= 0.1.2 , mtl >= 2.1.2+ , network-simple >= 0.3.0 , optparse-applicative >= 0.5.2.1 , vector >= 0.9 , websockets >= 0.8.0.0@@ -67,17 +81,20 @@ executable hemokit-mouse+ if !flag(mouse)+ buildable: False default-language: Haskell2010 hs-source-dirs: apps main-is: Mouse.hs- build-depends:- base- , hemokit- , pretty-show >= 1.0- , robot >= 1.0.1.1- , xhb >= 0.5.2012.11.23+ if flag(mouse)+ build-depends:+ base+ , hemokit+ , pretty-show >= 1.0+ , robot >= 1.0.1.1+ , xhb >= 0.5.2012.11.23 ghc-options: -Wall @@ -88,6 +105,7 @@ main-is: Dump.hs other-modules:+ SocketUtils WebsocketUtils build-depends: base@@ -95,11 +113,13 @@ , aeson >= 0.6.1.0 , base64-bytestring >= 1.0.0.1 , bytestring >= 0.9.2.1+ , network-simple >= 0.3.0 , optparse-applicative >= 0.5.2.1 , pretty-show >= 1.0 , split >= 0.2.2- , time >= 1.4.0.1+ , time >= 1.4 , transformers >= 0.3.0.0+ , vector >= 0.9 , websockets >= 0.8.0.0 ghc-options: -Wall @@ -117,46 +137,54 @@ , base64-bytestring >= 1.0.0.1 , bytestring >= 0.9.2.1 , conduit >= 1+ , network-simple >= 0.3.0 , optparse-applicative >= 0.5.2.1 , pretty-show >= 1.0 , split >= 0.2.2- , time >= 1.4.0.1+ , time >= 1.4 , transformers >= 0.3.0.0+ , vector >= 0.9 ghc-options: -Wall executable hemokit-fft+ if !flag(fft)+ buildable: False default-language: Haskell2010 hs-source-dirs: apps main-is: FFT.hs- build-depends:- base- , hemokit- , conduit- , mtl >= 2.1.2- , pretty-show >= 1.0- , vector >= 0.9- , vector-fftw >= 0.1.3.1+ if flag(fft)+ build-depends:+ base+ , hemokit+ , conduit+ , mtl >= 2.1.2+ , pretty-show >= 1.0+ , vector >= 0.9+ , vector-fftw >= 0.1.3.1 ghc-options: -Wall executable hemokit-headmap+ if !flag(headmap)+ buildable: False default-language: Haskell2010 hs-source-dirs: apps main-is: Headmap.hs- build-depends:- base- , hemokit- , cairo >= 0.12.4- , gtk >= 0.12.4- , mtl >= 2.1.2- , pretty-show >= 1.0- , svgcairo >= 0.12.1.1- , vector >= 0.9+ if flag(headmap)+ build-depends:+ base+ , hemokit+ , cairo >= 0.12.4+ , gtk >= 0.12.4+ , mtl >= 2.1.2+ , pretty-show >= 1.0+ , svgcairo >= 0.12.1.1+ , vector >= 0.9 ghc-options: -Wall
src/Hemokit.hs view
@@ -86,11 +86,11 @@ -- -- You can check if you are using the correct model by seeing if the packet -- `counter` increases from 0 until 128 on subsequent packets.-data EmotivModel = Consumer | Developer deriving (Eq, Show, Generic)+data EmotivModel = Consumer | Developer deriving (Eq, Ord, Show, Generic) -- | A valid Emotiv serial number. 16 bytes.-newtype SerialNumber = SerialNumber ByteString deriving (Eq, Show, Generic)+newtype SerialNumber = SerialNumber ByteString deriving (Eq, Ord, Show, Generic) -- | Checks an Emotiv serial, returning a `SerialNumber` if it's valid. makeSerialNumber :: ByteString -> Maybe SerialNumber@@ -145,7 +145,7 @@ -- | Describes the indices of bits to make up a certain value.-newtype BitMask = BitMask [Word8] deriving (Eq, Show)+newtype BitMask = BitMask [Word8] deriving (Eq, Ord, Show) -- | Describes which bits in a raw data packet make up the given sensor. getSensorMask :: Sensor -> BitMask@@ -270,7 +270,7 @@ , packetGyroY :: Int -- ^ turning "down" gives positive numbers , packetSensors :: Vector Int -- ^ EEG sensor values , packetQuality :: Maybe (Sensor, Int) -- ^ EEG sensor-to-skin connectivity- } deriving (Eq, Show, Generic)+ } deriving (Eq, Ord, Show, Generic) -- | Contains the "current state" of the EEG, cumulateively updated by@@ -282,13 +282,13 @@ , gyroY :: Int -- ^ turning "down" gives positive numbers , sensors :: Vector Int -- ^ EEG sensor values , qualities :: Vector Int -- ^ EEG sensor-to-skin connectivity- } deriving (Eq, Show, Generic)+ } deriving (Eq, Ord, Show, Generic) -- | Wraps (unencrypted) Emotiv raw data. Ensures that it is 32 bytes. newtype EmotivRawData = EmotivRawData { emotivRawDataBytes :: ByteString- } deriving (Eq, Show, Generic)+ } deriving (Eq, Ord, Show, Generic) -- | Treat a `ByteString` as Emotiv raw data.
src/Hemokit/Conduit.hs view
@@ -6,7 +6,9 @@ import Control.Monad.Trans import Data.Aeson (ToJSON (..), encode) import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BSL import Data.Conduit+import qualified Network.Simple.TCP as TCP import qualified Network.WebSockets as WS import Hemokit@@ -37,9 +39,30 @@ jsonConduit = awaitForever (yield . encode) +-- * TCP sockets++tcpSink :: (MonadIO m) => String -> Int -> Sink ByteString m ()+tcpSink host port = do+ chan <- liftIO $ newChan++ -- Server loop: Send what comes in via the chan; Nothing shuts down+ let jsonTCPServerFromChan :: (TCP.Socket, TCP.SockAddr) -> IO ()+ jsonTCPServerFromChan = \(sock, _remoteAddr) -> do+ void $ untilNothing (readChan chan) (TCP.send sock . BSL.toStrict)++ -- Fork off Websocket server+ _ <- liftIO $ forkIO $ TCP.withSocketsDo $ TCP.serve (TCP.Host host) (show port) jsonTCPServerFromChan++ -- Send messages to server via the chan+ void $ awaitForever (liftIO . writeChan chan . Just)++ -- Tell server to shut down+ liftIO $ writeChan chan Nothing++ -- * Websockets -websocketSink :: (MonadIO m, ToJSON a) => String -> Int -> Sink a m ()+websocketSink :: (MonadIO m) => String -> Int -> Sink ByteString m () websocketSink host port = do chan <- liftIO $ newChan @@ -47,7 +70,7 @@ let jsonWSServerFromChan :: WS.PendingConnection -> IO () jsonWSServerFromChan = \req -> do conn <- WS.acceptRequest req- void $ untilNothing (readChan chan) (WS.sendTextData conn . encode)+ void $ untilNothing (readChan chan) (WS.sendTextData conn) -- Fork off Websocket server _ <- liftIO $ forkIO $ WS.runServer host port jsonWSServerFromChan
src/Hemokit/Start.hs view
@@ -27,7 +27,7 @@ -- Also allows to pick a certain device. , fromFile :: Maybe FilePath -- ^ Use the given device or dump file for input. -- If not given, HIDAPI is used.- } deriving (Eq, Show)+ } deriving (Eq, Ord, Show) -- | EEG model command line parser.@@ -86,5 +86,7 @@ Nothing -> fail $ "No device with serial " ++ show s Just d -> Right <$> openEmotivDevice model d + -- TODO Do smarter auto detection, e.g. filter for Emotiv vendorIDs+ -- or the "EPOC BCI" product string. -- The user selected no serial, we just use the last device _ -> Right <$> openEmotivDevice model (last devices)