packages feed

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 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)