| ... | ... |
@@ -0,0 +1,353 @@ |
| 1 |
+#!/usr/bin/python3 |
|
| 2 |
+import sqlite3 |
|
| 3 |
+import json |
|
| 4 |
+import argparse |
|
| 5 |
+import time |
|
| 6 |
+from datetime import datetime |
|
| 7 |
+import logging |
|
| 8 |
+from logging.handlers import RotatingFileHandler |
|
| 9 |
+from xml.etree.ElementTree import Element, SubElement, tostring, ElementTree |
|
| 10 |
+ |
|
| 11 |
+format = "%(asctime)s : %(levelname)s : %(message)s" |
|
| 12 |
+outputlog='create_table.log' |
|
| 13 |
+logging.basicConfig(format=format, |
|
| 14 |
+ level=logging.INFO, |
|
| 15 |
+ datefmt='%Y-%m-%dT%H:%M:%S%z', |
|
| 16 |
+ handlers=[RotatingFileHandler(outputlog,backupCount=5,maxBytes=10485760),logging.StreamHandler()]) |
|
| 17 |
+ |
|
| 18 |
+db_path = "http-okay.db" |
|
| 19 |
+table_name = "content" |
|
| 20 |
+ |
|
| 21 |
+def colorSnr(f): |
|
| 22 |
+ if f < -7.5: |
|
| 23 |
+ return "sf12" |
|
| 24 |
+ elif f < -5: |
|
| 25 |
+ return "sf11" |
|
| 26 |
+ elif f < -2.5: |
|
| 27 |
+ return "sf10" |
|
| 28 |
+ elif f < 0: |
|
| 29 |
+ return "sf9" |
|
| 30 |
+ elif f < 2.5: |
|
| 31 |
+ return "sf8" |
|
| 32 |
+ else: |
|
| 33 |
+ return "sf7" |
|
| 34 |
+ |
|
| 35 |
+def colorRssi(f): |
|
| 36 |
+ if f < -105: |
|
| 37 |
+ return "sf12" |
|
| 38 |
+ elif f < -95: |
|
| 39 |
+ return "sf11" |
|
| 40 |
+ elif f < -85: |
|
| 41 |
+ return "sf10" |
|
| 42 |
+ elif f < -80: |
|
| 43 |
+ return "sf9" |
|
| 44 |
+ else: |
|
| 45 |
+ return "sf7" |
|
| 46 |
+ |
|
| 47 |
+ |
|
| 48 |
+def calcSF(f): |
|
| 49 |
+ if f < -7.5: |
|
| 50 |
+ return "SF12" |
|
| 51 |
+ elif f < -5: |
|
| 52 |
+ return "SF11" |
|
| 53 |
+ elif f < -2.5: |
|
| 54 |
+ return "SF10" |
|
| 55 |
+ elif f < 0: |
|
| 56 |
+ return "SF9" |
|
| 57 |
+ elif f < 2.5: |
|
| 58 |
+ return "SF8" |
|
| 59 |
+ else: |
|
| 60 |
+ return "SF7" |
|
| 61 |
+ |
|
| 62 |
+def colorSnrValue(f): |
|
| 63 |
+ if f < -7.5: |
|
| 64 |
+ return "ffff0000" # Red |
|
| 65 |
+ elif f < -5: |
|
| 66 |
+ return "ffff5f1f" # Orange |
|
| 67 |
+ elif f < -2.5: |
|
| 68 |
+ return "ff7a8500" # Olive |
|
| 69 |
+ elif f < 0: |
|
| 70 |
+ return "ff52ad00" # Green |
|
| 71 |
+ elif f < 2.5: |
|
| 72 |
+ return "ff408000" # Dark Green |
|
| 73 |
+ else: |
|
| 74 |
+ return "ff15db00" # Bright Green |
|
| 75 |
+ |
|
| 76 |
+def generate_html_table(db_path, table_name,last): |
|
| 77 |
+ dateExec = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
| 78 |
+ logging.info(f"Exec : {dateExec}")
|
|
| 79 |
+ conn = sqlite3.connect(db_path) |
|
| 80 |
+ cursor = conn.cursor() |
|
| 81 |
+ query = f"SELECT content FROM {table_name} ORDER BY id DESC LIMIT {last}"
|
|
| 82 |
+ cursor.execute(query) |
|
| 83 |
+ records = cursor.fetchall() |
|
| 84 |
+ conn.close() |
|
| 85 |
+ |
|
| 86 |
+ headers = ["Time", |
|
| 87 |
+ "DevEUI", |
|
| 88 |
+ "FPort", |
|
| 89 |
+ "FCntUp", |
|
| 90 |
+ "FCntDn", |
|
| 91 |
+ "Payload", |
|
| 92 |
+ "SF", |
|
| 93 |
+ "RSSI", |
|
| 94 |
+ "SNR", |
|
| 95 |
+ "LC", |
|
| 96 |
+ "LRRID", |
|
| 97 |
+ "Late", |
|
| 98 |
+ "Count", |
|
| 99 |
+ "MAC" |
|
| 100 |
+ ] |
|
| 101 |
+ html=""" |
|
| 102 |
+<!DOCTYPE html> |
|
| 103 |
+<html> |
|
| 104 |
+<head> |
|
| 105 |
+<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
| 106 |
+<link rel="icon" type="image/png" href="favicon.png" sizes="32x32"> |
|
| 107 |
+<title>ApplicationServer</title> |
|
| 108 |
+<style> |
|
| 109 |
+html{font-family: "Lucida Sans", sans-serif;}
|
|
| 110 |
+h1,h2{background-color:#3f87a6;color: #fff;}
|
|
| 111 |
+table {margin: 0 auto;text-align: center;border-collapse: collapse;border: 1px solid #d4d4d4;}
|
|
| 112 |
+tr:nth-child(even) {background: #d4d4d4;}
|
|
| 113 |
+th{background-color: #3f87a6;color: #fff;font-weight: bold;}
|
|
| 114 |
+th,td{padding: 1px 4px;font-size: 0.8em;}
|
|
| 115 |
+th {border-bottom: 1px solid #d4d4d4;}
|
|
| 116 |
+.sf7 {background-color: #15db00;font-size: 0.7em}
|
|
| 117 |
+.sf8 {background-color: #408000;font-size: 0.7em}
|
|
| 118 |
+.sf9 {background-color: #52ad00;font-size: 0.7em}
|
|
| 119 |
+.sf10 {background-color: #7a8500;font-size: 0.7em}
|
|
| 120 |
+.sf11 {background-color: #FF5F1F;color: #fff;font-size: 0.7em}
|
|
| 121 |
+.sf12 {background-color: #f00;color: #fff;font-size: 0.7em}
|
|
| 122 |
+.sfna {background-color: #fff;}
|
|
| 123 |
+#all-stats {font-size: 1.0em;font-family: monospace; display:inline;}
|
|
| 124 |
+li {
|
|
| 125 |
+ display: list-item; |
|
| 126 |
+ margin-left: 1em; |
|
| 127 |
+ list-style-type: none; |
|
| 128 |
+} |
|
| 129 |
+.box {
|
|
| 130 |
+ width: 30px; |
|
| 131 |
+ text-align: center; |
|
| 132 |
+ display: inline-block; |
|
| 133 |
+ margin-right: 2px; |
|
| 134 |
+ font-size: 0.9em; |
|
| 135 |
+} |
|
| 136 |
+ |
|
| 137 |
+.tooltippacket {
|
|
| 138 |
+ visibility: hidden; |
|
| 139 |
+ width: 700px; |
|
| 140 |
+ background-color: rgba(255,255,255,1); |
|
| 141 |
+ color: #000; |
|
| 142 |
+ padding: 5px 0; |
|
| 143 |
+ padding-left: 10px; |
|
| 144 |
+ position: absolute; |
|
| 145 |
+ z-index: 1; |
|
| 146 |
+ font-size: 0.9em; |
|
| 147 |
+} |
|
| 148 |
+.arrow-up{width: 0;height: 0;margin-bottom: 0px;border-left: 10px solid transparent;border-right: 10px solid transparent;border-bottom: 10px solid #108000;display: inline-block;}
|
|
| 149 |
+.arrow-up-gray{width: 0;height: 0;margin-bottom: 0px;border-left: 10px solid transparent;border-right: 10px solid transparent;border-bottom: 10px solid #ccc;display: inline-block;}
|
|
| 150 |
+.arrow-up:hover .tooltippacket, |
|
| 151 |
+.arrow-up-gray:hover .tooltippacket {
|
|
| 152 |
+ visibility: visible; |
|
| 153 |
+} |
|
| 154 |
+.arrow-up:hover, |
|
| 155 |
+.arrow-up-gray:hover {
|
|
| 156 |
+ background-color: yellow; |
|
| 157 |
+} |
|
| 158 |
+ |
|
| 159 |
+ |
|
| 160 |
+</style> |
|
| 161 |
+</head> |
|
| 162 |
+<body> |
|
| 163 |
+<h2>Application Server</h2> |
|
| 164 |
+<p> |
|
| 165 |
+<a href="positions.kml">KML</a> |
|
| 166 |
+<a href="positions.geojson">GeoJSON</a> |
|
| 167 |
+<a href="map.html" target="_blank">Map</a> |
|
| 168 |
+</p> |
|
| 169 |
+""" |
|
| 170 |
+ html += f"<p>Generated at {dateExec}</p>"
|
|
| 171 |
+ html += "<table border='1' cellspacing='0' cellpadding='5'>\n" |
|
| 172 |
+ |
|
| 173 |
+ html += "<tr>" + "".join(f"\n <th>{header}</th>" for header in headers) + "\n</tr>\n"
|
|
| 174 |
+ |
|
| 175 |
+ stats=""" |
|
| 176 |
+<button onclick="clickAll()">ToggleAll</button> |
|
| 177 |
+<hr /> |
|
| 178 |
+<div id=\"all-stats\"> |
|
| 179 |
+""" |
|
| 180 |
+ |
|
| 181 |
+ html_table="" |
|
| 182 |
+ |
|
| 183 |
+ deveuis=[] |
|
| 184 |
+ for record in records: |
|
| 185 |
+ try: |
|
| 186 |
+ data = json.loads(record[0]) |
|
| 187 |
+ uplink = data.get("DevEUI_uplink", {})
|
|
| 188 |
+ deveui = uplink.get("DevEUI", "N/A")
|
|
| 189 |
+ if deveui not in deveuis: |
|
| 190 |
+ stats += f"<div class=\"stat-box\" id=\"f{deveui}\" onclick=\"filterOut('{deveui}');\" style=\"cursor: pointer;\">{deveui}</div>\n"
|
|
| 191 |
+ deveuis.append(deveui) |
|
| 192 |
+ except json.JSONDecodeError: |
|
| 193 |
+ pass |
|
| 194 |
+ stats+="</div>\n<hr>\n" |
|
| 195 |
+ |
|
| 196 |
+ kml = Element('kml', xmlns="http://www.opengis.net/kml/2.2")
|
|
| 197 |
+ document = SubElement(kml, 'Document') |
|
| 198 |
+ styles = {}
|
|
| 199 |
+ |
|
| 200 |
+ geojson = {
|
|
| 201 |
+ "type": "FeatureCollection", |
|
| 202 |
+ "features": [] |
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+ |
|
| 206 |
+ for record in records: |
|
| 207 |
+ try: |
|
| 208 |
+ data = json.loads(record[0]) |
|
| 209 |
+ uplink = data.get("DevEUI_uplink", {})
|
|
| 210 |
+ |
|
| 211 |
+ time = uplink.get("Time", "N/A")
|
|
| 212 |
+ deveui = uplink.get("DevEUI", "N/A")
|
|
| 213 |
+ fport = uplink.get("FPort", "N/A")
|
|
| 214 |
+ FCntUp = uplink.get("FCntUp", "N/A")
|
|
| 215 |
+ FCntDn = uplink.get("FCntDn", "N/A")
|
|
| 216 |
+ payload_hex = uplink.get("payload_hex", "N/A")
|
|
| 217 |
+ SpFact = uplink.get("SpFact", "na")
|
|
| 218 |
+ LrrRSSI = uplink.get("LrrRSSI", -199)
|
|
| 219 |
+ LrrSNR = uplink.get("LrrSNR", -99)
|
|
| 220 |
+ Channel = uplink.get("Channel", "-1")
|
|
| 221 |
+ Channel = int(Channel.replace("LC",""))
|
|
| 222 |
+ Lrrid = uplink.get("Lrrid", "N/A")
|
|
| 223 |
+ Late = uplink.get("Late", -1)
|
|
| 224 |
+ DevLrrCnt = uplink.get("DevLrrCnt", -1)
|
|
| 225 |
+ rawMacCommands = uplink.get("rawMacCommands", "N/A")
|
|
| 226 |
+ uplink_type="arrow-up" |
|
| 227 |
+ if int(Late) == 1: |
|
| 228 |
+ uplink_type="arrow-up-gray" |
|
| 229 |
+ #table_line = f"<tr class=\"{deveui}\"><td><div class=\"{uplink_type}\"></div>{time}</td><td>{deveui}</td><td>{fport}</td><td>{FCntUp}</td><td>{FCntDn}</td><td>{payload_hex}</td>"
|
|
| 230 |
+ collapse=json.dumps(data,indent=2) |
|
| 231 |
+ table_line = f"<tr class=\"{deveui}\"><td><div class=\"{uplink_type}\"><span class=\"tooltippacket\">{collapse}</span></div>{time}</td><td>{deveui}</td><td>{fport}</td><td>{FCntUp}</td><td>{FCntDn}</td><td>{payload_hex}</td>"
|
|
| 232 |
+ table_line += f"<td class=\"sf{SpFact}\">{SpFact}</td><td class=\"{colorRssi(float(LrrRSSI))}\">{LrrRSSI}</td><td class=\"{colorSnr(float(LrrSNR))}\">{LrrSNR}</td>"
|
|
| 233 |
+ table_line += f"<td>{Channel}</td><td>{Lrrid}</td><td>{Late}</td><td>{DevLrrCnt}</td><td>{rawMacCommands}</td></tr>"
|
|
| 234 |
+ logline=f"{time} {deveui} {fport} {FCntUp} {FCntDn} {payload_hex} {SpFact} {LrrRSSI} {LrrSNR} {Channel} {Lrrid}"
|
|
| 235 |
+ logging.info(logline) |
|
| 236 |
+ |
|
| 237 |
+ html_table += table_line+"\n" |
|
| 238 |
+ payload = uplink.get("payload", {})
|
|
| 239 |
+ |
|
| 240 |
+ if payload.get("messageType") != "EXTENDED_POSITION_MESSAGE":
|
|
| 241 |
+ continue |
|
| 242 |
+ |
|
| 243 |
+ |
|
| 244 |
+ lat = payload.get("gpsLatitude")
|
|
| 245 |
+ lon = payload.get("gpsLongitude")
|
|
| 246 |
+ alt = payload.get("gpsAltitude", 0)
|
|
| 247 |
+ |
|
| 248 |
+ if lat is None or lon is None or LrrSNR is None: |
|
| 249 |
+ continue |
|
| 250 |
+ logging.info(f"{lat},{lon}")
|
|
| 251 |
+ color = colorSnrValue(LrrSNR) |
|
| 252 |
+ style_id = f"snr_{color}"
|
|
| 253 |
+ calc_sf = calcSF(LrrSNR) |
|
| 254 |
+ |
|
| 255 |
+ if style_id not in styles: |
|
| 256 |
+ style = SubElement(document, "Style", id=style_id) |
|
| 257 |
+ icon_style = SubElement(style, "IconStyle") |
|
| 258 |
+ SubElement(icon_style, "color").text = color |
|
| 259 |
+ SubElement(icon_style, "scale").text = "1.2" |
|
| 260 |
+ icon = SubElement(icon_style, "Icon") |
|
| 261 |
+ SubElement(icon, "href").text = "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png" |
|
| 262 |
+ |
|
| 263 |
+ # Hide label |
|
| 264 |
+ label_style = SubElement(style, "LabelStyle") |
|
| 265 |
+ SubElement(label_style, "scale").text = "0" |
|
| 266 |
+ |
|
| 267 |
+ styles[style_id] = True |
|
| 268 |
+ |
|
| 269 |
+ # Create Placemark |
|
| 270 |
+ placemark = SubElement(document, 'Placemark') |
|
| 271 |
+ SubElement(placemark, 'name').text = "" |
|
| 272 |
+ desc = f""" |
|
| 273 |
+Time: {time}
|
|
| 274 |
+DevEUI: {deveui}
|
|
| 275 |
+FCntUp: {FCntUp}
|
|
| 276 |
+SNR: {LrrSNR}
|
|
| 277 |
+SF: {calc_sf}
|
|
| 278 |
+CalculatedSF: {calc_sf}
|
|
| 279 |
+""".strip() |
|
| 280 |
+ SubElement(placemark, 'description').text = desc |
|
| 281 |
+ SubElement(placemark, 'styleUrl').text = f"#{style_id}"
|
|
| 282 |
+ |
|
| 283 |
+ point = SubElement(placemark, 'Point') |
|
| 284 |
+ SubElement(point, 'coordinates').text = f"{lon},{lat},{alt}"
|
|
| 285 |
+ |
|
| 286 |
+ feature = {
|
|
| 287 |
+ "type": "Feature", |
|
| 288 |
+ "geometry": {
|
|
| 289 |
+ "type": "Point", |
|
| 290 |
+ "coordinates": [lon, lat, alt] |
|
| 291 |
+ }, |
|
| 292 |
+ "properties": {
|
|
| 293 |
+ "DevEUI": deveui, |
|
| 294 |
+ "Time": time, |
|
| 295 |
+ "FCntUp": FCntUp, |
|
| 296 |
+ "SNR": LrrSNR, |
|
| 297 |
+ "SpFact": SpFact, |
|
| 298 |
+ "SF": calc_sf, |
|
| 299 |
+ "style": {
|
|
| 300 |
+ "marker-color": "#" + color[-6:], # Remove alpha from KML ARGB |
|
| 301 |
+ "marker-symbol": "circle", |
|
| 302 |
+ "marker-size": "medium" |
|
| 303 |
+ } |
|
| 304 |
+ } |
|
| 305 |
+ } |
|
| 306 |
+ geojson["features"].append(feature) |
|
| 307 |
+ |
|
| 308 |
+ except json.JSONDecodeError: |
|
| 309 |
+ #html_table += "<tr><td colspan='7'>Invalid JSON Data</td></tr>" |
|
| 310 |
+ pass |
|
| 311 |
+ |
|
| 312 |
+ tree = ElementTree(kml) |
|
| 313 |
+ tree.write("positions.kml", encoding='utf-8', xml_declaration=True)
|
|
| 314 |
+ with open("positions.geojson", "w") as f:
|
|
| 315 |
+ json.dump(geojson, f, indent=2) |
|
| 316 |
+ |
|
| 317 |
+ html += stats |
|
| 318 |
+ html += html_table |
|
| 319 |
+ html += "</table>" |
|
| 320 |
+ html += """ |
|
| 321 |
+<script> |
|
| 322 |
+function filterOut(classname) {
|
|
| 323 |
+ const collection = document.getElementsByClassName(classname); |
|
| 324 |
+ for (let i = 0; i < collection.length; i++) {
|
|
| 325 |
+ if(collection[i].style.display == 'none' ) {
|
|
| 326 |
+ collection[i].style.display='table-row'; |
|
| 327 |
+ document.getElementById("f"+classname).style.color="#000";
|
|
| 328 |
+ } else {
|
|
| 329 |
+ collection[i].style.display='none'; |
|
| 330 |
+ document.getElementById("f"+classname).style.color="#faa";
|
|
| 331 |
+ } |
|
| 332 |
+ } |
|
| 333 |
+} |
|
| 334 |
+function clickAll() {
|
|
| 335 |
+ document.querySelectorAll('.stat-box').forEach(box => box.click());
|
|
| 336 |
+} |
|
| 337 |
+</script> |
|
| 338 |
+""" |
|
| 339 |
+ html += "</body>" |
|
| 340 |
+ html += "</html>" |
|
| 341 |
+ |
|
| 342 |
+ return html |
|
| 343 |
+ |
|
| 344 |
+if __name__ == "__main__": |
|
| 345 |
+ parser = argparse.ArgumentParser() |
|
| 346 |
+ parser.add_argument("-l", "--last", help="N last records")
|
|
| 347 |
+ args = parser.parse_args() |
|
| 348 |
+ while True: |
|
| 349 |
+ html_output = generate_html_table(db_path, table_name,args.last) |
|
| 350 |
+ with open("index.html","w") as f:
|
|
| 351 |
+ f.write(html_output); |
|
| 352 |
+ time.sleep(30) |
|
| 353 |
+ |