chore: micodus_server rework #1

Open
paulbsd wants to merge 8 commits from develop into master
48 changed files with 2921 additions and 528 deletions

1783
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ bitvec = { version = "1.0" }
chrono = { version = "0.4" }
encoding_rs = { version = "0.8" }
lazy_static = { version = "1.5" }
libsql = { version = "0.9", features = ["replication"] }
rand = { version = "0.9" }
rusqlite = { version = "0.34", features = ["bundled"] }
tokio = { version = "1.44", features = ["full", "sync"] }
rusqlite = { version = "0.37", features = ["bundled"] }
tokio = { version = "1.47", features = ["full", "sync"] }

10
html/const.lua Normal file
View File

@ -0,0 +1,10 @@
basepath = "/home/paul/git/micodus_server"
dbfile = string.format("%s/data/tracker.db",basepath)
query = [[
SELECT time,latitude,longitude,height,speed,direction,serial
FROM log
ORDER BY id DESC, serial DESC
LIMIT 1;
]]
return {["basepath"]=basepath, ["dbfile"]=dbfile, ["query"]=query}

View File

@ -1,61 +0,0 @@
/*
*/
let map;
let point;
const arrows = ["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"];
const socket = new WebSocket("wss://geo.paulbsd.com/ws");
function create_location(coords,text) {
point = L.marker(coords, {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 50
}).addTo(map).bindPopup(text);
point.getPopup().autoPan=false;
point.openPopup();
}
function create_map(coords) {
map = L.map('map', {
center: coords,
zoom: 15
});
L.tileLayer(`https://tile.openstreetmap.org/{z}/{x}/{y}.png`, {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> / <a href="https://www.paulbsd.com">PaulBSD</a>'
}).addTo(map);
}
function update(data) {
const coords = [data.latitude,data.longitude];
const speed = data.speed/10;
let section = parseInt(data.direction/45 + 0.5);
section = section % 8;
console.log(arrows[section]);
const text = `time: ${data.time}<br/>latitude: ${data.latitude}<br/>longitude: ${data.longitude}<br/>height: ${data.height}<br/>speed: ${speed}<br/>direction: ${arrows[section]} ${data.direction}`;
if (!map) {
create_map(coords);
} else {
map.setView(coords);
map.flyTo(coords);
}
if (!point) {
create_location(coords, text);
} else {
point.setLatLng(coords);
point.setPopupContent(text);
}
}
socket.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
update(data);
});
socket.addEventListener("open", (event) => {
socket.send("ping");
});

BIN
html/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,15 +1,30 @@
<html>
<title>PaulBSD geo</title>
<link rel="icon" href="https://paulbsd.com/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="leaflet/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<link rel="stylesheet" href="style.css" crossorigin=""/>
<script src="leaflet/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin="">
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black"/>
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#0313fc"/>
<body>
<div id="map" style="height: 100%;"></div>
<script src="engine.js"></script>
</body>
<html lang="en">
<title>Trackme</title>
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="static/style.css" crossorigin="">
<link rel="stylesheet" href="static/leaflet/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<link rel="stylesheet" href="static/bootstrap/bootstrap.min.css" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="">
<script src="static/leaflet/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#0313fc">
<body>
<div id="dash">
<div id="map">
</div>
<div class="buttonscontainer">
<button id="togglefollowbtn" class="btn btn-primary btn-lg btn-block">Unfollow tracker</button>
<button id="upgradebtn" class="btn btn-secondary btn-lg btn-block">Upgrade</button>
<button id="pingtrackerbtn" class="btn btn-secondary btn-lg btn-block">Ping tracker</button>
</div>
<div class="buttonscontainer">
<button id="optionsbtn" class="btn btn-secondary btn-lg btn-block">Options</button>
<button id="socketstatusbtn" class="btn btn-secondary btn-lg btn-block">WS Status</button>
<button id="reloadwsbtn" class="btn btn-secondary btn-lg btn-block">Reload WS</button>
</div>
</div>
<script src="static/bootstrap/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script defer src="static/engine.js"></script>
<script src="static/service.js"></script>
</body>
</html>

43
html/old/engine.js Normal file
View File

