Showing 1 changed files with 353 additions and 0 deletions
+353
create_table.py
... ...
@@ -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
+