diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2016 Brian W Bush
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/ReadMe.md b/ReadMe.md
new file mode 100644
--- /dev/null
+++ b/ReadMe.md
@@ -0,0 +1,14 @@
+Linux joystick events via a Kafka message broker
+================================================
+
+This package contains functions for passing [Linux joystick](https://www.kernel.org/doc/Documentation/input/joystick-api.txt) events to topics on a [Kafka message broker](https://kafka.apache.org/).
+
+
+Clients
+-------
+
+The simple Kafka client that produces events from the joystick can be run, for example, as follows:
+
+	cabal run kafka-device-joystick -- /dev/input/js0 joystick-client localhost 9092 events joystick
+
+Also see https://hackage.haskell.org/package/kafka-device/.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/kafka-device-joystick.cabal b/kafka-device-joystick.cabal
new file mode 100644
--- /dev/null
+++ b/kafka-device-joystick.cabal
@@ -0,0 +1,49 @@
+name:                kafka-device-joystick
+version:             0.1.5.0
+synopsis:            Linux joystick events via a Kafka message broker
+description:         This package contains functions for passing Linux joystick events to topics on a Kafka message broker \<<https://kafka.apache.org/>\>.  The joystick's driver must conform to the Linux Joystick API \<<https://www.kernel.org/doc/Documentation/input/joystick-api.txt>\>.  Also see \<<https://hackage.haskell.org/package/kafka-device/>\>.
+license:             MIT
+license-file:        LICENSE
+author:              Brian W Bush <consult@brianwbush.info>
+maintainer:          Brian W Bush <consult@brianwbush.info>
+copyright:           (c) 2016 Brian W Bush
+category:            Hardware
+build-type:          Simple
+cabal-version:       >= 1.10
+stability:           Experimental
+homepage:            https://bitbucket.org/functionally/kafka-device-joystick
+bug-reports:         https://bwbush.atlassian.net/projects/HKAFDEV/issues/
+package-url:         https://bitbucket.org/functionally/kafka-device-joystick/downloads/kafka-device-joystick-0.1.5.0.tar.gz
+
+extra-source-files:  ReadMe.md
+
+source-repository head
+  type: git
+  location: https://bitbucket.org/functionally/kafka-device-joystick
+ 
+library
+  exposed-modules:  Network.UI.Kafka.Joystick
+                    System.Hardware.Linux.Joystick
+  build-depends:    base         >= 4.8 && < 5
+               ,    aeson
+               ,    binary
+               ,    bytestring
+               ,    cereal
+               ,    kafka-device
+               ,    milena
+  hs-source-dirs:   src
+  ghc-options:      -Wall
+  default-language: Haskell2010
+
+executable kafka-device-joystick
+  main-is:          Main.hs
+  build-depends:    base
+               ,    aeson
+               ,    binary
+               ,    bytestring
+               ,    cereal
+               ,    kafka-device
+               ,    milena
+  hs-source-dirs:   src
+  ghc-options:      -Wall
+  default-language: Haskell2010
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,46 @@
+{-|
+Module      :  Main
+Copyright   :  (c) 2016 Brian W Bush
+License     :  MIT
+Maintainer  :  Brian W Bush <consult@brianwbush.info>
+Stability   :  Experimental
+Portability :  Linux
+
+Produce events for a Kafka topic from a Linux joystick.
+-}
+
+
+module Main (
+-- * Main entry
+  main
+) where
+
+
+import Data.String (IsString(fromString))
+import Network.UI.Kafka.Joystick (joystickLoop)
+import System.Environment (getArgs)
+
+
+-- The main action.
+main :: IO ()
+main =
+  do
+    args <- getArgs
+    case args of
+      [device, client, host, port, topic, sensor] ->
+        do
+          putStrLn $ "Device:        " ++ device
+          putStrLn $ "Kafka client:  " ++ client
+          putStrLn $ "Kafka address: (" ++ host ++ "," ++ port ++ ")"
+          putStrLn $ "Kafka topic:   " ++ topic
+          putStrLn $ "Sensor name:   " ++ sensor
+          (_, loop) <-
+            joystickLoop
+              device
+              (fromString client)
+              (fromString host, toEnum $ read port)
+              (fromString topic)
+              sensor
+          result <- loop
+          either print return result
+      _ -> putStrLn "USAGE: kafka-device-joystick device client host port topic sensor"
diff --git a/src/Network/UI/Kafka/Joystick.hs b/src/Network/UI/Kafka/Joystick.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/UI/Kafka/Joystick.hs
@@ -0,0 +1,71 @@
+{-|
+Module      :  Network.UI.Kafka.Joystick
+Copyright   :  (c) 2016 Brian W Bush
+License     :  MIT
+Maintainer  :  Brian W Bush <consult@brianwbush.info>
+Stability   :  Experimental
+Portability :  Linux
+
+Produce events for a Kafka topic from a Linux joystick, which must conform to the Linux Joystick API \<<https://www.kernel.org/doc/Documentation/input/joystick-api.txt>\>.
+-}
+
+
+{-# LANGUAGE RecordWildCards #-}
+
+
+module Network.UI.Kafka.Joystick (
+-- * Event handling
+  joystickLoop
+) where
+
+
+import Control.Monad (guard)
+import Data.ByteString.Lazy.Char8 (hGet)
+import Data.Maybe (catMaybes)
+import Network.Kafka (KafkaAddress, KafkaClientId)
+import Network.Kafka.Protocol (TopicName)
+import Network.UI.Kafka (ExitAction, LoopAction, Sensor, producerLoop)
+import Network.UI.Kafka.Types (Button(..), Event(..), Toggle(..))
+import System.Hardware.Linux.Joystick (Joystick(..), byteLength, interpretJoystick, maxValue)
+import System.IO (IOMode(ReadMode), hClose, openFile)
+
+
+-- | Produce events for a Kafka topic from a Linux Joystick.
+joystickLoop :: FilePath                    -- ^ The path to the joystick device, e.g. "\/dev\/input\/js0".
+             -> KafkaClientId               -- ^ A Kafka client identifier for the producer.
+             -> KafkaAddress                -- ^ The address of the Kafka broker.
+             -> TopicName                   -- ^ The Kafka topic name.
+             -> Sensor                      -- ^ The name of the sensor producing events.
+             -> IO (ExitAction, LoopAction) -- ^ Action to create the exit and loop actions.
+joystickLoop path client address topic sensor =
+  do
+    joystick <- openFile path ReadMode
+    (exit, loop) <-
+      producerLoop client address topic sensor
+        $ translate
+        . interpretJoystick
+        <$> hGet joystick byteLength
+    return
+      (
+        do
+          exit
+          hClose joystick
+      , loop
+      )
+
+
+-- | Translate a Linux Joystick event into events for Kafka.
+translate :: Joystick -- ^ The joystick event.
+          -> [Event]  -- ^ The corresponding events for Kafka.
+translate Joystick{..} =
+  catMaybes
+    [
+      do
+        guard button
+        return
+          $ ButtonEvent (IndexButton number, if value == 0 then Up else Down)
+    , do
+        guard axis
+        return
+          $ AnalogEvent number (fromIntegral value / fromIntegral maxValue)
+    ]
diff --git a/src/System/Hardware/Linux/Joystick.hs b/src/System/Hardware/Linux/Joystick.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Hardware/Linux/Joystick.hs
@@ -0,0 +1,149 @@
+{-|
+Module      :  System.Hardware.Linux.Joystick
+Copyright   :  (c) 2016 Brian W Bush
+License     :  MIT
+Maintainer  :  Brian W Bush <consult@brianwbush.info>
+Stability   :  Experimental
+Portability :  Linux
+
+Interpret events from a Linux joystick, which must conform to the Linux Joystick API \<<https://www.kernel.org/doc/Documentation/input/joystick-api.txt>\>.
+-}
+
+
+{-# LANGUAGE DeriveGeneric   #-}
+{-# LANGUAGE RecordWildCards #-}
+
+
+module System.Hardware.Linux.Joystick (
+-- * Types and values
+  Joystick(..)
+, minValue
+, maxValue
+, byteLength
+-- * Event handling
+, interpretJoystick
+, readJoystick
+) where
+
+
+import Data.Aeson.Types (FromJSON, ToJSON)
+import Data.Binary (Binary(..), decode, encode)
+import Data.Binary.Get (getWord16host, getWord32host)
+import Data.Binary.Put (putWord16host, putWord32host)
+import Data.Bits ((.&.), complement, shift)
+import Data.ByteString.Lazy.Char8 as BS (ByteString, length, readFile, splitAt)
+import Data.Serialize (Serialize)
+import Data.Word (Word8, Word16, Word32)
+import GHC.Generics (Generic)
+
+
+-- | The minimum for 'value'.
+minValue :: Int
+minValue = - maxValue
+
+
+-- | The maximum for 'value'.
+maxValue :: Int
+maxValue = 1 `shift` 15 - 1
+
+
+-- | The number of bytes in a joystick event.
+byteLength :: Integral a => a
+byteLength =
+  fromIntegral
+    . BS.length
+    . encode
+    $ RawJoystick 0 0 0 0
+
+
+
+data RawJoystick =
+  RawJoystick
+  {
+    rawTime :: Word32
+  , rawValue :: Word16
+  , rawType  :: Word8
+  , rawNumber :: Word8
+  }
+  deriving (Eq, Ord, Read, Show)
+
+instance Binary RawJoystick where
+  get =
+    do
+      rawTime   <- getWord32host
+      rawValue  <- getWord16host
+      rawType   <- get
+      rawNumber <- get
+      return RawJoystick{..}
+  put RawJoystick{..} =
+    do
+      putWord32host rawTime
+      putWord16host rawValue
+      put rawType
+      put rawNumber
+
+
+-- | Joystick data. See \<<https://www.kernel.org/doc/Documentation/input/joystick-api.txt>\> for details.
+data Joystick =
+  Joystick
+  {
+    timestamp :: Int  -- ^ The event timestamp in milliseconds.
+  , value     :: Int  -- ^ The data value.
+  , number    :: Int  -- ^ The button or axis number.
+  , button    :: Bool -- ^ Whether the button is being reported.
+  , axis      :: Bool -- ^ Whether the axis is being reported.
+  , initial   :: Bool -- ^ Whether the initial value is being reported.
+  }
+    deriving (Eq, Generic, Ord, Read, Show)
+
+instance FromJSON Joystick
+
+instance ToJSON Joystick
+
+instance Binary Joystick
+
+instance Serialize Joystick
+
+
+-- | Interpret Linux Joystick bytes accoding to \<<https://www.kernel.org/doc/Documentation/input/joystick-api.txt>\>.
+interpretJoystick :: ByteString -- ^ The eight bytes.
+                  -> Joystick   -- ^ The corresponding joystick data.
+interpretJoystick x =
+  let
+    RawJoystick{..} = decode x
+    timestamp = fromIntegral rawTime
+    value = twosComplement rawValue
+    number = fromIntegral rawNumber
+    typ = fromIntegral rawType :: Int
+    button = 0x01 .&. typ /= 0
+    axis = 0x02 .&. typ /= 0
+    initial = 0x80 .&. typ /= 0
+  in
+    Joystick{..}
+
+
+-- | Decode a two's complement.
+twosComplement :: Word16 -- ^ The two's complement.
+               -> Int    -- ^ The corresponding integer.
+twosComplement x =
+  fromIntegral (x' .&. complement mask) - fromIntegral (x' .&. mask)
+    where
+      x' = fromIntegral x :: Int
+      mask = 1 `shift` 15
+
+
+-- | Read a stream of joystick data.
+readJoystick :: FilePath      -- ^ The joystick device, e.g., "\/dev\/input\/js0".
+             -> IO [Joystick] -- ^ Action to read the joystick data.
+readJoystick path =
+  let
+    chunks :: ByteString -> [ByteString]
+    chunks x =
+      let
+        (y, ys) = BS.splitAt 8 x
+      in
+        y : chunks ys
+  in
+    map interpretJoystick
+      . chunks 
+      <$> BS.readFile path