@ -0,0 +1,43 @@
let map;
let circle;
function update() {
get_data().then(c=> {
const d = [c.latitude,c.longitude];
if (!map) {
map = L.map('map', {
center: d,
zoom: 13
});
L.tileLayer(`https://tile.openstreetmap.org/{z}/{x}/{y}.png`, {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
}
if (circle) {
circle.remove();
circle=null;
}
if (circle == null) {
circle = L.circle(d, {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 50
}).addTo(map).bindPopup('I\'m here.');
}
});
}
function get_data() {
const res = fetch("lastloc.json").then((a)=> {
const b = a.json().then((j) => {
return j;
})
return b;
})
return res;
}
update();
setInterval(update,10000);

1
html/old/lastloc.json Normal file
View File

@ -0,0 +1 @@
{"latitude": 49.173069, "longitude": -0.342916}

21
html/old/lastloc.lua Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/lua
local const = require('const')
local json = require("json")
local sqlite = require("lsqlite3")
--local output = string.format("%s/html/lastloc.json",basepath)
function main()
local db = sqlite.open(const.dbfile,sqlite.OPEN_READONLY)
local res, vm = db:nrows(const.query)
for row in res, vm do
local locstr = string.format("{\"latitude\": %s, \"longitude\": %s}", row.latitude, row.longitude)
--f = io.open(output, "w")
--f:write(locstr)
ngx.say(locstr)
end
db:close()
end
main()

6
html/old/test.html Normal file
View File

@ -0,0 +1,6 @@
<html>
<script>
window.navigator.vibrate(1000);
document.write("<h1>"+window.navigator.userAgent+"</h1>");
</script>
</html>

View File

@ -1,8 +1,8 @@
// Create WebSocket connection.
const socket = new WebSocket("wss://geo.paulbsd.com/ws");
const socket = new WebSocket("wss://trackme.ovh/ws");
// Connection opened
socket.addEventListener("open", (event) => {
socket.addEventListener("open", (_event) => {
socket.send("Hello Server!");
});

90
html/old/ws2.lua Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/lua
package.path = package.path..";/home/paul/git/micodus_server/html/?.lua"
local const = require("const")
local json = require("json")
local server = require("nginx.websocket.server")
local sqlite = require("lsqlite3")
--ngx.shared.geo:set("last_time","")
local db = sqlite.open(const.dbfile, sqlite.OPEN_READONLY)
function getdata()
local res, vm = db:nrows(const.query)
local data = {}
for row in res, vm do
data = {
["time"] = row.time,
["latitude"] = row.latitude,
["longitude"] = row.longitude,
["height"] = row.height,
["speed"] = row.speed,
["direction"] = row.direction,
["serial"] = row.serial,
}
end
-- db:close()
return data
end
function handle_ping(wb)
local data, typ, err = wb:recv_frame()
if data then
ngx.log(ndx.ERR,data)
end
coroutine.yield()
end
function send_data(wb, last_time)
while true do
ngx.log(ndx.ERR,"test")
local data = getdata()
ngx.log(ndx.ERR,data)
if data.time ~= last_time then
local locstr = json.encode(data)
local bytes, err = wb:send_text(locstr)
ngx.log(ndx.ERR,bytes)
if not bytes then
ngx.log(ngx.ERR, "failed to send text: ", err)
return ngx.exit(444)
end
last_time = data.time
end
ngx.sleep(0.5)
coroutine.yield()
end
end
function geows()
local locstr = nil
local wb, err = server:new {
timeout = 5000,
max_payload_len = 65535
}
if not wb then
ngx.log(ngx.ERR, "failed to new websocket: ", err)
return ngx.exit(444)
end
local last_time = nil
local h = coroutine.create(handle_ping, wb)
local s = coroutine.create(send_data, wb, last_time)
local i=0
while true do
ngx.log(ngx.ERR,i)
coroutine.resume(h,wb)
coroutine.resume(s,wb,last_time)
if not wb then
ngx.log(ngx.ERR, "failed to new websocket: ", err)
return ngx.exit(444)
end
i=i+1
ngx.sleep(0.5)
end
wb:send_close()
end
geows()

54
html/old/ws_test.lua Normal file
View File

@ -0,0 +1,54 @@
local server = require "nginx.websocket.server"
function geows()
local wb, err = server:new {
timeout = 5000,
max_payload_len = 65535
}
if not wb then
ngx.log(ngx.ERR, "failed to new websocket: ", err)
return ngx.exit(444)
end
while true do
local bytes, err = wb:send_text(string.format("%s haha!",data))
if not bytes then
ngx.log(ngx.ERR, "failed to send text: ", err)
return ngx.exit(444)
end
ngx.sleep(1)
end
--[[while true do
local data, typ, err = wb:recv_frame()
if wb.fatal then
ngx.log(ngx.ERR, "failed to receive frame: ", err)
return ngx.exit(444)
end
if not data then
local bytes, err = wb:send_ping()
if not bytes then
ngx.log(ngx.ERR, "failed to send ping: ", err)
return ngx.exit(444)
end
elseif typ == "close" then break
elseif typ == "ping" then
local bytes, err = wb:send_pong()
if not bytes then
ngx.log(ngx.ERR, "failed to send pong: ", err)
return ngx.exit(444)
end
elseif typ == "pong" then
ngx.log(ngx.INFO, "client ponged")
elseif typ == "text" then
local bytes, err = wb:send_text(string.format("%s haha!",data))
if not bytes then
ngx.log(ngx.ERR, "failed to send text: ", err)
return ngx.exit(444)
end
end
end--]]
wb:send_close()
end
geows()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

193
html/static/engine.js Normal file
View File

@ -0,0 +1,193 @@
// engine.js
let socket = new WebSocket("wss://trackme.ovh/ws");
const arrows = ["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"];
let togglefollowbtn;
class Position {
constructor(type, iconUrl, iconRetinaUrl) {
this.type = type;
this.iconUrl = iconUrl;
this.iconRetinaUrl = iconRetinaUrl;
this.text = "";
}
setData(obj) {
this.data = obj;
}
setDefaultData() {
this.data = {latitude:49, longitude:0};
}
static getMiddle(t, u) {
const mid = {coords: [49, 0]};
if (t.coords && u.coords) {
for (const i in mid.coords) {
mid.coords[i] = Math.min(t.coords[i], u.coords[i]) + (Math.abs(t.coords[i] - u.coords[i])/2);
}
} else mid.coords = t.coords;
return mid.coords;
}
createLocation() {
const icon = new L.Icon.Default;
icon.options.iconUrl = this.iconUrl;
icon.options.iconRetinaUrl = this.iconRetinaUrl;
try {
this.point = L.marker(this.coords, {
color: "red",
fillColor: "#f03",
fillOpacity: 0.5,
radius: 50,
icon: icon
}).addTo(map).bindPopup(this.text);
this.point.getPopup().autoPan = false;
//this.point.openPopup();
} catch (err) {
console.log(`unable to create point (${err})`);
}
}
}
let map;
let follow = true;
const tracker = new Position("tracker", "marker-icon.png", "marker-icon-2x.png");
const user = new Position("user", "marker-icon-alt.png", "marker-icon-2x-alt.png");
function userGPSsuccess(pos) {
user.data = {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
height: pos.coords.altitude,
speed: pos.coords.speed,
direction: pos.coords.heading,
};
user.coords = [pos.coords.latitude, pos.coords.longitude];
}
function userGPSerror(err) {
console.log(err);
user.setDefaultData();
}
function getPosition() {
if (navigator.geolocation) {
return new Promise((resolve, _reject) => {
navigator.geolocation.getCurrentPosition(userGPSsuccess, userGPSerror);
resolve("Position fetched");
});
}
}
function createMap(coords) {
map = L.map("map", {
center: coords,
zoom: 10
});
L.tileLayer(`https://tile.openstreetmap.org/{z}/{x}/{y}.png`, {
maxZoom: 19,
attribution: `&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> / <a href="https://www.paulbsd.com">PaulBSD</a>`
}).addTo(map);
map.zoomControl.setPosition('bottomright');
map.addEventListener("zoomlevelschange", (_event) => {
update();
});
}
function update() {
if (tracker.data) {
tracker.coords = [tracker.data.latitude, tracker.data.longitude];
tracker.data.speed = tracker.data.speed/10;
tracker.section = parseInt(tracker.data.direction/45 + 0.5) % 8;
tracker.text = `<b>Tracker</b><br/>time: ${tracker.data.time}<br/>latitude: ${tracker.data.latitude}<br/>longitude: ${tracker.data.longitude}<br/>height: ${tracker.data.height}<br/>speed: ${tracker.data.speed}<br/>direction: ${arrows[tracker.section]} ${tracker.data.direction}°<br/>serial: ${tracker.data.serial}`;
}
//user.text = `<b>You</b><br/>latitude: ${user.data.latitude}<br/>longitude: ${user.data.longitude}<br/>height: ${user.data.height}<br/>speed: ${user.data.speed}<br/>direction: ${arrows[user.section]} ${user.data.direction}°`;
if (user.data) {
user.text = `<b>You</b><br/>latitude: ${user.data.latitude}<br/>longitude: ${user.data.longitude}`;
}
if (tracker.data) {
const center = [tracker.data.latitude, tracker.data.longitude];
if (!map) {
createMap(center);
}
}
if (map && follow) {
const coords = Position.getMiddle(tracker, user);
try {
map.setView(coords);
map.flyTo(coords);
map.fitBounds([tracker.coords, user.coords]);
} catch (e) {
console.log(e, coords);
}
}
if (!tracker.point && tracker.data) {
tracker.createLocation();
} else {
if (tracker.coords) {
tracker.point.setLatLng(tracker.coords);
tracker.point.setPopupContent(tracker.text);
}
}
if (!user.point && user.data) {
user.createLocation();
} else {
if (user.coords) {
user.point.setLatLng(user.coords);
user.point.setPopupContent(user.text);
}
}
}
function ping() {
try {
if (socket.readyState > 1) {
throw "closed socket";
}
socket.send("ping")
} catch (err) {
console.log(err);
socket = new WebSocket("wss://trackme.ovh/ws");
}
}
function followButton() {
if (follow) togglefollowbtn.innerHTML = "Unfollow tracker";
else togglefollowbtn.innerHTML = "Follow tracker";
}
function main() {
togglefollowbtn = document.querySelector("#togglefollowbtn");
follow = JSON.parse(localStorage.getItem("follow"));
followButton();
togglefollowbtn.addEventListener("click", function() {
follow = !follow;
localStorage.setItem("follow", follow);
followButton();
});
socket.addEventListener("message", (event) => {
tracker.data = JSON.parse(event.data);
update();
});
socket.addEventListener("open", (_event) => {
ping();
});
setInterval(getPosition, 1000);
setInterval(ping, 1000);
setInterval(update, 1000);
}
main();

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 618 B

32
html/static/service.js Normal file
View File

@ -0,0 +1,32 @@
/* service worker */
const CACHE_NAME = "trackme-v1";
const urlsToCache = [
"/",
"/static/leaflet/leaflet.js",
"/static/engine.js",
"/static/style.css",
];
{
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/static/service.js")
.then((_reg) => {
console.log("Registration successful");
}).catch((error) => {
console.log(`Registration error: ${error}`);
});
}
self.addEventListener("install", function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log("Opened cache");
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener("fetch", function(event) {
console.log("fetch");
});
}

35
html/static/style.css Normal file
View File

@ -0,0 +1,35 @@
html, body {
width: 100%;
height: 100%;
}
body {
display: flex;
}
.leaflet-popup-content {
/*font-size: 13pt;*/
}
#map {
height: 100%;
width: 100%;
}
#dash {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.buttonscontainer {
display: flex;
flex-direction: row;
width: 100%;
}
.buttonscontainer > button {
margin: 3px;
width: 100%;
}

View File

@ -1,3 +0,0 @@
.leaflet-popup-content {
/*font-size: 13pt;*/
}

View File

@ -1,36 +1,29 @@
#!/usr/bin/lua
package.path = package.path..";/home/paul/git/micodus_server/html/?.lua"
local const = require("const")
local json = require("json")
local server = require("nginx.websocket.server")
local sqlite = require("lsqlite3")
local basepath = "/home/paul/git/micodus_server"
local dbfile = string.format("%s/data/tracker.db",basepath)
local query = [[
SELECT time,latitude,longitude,height,speed,direction,serial
FROM log
ORDER BY time DESC
LIMIT 1;
]]
--ngx.shared.geo:set("last_time","")
local db = sqlite.open(const.dbfile, sqlite.OPEN_READONLY)
function getdata()
local db = sqlite.open(dbfile,sqlite3.OPEN_READONLY)
local res, vm = db:nrows(query)
local data
local res, vm = db:nrows(const.query)
local data = {}
for row in res, vm do
data = {
["time"]=row.time,
["latitude"]=row.latitude,
["longitude"]=row.longitude,
["height"]=row.height,
["speed"]=row.speed,
["direction"]=row.direction,
["serial"]=row.serial,
["time"] = row.time,
["latitude"] = row.latitude,
["longitude"] = row.longitude,
["height"] = row.height,
["speed"] = row.speed,
["direction"] = row.direction,
["serial"] = row.serial,
}
end
db:close()
return data
end
@ -57,7 +50,7 @@ function geows()
end
last_time = data.time
end
ngx.sleep(1)
ngx.sleep(0.5)
end
wb:send_close()
end

103
src/db.rs
View File

@ -1,103 +0,0 @@
use rusqlite::{types::*, *};
const DBPATH: &'static str = "data/tracker.db";
const STATEMENTS: &'static [&str] = &[
"CREATE TABLE log (
id integer primary key autoincrement,
time text,
serial integer,
latitude float,
longitude float,
speed integer,
height integer,
direction integer,
is_satellite bool);",
"CREATE INDEX idx_time ON log (time);",
"CREATE INDEX idx_serial ON log (serial);",
];
const QUERY_INSERT: &'static str = "
INSERT INTO log (
time,
latitude,
longitude,
speed,
height,
direction,
serial,
is_satellite
)
VALUES (
:time,
:latitude,
:longitude,
:speed,
:height,
:direction,
:serial,
:is_satellite
)";
pub fn connectdb() -> Result<Connection> {
let conn = Connection::open(DBPATH)?;
Ok(conn)
}
pub fn initdb(conn: &Connection) -> Result<()> {
create_tables(&conn)?;
set_pragmas(&conn)?;
Ok(())
}
fn create_tables(conn: &Connection) -> Result<()> {
for s in STATEMENTS {
match conn.execute(s, ()) {
Ok(_) => {}
Err(err) => println!("update failed: {}", err),
}
}
Ok(())
}
fn set_pragmas(conn: &Connection) -> Result<()> {
conn.pragma_update(Some(DatabaseName::Main), "journal_mode", "WAL")?;
Ok(())
}
pub fn prepare_insert(conn: &Connection) -> Statement {
conn.prepare(QUERY_INSERT).unwrap()
}
pub fn insert(conn: &Connection, dblog: &DbLog) -> Result<()> {
let mut stmt = prepare_insert(&conn);
match stmt.execute(params![
dblog.time,
dblog.latitude,
dblog.longitude,
dblog.speed,
dblog.height,
dblog.direction,
dblog.serial,
dblog.is_satellite,
]) {
Ok(i) => println!("{} rows were inserted", i),
Err(err) => println!("insert failed: {}", err),
}
Ok(())
}
pub struct DbLog {
pub time: String,
pub latitude: f64,
pub longitude: f64,
pub speed: u16,
pub height: u16,
pub direction: u16,
pub serial: u16,
pub is_satellite: bool,
}
impl ToSql for DbLog {
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
Ok(self.time.to_sql().unwrap())
}
}

