aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuebinm2023-03-11 01:36:35 +0100
committerstuebinm2023-03-11 01:37:54 +0100
commit7798666c81b390183e2e227232d936abf0cc4a65 (patch)
treea9ecbe352d7dc28faf7f74720022e27640edea5b
parent99463395ee9497256b794f4ad2c94b490ca5d0fd (diff)
simple on-board tools
these are just enough to send train positions to tracktrain with the current API, but are somewhat brittle (e.g. will fail if not restarted between trips, etc.)
Diffstat (limited to '')
-rw-r--r--lib/API.hs9
-rw-r--r--lib/Server.hs27
-rwxr-xr-xtools/obu-config59
-rwxr-xr-xtools/obu-guess-trip75
-rwxr-xr-xtools/obu-ping126
-rw-r--r--tools/obu-state.edn1
-rw-r--r--tools/other.edn1
-rw-r--r--tools/test.edn1
8 files changed, 289 insertions, 10 deletions
diff --git a/lib/API.hs b/lib/API.hs
index 79a467a..5bf9877 100644
--- a/lib/API.hs
+++ b/lib/API.hs
@@ -14,7 +14,8 @@ import Data.Swagger (MimeList (MimeList),
PathItem (_pathItemGet),
Scheme (Wss), Swagger,
ToSchema (..), _swaggerPaths,
- genericDeclareNamedSchema)
+ genericDeclareNamedSchema, type_,
+ NamedSchema(..), SwaggerType (SwaggerObject))
import Data.Swagger.ParamSchema (ToParamSchema (..))
import Data.Text (Text)
import Data.Time (Day, UTCTime)
@@ -31,7 +32,7 @@ import Servant.Swagger (HasSwagger (..))
import Web.Internal.FormUrlEncoded (Form)
import Control.Lens (At (at), (&), (?~))
-import Data.Aeson (FromJSON (..), genericParseJSON)
+import Data.Aeson (FromJSON (..), genericParseJSON, Value)
import Data.ByteString.Lazy (ByteString)
import Data.HashMap.Strict.InsOrd (singleton)
import GHC.Generics (Generic)
@@ -50,10 +51,14 @@ instance FromJSON RegisterJson where
parseJSON = genericParseJSON (aesonOptions "register")
instance ToSchema RegisterJson where
declareNamedSchema = genericDeclareNamedSchema (swaggerOptions "register")
+instance ToSchema Value where
+ declareNamedSchema _ = pure $ NamedSchema (Just "json") $ mempty
+ & type_ ?~ SwaggerObject
-- | The server's API (as it is actually intended).
type API = "stations" :> Get '[JSON] (Map StationID Station)
:<|> "timetable" :> Capture "Station ID" StationID :> QueryParam "day" Day :> Get '[JSON] (Map TripID (Trip Deep Deep))
+ :<|> "timetable" :> "stops" :> Capture "Date" Day :> Get '[JSON] Value
:<|> "trip" :> Capture "Trip ID" TripID :> Get '[JSON] (Trip Deep Deep)
-- ingress API (put this behind BasicAuth?)
-- TODO: perhaps require a first ping for registration?
diff --git a/lib/Server.hs b/lib/Server.hs
index 8d81127..7fdfd71 100644
--- a/lib/Server.hs
+++ b/lib/Server.hs
@@ -7,6 +7,7 @@
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE DataKinds #-}
-- Implementation of the API. This module is the main point of the program.
@@ -23,6 +24,7 @@ import Control.Monad.Logger (LoggingT, logWarnN)
import Control.Monad.Reader (forM)
import Control.Monad.Trans (lift)
import qualified Data.Aeson as A
+import Data.Aeson ((.=))
import qualified Data.ByteString.Char8 as C8
import Data.Coerce (coerce)
import Data.Functor ((<&>))
@@ -85,7 +87,7 @@ doMigration pool = runSql pool $
server :: GTFS -> Metrics -> TVar (M.Map TripID [TQueue (Maybe TrainPing)]) -> Pool SqlBackend -> ServerConfig -> Service CompleteAPI
server gtfs@GTFS{..} Metrics{..} subscribers dbpool settings = handleDebugAPI
- :<|> (handleStations :<|> handleTimetable :<|> handleTrip
+ :<|> (handleStations :<|> handleTimetable :<|> handleTimetableStops :<|> handleTrip
:<|> handleRegister :<|> handleTrainPing (throwError err401) :<|> handleWS
:<|> handleSubscribe :<|> handleDebugState :<|> handleDebugTrain
:<|> handleDebugRegister :<|> gtfsRealtimeServer gtfs dbpool)
@@ -93,13 +95,22 @@ server gtfs@GTFS{..} Metrics{..} subscribers dbpool settings = handleDebugAPI
:<|> serveDirectoryFileServer (serverConfigAssets settings)
:<|> pure (unsafePerformIO (toWaiAppPlain (ControlRoom gtfs dbpool settings)))
where handleStations = pure stations
- handleTimetable station maybeDay = do
- -- TODO: resolve "overlay" trips (perhaps just additional CalendarDates?)
- day <- liftIO $ maybeM (getCurrentTime <&> utctDay) pure (pure maybeDay)
- pure
- -- don't send stations ending at this station
- . M.filter ((==) station . stationId . stopStation . V.last . tripStops)
- $ tripsOnDay gtfs day
+ handleTimetable station maybeDay =
+ M.filter isLastStop . tripsOnDay gtfs <$> liftIO day
+ where isLastStop = (==) station . stationId . stopStation . V.last . tripStops
+ day = maybeM (getCurrentTime <&> utctDay) pure (pure maybeDay)
+ handleTimetableStops day =
+ pure . A.toJSON . fmap mkJson . M.elems $ tripsOnDay gtfs day
+ where mkJson :: Trip Deep Deep -> A.Value
+ mkJson Trip {..} = A.object
+ [ "trip" .= tripTripID
+ , "stops" .= fmap (\Stop{..} -> A.object
+ [ "departure" .= stopDeparture
+ , "station" .= stationId stopStation
+ , "lat" .= stationLat stopStation
+ , "lon" .= stationLon stopStation
+ ]) tripStops
+ ]
handleTrip trip = case M.lookup trip trips of
Just res -> pure res
Nothing -> throwError err404
diff --git a/tools/obu-config b/tools/obu-config
new file mode 100755
index 0000000..0cce2ee
--- /dev/null
+++ b/tools/obu-config
@@ -0,0 +1,59 @@
+#!/usr/bin/env gosh
+
+(use text.edn)
+(use file.util)
+(use scheme.mapping.hash)
+(use gauche.parseopt)
+
+(define (show-help progname)
+ (display (format
+"~a: tracktrain's onboard config-and-state manager.
+
+Usage: ~a [options] key [value]
+
+This is a very simple key-value store. Give both to set,
+give just the key to look something up
+
+Options:
+ -s --statefile: state file
+ -h --help: display this help
+" progname))
+ (exit 0))
+
+(define (main args)
+ (let-args
+ (cdr args)
+ ((statefile "s|state=s")
+ (help "h|help" => (cut show-help (car args)))
+ . restargs)
+
+ (define file
+ (if (equal? statefile #f)
+ "./obu-state.edn"
+ statefile))
+
+ (if (= (length restargs) 2)
+ (set file
+ (list-ref restargs 0)
+ (list-ref restargs 1))
+ (display
+ (get file
+ (list-ref restargs 0))))
+ (exit 0)))
+
+(define (set file key value)
+ (define data
+ (if (file-exists? file)
+ (call-with-input-file file parse-edn)
+ (edn-map)))
+ (define data2
+ (hashmap-set data (string->symbol key) value))
+ (call-with-output-file file
+ (cut construct-edn data2 <>)))
+
+(define (get file key)
+ (if (file-exists? file)
+ (hashmap-ref
+ (call-with-input-file file parse-edn)
+ (string->symbol key))
+ #f))
diff --git a/tools/obu-guess-trip b/tools/obu-guess-trip
new file mode 100755
index 0000000..478ba2b
--- /dev/null
+++ b/tools/obu-guess-trip
@@ -0,0 +1,75 @@
+#!/usr/bin/env gosh
+
+(use rfc.http)
+(use rfc.uri)
+(use rfc.json)
+(use file.util)
+(use srfi-19)
+(use gauche.collection)
+(use gauche.parseopt)
+(use gauche.process)
+
+
+(define (main args)
+ (let-args
+ (cdr args)
+ ((adir "d|dir=s")
+ (baseurl "u|url=s")
+ (statefile "s|statefile=s")
+ (help "h|help" => (cut show-help (car args)))
+ . restargs)
+ (if (equal? baseurl #f)
+ (show-help (car args))
+ (guesstimate
+ (if (equal? adir #f) "." adir)
+ baseurl
+ (if statefile statefile "./obu-state.edn")))
+ (exit 0)))
+
+
+(define (show-help progname)
+ (display (format
+"~a: tracktrain's on-board guesstimater for which trip you're on
+
+Arguments:
+ -u --url: base url of the tracktrain server [required]
+ -s --statefile: state file
+ -c --config: config file (default is /etc/tracktrain/obu.conf)
+ -h --help: display this help
+" progname))
+ (exit 0))
+
+(define (guesstimate dir url statefile)
+ (define stops (fetch-stations url))
+ (define pos
+ (with-input-from-process `(obu-ping -s ,statefile -n 1 -d) read))
+ (define trip (assoc-ref (closest-stop-to stops pos) 'trip))
+ (display trip))
+
+(define (closest-stop-to stops pos)
+ (define stops-by-station
+ (apply vector-append (vector->list
+ (vector-map (lambda (trip)
+ (vector-map
+ (lambda (stop) (cons `(trip . ,(assoc-ref trip 'trip)) stop))
+ (assoc-ref trip 'stops)))
+ stops))))
+ (find-min
+ stops-by-station
+ :key (lambda (stop)
+ (+ (square (- (assoc-ref pos 'lat) (assoc-ref stop 'lat)))
+ (square (- (assoc-ref pos 'lon) (assoc-ref stop 'lon)))))
+ :default (vector-ref stops 0)))
+
+(define (fetch-stations url)
+ (define day (date->string (current-date) "~1"))
+ (define tls
+ (equal? (uri-ref url 'scheme) 'https))
+ (parameterize
+ ; replace all json keys with symbols; everything else is confusing
+ ([json-object-handler
+ (cut map (lambda p `(,(string->symbol (car (car p))) . ,(cdr (car p)))) <>)])
+ (parse-json-string
+ (values-ref (http-get (uri-ref url 'host+port)
+ (format "/api/timetable/stops/~a" day)
+ :secure tls) 2))))
diff --git a/tools/obu-ping b/tools/obu-ping
new file mode 100755
index 0000000..ee75402
--- /dev/null
+++ b/tools/obu-ping
@@ -0,0 +1,126 @@
+#!/usr/bin/env gosh
+
+(use rfc.json)
+(use rfc.uri)
+(use rfc.http)
+(use srfi-19)
+(use file.util)
+(use util.match)
+(use gauche.parseopt)
+(use gauche.process)
+(use text.edn)
+(use scheme.mapping.hash)
+
+(define (show-help progname)
+ (display (format
+"~a: tracktrain's on-board reporting thing. Excepts input to be
+piped from gpsd's `gpspipe --json` onto stdin.
+
+Arguments:
+ -u --url: base url of the tracktrain server [required]
+ -n: number of positions to fetch (default is to loop forever)
+ -d --no-ping: just print the positions, don't send anything
+ -s --statefile: state file
+ -c --config: config file (default is /etc/tracktrain/obu.conf)
+ -h --help: display this help
+" progname))
+ (exit 0))
+
+(define (main args)
+ (let-args
+ (cdr args)
+ ((statefile "s|state=s")
+ (baseurl "u|url=s")
+ (n "n=i")
+ (no-ping "d|no-ping")
+ (help "h|help" => (cut show-help (car args)))
+ . restargs)
+ (if (and (equal? baseurl #f) (equal? no-ping #f))
+ (show-help (car args))
+ (with-input-from-process
+ '(gpspipe --json)
+ (cut report-position
+ (if (equal? statefile #f) "./obu-state.edn" statefile)
+ baseurl
+ n
+ no-ping)
+ :on-abnormal-exit :ignore))
+ (exit 0)))
+
+
+(define (try-get-current-pos)
+ (define json
+ (parameterize
+ ; replace all json keys with symbols; everything else is confusing
+ ([json-object-handler
+ (cut map (lambda p `(,(string->symbol (car (car p))) . ,(cdr (car p)))) <>)])
+ (parse-json)))
+ (define class
+ (assoc-ref json 'class))
+ (match class
+ ("TPV" `((lat . ,(assoc-ref json 'lat))
+ (lon . ,(assoc-ref json 'lon))
+ (time . ,(assoc-ref json 'time))))
+ (_ (try-get-current-pos))))
+
+(define json-symbol-key
+ (cut map (lambda p `(,(string->symbol (car (car p))) . ,(cdr (car p)))) <>))
+
+(define (report-position statefile url n no-ping)
+ (define trip
+ (if no-ping #f
+ (process-output->string `(obu-guess-trip -u ,url))))
+ (define token
+ (if no-ping #f (get-token url trip statefile)))
+ (if n
+ (dotimes (i n)
+ (ping-pos statefile url token))
+ (while #t
+ (ping-pos statefile url token))))
+
+(define (ping-pos dir url token)
+ (let ([pos (try-get-current-pos)])
+ (display (format "~a\n" pos))
+ (if (not (equal? token #f))
+ (display (format "server: ~a\n" (ping url token pos))))))
+
+(define (get-token url trip statefile)
+ (define cached?
+ (with-input-from-process `(obu-config -s ,statefile token) read))
+ (if cached?
+ (symbol->string cached?)
+ (let* ([token (fetch-token url trip)])
+ (do-process `(obu-config -s ,statefile token ,token))
+ token)))
+
+(define (fetch-token url trip)
+ (define tls
+ (equal? (uri-ref url 'scheme) 'https))
+ (display (format "fetching new token for trip ~a…\n" trip))
+ (parameterize
+ ; replace all json keys with symbols; everything else is confusing
+ ([json-object-handler json-symbol-key])
+ (parse-json-string
+ (values-ref (http-post
+ (uri-ref url 'host+port)
+ (format "/api/train/register/~a" trip)
+ (construct-json-string '((agent . "onboard-unit")))
+ :content-type "application/json"
+ :secure tls)
+ 2))))
+
+(define (ping url token pos)
+ (define tls
+ (equal? (uri-ref url 'scheme) 'https))
+ (parameterize ([json-object-handler json-symbol-key])
+ (parse-json-string
+ (values-ref (http-post
+ (uri-ref url 'host+port)
+ "/api/train/ping"
+ (construct-json-string `((token . ,token)
+ (lat . ,(assoc-ref pos 'lat))
+ (long . ,(assoc-ref pos 'lon))
+ (timestamp . ,(assoc-ref pos 'time))))
+ :content-type "application/json"
+ :secure tls)
+ 2))))
diff --git a/tools/obu-state.edn b/tools/obu-state.edn
new file mode 100644
index 0000000..db989c8
--- /dev/null
+++ b/tools/obu-state.edn
@@ -0,0 +1 @@
+{token "5ab95c26-367e-40fc-8d3e-2956af6f61e4"} \ No newline at end of file
diff --git a/tools/other.edn b/tools/other.edn
new file mode 100644
index 0000000..b4b44d4
--- /dev/null
+++ b/tools/other.edn
@@ -0,0 +1 @@
+{a "b", c "b"} \ No newline at end of file
diff --git a/tools/test.edn b/tools/test.edn
new file mode 100644
index 0000000..aa0da75
--- /dev/null
+++ b/tools/test.edn
@@ -0,0 +1 @@
+{token "string"}