From b7fa894b0dc2859c32dddf77655b57086fe561fb Mon Sep 17 00:00:00 2001
From: Eike Baran <eike.baran@uni-oldenburg.de>
Date: Sat, 15 Aug 2015 23:25:05 +0200
Subject: [PATCH] completely rewrite of nodewatcher2 due to the unbearable an
 unmaintainable pseudo-array code for emulating bash-arrays.

nodewatcher2 is now written in lua and can be configured by uci. It outputs json- and xml-files containing the nodedata. Optionally those files can
additionally be gzipped to save some traffic. Test showed gzipping costs nearly no time and reduces the size of the json- and xml-files by about 60%)

The nodewatcher2 divides its operation into two parts:

1) At first the lua-script collects all data it needs from the the system and puts it in a lua-table
2) The script now uses this lua-table to generate a abitrary set of output files by using it's defined and enabled output generators (xml, json,...)

The generated files then are put in /tmp/nodedata, which is symlinked into the /nodedata-url of the http-server of the gluon status-page

TODO:
- make nodewatcher-compat generator
- remove files whose generator was disabled via uci in the mean time
---
 nodewatcher2/Makefile                         |   5 +-
 .../files/etc/config/nodewatcher2.config      |   5 +
 .../lib/ffnw/nodewatcher2/nodewatcher.lua     | 287 ++++++++++++++
 .../lib/ffnw/nodewatcher2/nodewatcher.sh      | 254 ------------
 .../files/lib/gluon/cron/nodewatcher2         |   2 +-
 nodewatcher2/files/usr/lib/lua/dkjson.lua     | 369 ++++++++++++++++++
 6 files changed, 666 insertions(+), 256 deletions(-)
 create mode 100644 nodewatcher2/files/etc/config/nodewatcher2.config
 create mode 100755 nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.lua
 delete mode 100755 nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.sh
 create mode 100644 nodewatcher2/files/usr/lib/lua/dkjson.lua

diff --git a/nodewatcher2/Makefile b/nodewatcher2/Makefile
index dffe4d7..e4ebf87 100644
--- a/nodewatcher2/Makefile
+++ b/nodewatcher2/Makefile
@@ -17,7 +17,7 @@ endef
 define Package/ffnw-nodewatcher2/description
 	Provides an xml-File containing the most important
 	informations about the router. Nodewatcher2 is meant to
-	be more expansable for other export-formats in the future
+	be more expandable for other export-formats in the future
 	by internally seperating retrieving and outputting the router-data
 endef
 
@@ -36,6 +36,9 @@ define Package/ffnw-nodewatcher/install
 	$(INSTALL_DATA) files/lib/gluon/cron/nodewatcher2 $(1)/lib/gluon/cron/nodewatcher2
 	$(INSTALL_DIR) $(1)/lib/ffnw/nodewatcher2/
 	$(INSTALL_BIN) files/lib/ffnw/nodewatcher2/nodewatcher.sh $(1)/lib/ffnw/nodewatcher2/
+	$(INSTALL_DIR) $(1)/usr/lib/lua/
+	$(INSTALL_DATA) files/usr/lib/lua/dkjson.lua $(1)/usr/lib/lua/
+	$(INSTALL_DATA) files/etc/config/nodewatcher2 $(1)/etc/config/nodewatcher2
 endef
 
 $(eval $(call BuildPackage,ffnw-nodewatcher2))