112
src/db/libsql_engine.rs Normal file
View File

@ -0,0 +1,112 @@
use libsql::*;
impl LibSQLEngine {
pub const TABLE_STATEMENTS: &'static [&str] = &[
"CREATE TABLE log (
id integer primary key autoincrement,
time text,
serial integer,
latitude float,
longitude float,
speed integer,
height integer,
direction integer,
is_satellite bool
);",
"CREATE INDEX idx_time ON log (time);",
"CREATE INDEX idx_serial ON log (serial);",
];
pub const QUERY_INSERT: &'static str = "
INSERT INTO log (
time,
latitude,
longitude,
speed,
height,
direction,
serial,
is_satellite
)
VALUES (
:time,
:latitude,
:longitude,
:speed,
:height,
:direction,
:serial,
:is_satellite
);";
pub const DBPATH: &'static str = "data/tracker.db";
async fn initdb(&self) {
self.create_tables().await;
}
async fn create_tables(&self) -> Result<()> {
for stmt in Self::TABLE_STATEMENTS {
match self.conn.as_ref().unwrap().execute(stmt, ()).await {
Ok(_) => {}
Err(err) => println!("update failed: {}", err),
}
}
Ok(())
}
async fn prepare_insert(&self) -> Statement {
self.conn
.as_ref()
.unwrap()
.prepare(Self::QUERY_INSERT)
.await
.unwrap()
}
}
impl super::Engine for LibSQLEngine {
#[tokio::main]
async fn connect(&mut self) {
println!("{}", self.path.clone().unwrap_or("".to_string()));
let path = match self.path.clone() {
Some(o) => o,
None => Self::DBPATH.to_string(),
};
let b = Builder::new_remote_replica(path, "".into(), "".into())
.build()
.await
.unwrap();
self.conn = Some(b.connect().unwrap());
self.initdb().await
}
#[tokio::main]
async fn insert(&mut self, dblog: &DbLog) {
let mut stmt = self.prepare_insert().await;
match stmt
.execute(params![
dblog.time.clone(),
dblog.latitude,
dblog.longitude,
dblog.speed,
dblog.height,
dblog.direction,
dblog.serial,
dblog.is_satellite,
])
.await
{
Ok(i) => println!("{} rows were inserted", i),
Err(err) => println!("insert failed: {}", err),
}
}
}
#[derive(Default, Clone)]
pub struct LibSQLEngine {
pub conn: Option<Connection>,
pub path: Option<String>,
pub url: Option<String>,
pub token: Option<String>,
}

