Compare commits

...

8 Commits

Author SHA1 Message Date
c56943e6e1 use cqueues for gnss socket
This is to avoid a weird bug in the previous glib iochannel
implementation, where the socket was randomly being closed
after about a minute run time. But it's also less code, so
that's OK
2025-06-30 23:02:54 +01:00
b739d9a411 experiment: smooth orientation a bit
remember three most recent orientation values and pick the middle one
but clamped between the values of the least and most recent
2025-06-30 18:19:20 +01:00
64789d1fe9 rewrite fake-nmea to not need socat 2025-06-30 18:17:13 +01:00
01e260e9aa remove debug print 2025-06-17 22:10:41 +01:00
0b91d4200f wrap expectations in define-tests
this means they'll be executed by "make test" and not compiled
at all in AOT mode
2025-06-17 22:05:03 +01:00
6ca0e77604 add Makefile targets to run in-place and to run tests 2025-06-17 22:05:03 +01:00
360c381b33 move top-level forms into a main function
this is to prepare for making this a module
2025-06-17 21:40:35 +01:00
e5fdab4852 turn faster 2025-06-16 11:51:12 +01:00
5 changed files with 181 additions and 94 deletions

View File

@ -2,7 +2,7 @@ FENNEL?=fennel
PREFIX?=/usr/local
NAME?=maps
MODULES=main.fnl
MODULES=main.fnl tiles.fnl
%.lua : %.fnl
$(FENNEL) --compile $< > $@
@ -11,6 +11,12 @@ $(NAME): $(patsubst %.fnl,%.lua,$(MODULES)) Makefile
(echo -e "#!/usr/bin/env lua\n" ; cat main.lua ) > $@
chmod +x $@
run:
fennel -e '((. (require :main) :run) "/tmp/gnss")'
test:
fennel run-tests.fnl $(MODULES)
install:
mkdir -p $(PREFIX)/bin $(PREFIX)/
cp $(NAME) $(PREFIX)/bin

View File

@ -1,10 +1,72 @@
(local { : sleep } (require :posix.unistd))
(local { : sleep : close } (require :posix.unistd))
(local sock (require :posix.sys.socket))
(local poll (require :posix.poll))
(local { : view } (require :fennel))
(fn handle-client [fd event]
(case event
{:HUP true}
(do
(print fd " disconnected")
(close fd)
(values fd nil))
{:IN true}
(do
(sock.recv fd 1024) ; discard input and carry on
nil)))
(fn accept-new-connection [server-sock event]
(case event
{:IN true}
(do
(print "new connection")
(let [connected (sock.accept server-sock)]
(print connected)
(values connected handle-client)))
{:HUP true}
(do
(print "HUP on server socket")
(os.exit 1))
_
(values nil nil)))
(fn server-socket [path]
(let [s (assert (sock.socket sock.AF_UNIX sock.SOCK_STREAM 0))
sa { :family sock.AF_UNIX
:path path
}]
(assert (sock.bind s sa))
(assert (sock.listen s 10))
s))
(fn read-with-rewind [handle]
(let [(s err) (handle:read "L")]
(if s
s
(if (and (not err) (handle:seek "set" 0)) ; eof
(read-with-rewind handle)
(error err)))))
(fn write-sentences [handle outputs]
(var done false)
(while (not done)
(let [l (read-with-rewind handle)]
(if (string.match l "GNS") (set done true))
(each [fd action (pairs outputs)]
(when (sock.getpeername fd) ; cheeky
(sock.send fd l))))))
(let [[filename socketname] arg
socket (io.popen (.. "socat - unix-listen:" socketname) :w)]
server (server-socket socketname)
fd-actions { server accept-new-connection }]
(with-open [handle (io.open filename :r)]
(each [l (handle:lines "L")]
(if (string.match l "GNS") (sleep 1))
(socket:write l)
(socket:flush)
)))
(while true
(let [poll-fds (collect [fd _ (pairs fd-actions)]
(values fd {:events { :IN true } }))]
(if (> (poll.poll poll-fds (* 1 1000)) 0)
(each [fd v (pairs poll-fds)]
(when (. v :revents)
(print :polled fd (view v.revents))
(let [(k v) ((. fd-actions fd) fd v.revents)]
(and k (tset fd-actions k v)))))
(write-sentences handle fd-actions))))))

