├── .gitignore ├── Dropline.cabal ├── LICENSE ├── README.md ├── Setup.hs └── src ├── Dropline.hs └── Dropline ├── Kismet.hs ├── Server.hs ├── Tracker.hs ├── Util.hs └── Wifi.hs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.o 3 | *.hi 4 | dist/ 5 | .cabal-sandbox/ 6 | cabal.sandbox.config 7 | -------------------------------------------------------------------------------- /Dropline.cabal: -------------------------------------------------------------------------------- 1 | -- Initial Dropline.cabal generated by cabal init. For further 2 | -- documentation, see http://haskell.org/cabal/users-guide/ 3 | 4 | name: Dropline 5 | version: 0.1.0.0 6 | synopsis: A tool for monitoring how busy an area is based on Wi-Fi signals 7 | description: Dropline is a tool for estimating how busy an area is based on the number of unique Wi-Fi MACs in range (as well as their signal strengths). It uses Kismet to monitor the aether and Happstack to present data to users via a web interface. 8 | license: Apache-2.0 9 | license-file: LICENSE 10 | author: Will Yager 11 | maintainer: will.yager@gmail.com 12 | copyright: (c) 2015 Dropbox, Inc. 13 | category: Network 14 | build-type: Simple 15 | -- extra-source-files: 16 | cabal-version: >=1.10 17 | 18 | executable Dropline 19 | main-is: Dropline.hs 20 | -- other-modules: 21 | other-extensions: OverloadedStrings 22 | build-depends: base >=4.5 && <4.9, network >=2.6 && <2.7, stm >=2.4 && <2.5, bytestring >=0.10 && <0.11, happstack-server >=7.4 && <7.5, blaze-html >=0.8 && <0.9, mtl >=2.2 && <2.3, containers >=0.5 && <0.6, time >=1.5 && <1.6 23 | hs-source-dirs: src 24 | default-language: Haskell2010 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Dropbox, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A simple utility to estimate how busy an area is using wifi signals. 2 | Built for Dropbox's Hack Week 2015. 3 | Inspired by long lines at the Tuck Shop. 4 | 5 | It connects to a Kismet server to gather Wi-Fi data. 6 | 7 | It processes this data and keeps track of all MAC addresses seen in the last 5 minutes, as well as their respective signal strengths. 8 | 9 | It then presents this data over HTTP. 10 | 11 | `http://server/` shows a human-friendly signal strength indicator. 12 | 13 | `http://server/raw` shows a list of RSSIs and how long they've been visible (without their associated MAC addresses). 14 | 15 | To install, 16 | 17 | cd Dropline 18 | cabal install 19 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /src/Dropline.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | import Dropline.Kismet (connect) 4 | import Dropline.Tracker (track) 5 | import Dropline.Server (serve) 6 | 7 | main = do 8 | signals <- connect 9 | visible <- track signals 10 | serve visible 11 | -------------------------------------------------------------------------------- /src/Dropline/Kismet.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} -- Allows for bytestring literals 2 | 3 | module Dropline.Kismet (connect) where 4 | 5 | import qualified Network as Net 6 | import Control.Concurrent (forkIO) 7 | import Control.Concurrent.STM (TChan, newTChan, writeTChan, atomically) 8 | import Data.ByteString.Char8 (ByteString, 9 | hGetLine, hPut, isPrefixOf, split, unpack) 10 | import Control.Monad (forever, when) 11 | import Dropline.Wifi (Signal(..), MAC(..), RSSI(..)) 12 | 13 | connect :: IO (TChan Signal) 14 | connect = do 15 | kismet <- Net.connectTo "localhost" (Net.PortNumber 2501) 16 | hPut kismet "!0 REMOVE TIME\n" -- Stop sending heartbeats 17 | hPut kismet "!0 ENABLE CLIENT mac,signal_dbm\n" -- Subscribe to MAC data 18 | chan <- atomically newTChan 19 | forkIO $ forever $ do 20 | message <- hGetLine kismet 21 | when ("*CLIENT:" `isPrefixOf` message) (send message chan) 22 | return chan 23 | 24 | send :: ByteString -> TChan Signal -> IO () 25 | send message chan = do 26 | let [_, addr, rssi, _] = split ' ' message 27 | let signal = Signal (MAC addr) (RSSI $ read $ unpack rssi) 28 | atomically (writeTChan chan signal) 29 | -------------------------------------------------------------------------------- /src/Dropline/Server.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} -- For HTML literals 2 | 3 | module Dropline.Server (serve) where 4 | 5 | import Dropline.Tracker (Statuses, Status(..)) 6 | import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) 7 | import Dropline.Wifi (RSSI(..)) 8 | import Control.Concurrent.STM (TVar, readTVar, atomically) 9 | import Happstack.Server (Response) 10 | import Happstack.Server.Routing (dir) 11 | import Happstack.Server.SimpleHTTP (ServerPart, 12 | simpleHTTP, nullConf, port, toResponse, ok) 13 | import Text.Blaze.Html5 as H (html, head, title, body, p, h1, (!)) 14 | import Text.Blaze.Html5.Attributes as A (style) 15 | import Control.Applicative ((<|>)) 16 | import Control.Monad.Trans (liftIO) 17 | import Data.Map.Strict (elems) 18 | import Data.List (sort) 19 | 20 | serve :: TVar Statuses -> IO () 21 | serve signals = do 22 | let conf = nullConf { port = 80 } 23 | let rssis = process signals 24 | simpleHTTP conf (raw rssis <|> friendly rssis) 25 | 26 | raw :: ServerPart [(RSSI, POSIXTime)] -> ServerPart Response 27 | raw rssis = dir "raw" $ do 28 | signalData <- rssis 29 | ok $ toResponse $ show $ sort signalData 30 | 31 | friendly :: ServerPart [(RSSI, POSIXTime)] -> ServerPart Response 32 | friendly rssis = do 33 | signalData <- rssis 34 | let busyScore = round $ sum $ map score signalData 35 | let busyness | busyScore < 20 = "Not busy" 36 | | busyScore < 50 = "A little busy" 37 | | busyScore < 100 = "Busy" 38 | | otherwise = "Very busy" 39 | ok $ toResponse $ H.html $ do 40 | H.head $ do 41 | H.title "Dropline busy-o-meter" 42 | H.body $ do 43 | let center x = x ! A.style "text-align:center" 44 | center $ H.p $ "It is" 45 | center $ H.h1 $ busyness 46 | 47 | process :: TVar Statuses -> ServerPart [(RSSI, POSIXTime)] 48 | process signals = do 49 | signals' <- liftIO . atomically . readTVar $ signals 50 | time <- liftIO getPOSIXTime 51 | let format (Status rssi firstSeen lastSeen) = (rssi, time - firstSeen) 52 | return $ map format $ elems signals' 53 | 54 | score :: (RSSI, POSIXTime) -> Double 55 | -- This algorithm depends entirely on your wifi card and environment. 56 | score (RSSI signal, duration) = timeScore * signalScore / 7 57 | where 58 | -- Starts to ignore signals around 2 or more hours old 59 | timeScore :: Double 60 | timeScore = (1/) . (1+) . exp . (/200) $ realToFrac (duration - 2*60*60) 61 | -- Starts to ignore signals below around 50 dBm 62 | signalScore :: Double 63 | signalScore = (1/) . (1+) . exp . (/10) $ realToFrac (negate signal - 50) -------------------------------------------------------------------------------- /src/Dropline/Tracker.hs: -------------------------------------------------------------------------------- 1 | module Dropline.Tracker (track, Statuses, Status(..)) where 2 | 3 | import Dropline.Wifi (Signal(..), MAC, RSSI) 4 | import Dropline.Util (periodically) 5 | import Control.Concurrent (forkIO) 6 | import Control.Concurrent.STM (TChan, readTChan, atomically, 7 | TVar, newTVar, modifyTVar, readTVar, writeTVar) 8 | import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) 9 | import Data.Map.Strict (Map) 10 | import qualified Data.Map.Strict as Map 11 | import Data.Sequence (Seq, (<|)) 12 | import qualified Data.Sequence as Seq 13 | import Control.Monad (forever) 14 | import Data.Foldable (foldl') 15 | 16 | -- A map from a MAC address to its signal strength and when it was last seen 17 | data Status = Status {rssi :: RSSI, 18 | firstSeen :: POSIXTime, 19 | lastSeen :: POSIXTime } 20 | type Statuses = Map MAC Status 21 | type DeleteQueue = Seq (MAC, POSIXTime) 22 | 23 | track :: TChan Signal -> IO (TVar Statuses) 24 | track signalStream = do 25 | statuses <- atomically (newTVar Map.empty) 26 | deleteQueue <- atomically (newTVar Seq.empty) 27 | forkIO $ process signalStream statuses deleteQueue 28 | forkIO $ periodically (reap statuses deleteQueue) 29 | return statuses 30 | 31 | process :: TChan Signal -> TVar Statuses -> TVar DeleteQueue -> IO () 32 | process signals statuses deleteQueue = forever $ do 33 | Signal mac rssi <- atomically (readTChan signals) 34 | time <- getPOSIXTime 35 | let status' = Status rssi time time 36 | atomically $ modifyTVar statuses $ Map.insertWith update mac status' 37 | atomically $ modifyTVar deleteQueue ((mac, time) <|) 38 | where 39 | update (Status _ firstSeen _) (Status rssi _ lastSeen) = 40 | Status rssi firstSeen lastSeen 41 | 42 | reap :: TVar Statuses -> TVar DeleteQueue -> IO () 43 | reap signals deleteQueue = do 44 | currentTime <- getPOSIXTime 45 | -- If a signal was recorded over 5 minutes ago, we should try to delete 46 | let old = currentTime - 300.0 47 | atomically $ do 48 | dq <- readTVar deleteQueue 49 | sigs <- readTVar signals 50 | let isOld (mac, time) = time <= old 51 | let (expired, unexpired) = Seq.spanr isOld dq 52 | let sigs' = foldl' remove sigs expired 53 | writeTVar deleteQueue unexpired 54 | writeTVar signals sigs' 55 | where 56 | remove sigs (mac, time) = case Map.lookup mac sigs of 57 | Just (Status _ _ rxTime) | rxTime == time -> Map.delete mac sigs 58 | _ -> sigs 59 | -------------------------------------------------------------------------------- /src/Dropline/Util.hs: -------------------------------------------------------------------------------- 1 | module Dropline.Util (periodically) where 2 | 3 | import Control.Monad (forever) 4 | import Control.Concurrent (threadDelay) 5 | 6 | periodically :: IO a -> IO b 7 | periodically act = forever (act >> threadDelay (10^6)) -------------------------------------------------------------------------------- /src/Dropline/Wifi.hs: -------------------------------------------------------------------------------- 1 | module Dropline.Wifi (Signal(..), MAC(..), RSSI(..)) where 2 | 3 | import Data.ByteString (ByteString) 4 | 5 | data Signal = Signal MAC RSSI deriving (Show) 6 | 7 | newtype RSSI = RSSI Int deriving (Eq, Ord, Show) 8 | 9 | newtype MAC = MAC ByteString deriving (Eq, Ord, Show) 10 | --------------------------------------------------------------------------------