77
src/db/mod.rs Normal file
View File

@ -0,0 +1,77 @@
//pub mod libsql_engine;
pub mod sqlite_engine;
pub enum SQLEngine {
SQLite(sqlite_engine::SQLiteEngine),
//LibSQL(libsql_engine::LibSQLEngine),
}
pub trait Engine {
fn connect(&mut self);
fn init(&mut self);
fn insert(&mut self, dblog: &DbLog);
}
impl Engine for SQLEngine {
fn connect(&mut self) {
match self {
Self::SQLite(engine) => engine.connect(),
//Self::LibSQL(engine) => engine.connect(),
}
}
fn init(&mut self) {
match self {
Self::SQLite(engine) => engine.init(),
//Self::LibSQL(engine) => engine.connect(),
}
}
fn insert(&mut self, dblog: &DbLog) {
match self {
Self::SQLite(engine) => engine.insert(&dblog),
//Self::LibSQL(engine) => engine.connect(),
}
}
}
impl From<sqlite_engine::SQLiteEngine> for SQLEngine {
fn from(store: sqlite_engine::SQLiteEngine) -> Self {
Self::SQLite(store)
}
}
/*impl From<libsql_engine::LibSQLEngine> for SQLEngine {
fn from(store: libsql_engine::LibSQLEngine) -> Self {
Self::LibSQL(store)
}
}*/
#[derive(Default, Clone)]
pub struct DbLog {
pub time: String,
pub latitude: f64,
pub longitude: f64,
pub speed: u16,
pub height: u16,
pub direction: u16,
pub serial: u16,
pub is_satellite: bool,
}
impl std::fmt::Display for DbLog {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} {} {} {} {} {} {} {} ",
self.time,
self.latitude,
self.longitude,
self.speed,
self.height,
self.direction,
self.serial,
self.is_satellite,
)
}
}

