NEW: support for geo fences (waypoints)

WARNING: you must run --initialize to create the new sub db
  see documentation in doc/FENCES.md
This commit is contained in:
Jan-Piet Mens
2016-12-07 19:40:59 +01:00
parent 5e6a0c3335
commit 491a8f6f20
15 changed files with 481 additions and 28 deletions

View File

@@ -13,6 +13,7 @@ OTR_OBJS = json.o \
misc.o \
util.o \
storage.o \
fences.o \
listsort.o
OTR_EXTRA_OBJS =
@@ -82,7 +83,7 @@ ocat: ocat.o $(OTR_OBJS)
$(OTR_OBJS): config.mk Makefile
recorder.o: recorder.c storage.h util.h Makefile geo.h udata.h json.h http.h gcache.h config.mk hooks.h base64.h recorder.h version.h
recorder.o: recorder.c storage.h util.h Makefile geo.h udata.h json.h http.h gcache.h config.mk hooks.h base64.h recorder.h version.h fences.h
geo.o: geo.h geo.c udata.h
geohash.o: geohash.h geohash.c udata.h
base64.o: base64.h base64.c
@@ -96,6 +97,7 @@ ocat.o: ocat.c storage.h util.h version.h config.mk Makefile
storage.o: storage.c storage.h util.h gcache.h listsort.h
hooks.o: hooks.c udata.h hooks.h util.h version.h gcache.h
listsort.o: listsort.c listsort.h
fences.o: fences.c fences.h util.h json.h udata.h gcache.h hooks.h
clean:

51
doc/FENCES.md Normal file
View File

@@ -0,0 +1,51 @@
## Geo fences
Since a version > 0.6.9, Recorder has support for Geofences (irrespective of their use on the OwnTracks devices). In particular, Recorder can read a list of fences from `.otrw` files, and it will monitor a user's position to determine whether the user is transitioning in to or out of a fence, in which case Recorder will invoke a Lua hook (called `transition`).
### `.otrw`
Recorder reads `.otrw` files from `<store>/waypoints/user/device/user-device.otrw` upon startup and loads these into an internal LMDB database. Each waypoint (geo fence) is keyed by `user-device-geohash(lat,lon)` in the LMDB sub table.
For example, the following otrw
```json
{
"_type": "waypoints",
"waypoints": [
{
"_type": "waypoint",
"tst": 9999,
"lat": 48.85833,
"lon": 3.29513,
"rad": 1000,
"desc": "chez Madelaine"
}
]
}
```
is read in as
```
$ ocat -S jp --dump=wp
uno-lua-u0dmfyrkqh {"lat":48.85833,"lon":3.29513,"rad":1000,"desc":"chez Madelaine","io":false}
```
Note how the `io` (in / out) element in the JSON indicates whether the last position reported was in or out of the fence.
### Transition hook
When Recorder determines that the user's device has entered or left the geo fence, it invokes a user-provided Lua function called `transition`:
```lua
function otr_init()
end
function otr_exit()
end
function transition(topic, _type, data)
print("IN TRANSITION " .. _type .. " " .. topic)
otr.publish('special/topic', data['event'] .. " " .. data['desc'])
end
```

View File

@@ -86,6 +86,10 @@ non-zero value, the Recorder will *not* write the REC file for this publish.
An optional function you provide is called `otr_httpobject(u, d, t, data)` where `u` is the username used by the client (`?u=`), `d` is the device name (`&d=` in the URI), `t` is the OwnTracks JSON `_type` and `data` a Lua table built from the OwnTracks JSON payload of `_type`. If it exists, this function is called whenever a POST is received in httpmode and the Recorder is gathering data to return to the client app. The function *must* return a Lua table containing any number of string, number, or boolean values which are converted to a JSON object and appended to the JSON array returned to the client. An [example](etc/example.lua) shows how, say, a transition event can be used to open the Featured content tab in the app.
## `transition`
See [geo fences](FENCES.md).
## Hooklets
After running `otr_hook()`, the Recorder attempts to invoke a Lua function for each of the elements in the extended JSON. If, say, your Lua script contains a function called `hooklet_lat`, it will be invoked every time a `lat` is received as part of the JSON payload. Similarly with `hooklet_addr`, `hooklet_cc`, `hooklet_tst`, etc. These _hooklets_ are invoked with the same parameters as `otr_hook()`.

