; (local { : view } (require :fennel)) (local { : fdopen } (require :posix.stdio)) (local nmea (require :nmea)) (import-macros { : define-tests : expect : expect= } :assert) (local { : Gtk : OsmGpsMap : Gdk : Gio : GLib } (require :lgi)) (local CSS " label.readout { font: 48px \"Noto Sans\"; margin: 10px; padding: 5px; background-color: rgba(0, 0, 0, 0.2); } ") (local utc-offset (let [now (os.time) localt (os.date "*t" now) utct (os.date "!*t" now)] (tset localt :isdst false) (os.difftime (os.time localt) (os.time utct)))) (fn styles [] (let [style_provider (Gtk.CssProvider)] (Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default) style_provider Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) (style_provider:load_from_data CSS))) (local window (Gtk.Window { :title "Map" :name "toplevel" :default_width 720 :default_height 800 :on_destroy Gtk.main_quit })) (local state-widgets { }) (fn osm-widget [] (let [w (doto (OsmGpsMap.Map {}) (tset :map-source OsmGpsMap.MapSource_t.OPENSTREETMAP) (: :set_center_and_zoom 52.595 -0.1 17) (: :layer_add (OsmGpsMap.MapOsd { :show_copyright true ; :show_coordinates true :show_scale true })) )] (tset state-widgets :osm w) w)) (fn readout [name text] (let [w (doto (Gtk.Label {:label text : name}) (-> (: :get_style_context) (: :add_class :readout)))] (tset state-widgets name w) w)) (local knot-in-m-s (/ 1852 ; metres in nautical mile 3600 ; seconds in an hour )) (fn hhmmss [seconds-since-midnight] (let [s (% seconds-since-midnight 60) m (% (// (- seconds-since-midnight s) 60) 60) h (// (- seconds-since-midnight (* m 60) s) 3600)] (string.format "%d:%02d:%02d" h m s))) (expect= (hhmmss (+ 45 (* 60 12) (* 60 60 3))) "3:12:45") (local app-state { :time-of-day 0 :elapsed-time 0 :speed 14 :lat 49 :lon 0 :course 22 } ) (fn merge [table1 table2] (collect [k v (pairs table2) &into table1] k v)) (fn update-app-state [new-vals] (merge app-state new-vals) (each [name widget (pairs state-widgets)] (case name :speed (widget:set_label (string.format "%.1f km/h" (* app-state.speed 3.6))) :osm (widget:set_center app-state.lat app-state.lon) :time (widget:set_label (hhmmss (+ utc-offset app-state.time-of-day))) ))) (fn readouts [] (doto (Gtk.Box { :orientation Gtk.Orientation.VERTICAL :halign Gtk.Align.END }) (-> (: :get_style_context) (: :add_class :readouts)) (: :add (readout :time "")) (: :add (readout :elapsed-time "")) (: :add (readout :speed "0")))) (fn arrow [] (let [height 40 w (Gtk.Label { :halign Gtk.Align.CENTER :valign Gtk.Align.CENTER :width height :height height :on_draw (fn [self g] (g:set_source_rgb 0.4 0.0 0.1) (g:translate (// height 2) (// height 2)) (g:rotate (/ (* -2 app-state.course math.pi) 360) ) (g:translate (// height -2) (// height -2)) (g:set_line_width 4) (g:move_to 10 height) (g:line_to (// height 2) 0) (g:line_to (- height 10) height) (g:fill) true) })] w)) (local socket-path (or (. arg 1) "/var/run/gnss-share.sock")) (local gnss-socket (let [addr (Gio.UnixSocketAddress { :path socket-path })] (: (Gio.SocketClient) :connect addr nil))) (fn read-gnss [socket] (each [l #(socket:read "l")] ; (print "gnss" l) (if (not (= l "")) (let [message (nmea.parse l)] (case message { : lat : lon : utc} (update-app-state { : lat : lon :time-of-day (let [(h m s) (string.match utc "(..)(..)(..)")] (+ s (* m 60) (* h 60 60))) } ) { : speed-knots } (update-app-state { :speed (* speed-knots knot-in-m-s) })) (if message.bearing-true (update-app-state { :course message.bearing-true })) ))) true) (let [sock (gnss-socket:get_socket) fd (sock:get_fd) events [ GLib.IOCondition.IN GLib.IOCondition.HUP] channel (GLib.IOChannel.unix_new fd) handle (fdopen fd :r)] (GLib.io_add_watch channel 0 events #(read-gnss handle))) (window:add (doto (Gtk.Overlay {}) (: :add (osm-widget)) (: :add_overlay (readouts)) (: :add_overlay (arrow)) )) (window:show_all) (styles) (Gtk:main)