115
src/db/sqlite_engine.rs Normal file
View File

@ -0,0 +1,115 @@
use super::DbLog;
use rusqlite::{types::*, *};
impl SQLiteEngine {
pub const DBPATH: &'static str = "data/tracker.db";
pub const TABLE_STATEMENTS: &'static [&str] = &[
"CREATE TABLE IF NOT EXISTS log (
id integer primary key autoincrement,
time text,
serial integer,
latitude float,
longitude float,
speed integer,
height integer,
direction integer,
is_satellite bool
);",
"CREATE INDEX IF NOT EXISTS idx_time ON log (time);",
"CREATE INDEX IF NOT EXISTS idx_serial ON log (serial);",
];
pub const QUERY_INSERT: &'static str = "
INSERT INTO log (
time,
latitude,
longitude,
speed,
height,
direction,
serial,
is_satellite
)
VALUES (
:time,
:latitude,
:longitude,
:speed,
:height,
:direction,
:serial,
:is_satellite
);";
fn create_tables(&mut self) -> Result<()> {
for s in Self::TABLE_STATEMENTS {
match self.conn.as_ref().unwrap().execute(s, ()) {
Ok(_) => {}
Err(err) => println!("update failed: {}", err),
}
}
Ok(())
}
fn set_pragmas(&self) -> Result<()> {
self.conn
.as_ref()
.unwrap()
.pragma_update(None, "journal_mode", "WAL")?;
Ok(())
}
fn prepare_insert(&self) -> Statement {
self.conn
.as_ref()
.unwrap()
.prepare(Self::QUERY_INSERT)
.unwrap()
}
}
impl ToSql for DbLog {
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
Ok(self.time.to_sql().unwrap())
}
}
impl super::Engine for SQLiteEngine {
fn connect(&mut self) {
let path = match self.path.clone() {
Some(o) => o,
None => Self::DBPATH.to_string(),
};
self.conn = Some(Connection::open(path).unwrap());
}
fn init(&mut self) {
self.create_tables().unwrap();
self.set_pragmas().unwrap();
}
fn insert(&mut self, dblog: &DbLog) {
let mut stmt = self.prepare_insert();
match stmt.execute(params![
dblog.time,
dblog.latitude,
dblog.longitude,
dblog.speed,
dblog.height,
dblog.direction,
dblog.serial,
dblog.is_satellite,
]) {
Ok(_) => {}
Err(err) => println!("insert failed: {}", err),
}
}
}
#[derive(Default)]
pub struct SQLiteEngine {
pub conn: Option<Connection>,
pub path: Option<String>,
}

0
src/lib.rs Normal file
View File

View File

@ -1,23 +1,25 @@
mod db;
mod parser;
mod serve;
use crate::db::*;
use crate::parser::*;
use std::process::exit;
use std::io;
use tokio::net::TcpListener;
const ADDR: &'static str = "0.0.0.0";
const PORT: u64 = 7701;
const BUFSIZE: usize = 1024;
use db::sqlite_engine::SQLiteEngine;
use db::{SQLEngine::SQLite, *};
use serve::*;
#[tokio::main]
async fn main() {
//test();
let conn = connectdb().unwrap();
initdb(&conn).unwrap();
apiserver().await.unwrap();
println!(
"starting micodus_server version {}",
env!("CARGO_PKG_VERSION")
);
let mut s = SQLite(SQLiteEngine::default());
s.connect();
s.init();
let receiver = control_server().await;
micodus_protocol_server(receiver).await;
}
#[allow(dead_code)]
@ -26,87 +28,5 @@ fn test() {
let data: Vec<u8> = vec![0x36, 0x31, 0x33, 0x32, 0x31, 0x31, 0x38];
let code = BcdNumber::try_from(&data as &[u8]).unwrap();
println!("{code}");
std::process::exit(0);
}
async fn apiserver() -> io::Result<()> {
let listener = match TcpListener::bind(format!("{}:{}", ADDR, PORT)).await {
Ok(o) => o,
Err(e) => {
println!("error: {e}");
std::process::exit(1);
}
};
loop {
let (socket, _remote_addr) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = vec![0; BUFSIZE];
'nest: loop {
#[allow(unused_variables)]
let terminal_id = 0;
match socket.readable().await {
Ok(_) => {
match socket.try_read(&mut buf) {
Ok(_) => match handle(&buf) {
Some(o) => match socket.try_write(&o) {
Ok(_) => {}
Err(e) => {
println!("error: {e}");
break 'nest;
}
},
None => {
break 'nest;
}
},
Err(e) => {
println!("error read: {e}");
if e.kind() == io::ErrorKind::WouldBlock {
continue 'nest;
}
}
};
}
Err(e) => {
println!("error socket readable: {e}");
}
}
match socket.writable().await {
Ok(_) => {}
Err(e) => {
println!("error socket writable: {e}")
}
}
}
});
}
}
fn handle(buf: &Vec<u8>) -> Option<Vec<u8>> {
let mut rawdata = InboundDataWrapper::new(buf.to_vec());
let reply = match parse_inbound_msg(&mut rawdata) {
Ok(o) => {
println!("query: {}", o);
//println!("raw query: {:X?}", o.to_raw());
Message::store(&o);
Message::set_reply(o)
}
Err(e) => {
println!("parse inbound message error: {}", e);
return None;
}
};
match reply {
Some(o) => {
println!("reply: {}", o);
//println!("raw reply {:X?}", o.to_raw());
println!("--------------");
return Some(o.to_raw().into());
}
None => {
return None;
}
}
exit(0);
}