94
fences.c Normal file
View File

@@ -0,0 +1,94 @@
#include <stdio.h>
#include <stdlib.h>
#include "json.h"
#include "udata.h"
#include "fences.h"
#include "gcache.h"
#include "util.h"
#ifdef WITH_LUA
# include "hooks.h"
#endif
/*
* OwnTracks Recorder
* Copyright (C) 2015-2016 Jan-Piet Mens <jpmens@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/*
* lat/lon are the position that has been reported by the device as it is at
* now, and wp contains the point read from the list of waypoints. Calculate
* distance betwee the two, and if that's less than waypoint radius, user is
* now IN the geofence, else OUT of the geofence. Indicate transition by
* changing IO in wp and telling caller to store.
*/
static int check_a_waypoint(char *key, wpoint *wp, double lat, double lon)
{
double d;
bool rewrite = false;
d = haversine_dist(wp->lat, wp->lon, lat, lon);
// printf("WP= rad=%ld, io=%d, dist=%lf %s\n", wp->rad, wp->io, d, wp->desc);
if (d < wp->rad) {
// printf("YEAH: key(%s)\n", key);
if (wp->io == false) {
wp->event = ENTER;
wp->io = true;
rewrite = true;
}
} else if (wp->io == true) {
wp->event = LEAVE;
wp->io = false;
rewrite = true;
}
if (rewrite) {
// printf("%s - %s: EVENT == %s\n", wp->user, wp->device, wp->event == 0 ? "ENTER" : "LEAVE");
#ifdef WITH_LUA
if (wp->ud->luadata) {
hooks_transition(wp->ud, wp->user, wp->device, wp->event, wp->desc, wp->lat, wp->lon, lat, lon);
}
#endif /* WITH_LUA */
}
return (rewrite);
}
/*
* Every time a position is obtained, calculate the distance to the center
* of each geofence and check whether that distance is less than the radius
* of the geofence. If the distance is <= to the radius, the obtained position
* is considered to be inside the geofence. This position is calculated using
* the Haversine formula.
*/
void check_fences(struct udata *ud, char *username, char *device, double lat, double lon, JsonNode *json)
{
static UT_string *userdev;
utstring_renew(userdev);
utstring_printf(userdev, "%s-%s", username, device);
/*
* For each of this user's geofences (username-device-*) obtain lat/lon/rad
* and do as described above.
*/
gcache_enum(username, device, ud->wpdb, UB(userdev), check_a_waypoint, lat, lon, ud);
}

22
fences.h Normal file
View File

@@ -0,0 +1,22 @@
#ifndef FENCES_H_INCLUDED
# define FENCES_H_INCLUDED
#include <stdbool.h>
typedef struct {
double lat;
double lon;
long rad;
char *desc;
bool io; /* true: is IN region. false: is OUT of region */
char *user;
char *device;
enum { ENTER, LEAVE } event;
struct udata *ud;
} wpoint;
void check_fences(struct udata *ud, char *username, char *device, double lat, double lon, JsonNode *json);
#endif

View File