View File

@ -2,6 +2,7 @@
(local { : fdopen } (require :posix.stdio))
(local ptime (require :posix.time))
(local cqueues (require :cqueues))
(local socket (require :cqueues.socket))
(local nmea (require :nmea))
(local tiles (require :tiles))
@ -86,6 +87,7 @@ label.readout {
:lon 0
:zoom 17
:course 0 ; direction of travel
:courses [0 0 0]
:orientation-target 0 ; map rotation angle from north
:orientation-actual 0 ; map rotation angle from north
:tiles {}
@ -140,18 +142,20 @@ label.readout {
}
}))
;; diagonal radius is 538 pixels, 2.1 tiles
(define-tests
:map-bounds-tile
;; diagonal radius is 538 pixels, 2.1 tiles
(let [bounds (map-bounds-tile 65539.5 45014.5)]
(expect= bounds.min {:x 65537 :y 45012})
(expect= bounds.max {:x 65541 :y 45016})
(expect= bounds.centre {:x 65539.5 :y 45014.5}))
(let [bounds (map-bounds-tile 65539.5 45014.5)]
(expect= bounds.min {:x 65537 :y 45012})
(expect= bounds.max {:x 65541 :y 45016})
(expect= bounds.centre {:x 65539.5 :y 45014.5}))
(let [bounds (map-bounds-tile 65539.0 45014.0)]
(expect= bounds.min {:x 65536 :y 45011})
(expect= bounds.max {:x 65541 :y 45016})
(expect= bounds.centre {:x 65539 :y 45014})
)
(let [bounds (map-bounds-tile 65539.0 45014.0)]
(expect= bounds.min {:x 65536 :y 45011})
(expect= bounds.max {:x 65541 :y 45016})
(expect= bounds.centre {:x 65539 :y 45014})
))
(fn map-bounds [lat lon zoom]
(let [(tile-x tile-y) (tiles.latlon->tile app-state.lat app-state.lon app-state.zoom)]
@ -279,7 +283,6 @@ label.readout {
(when (not (= app-state.orientation-actual app-state.orientation-target))
(print (- app-state.orientation-actual app-state.orientation-target))
(g:translate (+ (/ viewport-width 2)) (+ (/ viewport-height 2)))
(g:rotate (* (/ (- 360 (- app-state.orientation-actual app-state.orientation-target)) 180) math.pi))
(g:translate (- (/ viewport-width 2)) (- (/ viewport-height 2))))
@ -322,13 +325,13 @@ label.readout {
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")
(define-tests
:hhmmss
(expect= (hhmmss (+ 45 (* 60 12) (* 60 60 3))) "3:12:45"))
(fn turn-smoothly [from to]
(if (< (math.abs (- from to)) 10) to
(+ from (* 0.05 (- to from)))))
(if (< (math.abs (- from to)) 3) to
(+ from (* 0.3 (- to from)))))
(fn update-app-state [new-vals]
(let [old-state (merge {} app-state)
@ -445,55 +448,34 @@ label.readout {
}))))
(local socket-path (or (. arg 1) "/var/run/gnss-share.sock"))
(fn clamp [a b c]
(if (< b a) a
(> b c) c
b))
(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}
(fn read-gnss-sentence [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) }))
(when message.bearing-true
(let [c1 message.bearing-true
c2 (. app-state.courses 1)
c3 (. app-state.courses 2)]
(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)))
(GLib.timeout_add
GLib.PRIORITY_DEFAULT
100 ; ms
(fn []
;; run cqueues scheduler
(cq:step 0)
;; for smoother rotation when course changes, repaint more often than
;; once per gnss message
(update-app-state {})
true)
nil nil)
{ :course (clamp c1 c2 c3)
:courses [c1 c2 c3]
}))))))
(fn collect-profile []
(GLib.timeout_add
@ -503,16 +485,39 @@ label.readout {
(print "profiling for 60 seconds")
(profile.start 0))
(fn watch-gnss-socket [socket-path]
(let [sock (socket.connect { :path socket-path :unlink true })]
(cq:wrap
#(each [l (sock:lines "*L")]
(read-gnss-sentence l)))))
(window:add
(doto (Gtk.Overlay {})
(: :add (osm-widget))
(: :add_overlay (readouts))
(: :add_overlay (arrow))
(: :add_overlay (rose))
))
(window:show_all)
(styles)
(when (os.getenv "MAP_PROFILE") (collect-profile))
(Gtk:main)
(fn run [gnss-socket]
(watch-gnss-socket (or gnss-socket "/var/run/gnss-share.sock"))
(GLib.timeout_add
GLib.PRIORITY_DEFAULT
100 ; ms
(fn []
;; run cqueues scheduler
(cq:step 0)
;; for smoother rotation when course changes, repaint more often than
;; once per gnss message
(update-app-state {})
true)
nil nil)
(window:add
(doto (Gtk.Overlay {})
(: :add (osm-widget))
(: :add_overlay (readouts))
(: :add_overlay (arrow))
(: :add_overlay (rose))
))
(window:show_all)
(styles)
(when (os.getenv "MAP_PROFILE") (collect-profile))
(Gtk:main))
{ : run }

10
pkgs/maps/run-tests.fnl Normal file
View File

@ -0,0 +1,10 @@
(local fennel (require :fennel))
(local specials (require :fennel.specials))
(local compiler-env
(doto (. (specials.make-compiler-env) :_G)
(tset "RUNNING_TESTS" true)))
(each [_ f (ipairs arg)]
(print :testing f)
(fennel.dofile f { :correlate true :compilerEnv compiler-env }))

View File

@ -10,7 +10,8 @@
(fn sinh [x] (/ (- 1 (math.exp (* -2 x))) (* 2 (math.exp (- x)))))
(expect (< (math.abs (- (sinh 2) 3.626860407847)) 0.001))
(define-tests :sinh
(expect (< (math.abs (- (sinh 2) 3.626860407847)) 0.001)))
(fn tile->latlon [xtile ytile zoom]
(let [n (^ 2 zoom)
@ -21,15 +22,17 @@
(values (/ (* lat-rad 180) math.pi) lon-deg)))
(let [(lat lon) (tile->latlon 0 0 0)]
(expect= lon -180)
(expect-near lat 85.05112877)
)
(define-tests
:tile->latlon
(let [(lat lon) (tile->latlon 0 0 0)]
(expect= lon -180)
(expect-near lat 85.05112877)
)
(let [(lat lon) (tile->latlon 232798 103246 18)]
(expect-near lon 139.699401855)
(expect-near lat 35.6595278648)
)
(let [(lat lon) (tile->latlon 232798 103246 18)]
(expect-near lon 139.699401855)
(expect-near lat 35.6595278648)
))
(fn latlon->tile [lat lon zoom]
(let [n (^ 2 zoom)
@ -39,9 +42,10 @@
y (* (- 1 (/ t math.pi)) (/ n 2))]
(values x y)))
(let [(x y) (latlon->tile 52.1234 -0.53 17)]
(expect= (math.floor x) 65343)
(expect= (math.floor y) 43221))
(define-tests :latlon->tile
(let [(x y) (latlon->tile 52.1234 -0.53 17)]
(expect= (math.floor x) 65343)
(expect= (math.floor y) 43221)))
(fn overpass [lat lon zoom]