51
src/old/misc.rs Normal file
View File

@ -0,0 +1,51 @@
async fn serve_old(stream: &TcpStream, recv_clone: &Arc<RwLock<Receiver<u8>>>) {
loop {
let ready = stream
.ready(Interest::READABLE | Interest::WRITABLE)
.await
.unwrap();
if ready.is_readable() {
let mut buf = vec![0; BUFSIZE];
match stream.try_read(&mut buf) {
Ok(_) => match handle(&buf) {
Some(o) => match stream.try_write(&o) {
Ok(_) => {}
Err(e) => {
println!("error: {e}");
break;
}
},
None => {
println!("none");
break;
}
},
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
println!("{e}");
continue;
}
Err(e) => {
println!("{e}");
break;
}
}
}
if ready.is_writable() {
let mut recv = recv_clone.write().await;
if !recv.is_empty() {
match recv.recv().await {
Some(o) => match o {
1 => {
//socket.try_write(&[o, 0x01]).unwrap();
println!("sent");
}
_ => {}
},
None => {}
}
}
}
}
}

View File

@ -376,7 +376,6 @@ impl LocationInformationReport {
fn parse_time(&mut self, timeslice: [u8; 6]) {
let code = BcdNumber::try_from(&timeslice as &[u8]).unwrap();
println!("{}", code);
let time = format!("{}", code.to_u64().unwrap());
match NaiveDateTime::parse_from_str(time.as_str(), "%y%m%d%H%M%S") {
Ok(o) => {
@ -521,6 +520,26 @@ impl From<u32> for LocationInformationReportStatus {
}
}
#[derive(Default, Debug, Clone)]
pub struct LocationInformationQuery {}
impl LocationInformationQuery {
pub const ID: u16 = 0x8201;
}
impl BodyMessage for LocationInformationQuery {
fn parse(&mut self, rawbody: &Vec<u8>) {}
}
#[derive(Default, Debug, Clone)]
pub struct LocationInformationQueryResponse {}
impl LocationInformationQueryResponse {
pub const ID: u16 = 0x0201;
}
impl BodyMessage for LocationInformationQueryResponse {
fn parse(&mut self, rawbody: &Vec<u8>) {}
}
#[derive(Default, Debug, Clone)]
pub struct StartOfTrip {}
impl StartOfTrip {
@ -559,6 +578,7 @@ macro_rules! generate_impl {
rawbody.into_iter()
}
}*/
#[allow(unused)]
impl $t {
pub fn new(rawbody: &Vec<u8>) -> Self {
let mut res = Self::default();
@ -583,6 +603,8 @@ generate_impl!(
QueryTerminalParameterResponse,
TerminalControl,
LocationInformationReport,
LocationInformationQuery,
LocationInformationQueryResponse,
StartOfTrip,
EndOfTrip
);

View File

@ -1,5 +1,8 @@
#![allow(unused_variables)]
pub struct MessageError;
pub enum MessageError {
NotOurProtocolError,
BasicError,
}
impl std::fmt::Debug for MessageError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
@ -13,16 +16,23 @@ impl std::fmt::Display for MessageError {
}
}
pub struct NotOurProtocolError;
macro_rules! errorgen {
($t:ident,$msg:expr) => {
pub struct $t;
impl std::fmt::Debug for NotOurProtocolError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "invalid protocol")
}
impl std::fmt::Debug for $t {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, $msg)
}
}
impl std::fmt::Display for $t {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, $msg)
}
}
};
}
impl std::fmt::Display for NotOurProtocolError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "invalid protocol")
}
}
errorgen!(NotOurProtocolError, "invalid protocol");
errorgen!(BasicError, "basic error");

View File

@ -89,7 +89,7 @@ impl std::fmt::Display for MessageHeader {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"id: {:X?}, length: {}, terminal id: {}, serial: {:X?}",
"id: {:#06x}, length: {}, terminal id: {}, serial: {:X?}",
self.id, self.bodylength, self.terminal_id, self.serial_number
)
}

View File