@@ -21,6 +21,8 @@
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include "udata.h"
#include "fences.h"
#include "gcache.h"
#include "util.h"
@@ -345,3 +347,94 @@ void gcache_load(char *path, char *lmdbname)
gcache_close(gc);
}
/*
* Enumerate (list) keys in lmdb `gc` and invoke func() on each. If func() returns true
* update the data.
*/
bool gcache_enum(char *user, char *device, struct gcache *gc, char *key_part, int (*func)(char *key, wpoint *wp, double lat, double lon), double lat, double lon, struct udata *ud)
{
MDB_val key, data;
MDB_txn *txn;
MDB_cursor *cursor;
int rc, op;
static UT_string *ks; /* key string */
wpoint wp;
if (gc == NULL)
return (NULL);
rc = mdb_txn_begin(gc->env, NULL, MDB_RDONLY, &txn);
if (rc) {
olog(LOG_ERR, "gcache_enum: mdb_txn_begin: (%d) %s", rc, mdb_strerror(rc));
return (NULL);
}
key.mv_data = key_part;
key.mv_size = strlen(key_part);
rc = mdb_cursor_open(txn, gc->dbi, &cursor);
op = MDB_SET_RANGE;
do {
JsonNode *json, *jlat, *jlon, *jrad, *jio, *jdesc;
rc = mdb_cursor_get(cursor, &key, &data, op);
if (rc != 0)
break;
/* FIXME: strlen??? */
if (memcmp(key_part, key.mv_data, strlen(key_part)) != 0) {
break;
}
/* -1 because we 0-terminate strings */
//printf("ENUM---- %*.*s %*.*s\n",
// (int)key.mv_size, (int)key.mv_size, (char *)key.mv_data,
// (int)data.mv_size - 1, (int)data.mv_size - 1, (char *)data.mv_data);
utstring_renew(ks);
utstring_printf(ks, "%*.*s",
(int)key.mv_size, (int)key.mv_size, (char *)key.mv_data);
if ((json = json_decode(data.mv_data)) == NULL)
continue;
if ((jlat = json_find_member(json, "lat")) == NULL) continue;
if ((jlon = json_find_member(json, "lon")) == NULL) continue;
if ((jrad = json_find_member(json, "rad")) == NULL) continue;
if ((jdesc = json_find_member(json, "desc")) == NULL) continue;
if ((jio = json_find_member(json, "io")) == NULL) {
json_append_member(json, "io", json_mkbool(false));
jio = json_find_member(json, "io");
}
wp.lat = jlat->number_;
wp.lon = jlon->number_;
wp.rad = (long)jrad->number_;
wp.io = jio->bool_;
wp.desc = strdup(jdesc->string_);
wp.ud = ud;
wp.user = user;
wp.device = device;
if (func && func(UB(ks), &wp, lat, lon) == true) {
json_remove_from_parent(jio);
json_append_member(json, "io", json_mkbool(wp.io));
if (gcache_json_put(gc, UB(ks), json) != 0) {
olog(LOG_ERR, "gcache_enum: cannot rewrite key %s", UB(ks));
}
}
free(wp.desc);
json_delete(json);
op = MDB_NEXT;
} while (rc == 0);
mdb_cursor_close(cursor);
mdb_txn_commit(txn);
return (true);
}

View File

@@ -20,5 +20,6 @@ JsonNode *gcache_json_get(struct gcache *, char *key);
void gcache_dump(char *path, char *lmdbname);
void gcache_load(char *path, char *lmdbname);
int gcache_del(struct gcache *gc, char *keystr);
bool gcache_enum(char *user, char *device, struct gcache *gc, char *key_part, int (*func)(char *key, wpoint *wp, double lat, double lon), double lat, double lon, struct udata *ud);
#endif

31
hooks.c
View File

@@ -32,9 +32,11 @@
# include <lua.h>
# include <lualib.h>
# include <lauxlib.h>
# include "fences.h"
# include "gcache.h"
# include "json.h"
# include "version.h"
# include "fences.h"
static int otr_log(lua_State *lua);
static int otr_strftime(lua_State *lua);
@@ -377,6 +379,35 @@ void hooks_hook(struct udata *ud, char *topic, JsonNode *fullo)
hooks_hooklet(ud, topic, fullo);
}
/*
* This hook is invoked through fences.c when we determine that a movement
* into or out of a geofence has caused a transition.
*/
void hooks_transition(struct udata *ud, char *user, char *device, int event, char *desc, double wplat, double wplon, double lat, double lon)
{
JsonNode *json = json_mkobject();
char *topic = "transition";
json_append_member(json, "_type", json_mkstring("event"));
json_append_member(json, "user", json_mkstring(user));
json_append_member(json, "device", json_mkstring(device));
json_append_member(json, "desc", json_mkstring(desc));
json_append_member(json, "event",
event == ENTER ? json_mkstring("enter") : json_mkstring("leave"));
json_append_member(json, "wplat", json_mknumber(wplat));
json_append_member(json, "wplon", json_mknumber(wplon));
json_append_member(json, "lat", json_mknumber(lat));
json_append_member(json, "lon", json_mknumber(lon));
olog(LOG_DEBUG, "**** Lua hook for %s %s\n",
event == ENTER ? "ENTER" : "LEAVE", desc);
do_hook("transition", ud, topic, json);
json_delete(json);
}
/*
* --- Here come the functions we provide to Lua scripts.
*/