diff --git a/nodewatcher2/files/etc/config/nodewatcher2.config b/nodewatcher2/files/etc/config/nodewatcher2.config
new file mode 100644
index 0000000..f60f95f
--- /dev/null
+++ b/nodewatcher2/files/etc/config/nodewatcher2.config
@@ -0,0 +1,5 @@
+config 'prefs' 'prefs' 
+        option 'generate_xml'   '1'
+        option 'generate_json'  '1'
+        option 'xml_data_file'  '/tmp/node2data.xml'
+        option 'json_data_file' '/tmp/node2data.json'
diff --git a/nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.lua b/nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.lua
new file mode 100755
index 0000000..8a1a44c
--- /dev/null
+++ b/nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.lua
@@ -0,0 +1,287 @@
+#!/usr/bin/env lua
+
+
+local json = require ("dkjson")
+--require("uci")
+
+local uci = require("uci").cursor()
+
+
+data ={}
+
+function file_exists(name)
+   local f=io.open(name,"r")
+   if f~=nil then io.close(f) return true else return false end
+end
+
+local function isArray(t)
+  local i = 0
+  for _ in pairs(t) do
+      i = i + 1
+      if t[i] == nil then return false end
+  end
+  return true
+end
+
+function xmlEncode(tbl,name,layer)
+    local out=""
+    if layer==1 then out=out.."<?xml version='1.0' ?>\n" end
+    for i=1,layer do out=out..'\t' end
+    out=out.."<"..name..">"
+    if type(tbl)=="table" then
+
+        out=out..'\n'
+        local akey=nil
+        if isArray(tbl) then
+            akey=name:sub(0,-2)
+        end	
+        for k,v in pairs(tbl) do
+            out=out..(xmlEncode(v,akey or k,layer+1))
+        end
+        for i=1,layer do out=out..'\t' end
+    else
+        out=out..(tostring(tbl))
+    end
+    out=out.."</"..name..">\n"
+    return out
+end
+
+function linesToTable(lines)
+    if lines==nil then
+        return {}
+    end
+    local tab = {}
+    for line in lines:lines() do
+        table.insert (tab, line);
+    end
+   -- if next(tab) == nil then
+   --     return nil
+   -- end
+    return tab
+end
+
+function readFile(filepath)
+    local file = io.open(filepath, "r");
+    return linesToTable(file)
+end
+
+function readOutput(command)
+
+    local file = io.popen(command)
+    return linesToTable(file)
+end
+
+function readFirstRow(tbl)
+   if next(tbl)~=nil then --and table.getn(tbl)>0 then
+       return tbl[1]
+   else
+       return nil
+   end
+end
+
+
+function fetchMemory()
+    local tmp=readOutput("cat /proc/meminfo | grep -E '^(MemFree|MemTotal|Cached|Buffers)'")
+    local memLookup={MemT="total",MemF="free",Buff="buffer",Cach="cache"}
+    data.memory={}
+    for k,v in pairs(tmp) do 
+        local t={string.match(v,"(.*) (%d+) .*")}
+        --print(t[1])
+        key=memLookup[string.sub(v,1,4)]
+        if key~=nil then
+            data.memory[key]=tonumber(t[2])
+        end
+    end
+end
+
+function fetchTimes()
+    data.times={}
+    data.times.up,data.times.idle=string.match(readFile("/proc/uptime")[1],"(.+) (.+)")
+    for k,v in pairs(data.times) do 
+        data.times[k]=math.floor(tonumber(v)*1000) 
+    end
+end
+
+function fetchPositions()
+    data.position={}
+    data.position.lon=readFirstRow(readOutput("uci get gluon-node-info.@location[0].longitude 2>/dev/null"))
+    data.position.lat=readFirstRow(readOutput("uci get gluon-node-info.@location[0].latitude 2>/dev/null"))
+    if next(data.position)==nil then data.position=nil end
+end
+
+function fetchSoftware()
+    data.software={}
+    data.software.firmware=readFile("/lib/gluon/release")[1]
+    data.software.kernel=readOutput("uname -r")[1]
+    data.software.mesh="B.A.T.M.A.N. "..(readFile("/sys/module/batman_adv/version")[1])
+    data.software.vpn=readOutput("fastd -v")[1]
+--    if readFirstRow(getOutput("uci get autoupdater.settings.enabled 2>/dev/null")) == "1" then
+--       data.software.autoupdate=readFirstRow(getOutput("uci get autoupdater.settings.branch 2>/dev/null"))
+--    end
+end
+
+function fetchOriginators()
+    data.originators={}
+    local tmp=readOutput("batctl o|sed -r 's/([0-9a-f:]+)[[:space:]]+([0-9.]+)s[[:space:]]+\\([[:space:]]*([0-9]{1,3})\\)[[:space:]]+([0-9a-f:]+)[[:space:]]+\\[[[:space:]]*(.+)\\]:.*/\\1 \\2 \\3 \\4 \\5/;tx;d;:x'")
+    for k,v in pairs(tmp) do
+        local o={}
+        local m={}
+        for v1 in string.gmatch(v,"[^ ]+") do
+            table.insert(m,v1)
+        end
+        o.mac=m[1]
+        o.nexthop=m[4]
+        o.linkquality=tonumber(m[3])
+        o.lastseen=math.floor(tonumber(m[2])*1000)
+    --    print(o.mac.."->"..o.nexthop)
+        if o.mac==o.nexthop then
+                table.insert(data.originators,o);
+        end
+
+        
+    end
+end
+
+function fetchInterfaces()
+    data.interfaces={}
+    for _,iface in pairs(readOutput("grep -E 'up|unknown' /sys/class/net/*/operstate")) do
+     --  print(iface)
+        i={}
+        ipath,i.name=string.match(iface,"(.+/(.-))/operstate.*")
+    --     print(ipath.." jjjjj "..i.name)
+        if i.name~="lo" then
+   --    print(ipath.." jjjjj "..i.name)
+            i.ipv6={}
+            i.traffic={}
+            i.traffic.rx=readFirstRow(readFile(ipath.."/statistics/rx_bytes"))
+            i.traffic.tx=readFirstRow(readFile(ipath.."/statistics/tx_bytes"))
+            -- general interface info
+            for _,ipl in pairs(readOutput("ip addr show "..i.name)) do
+                local match=ipl:match("%s*inet6 ([0-9a-f:]+)(/%d+) .*")
+                --ugly cascading if-else-if-... because of lua missing continue-command
+                if match~=nil then
+                    table.insert(i.ipv6,match)
+                    --i.mac=match
+                else
+                    match=ipl:match("%s*link/ether (.-) .*")
+                    if match ~=nil then 
+                        i.mac=match
+                    else    
+                        match=ipl:match("%s*inet ([0-9.]+)(/%d+) .*")
+                        if match~=nil then
+                            i.ipv4=match
+                        else
+                            match=ipl:match(".* mtu (%d+) .*")
+                            if match~=nil then
+                                i.mtu=tonumber(match)
+                            end
+                        end
+                    end
+                end
+            end
+            if next(i.ipv6)==nil then i.ipv6=nil end
+            -- wifi info
+            i.radio={}
+            for _,ipl in pairs(readOutput("iwinfo "..i.name.." info 2>/dev/null")) do
+           --     print(ipl)
+                local match=ipl:match('ESSID:%s+"(.*)".*')
+                if match~=nil then
+                    i.radio.essid=match
+                else
+                    match=ipl:match('Access Point:%s+([A-Za-z0-9:]+).*')
+                    if match~=nil then
+                        i.radio.bssid=match:lower()
+                    else    
+                        match={ipl:match('Tx%-Power: ([0-9]+) dBm  Link Quality: ([a-z0-9]+/[0-9]+)')}
+                        
+                        if next(match)~=nil then
+                            i.radio.txpower=tonumber(match[1])                    
+                            i.radio.linkquality=match[2]
+                        end
+                    end
+                end
+            end
+            if next(i.radio)==nil then i.radio=nil end 
+            --batman?
+            if i.name~="bat0" then
+                local bat=readFirstRow(readFile("/sys/class/net/"..i.name.."/batman_adv/iface_status"))
+                i.meshstatus=(bat~=nil and bat~="not in use")
+            else
+                i.meshstatus=false
+            end
+            table.insert(data.interfaces,i)
+        end
+    end 
+    
+end
+
+function fetchGateways()
+    data.gateways={}
+    for _,v in pairs(readOutput("batctl gwl|sed -r 's/^[[:space:]]+([a-f0-9:].*)/false \\1/ ; s/^=>(.*)/true \\1/ ; s/(true|false)[[:space:]]+([0-9a-f:]+)[[:space:]]+\\([[:space:]]*([0-9]+)\\)[[:space:]]+[a-f0-9:]+[[:space:]]+\\[[[:space:]]*(.+)\\]:[[:space:]]+([0-9.\\/]+).*$/\\1 \\2 \\3 \\4 \\5/;tx;d;:x'")) do 
+        local g={}
+        local m={}
+        for v1 in string.gmatch(v,"[^ ]+") do
+            table.insert(m,v1)
+        end
+        g.active=(m[1] == "true")
+        g.mac=m[2]
+        g.linkquality=tonumber(m[3])
+        g.interface=m[4]
+        g.class=m[5]
+        table.insert(data.gateways,g)
+    end
+end
+
+
+-- do the fetching
+data.hostname=readFile("/etc/hostname")[1]
+data.client_count=tonumber(readOutput("echo '5'")[1]);
+fetchTimes()
+fetchMemory()
+fetchPositions()
+fetchSoftware()
+fetchOriginators()
+fetchInterfaces()
+fetchGateways()
+
+local jsonString=json.encode (data, { indent = true })
+
+
+function writeToFile(string,filename)
+    local path=uci:get("nodewatcher2","prefs","destination_folder")
+    if path == nil then
+        return
+    end
+    os.execute('if [[ ! -d "'..path..'" ]] ; then mkdir -p "'..path..'" ; fi')
+    os.execute('if [[ ! -h /lib/gluon/status-page/www/nodedata ]] ; then ln -s '..path..' /lib/gluon/status-page/www/nodedata ; fi')
+    path=path.."/"..filename
+    local file = io.open(path, "w")
+    file:write(string)
+    file:close()
+    if uci:get("nodewatcher2","prefs","enable_gzip") == "1" then
+        os.execute("echo '"..string.."' |gzip > "..path..".gz") 
+    end
+end
+
+function generateXml()
+    if uci:get("nodewatcher2","prefs","generate_xml") == "1" then
+        local string=xmlEncode(data,"data",1)
+        writeToFile(string,"nodedata.xml")
+    end
+end
+
+function generateJson()
+    if uci:get("nodewatcher2","prefs","generate_json") == "1" then
+        local string=json.encode (data, { indent = true })
+        writeToFile(string,"nodedata.json")
+        
+    end
+end
+
+generateXml()
+
+generateJson()
+
+
+
+
diff --git a/nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.sh b/nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.sh
deleted file mode 100755
index a1510bf..0000000
--- a/nodewatcher2/files/lib/ffnw/nodewatcher2/nodewatcher.sh
+++ /dev/null
@@ -1,254 +0,0 @@
-#!/bin/sh
-#OUTPUT_DATA_FILE="$(uci get nodewatcher2.@script[0].data_file)"
-
-hostname="$(cat /proc/sys/kernel/hostname)"
-times_up="$(sed -r 's/([0-9.]+) ([0-9.]+)/\1/' /proc/uptime)"
-times_idle="$(sed -r 's/([0-9.]+) ([0-9.]+)/\2/' /proc/uptime)"
-
-position_lon="$(uci get gluon-node-info.@location[0].longitude)"
-position_lat="$(uci get gluon-node-info.@location[0].latitude)"
-clientcount="$(($(batctl tl|wc -l) - 3))"
-model="$(grep -E '^machine' /proc/cpuinfo|sed -r 's/machine[[:space:]]*:[[:space:]]*(.*)/\1/')"
-mac="$(ifconfig |grep bat0|sed -r 's/.*HWaddr ([0-9A-F:]*)[[:space:]]*/\1/'|tr '[ABCDEF]' '[abcdef]')"
-
-memory_total="$(grep -E "^MemTotal:" /proc/meminfo|sed -r 's/.*:[[:space:]]+([0-9]+) kB/\1/')"
-memory_free="$(grep -E "^MemFree:" /proc/meminfo|sed -r 's/.*:[[:space:]]+([0-9]+) kB/\1/')"
-memory_cache="$(grep -E "^Cached:" /proc/meminfo|sed -r 's/.*:[[:space:]]+([0-9]+) kB/\1/')"
-memory_buffer="$(grep -E "^Buffers:" /proc/meminfo|sed -r 's/.*:[[:space:]]+([0-9]+) kB/\1/')"
-
-set -- $(sed -r "s/[0-9.]+ [0-9.]+ ([0-9.]+) ([0-9]+)\/([0-9]+) [0-9]+/\1 \2 \3/" /proc/loadavg)
-processes_total=$3
-processes_runnable=$2
-processes_loadavg=$1
-
-software_firmware="$(cat /lib/gluon/release)"
-software_kernel="$(uname -r)"
-software_mesh="B.A.T.M.A.N. $(cat /sys/module/batman_adv/version)"
-software_vpn="$(fastd -v)"
-
-software_autoupdate_enabled="$(uci get autoupdater.settings.enabled 2>/dev/null)"
-software_autoupdate_branch="$(uci get autoupdater.settings.branch 2>/dev/null)"
-
-
-
-ifc=0
-for filename in `grep 'up\|unknown' /sys/class/net/*/operstate`; do
-	ifpath=${filename%/operstate*}
-	iface=${ifpath#/sys/class/net/}
-	if [ "$iface" = "lo" ]; then
-		continue
-	fi
-	eval interface${ifc}_name="\"$iface\""
-
-	local addrs="$(ip addr show dev ${iface})"
-	eval interface${ifc}_mtu=$(echo \"$addrs\" | grep -E "mtu"|sed -r "s/.* mtu ([0-9]+) .*/\1/")
-	eval interface${ifc}_mac=$(echo \"$addrs\" | grep -E "link/ether"|sed -r "s/.* link\/ether ([0-9a-f:]+) .*/\1/")
-
-	eval "interface${ifc}_traffic_rx=\"$(cat $ifpath/statistics/rx_bytes)\""
-	eval "interface${ifc}_traffic_tx=\"$(cat $ifpath/statistics/tx_bytes)\""
-#	echo -e $addrs | grep -E 'inet '|sed -r 's/.* inet ([0-9.]+)(\/[0-9]+)? .*/\1/'
-  	eval "interface${ifc}_ipv4=\"$(echo -e \"$addrs\" | grep -E 'inet '|sed -r 's/.* inet ([0-9.]+)(\/[0-9]+)? .*/\1/')\""
-#	eval "echo \"ip: \$interface${ifc}_ipv4\""
-	local ipv6_adresses=$(echo "$addrs" | grep -E 'inet6 ' |sed -r 's/[[:space:]]*inet6 (([0-9a-f:]+)(\/[0-9]*)?) .*/\2/')
-	local ipc=0
-#	echo $ipv6_adresses
-	for ip in $ipv6_adresses ;do
-		echo "--"$ip
-		eval "interface${ifc}_ipv6_${ipc}=\"$ip\""
-		ipc=$(($ipc+1)) 
-	done
-	eval "interface${ifc}_ipv6count=$ipc"
-	
-
-	if [ "$iface" != "bat0" ] ; then
-		local tmp=$(cat "/sys/class/net/$iface/batman_adv/iface_status")
-		if [ "$tmp" != "not in use" ] ; then
-			eval interface${ifc}_meshstatus=\"$tmp\"
-		fi
-	fi
-	
-
-	local iwi="$(iwinfo ${iface} info 2>/dev/null)"
-	if [ "$iwi" != "" ] ; then
-		eval "interface${ifc}_txpower=\"\$(echo \"${iwi}\"|grep 'Tx-Power'|sed -r 's/[[:space:]]+Tx-Power:[[:space:]]+([0-9]+).*/\1/')\""
-		eval "interface${ifc}_channel=\"\$(echo \"${iwi}\"|grep 'Channel: '|sed -r 's/.*Channel:[[:space:]]+([0-9]+).*/\1/')\""
-		eval "interface${ifc}_linkquality=\"\$(echo \"${iwi}\"|grep 'Link Quality: '|sed -r 's/.*Link Quality:[[:space:]]+(([0-9]+|unknown)\/[0-9]+).*/\1/')\"" 
-	fi
-	ifc=$(($ifc+1))
-done
-
-orc=0
-origs="$(batctl o|sed -r 's/([0-9a-f:]+)[[:space:]]+([0-9.]+)s[[:space:]]+\([[:space:]]*([0-9]{1,3})\)[[:space:]]+([0-9a-f:]+)[[:space:]]+\[[[:space:]]*(.+)\]:.*/\1 \2 \3 \4 \5/;tx;d;:x')"
-
-OIFS="$IFS"
-NIFS=$'\n'
-IFS="${NIFS}"
-#echo "$origs"
-for orig in $origs ; do
-	IFS="${OIFS}"
-	set -- $orig
-	eval "originator${orc}_mac=\"$1\""
-	eval "originator${orc}_linkquality=\"$3\""
-	eval "originator${orc}_lastseen=\"$2\""
-	eval "originator${orc}_nexthop=\"$4\""
-	eval "originator${orc}_interface=\"$5\""
-
- 	if eval "[ \"\${originator${orc}_mac}\" != \"\${originator${orc}_nexthop}\" -o \"\${originator${orc}_interface}\" == \"mesh-vpn\" ]"
-	then
-	#	eval "echo \"\$originator${orc}_mac  --    \$originator${orc}_nexthop\"" 
-	#	echo "skipped"
-		continue
-	fi
-	orc=$(($orc+1))
-	IFS="${NIFS}"
-done
-IFS="${OIFS}"
-
-gwc=0
-gws="$(batctl gwl|sed -r 's/^[[:space:]]+([a-f0-9:].*)/false \1/ ; s/^=>(.*)/true \1/ ; s/(true|false)[[:space:]]+([0-9a-f:]+)[[:space:]]+\([[:space:]]*([0-9]+)\)[[:space:]]+[a-f0-9:]+[[:space:]]+\[[[:space:]]*(.+)\]:[[:space:]]+([0-9.\/]+).*$/\1 \2 \3 \4 \5/;tx;d;:x')" 
-#echo "$gws"
-IFS="${NIFS}"
-for gw in $gws ; do
-	IFS=${OIFS} 
-#	gw="$(echo "$gw"|sed -r 's/^[[:space:]]+(.*)/false \1/ ; s/^=>(.*)/true \1/ ; s/(true|false)[[:space:]]+([0-9a-f:]+)[[:space:]]+\([[:space:]]*([0-9]+)\)[[:space:]]+[a-f0-9:]+[[:space:]]+\[[[:space:]]*(.+)\]:[[:space:]]+([0-9.\/]+).*$/\1 \2 \3 \4 \5/;tx;d;:x')" 
-#	echo "$gw"
-	set -- $gw
-#	echo "sel: $1"	
-	eval "gateway${gwc}_mac=\"$2\""
-	eval "gateway${gwc}_selected=\"$1\""
-	eval "gateway${gwc}_linkquality=\"$3\""
-	eval "gateway${gwc}_class=\"$5\""
-	eval "gateway${gwc}_interface=\"$4\""
-
-	gwc=$(($gwc+1))
-	IFS=${NIFS}  
-done
-IFS=${OIFS}  
-
-#################################output to xml
-out="<?xml version='1.0' ?>\n"
-out=$out"<data>\n"
-out=$out"\t<hostname>"$hostname"</hostname>\n"
-out=$out"\t<times>\n\t\t<up>$times_up</up>\n\t\t<idle>"$times_idle"</idle>\n\t</times>\n"
-out=$out"\t<model>"$model"</model>\n"
-out=$out"\t<mac>"$mac"</mac>\n"
-if [ $(uci get gluon-node-info.@location[0].share_location) = "1" ] ; then
-	out=$out"\t<position>\n"
-	out=$out"\t\t<lon>"$position_lon"</lon>\n"
-	out=$out"\t\t<lat>"$position_lon"</lat>\n"
-	out=$out"\t</position>\n"
-fi
-out=$out"\t<memory>\n"
-out=$out"\t\t<total>"$memory_total"</total>\n"
-out=$out"\t\t<free>"$memory_free"</free>\n" 
-out=$out"\t\t<buffer>"$memory_buffer"</buffer>\n" 
-out=$out"\t\t<cache>"$memory_cache"</cache>\n" 
-out=$out"\t</memory>\n"
-out=$out"\t<processes>\n"
-out=$out"\t\t<runnable>$processes_runnable</runnable>\n"
-out=$out"\t\t<total>$processes_total</total>\n"
-out=$out"\t\t<loadavg>$processes_loadavg</loadavg>\n"  
-out=$out"\t</processes>\n"
-out=$out"\t<software>\n"
-out=$out"\t\t<firmware>"$software_firmware"</firmware>\n"
-out=$out"\t\t<kernel>"$software_kernel"</kernel>\n"
-out=$out"\t\t<mesh>"$software_mesh"</mesh>\n"
-out=$out"\t\t<vpn>"$software_vpn"</vpn>\n" 
-if [ "$software_autoupdate_enabled" = "1" ] ; then
-	out="$out\t\t<autoupdate_branch>$software_autoupdate_branch</autoupdate_branch>\n"
-#	out="$out\t\t\t<enabled>$software_autoupdate_enabled</enabled>\n"
-#	out="$out\t\t\t<branch>$software_autoupdate_branch</branch>\n"  
-#	out="$out\t\t</autoupdate>\n"
-fi
-out=$out"\t</software>\n" 
-out=$out"\t<interfaces>\n"
-
-for i in $(seq 0 $(($ifc-1))) ; do
-	out=$out"\t\t<interface>\n"
-	eval "out=\"\${out}\t\t\t<name>\${interface${i}_name}</name>\n\""
-
-	eval "out=\"\${out}\t\t\t<mtu>\${interface${i}_mtu}</mtu>\n\""
-	eval "out=\"\${out}\t\t\t<mac>\${interface${i}_mac}</mac>\n\""
-	out="$out\t\t\t<traffic>\n"
-	eval "out=\"\${out}\t\t\t\t<rx>\${interface${i}_traffic_rx}</rx>\n\""
-	eval "out=\"\${out}\t\t\t\t<tx>\${interface${i}_traffic_tx}</tx>\n\"" 
-	out=$out"\t\t\t</traffic>\n"
-
-        eval "local meshstatus=\${interface${i}_meshstatus}"
-        if [ "$meshstatus" != "" ] ; then
-                                                                   
-
-
-
-		eval "out=\"\${out}\t\t\t<meshstatus>\${interface${i}_meshstatus}</meshstatus>\n\""
-	fi
-
-	eval "local ipv4=\"\${interface${i}_ipv4}\"" 
-#	echo "ip4: $ipv4"
-	if [ "$ipv4" != "" ]; then
-		out=$out"\t\t\t<ipv4>$ipv4</ipv4>\n" 
-	fi
-
-
-	eval "ic=\${interface${i}_ipv6count}"
-
-	if [ "$ic" != "0" ] ; then
-	        for ip in $(seq 0 $(($ic-1))) ; do
-        	       	eval "out=\"$out\t\t\t<ipv6>\${interface${i}_ipv6_${ip}}</ipv6>\n\""                                                                                                       
-#        	       	echo "hi$ip"
-			#eval interface${ifc}_ipv6_$ip=$ip                                                                             
-        	done    
-	fi
-	
-	eval "local txp=\"\${interface${i}_txpower}\""
-	eval "local ch=\"\${interface${i}_channel}\""
-#	if [ "$txp" != "" ]
-	if eval "[ \"\${interface${i}_txpower}\" != \"\"  ]"
-	then
-		out="$out\t\t\t<txpower>$txp</txpower>\n"      
-		out="$out\t\t\t<channel>$ch</channel>\n"      
-		eval "out=\"$out\t\t\t<linkquality>\${interface${i}_linkquality}</linkquality>\n\""
-	fi	
-
-	out=$out"\t\t</interface>\n"
-done
-out="$out\t</interfaces>\n"
-
-if [ "$orc" != "0" ] ; then
-	out="$out\t<originators>\n"
-	for i in $(seq 0 $(($orc-1))) ; do 
-		out="$out\t\t<originator>\n"
-		eval "out=\"$out\t\t\t<mac>\${originator${i}_mac}</mac>\n\"" 
-		eval "out=\"$out\t\t\t<linkquality>\${originator${i}_linkquality}</linkquality>\n\""
-		eval "out=\"$out\t\t\t<lastseen>\${originator${i}_lastseen}</lastseen>\n\""
-		eval "out=\"$out\t\t\t<interface>\${originator${i}_interface}</interface>\n\""
-
-		out="$out\t\t</originator>\n"
-	done
-	out="$out\t</originators>\n"
-fi
-
-if [ "$gwc" != "0" ] ; then
-	out="$out\t<gateways>\n"
-	for g in $(seq 0 $(($gwc-1))) ; do
-		out="$out\t\t<gateway>\n"
-		eval "out=\"$out\t\t\t<mac>\${gateway${g}_mac}</mac>\n\""
-
-
-		eval "out=\"$out\t\t\t<selected>\${gateway${g}_selected}</selected>\n\""
-		eval "out=\"$out\t\t\t<linkquality>\${gateway${g}_linkquality}</linkquality>\n\""
-eval "out=\"$out\t\t\t<interface>\${gateway${g}_interface}</interface>\n\""
-		eval "out=\"$out\t\t\t<class>\${gateway${g}_class}</class>\n\""
-		out="$out\t\t</gateway>\n"
- 		
-	done
-	out="$out\t</gateways>\n"
-fi
-
-out="$out</data>"
-
-
-echo -e "$out" > "/tmp/node2data.xml"
-if [ ! -h /lib/gluon/status-page/www/node2data.xml ]; then
-        ln -s /tmp/node2data.xml /lib/gluon/status-page/www/node2data.xml
-fi
diff --git a/nodewatcher2/files/lib/gluon/cron/nodewatcher2 b/nodewatcher2/files/lib/gluon/cron/nodewatcher2
index 1268c7e..4c421b8 100644
--- a/nodewatcher2/files/lib/gluon/cron/nodewatcher2
+++ b/nodewatcher2/files/lib/gluon/cron/nodewatcher2
@@ -1 +1 @@
-*/5 * * * *	sh /lib/ffnw/nodewatcher2/nodewatcher.sh;
+*/5 * * * *	sh /lib/ffnw/nodewatcher2/nodewatcher.lua;
diff --git a/nodewatcher2/files/usr/lib/lua/dkjson.lua b/nodewatcher2/files/usr/lib/lua/dkjson.lua
new file mode 100644
index 0000000..fc5350f
--- /dev/null
+++ b/nodewatcher2/files/usr/lib/lua/dkjson.lua
@@ -0,0 +1,369 @@
+-- Module options:
+local always_try_using_lpeg = true
+local register_global_module_table = false
+local global_module_name = 'json'
+
+--[==[
+
+David Kolf's JSON module for Lua 5.1/5.2
+
+Version 2.5
+
+
+For the documentation see the corresponding readme.txt or visit
+<http://dkolf.de/src/dkjson-lua.fsl/>.
+
+You can contact the author by sending an e-mail to 'david' at the
+domain 'dkolf.de'.
+
+
+Copyright (C) 2010-2013 David Heiko Kolf
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+--]==]
+
+-- global dependencies:
+local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
+      pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
+local error, require, pcall, select = error, require, pcall, select
+local floor, huge = math.floor, math.huge
+local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
+      string.rep, string.gsub, string.sub, string.byte, string.char,
+      string.find, string.len, string.format
+local strmatch = string.match
+local concat = table.concat
+
+local json = { version = "dkjson 2.5" }
+
+if register_global_module_table then
+  _G[global_module_name] = json
+end
+
+local _ENV = nil -- blocking globals in Lua 5.2
+
+pcall (function()
+  -- Enable access to blocked metatables.
+  -- Don't worry, this module doesn't change anything in them.
+  local debmeta = require "debug".getmetatable
+  if debmeta then getmetatable = debmeta end
+end)
+
+json.null = setmetatable ({}, {
+  __tojson = function () return "null" end
+})
+
+local function isarray (tbl)
+  local max, n, arraylen = 0, 0, 0
+  for k,v in pairs (tbl) do
+    if k == 'n' and type(v) == 'number' then
+      arraylen = v
+      if v > max then
+        max = v
+      end
+    else
+      if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
+        return false
+      end
+      if k > max then
+        max = k
+      end
+      n = n + 1
+    end
+  end
+  if max > 10 and max > arraylen and max > n * 2 then
+    return false -- don't create an array with too many holes
+  end
+  return true, max
+end
+
+local escapecodes = {
+  ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
+  ["\n"] = "\\n",  ["\r"] = "\\r",  ["\t"] = "\\t"
+}
+
+local function escapeutf8 (uchar)
+  local value = escapecodes[uchar]
+  if value then
+    return value
+  end
+  local a, b, c, d = strbyte (uchar, 1, 4)
+  a, b, c, d = a or 0, b or 0, c or 0, d or 0
+  if a <= 0x7f then
+    value = a
+  elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
+    value = (a - 0xc0) * 0x40 + b - 0x80
+  elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
+    value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
+  elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
+    value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
+  else
+    return ""
+  end
+  if value <= 0xffff then
+    return strformat ("\\u%.4x", value)
+  elseif value <= 0x10ffff then
+    -- encode as UTF-16 surrogate pair
+    value = value - 0x10000
+    local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
+    return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
+  else
+    return ""
+  end
+end
+
+local function fsub (str, pattern, repl)
+  -- gsub always builds a new string in a buffer, even when no match
+  -- exists. First using find should be more efficient when most strings
+  -- don't contain the pattern.
+  if strfind (str, pattern) then
+    return gsub (str, pattern, repl)
+  else
+    return str
+  end
+end
+
+local function quotestring (value)
+  -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
+  value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
+  if strfind (value, "[\194\216\220\225\226\239]") then
+    value = fsub (value, "\194[\128-\159\173]", escapeutf8)
+    value = fsub (value, "\216[\128-\132]", escapeutf8)
+    value = fsub (value, "\220\143", escapeutf8)
+    value = fsub (value, "\225\158[\180\181]", escapeutf8)
+    value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
+    value = fsub (value, "\226\129[\160-\175]", escapeutf8)
+    value = fsub (value, "\239\187\191", escapeutf8)
+    value = fsub (value, "\239\191[\176-\191]", escapeutf8)
+  end
+  return "\"" .. value .. "\""
+end
+json.quotestring = quotestring
+
+local function replace(str, o, n)
+  local i, j = strfind (str, o, 1, true)
+  if i then
+    return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
+  else
+    return str
+  end
+end
+
+-- locale independent num2str and str2num functions
+local decpoint, numfilter
+
+local function updatedecpoint ()
+  decpoint = strmatch(tostring(0.5), "([^05+])")
+  -- build a filter that can be used to remove group separators
+  numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
+end
+
+updatedecpoint()
+
+local function num2str (num)
+  return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
+end
+
+local function addnewline2 (level, buffer, buflen)
+  buffer[buflen+1] = "\n"
+  buffer[buflen+2] = strrep ("  ", level)
+  buflen = buflen + 2
+  return buflen
+end
+
+function json.addnewline (state)
+  if state.indent then
+    state.bufferlen = addnewline2 (state.level or 0,
+                           state.buffer, state.bufferlen or #(state.buffer))
+  end
+end
+
+local encode2 -- forward declaration
+
+local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
+  local kt = type (key)
+  if kt ~= 'string' and kt ~= 'number' then
+    return nil, "type '" .. kt .. "' is not supported as a key by JSON."
+  end
+  if prev then
+    buflen = buflen + 1
+    buffer[buflen] = ","
+  end
+  if indent then
+    buflen = addnewline2 (level, buffer, buflen)
+  end
+  buffer[buflen+1] = quotestring (key)
+  buffer[buflen+2] = ":"
+  return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
+end
+
+local function appendcustom(res, buffer, state)
+  local buflen = state.bufferlen
+  if type (res) == 'string' then
+    buflen = buflen + 1
+    buffer[buflen] = res
+  end
+  return buflen
+end
+
+local function exception(reason, value, state, buffer, buflen, defaultmessage)
+  defaultmessage = defaultmessage or reason
+  local handler = state.exception
+  if not handler then
+    return nil, defaultmessage
+  else
+    state.bufferlen = buflen
+    local ret, msg = handler (reason, value, state, defaultmessage)
+    if not ret then return nil, msg or defaultmessage end
+    return appendcustom(ret, buffer, state)
+  end
+end
+
+function json.encodeexception(reason, value, state, defaultmessage)
+  return quotestring("<" .. defaultmessage .. ">")
+end
+
+encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
+  local valtype = type (value)
+  local valmeta = getmetatable (value)
+  valmeta = type (valmeta) == 'table' and valmeta -- only tables
+  local valtojson = valmeta and valmeta.__tojson
+  if valtojson then
+    if tables[value] then
+      return exception('reference cycle', value, state, buffer, buflen)
+    end
+    tables[value] = true
+    state.bufferlen = buflen
+    local ret, msg = valtojson (value, state)
+    if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
+    tables[value] = nil
+    buflen = appendcustom(ret, buffer, state)
+  elseif value == nil then
+    buflen = buflen + 1
+    buffer[buflen] = "null"
+  elseif valtype == 'number' then
+    local s
+    if value ~= value or value >= huge or -value >= huge then
+      -- This is the behaviour of the original JSON implementation.
+      s = "null"
+    else
+      s = num2str (value)
+    end
+    buflen = buflen + 1
+    buffer[buflen] = s
+  elseif valtype == 'boolean' then
+    buflen = buflen + 1
+    buffer[buflen] = value and "true" or "false"
+  elseif valtype == 'string' then
+    buflen = buflen + 1
+    buffer[buflen] = quotestring (value)
+  elseif valtype == 'table' then
+    if tables[value] then
+      return exception('reference cycle', value, state, buffer, buflen)
+    end
+    tables[value] = true
+    level = level + 1
+    local isa, n = isarray (value)
+    if n == 0 and valmeta and valmeta.__jsontype == 'object' then
+      isa = false
+    end
+    local msg
+    if isa then -- JSON array
+      buflen = buflen + 1
+      buffer[buflen] = "["
+      for i = 1, n do
+        buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
+        if not buflen then return nil, msg end
+        if i < n then
+          buflen = buflen + 1
+          buffer[buflen] = ","
+        end
+      end
+      buflen = buflen + 1
+      buffer[buflen] = "]"
+    else -- JSON object
+      local prev = false
+      buflen = buflen + 1
+      buffer[buflen] = "{"
+      local order = valmeta and valmeta.__jsonorder or globalorder
+      if order then
+        local used = {}
+        n = #order
+        for i = 1, n do
+          local k = order[i]
+          local v = value[k]
+          if v then
+            used[k] = true
+            buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+            prev = true -- add a seperator before the next element
+          end
+        end
+        for k,v in pairs (value) do
+          if not used[k] then
+            buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+            if not buflen then return nil, msg end
+            prev = true -- add a seperator before the next element
+          end
+        end
+      else -- unordered
+        for k,v in pairs (value) do
+          buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
+          if not buflen then return nil, msg end
+          prev = true -- add a seperator before the next element
+        end
+      end
+      if indent then
+        buflen = addnewline2 (level - 1, buffer, buflen)
+      end
+      buflen = buflen + 1
+      buffer[buflen] = "}"
+    end
+    tables[value] = nil
+  else
+    return exception ('unsupported type', value, state, buffer, buflen,
+      "type '" .. valtype .. "' is not supported by JSON.")
+  end
+  return buflen
+end
+
+function json.encode (value, state)
+  state = state or {}
+  local oldbuffer = state.buffer
+  local buffer = oldbuffer or {}
+  state.buffer = buffer
+  updatedecpoint()
+  local ret, msg = encode2 (value, state.indent, state.level or 0,
+                   buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
+  if not ret then
+    error (msg, 2)
+  elseif oldbuffer == buffer then
+    state.bufferlen = ret
+    return true
+  else
+    state.bufferlen = nil
+    state.buffer = nil
+    return concat (buffer)
+  end
+end
+
+
+return json
+
-- 
GitLab