@ -7,7 +7,7 @@ use body::*;
use error::*;
use header::*;
use crate::db::*;
use super::db::*;
use std::collections::VecDeque;
@ -20,7 +20,7 @@ pub fn parse_inbound_msg(
let mut msg: Message = Message::default();
if rawdata.first_byte() != FLAG_DELIMITER {
return Err(MessageError);
return Err(MessageError::NotOurProtocolError);
}
match msg.parse_header(rawdata) {
@ -29,7 +29,7 @@ pub fn parse_inbound_msg(
}
Err(e) => {
println!("error parsing header {e}");
return Err(MessageError);
return Err(MessageError::BasicError);
}
};
@ -253,100 +253,91 @@ impl Message {
}
}
pub fn set_reply(inmsg: Message) -> Option<Message> {
pub fn send_reply(inmsg: Option<Message>) -> Option<Message> {
let mut reply: Message = Message::default();
let terminal_id = inmsg.header.get_raw_terminal_id().clone();
match inmsg.content {
MessageType::TerminalRegistration(t) => {
let cnt = t.generate_reply(terminal_id.into(), inmsg.header.serial_number);
reply.header.build(
TerminalRegistrationReply::ID,
cnt.to_raw().len(),
terminal_id,
);
reply.content = MessageType::TerminalRegistrationReply(cnt);
}
MessageType::TerminalAuthentication(t) => {
let cnt = t.generate_reply(TerminalAuthentication::ID, inmsg.header.serial_number);
reply.header.build(
PlatformUniversalResponse::ID,
cnt.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(cnt);
}
MessageType::LocationInformationReport(t) => {
let cnt =
t.generate_reply(LocationInformationReport::ID, inmsg.header.serial_number);
reply.header.build(
PlatformUniversalResponse::ID,
cnt.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(cnt);
}
MessageType::TerminalHeartbeat(t) => {
let cnt = t.generate_reply(TerminalHeartbeat::ID, inmsg.header.serial_number);
reply.header.build(
PlatformUniversalResponse::ID,
cnt.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(cnt);
}
MessageType::TerminalLogout(t) => {
let cnt = t.generate_reply(TerminalHeartbeat::ID, inmsg.header.serial_number);
reply.header.build(
PlatformUniversalResponse::ID,
cnt.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(cnt);
}
_ => {
println!("no type");
return None;
match inmsg {
Some(inmsg) => {
let terminal_id = inmsg.header.get_raw_terminal_id().clone();
match inmsg.content {
MessageType::TerminalRegistration(t) => {
let content =
t.generate_reply(terminal_id.into(), inmsg.header.serial_number);
reply.header.build(
TerminalRegistrationReply::ID,
content.to_raw().len(),
terminal_id,
);
reply.content = MessageType::TerminalRegistrationReply(content);
}
MessageType::TerminalAuthentication(t) => {
let content = t
.generate_reply(TerminalAuthentication::ID, inmsg.header.serial_number);
reply.header.build(
PlatformUniversalResponse::ID,
content.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(content);
}
MessageType::LocationInformationReport(t) => {
let content = t.generate_reply(
LocationInformationReport::ID,
inmsg.header.serial_number,
);
reply.header.build(
PlatformUniversalResponse::ID,
content.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(content);
}
MessageType::TerminalHeartbeat(t) => {
let content =
t.generate_reply(TerminalHeartbeat::ID, inmsg.header.serial_number);
reply.header.build(
PlatformUniversalResponse::ID,
content.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(content);
}
MessageType::TerminalLogout(t) => {
let content =
t.generate_reply(TerminalHeartbeat::ID, inmsg.header.serial_number);
reply.header.build(
PlatformUniversalResponse::ID,
content.to_raw().len(),
terminal_id,
);
reply.content = MessageType::PlatformUniversalResponse(content);
}
_ => {
println!("no type");
return None;
}
}
}
None => {}
}
reply.outbound_finalize();
Some(reply)
}
pub fn store(inmsg: &Message) {
match inmsg.content {
MessageType::LocationInformationReport(ref t) => {
/*{
use std::fs::OpenOptions;
use std::io::prelude::*;
let mut file = OpenOptions::new()
.write(true)
.append(true)
.open("data/log.txt")
.unwrap();
//if let Err(e) = writeln!(file, ) {
// eprintln!("Couldn't write to file: {}", e);
//}
file.write(format!("{},{},{}\n", t.time, t.latitude, t.longitude).as_bytes())
.unwrap();
}*/
let conn = connectdb().unwrap();
let dblog = crate::db::DbLog {
serial: inmsg.header.serial_number,
time: t.time.format("%Y-%m-%d %H:%M:%S").to_string(),
latitude: t.latitude,
longitude: t.longitude,
speed: t.speed,
height: t.height,
direction: t.direction,
is_satellite: t.is_satellite(),
};
insert(&conn, &dblog).unwrap();
}
_ => {}
}
pub fn store(inmsg: &Message) -> Option<DbLog> {
let res = match inmsg.content {
MessageType::LocationInformationReport(ref t) => Some(DbLog {
serial: inmsg.header.serial_number,
time: t.time.format("%Y-%m-%d %H:%M:%S").to_string(),
latitude: t.latitude,
longitude: t.longitude,
speed: t.speed,
height: t.height,
direction: t.direction,
is_satellite: t.is_satellite(),
}),
_ => None,
};
res
}
pub fn outbound_finalize(&mut self) {
@ -457,3 +448,22 @@ impl std::fmt::Display for MessageType {
res
}
}
/*
{
use std::fs::OpenOptions;
use std::io::prelude::*;
let mut file = OpenOptions::new()
.write(true)
.append(true)
.open("data/log.txt")
.unwrap();
//if let Err(e) = writeln!(file, ) {
// eprintln!("Couldn't write to file: {}", e);
//}
file.write(format!("{},{},{}\n", t.time, t.latitude, t.longitude).as_bytes())
.unwrap();
}
*/

186
src/serve.rs Normal file
View File

@ -0,0 +1,186 @@
use crate::db::sqlite_engine::SQLiteEngine;
use crate::db::{SQLEngine::*, *};
use crate::parser::*;
use std::net::SocketAddr;
use std::process::exit;
use std::{collections::HashMap, io, sync::Arc};
use tokio::net::{TcpListener, TcpSocket, TcpStream};
use tokio::sync::{
mpsc::{channel, Receiver, Sender},
RwLock,
};
const MICODUS_ADDR: &'static str = "0.0.0.0";
const MICODUS_PORT: u64 = 7701;
const BUFSIZE: usize = 1024;
const CTRL_ADDR: &'static str = "127.0.0.1";
const CTRL_PORT: u64 = 7702;
pub async fn control_server() -> Receiver<u8> {
let listener = TcpListener::bind(format!("{}:{}", CTRL_ADDR, CTRL_PORT))
.await
.unwrap();
let (sender, receiver): (Sender<u8>, Receiver<u8>) = channel(100);
tokio::spawn(async move {
loop {
let (socket, _) = listener.accept().await.unwrap();
socket.readable().await.unwrap();
let mut buf = [0; 16];
match socket.try_read(&mut buf) {
Ok(0) => {}
Ok(n) => {
let mut data = str::from_utf8(&buf).unwrap();
data = data.trim();
if data.starts_with("test") {
sender.send(1).await.unwrap();
println!("ok");
} else {
println!("n: {n}, msg: '{data}'");
}
}
Err(e) => {
println!("{e}")
}
}
}
});
receiver
}
pub async fn micodus_protocol_server(receiver: Receiver<u8>) {
let mut sql = SQLite(SQLiteEngine::default());
sql.connect();
let recv = Arc::new(RwLock::new(receiver));
let listen_addr_str = format!("{}:{}", MICODUS_ADDR, MICODUS_PORT);
let listen_addr: SocketAddr = listen_addr_str.parse().unwrap();
let socket = TcpSocket::new_v4().unwrap();
socket.set_keepalive(true).unwrap();
socket.set_reuseaddr(true).unwrap();
socket.bind(listen_addr).unwrap();
let listener = match socket.listen(1024) {
Ok(l) => l,
Err(e) => {
println!("error: {e}");
exit(1);
}
};
loop {
let recv = Arc::clone(&recv);
let (stream, _remote_addr) = listener.accept().await.unwrap();
println!("accept");
tokio::spawn(async move {
serve(&stream, &recv).await;
});
}
}
async fn serve(stream: &TcpStream, recv_clone: &Arc<RwLock<Receiver<u8>>>) {
loop {
let mut recv = recv_clone.write().await;
tokio::select! {
o = stream.readable() => {
match o {
Ok(_) => {
let mut buf = vec![0; BUFSIZE];
match stream.try_read(&mut buf) {
Ok(_) => match handle(&buf) {
Some(o) => match stream.try_write(&o) {
Ok(_) => {}
Err(e) => {
println!("error: {e}");
break;
}
},
None => {
println!("none");
break;
}
},
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
continue;
}
Err(e) => {
println!("{e}");
break;
}
}}
Err(e)=> { println!("error socket readable: {e}");}
}
},
b = recv.recv() => {
match b {
Some(res) => {
println!("test recv {res}");
},
None => {},
}
},
else => break,
}
//if ready.is_writable() {
// if !receiver.is_empty() {
// println!("test2");
// match receiver.recv().await {
// Some(o) => match o {
// 1 => {
// //socket.try_write(&[o, 0x01]).unwrap();
// println!("sent");
// }
// _ => {}
// },
// None => {}
// }
// }
//}
}
}
fn handle(buf: &Vec<u8>) -> Option<Vec<u8>> {
let mut rawdata = InboundDataWrapper::new(buf.to_vec());
//let sql = Arc::clone(s);
let reply = match parse_inbound_msg(&mut rawdata) {
Ok(o) => {
println!("query: {}", o);
//println!("raw query: {:X?}", o.to_raw());
match Message::store(&o) {
Some(log) => {
let mut s = SQLite(SQLiteEngine::default());
s.connect();
s.insert(&log);
}
None => {}
};
Message::send_reply(Some(o))
}
Err(e) => {
println!("parse inbound message error: {}", e);
return None;
}
};
match reply {
Some(o) => {
println!("reply: {}", o);
//println!("raw reply {:X?}", o.to_raw());
println!("--------------");
return Some(o.to_raw().into());
}
None => {
return None;
}
}
}
#[allow(unused)]
pub struct ControllerMap(HashMap<String, (Sender<u8>, Receiver<u8>)>);

10
html/lastloc.lua → tools/sql.lua Normal file → Executable file
View File

@ -1,10 +1,9 @@
#!/usr/bin/lua
--ngx.say(_VERSION)
local sqlite = require("lsqlite3")
local basepath = "/home/paul/git/micodus_server"
local dbfile = string.format("%s/data/tracker.db",basepath)
--local output = string.format("%s/html/lastloc.json",basepath)
local output = string.format("%s/html/lastloc.json",basepath)
local query = [[
SELECT latitude,longitude
FROM log
@ -13,14 +12,13 @@ local query = [[
]]
function main()
local db = sqlite.open(dbfile,sqlite3.OPEN_READONLY)
local db = sqlite.open(dbfile)
local res, vm = db:nrows(query)
for row in res, vm do
local locstr = string.format("{\"latitude\": %s, \"longitude\": %s}", row.latitude, row.longitude)
--f = io.open(output, "w")
--f:write(locstr)
ngx.say(locstr)
f = io.open(output, "w")
f:write(locstr)
end
db:close()
end

3
tools/systemd_run.sh Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env zsh
sudo systemd-run --uid=paul --working-directory=/home/paul/git/micodus_server ./target/release/micodus_server