View File

@@ -15,6 +15,7 @@ void hooks_exit(struct luadata *, char *reason);
void hooks_hook(struct udata *ud, char *topic, JsonNode *obj);
int hooks_norec(struct udata *ud, char *user, char *device, char *payload);
JsonNode *hooks_http(struct udata *ud, char *user, char *device, char *payload);
void hooks_transition(struct udata *ud, char *user, char *device, int event, char *desc, double wplat, double wplon, double lat, double lon);
#endif /* WITH_LUA */

2
http.c
View File

@@ -27,6 +27,8 @@
#include "json.h"
#include "util.h"
#include "misc.h"
#include "fences.h"
#include "gcache.h"
#include "storage.h"
#include "geohash.h"
#include "udata.h"

3
ocat.c
View File

@@ -27,6 +27,9 @@
# include <mosquitto.h>
#endif
#include "json.h"
#include "udata.h"
#include "fences.h"
#include "gcache.h"
#include "storage.h"
#include "util.h"
#include "misc.h"

View File

@@ -34,6 +34,7 @@
#include <sys/utsname.h>
#include <regex.h>
#include "recorder.h"
#include "udata.h"
#include "utstring.h"
#include "geo.h"
#include "geohash.h"
@@ -41,6 +42,7 @@
#include "misc.h"
#include "util.h"
#include "storage.h"
#include "fences.h"
#include "gcache.h"
#ifdef WITH_HTTP
# include "http.h"
@@ -388,6 +390,8 @@ void waypoints_dump(struct udata *ud, UT_string *username, UT_string *device, ch
}
xx_dump(ud, username, device, (js) ? js : payloadstring, "waypoints", "otrw");
load_otrw_from_string(ud, UB(username), UB(device), (js) ? js : payloadstring);
if (js)
free(js);
}
@@ -837,14 +841,6 @@ void handle_message(void *userdata, char *topic, char *payload, size_t payloadle
}
}
#if 0
/* Haversine */
{
double d = haversine_dist(lat, lon, 52.03431, 8.47654);
printf("*** d=%lf meters\n", d);
}
#endif
/*
* If the topic we are handling is in topic2tid, replace the TID
@@ -1055,7 +1051,8 @@ void handle_message(void *userdata, char *topic, char *payload, size_t payloadle
}
}
}
check_fences(ud, UB(username), UB(device), lat, lon, json);
cleanup:
if (geo) json_delete(geo);
@@ -1481,6 +1478,12 @@ int main(int argc, char **argv)
exit(2);
}
gcache_close(gt);
if ((gt = gcache_open(path, "wp", FALSE)) == NULL) {
fprintf(stderr, "Cannot lmdb-open `wp'\n");
exit(2);
}
gcache_close(gt);
exit(0);
}
@@ -1552,7 +1555,9 @@ int main(int argc, char **argv)
ud->keydb = gcache_open(err, "keys", TRUE);
# endif
ud->httpfriends = gcache_open(err, "friends", TRUE);
ud->wpdb = gcache_open(err, "wp", FALSE);
load_fences(ud);
#if WITH_ENCRYPT
if (sodium_init() == -1) {
@@ -1716,6 +1721,7 @@ int main(int argc, char **argv)
gcache_close(ud->gc);
gcache_close(ud->t2t);
gcache_close(ud->httpfriends);
gcache_close(ud->wpdb);
#ifdef WITH_LUA
if (ud->luadb)
gcache_close(ud->luadb);

172
storage.c
View File

@@ -32,8 +32,9 @@
#include "utstring.h"
#include "storage.h"
#include "geohash.h"
#include "fences.h"
#include "gcache.h"
#include "util.h"
#include "udata.h"
#include "listsort.h"
char STORAGEDIR[BUFSIZ] = STORAGEDEFAULT;
@@ -405,8 +406,8 @@ int make_times(char *time_from, time_t *s_lo, char *time_to, time_t *s_hi, int h
static void ls(char *path, JsonNode *obj)
{
DIR *dirp;
struct dirent *dp;
DIR *dirp;
struct dirent *dp;
JsonNode *jarr = json_mkarray();
if (obj == NULL || obj->tag != JSON_OBJECT) {
@@ -414,20 +415,20 @@ static void ls(char *path, JsonNode *obj)
return;
}
if ((dirp = opendir(path)) == NULL) {
if ((dirp = opendir(path)) == NULL) {
json_append_member(obj, "error", json_mkstring("Cannot open requested directory"));
json_delete(jarr);
return;
}
return;
}
while ((dp = readdir(dirp)) != NULL) {
if ((*dp->d_name != '.') && (dp->d_type == DT_DIR)) {
while ((dp = readdir(dirp)) != NULL) {
if ((*dp->d_name != '.') && (dp->d_type == DT_DIR)) {
json_append_element(jarr, json_mkstring(dp->d_name));
}
}
}
}
json_append_member(obj, "results", jarr);
closedir(dirp);
closedir(dirp);
}
/*
@@ -503,7 +504,7 @@ static void lsscan(char *pathpat, time_t s_lo, time_t s_hi, JsonNode *obj, int r
if ((n = scandir(pathpat, &namelist, filter_filename, cmp)) < 0) {
json_append_member(obj, "error", json_mkstring("Cannot lsscan requested directory"));
return;
return;
}
/* If our obj contains the "results" array, use that
@@ -926,7 +927,7 @@ static void append_to_feature_array(JsonNode *features, double lat, double lon,
JsonNode *geo_json(JsonNode *location_array)
{
JsonNode *one, *j;
JsonNode *feature_array, *fcollection;
JsonNode *feature_array, *fcollection;
if ((fcollection = json_mkobject()) == NULL)
return (NULL);
@@ -1031,7 +1032,7 @@ JsonNode *geo_linestring(JsonNode *location_array)
char *gpx_string(JsonNode *location_array)
{
JsonNode *one;
static UT_string *xml = NULL;
static UT_string *xml = NULL;
if (location_array->tag != JSON_ARRAY)
return (NULL);
@@ -1043,7 +1044,7 @@ char *gpx_string(JsonNode *location_array)
<trk>\n\
<trkseg>\n");
// <trkpt lat="xx.xxx" lon="yy.yyy"> <!-- Attribute des Trackpunkts --> </trkpt>
// <trkpt lat="xx.xxx" lon="yy.yyy"> <!-- Attribute des Trackpunkts --> </trkpt>
json_foreach(one, location_array) {
JsonNode *jlat, *jlon, *jisotst, *j;
@@ -1108,7 +1109,7 @@ JsonNode *kill_datastore(char *user, char *device)
json_append_member(obj, "status", json_mkstring("ERROR"));
json_append_member(obj, "error", json_mkstring(strerror(errno)));
json_append_member(obj, "reason", json_mkstring("cannot scandir"));
return (obj);
return (obj);
}
for (i = 0; i < n; i++) {
@@ -1516,3 +1517,142 @@ void extra_http_json(JsonNode *array, char *user, char *device)
}
free(js_string);
}
/*
* Process an array of waypoints as read from an .otrw file. If
* rad is positive and lat/lon exist, store in LMDB database for
* this user.
*/
static bool load_otrw_waypoints(struct udata *ud, JsonNode *wplist, char *user, char *device)
{
JsonNode *n;
static UT_string *key;
long len;
char buf[20];
json_foreach(n, wplist) {
JsonNode *rad, *lat, *lon, *desc, *tst, *type;
if ((type = json_find_member(n, "_type")) == NULL)
return (false);
if (strcmp(type->string_, "waypoint") != 0)
return (false);
json_remove_from_parent(type);
if ((rad = json_find_member(n, "rad")) == NULL)
continue;
if (rad->number_ <= 0)
continue;
lat = json_find_member(n, "lat");
lon = json_find_member(n, "lon");
tst = json_find_member(n, "tst");
desc = json_find_member(n, "desc");
/* It turns out tst was a key, but it breaks on iOS when dumped
* waypoints are re-imported. we'll use the geohash of lat/lon here */
json_remove_from_parent(tst);
utstring_renew(key);
utstring_printf(key, "%s-%s-%s", user, device,
geohash_encode(lat->number_, lon->number_, 10));
printf("--> %s: %s\t(%lf, %lf) (%ld)\n", UB(key), desc->string_, lat->number_, lon->number_, (long)rad->number_);
/*
* Just before initially storing in LMDB, we need to determine whether
* device is currently within or without a waypoint... FIXME
*/
/* Note: we don't need buf -- just checking if key exists */
len = gcache_get(ud->wpdb, UB(key), buf, sizeof(buf));
if (len == -1) {
gcache_json_put(ud->wpdb, UB(key), n);
}
}
return (true);
}
void load_otrw_from_string(struct udata *ud, char *username, char *device, char *js_string)
{
JsonNode *node, *wplist;
if ((node = json_decode(js_string)) == NULL) {
olog(LOG_ERR, "load_otrw_from_string: can't decode JSON from string for %s-%s\n", username, device);
return;
}
if ((wplist = json_find_member(node, "waypoints"))) {
load_otrw_waypoints(ud, wplist, username, device);
}
json_delete(node);
}
static int load_otrw_file(struct udata *ud, char *filename)
{
char *js_string, *bp, *parts[20], *user, *device;
JsonNode *node, *wplist;
if ((js_string = slurp_file(filename, TRUE)) == NULL) {
return (false);
}
if ((node = json_decode(js_string)) == NULL) {
olog(LOG_ERR, "load_otrw_file: can't decode JSON from file s\n", filename);
free(js_string);
return (false);
}
/*
* Find username / device name from filename component of
* path (i.e. "/user-device.otrw").
*/
if ((bp = strrchr(filename, '/')) == NULL)
return (false);
if (splitter(bp+1, "-.", parts) != 3)
return (false);
user = parts[0];
device = parts[1];
if ((wplist = json_find_member(node, "waypoints"))) {
load_otrw_waypoints(ud, wplist, user, device);
}
splitterfree(parts);
free(js_string);
json_delete(node);
return (true);
}
bool load_fences(struct udata *ud)
{
static UT_string *path = NULL;
int n, rc;
glob_t results;
/* Get list of waypoint files */
utstring_renew(path);
utstring_printf(path, "%s/waypoints/*/*/*.otrw", STORAGEDIR);
rc = glob(UB(path), 0, 0, &results);
if (rc == 0) {
for (n = 0; n < results.gl_pathc; n++) {
char *f = results.gl_pathv[n];
// puts(f);
load_otrw_file(ud, f);
}
}
globfree(&results);
return (true);
}

View File

@@ -2,8 +2,8 @@
# define _STORAGE_H_INCL_
#include <time.h>
#include "gcache.h"
#include "json.h"
#include "udata.h"
#define DEFAULT_HISTORY_HOURS 6
@@ -55,5 +55,7 @@ void csv_output(JsonNode *json, output_type otype, JsonNode *fields, void (*func
char *storage_userphoto(char *username);
void append_card_to_object(JsonNode *obj, char *user, char *device);
void extra_http_json(JsonNode *array, char *user, char *device);
void load_otrw_from_string(struct udata *ud, char *username, char *device, char *js);
bool load_fences(struct udata *ud);
#endif

View File

@@ -7,7 +7,7 @@
# include <stdarg.h>
# include "mongoose.h"
#endif
#include "gcache.h"
// #include "gcache.h"
struct udata {
@@ -49,6 +49,7 @@ struct udata {
char *geokey; /* Google reverse-geo API key */
int debug; /* enable for debugging */
struct gcache *httpfriends; /* lmdb named database 'friends' */
struct gcache *wpdb; /* lmdb named database 'wp' (waypoints) */
};
#endif