Browse code

initial release

gandolf authored on 01/13/2020 03:50:38
Showing 10 changed files
... ...
@@ -10,15 +10,22 @@
10 10
 <h4>Contents</h4>
11 11
 
12 12
 <p><b>Yaesu FT991A Transceiver</b><br>
13
+<ul>
13 14
 Includes utilities for<br>
14 15
   * CAT command passthrough for software development<br>
15 16
   * Programming FT991 vfo/channel memory<br>
16 17
   * Backup FT991 Configuration (menu items)<br>
18
+</ul>
17 19
 </p>
18 20
 
19 21
 <p>
20
-<b>Future projects</b><br>
22
+<b>FT991A Future projects</b><br>
21 23
   * Web based interface for Internet remote control<br>
22 24
 </p>
25
+
26
+<p>
27
+<b>AREDN Mesh</b>
28
+  * Web app for viewing long term SNR statistics
29
+</p>
23 30
 </body>
24 31
 </html>
25 32
new file mode 100755
... ...
@@ -0,0 +1,29 @@
1
+#!/bin/bash
2
+#
3
+# The monitor url can look something like http://192.168.1.155, or
4
+# something linke http://radiationMonitor.domain.com depending on
5
+# whether your local network uses a domain name server.
6
+#
7
+
8
+APP_PATH="/home/$USER/bin"
9
+LOG_PATH="/home/$USER/log"
10
+
11
+if [ `hostname` == "raspi2" ]; then
12
+  AGENT_NAME="[a]rednsigAgent.py"
13
+  NODE_URL="-u http://192.168.1.30/cgi-bin/signal.json"
14
+else
15
+  AGENT_NAME="[a]rednsigMirrorAgent.py"
16
+  NODE_URL="-u http://73.157.139.23:7361/arednsig/dynamic/arednsigOutputData.js"
17
+fi
18
+
19
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
20
+
21
+if [ -n "$PROCESS_ID" ]; then
22
+  if [ "$1" != "-q" ]; then
23
+    printf "arednsig agent running [%s]\n" $PROCESS_ID
24
+  fi
25
+else
26
+  printf "starting up arednsig agent\n"
27
+  cd $APP_PATH
28
+  ./$AGENT_NAME $NODE_URL >> $LOG_PATH/arednsigAgent.log 2>&1 &
29
+fi
0 30
new file mode 100755
... ...
@@ -0,0 +1,17 @@
1
+#!/bin/bash
2
+# Stop the radmon agent process and clean up environment.
3
+
4
+if [ `hostname` == "raspi2" ]; then
5
+  AGENT_NAME="[a]rednsigAgent.py"
6
+else
7
+  AGENT_NAME="[a]rednsigMirrorAgent.py"
8
+fi
9
+
10
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
11
+
12
+if [ -n "$PROCESS_ID" ]; then
13
+  printf "killing arednsig agent [%s]\n" $PROCESS_ID
14
+  kill $PROCESS_ID
15
+else
16
+  echo arednsig agent not running
17
+fi
0 18
new file mode 100755
... ...
@@ -0,0 +1,662 @@
1
+#!/usr/bin/python -u
2
+# The -u option above turns off block buffering of python output. This 
3
+# assures that each error message gets individually printed to the log file.
4
+#
5
+# Module: arednsigAgent.py
6
+#
7
+# Description: This module acts as an agent between the aredn node
8
+# and aredn mesh web services.  The agent periodically sends an http
9
+# request to the aredn node, processes the response from
10
+# the node, and performs a number of operations:
11
+#     - conversion of data items
12
+#     - update a round robin (rrdtool) database with the node data
13
+#     - periodically generate graphic charts for display in html documents
14
+#     - write the processed radmon data to a JSON file for use by html
15
+#       documents
16
+#
17
+# Copyright 2020 Jeff Owrey
18
+#    This program is free software: you can redistribute it and/or modify
19
+#    it under the terms of the GNU General Public License as published by
20
+#    the Free Software Foundation, either version 3 of the License, or
21
+#    (at your option) any later version.
22
+#
23
+#    This program is distributed in the hope that it will be useful,
24
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26
+#    GNU General Public License for more details.
27
+#
28
+#    You should have received a copy of the GNU General Public License
29
+#    along with this program.  If not, see http://www.gnu.org/license.
30
+#
31
+# Revision History
32
+#   * v20 released 11 Jan 2020 by J L Owrey; first release
33
+#
34
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
35
+
36
+import os
37
+import urllib2
38
+import sys
39
+import signal
40
+import subprocess
41
+import multiprocessing
42
+import time
43
+import json
44
+
45
+_USER = os.environ['USER']
46
+
47
+   ### DEFAULT AREDN NODE URL ###
48
+
49
+# ip address of the aredn node
50
+_DEFAULT_AREDN_NODE_URL = "http://192.168.1.30/cgi-bin/signal.json"
51
+
52
+    ### FILE AND FOLDER LOCATIONS ###
53
+
54
+# folder for containing dynamic data objects
55
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
56
+# folder for charts and output data file
57
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
58
+# location of data output file
59
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
60
+# small size output data file for heartbeat signal to html docs
61
+_SMALL_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
62
+# database that stores radmon data
63
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
64
+
65
+    ### GLOBAL CONSTANTS ###
66
+
67
+# max number of failed data requests allowed
68
+_MAX_FAILED_DATA_REQUESTS = 0
69
+# interval in minutes between data requests to the aredn node
70
+_DEFAULT_DATA_REQUEST_INTERVAL = 60
71
+# number seconds to wait for a response to HTTP request
72
+_HTTP_REQUEST_TIMEOUT = 10
73
+# standard chart width in pixels
74
+_CHART_WIDTH = 600
75
+# standard chart height in pixels
76
+_CHART_HEIGHT = 150
77
+
78
+   ### GLOBAL VARIABLES ###
79
+
80
+# turn on or off of verbose debugging information
81
+debugOption = False
82
+verboseDebug = False
83
+
84
+# The following two items are used for detecting system faults
85
+# and aredn node online or offline status.
86
+
87
+# count of failed attempts to get data from aredn node
88
+failedUpdateCount = 0
89
+# detected status of aredn node device
90
+nodeOnline = True
91
+
92
+# network address of aredn node
93
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
94
+# frequency of data requests to aredn node
95
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
96
+# last node request time
97
+lastDataPointTime = -1
98
+
99
+  ###  PRIVATE METHODS  ###
100
+
101
+def getTimeStamp():
102
+    """
103
+    Set the error message time stamp to the local system time.
104
+    Parameters: none
105
+    Returns: string containing the time stamp
106
+    """
107
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
108
+##end def
109
+
110
+def getLastDataPointTime():
111
+    """
112
+    Get the timestamp of the most recent update to the round robin
113
+    database.
114
+    Parameters: none
115
+    Returns: string epoch time stamp of the last rrd update
116
+    """
117
+    strCmd = "rrdtool lastupdate %s" % \
118
+             (_RRD_FILE)
119
+
120
+    # Run the command as a subprocess.
121
+    try:
122
+        result = subprocess.check_output(strCmd, shell=True,  \
123
+                             stderr=subprocess.STDOUT)
124
+    except subprocess.CalledProcessError, exError:
125
+        print "%s: rrdtool update failed: %s" % \
126
+                    (getTimeStamp(), exError.output)
127
+        return None
128
+
129
+    # Return just the epoch time stamp of the last rrd update
130
+    return int((result.split('\n')[-2]).split(':')[0])
131
+##end def
132
+
133
+def setStatusToOffline():
134
+    """Set the detected status of the aredn node to
135
+       "offline" and inform downstream clients by removing input
136
+       and output data files.
137
+       Parameters: none
138
+       Returns: nothing
139
+    """
140
+    global nodeOnline
141
+
142
+    # Inform downstream clients by removing output data file.
143
+    if os.path.exists(_OUTPUT_DATA_FILE):
144
+       os.remove(_OUTPUT_DATA_FILE)
145
+    if os.path.exists(_SMALL_OUTPUT_FILE):
146
+       os.remove(_SMALL_OUTPUT_FILE)
147
+    # If the aredn node was previously online, then send
148
+    # a message that we are now offline.
149
+    if nodeOnline:
150
+        print '%s aredn node offline' % getTimeStamp()
151
+    nodeOnline = False
152
+##end def
153
+
154
+def terminateAgentProcess(signal, frame):
155
+    """Send a message to log when the agent process gets killed
156
+       by the operating system.  Inform downstream clients
157
+       by removing input and output data files.
158
+       Parameters:
159
+           signal, frame - dummy parameters
160
+       Returns: nothing
161
+    """
162
+    # Inform downstream clients by removing output data file.
163
+    if os.path.exists(_OUTPUT_DATA_FILE):
164
+       os.remove(_OUTPUT_DATA_FILE)
165
+    if os.path.exists(_SMALL_OUTPUT_FILE):
166
+       os.remove(_SMALL_OUTPUT_FILE)
167
+    print '%s terminating arednsig agent process' % \
168
+              (getTimeStamp())
169
+    sys.exit(0)
170
+##end def
171
+
172
+  ###  PUBLIC METHODS  ###
173
+
174
+def getArednNodeData():
175
+    """Send http request to aredn node.  The response from the
176
+       node contains the node signal data as unformatted ascii text.
177
+       Parameters: none
178
+       Returns: a string containing the node signal data if successful,
179
+                or None if not successful
180
+    """
181
+    try:
182
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
183
+
184
+        # Format received data into a single string.
185
+        content = ""
186
+        for line in conn:
187
+            content += line.strip()
188
+        del conn
189
+
190
+    except Exception, exError:
191
+        # If no response is received from the node, then assume that
192
+        # the node is down or unavailable over the network.  In
193
+        # that case return None to the calling function.
194
+        print "%s http error: %s" % (getTimeStamp(), exError)
195
+        return None
196
+
197
+    if verboseDebug:
198
+        print "http request successful: %d bytes" % len(content)
199
+
200
+    return content
201
+##end def
202
+
203
+def parseNodeData(sData, ldData):
204
+    """Parse the node  signal data JSON string from the aredn node
205
+       into its component parts.  
206
+       Parameters:
207
+           sData - the string containing the data to be parsed
208
+           ldData - a list object to contain the parsed data items
209
+       Returns: True if successful, False otherwise
210
+    """
211
+    # Only interested in datapoints that have arrived during the
212
+    # current update cycle defined by the data request interval.
213
+    # The default is sixty minutes, and the node records a data point
214
+    # once a minute.  Therefore, in the default case, extract the last
215
+    # sixty data points from the node's response.  Otherwise, extract
216
+    # a number of datapoints equal the the data request interval
217
+    # (in minutes). 
218
+    iTrail = int(dataRequestInterval)
219
+
220
+    # The node response with json object containing 24 hours worth
221
+    # of datapoints.  Each data point is, itself, a json object.
222
+    # The following code converts the json object to a python list
223
+    # object containing dictionary objects.  Each dictionary object
224
+    # stores one data point.
225
+    try:
226
+        ldTmp = json.loads(sData[1:-1])
227
+        ldTmp = ldTmp[-iTrail:]
228
+        if len(ldTmp) != iTrail:
229
+            raise Exception("truncated list")
230
+    except Exception, exError:
231
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
232
+        return False
233
+       
234
+    # Store the dictionary objects in the list object passed by
235
+    # reference to this function.
236
+    del ldData[:]
237
+    for item in ldTmp:
238
+        ldData.append(item)
239
+
240
+    if verboseDebug:
241
+        print "parse successful: %d data points" % len(ldData)
242
+    return True
243
+##end def
244
+
245
+def convertData(ldData):
246
+    """Convert individual node signal data items as necessary.
247
+       Parameters:
248
+           ldData - a list object containing the node signal data
249
+       Returns: True if successful, False otherwise
250
+    """
251
+    # parse example string
252
+    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
253
+    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
254
+    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
255
+    #
256
+    for item in ldData:
257
+        try:
258
+            # Convert data items to the required data types
259
+            # (all integer in this case).  Change the dictionary
260
+            # object keys to more friendly names.
261
+            item['time'] = int(item.pop('x')) / 1000
262
+            item['signal'] = int(item['y'][0])
263
+            item['noise'] = int(item['y'][1])
264
+            item['snr'] = int(item.pop('m'))
265
+            item['rx_mcs'] = int(item.pop('rx_mcs'))
266
+            item['tx_mcs'] = int(item.pop('tx_mcs'))
267
+            item['rx_rate'] = int(item.pop('rx_rate'))
268
+            item['tx_rate'] = int(item.pop('tx_rate'))
269
+            item.pop('y')
270
+            item.pop('label')
271
+        except Exception, exError:
272
+            print "%s convert data failed: %s" % (getTimeStamp(), exError)
273
+            return False
274
+    ##end for
275
+    if verboseDebug:
276
+        print "convert data successful"
277
+    return True
278
+##end def
279
+
280
+def updateDatabase(ldData):
281
+    """
282
+    Update the rrdtool database by executing an rrdtool system command.
283
+    Format the command using the data extracted from the aredn node
284
+    response.   
285
+    Parameters: ldData - a list object containing data items to be
286
+                        written to the RRD file
287
+    Returns: True if successful, False otherwise
288
+    """
289
+    updateCount = 0
290
+    lastDataPointTime = getLastDataPointTime()
291
+
292
+    if verboseDebug:
293
+         print "updating database..."
294
+
295
+    for item in ldData:
296
+        # Throw out data points that have a time stamp earlier
297
+        # than the last entry in the round-robin database (RRD).
298
+        # rrdtool will throw an error in this case.
299
+        if item['time'] <= lastDataPointTime:
300
+            if verboseDebug:
301
+                print "%s invalid timestamp: discarding data" % \
302
+                      (getTimeStamp())
303
+            continue
304
+
305
+        # Format the rrdtool update command.
306
+        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
307
+        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
308
+                 item['noise'], item['snr'], item['rx_mcs'], \
309
+                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
310
+
311
+        if verboseDebug:
312
+            print "%s" % strCmd # DEBUG
313
+
314
+        # Run the command as a subprocess.
315
+        try:
316
+            subprocess.check_output(strCmd, shell=True,  \
317
+                                 stderr=subprocess.STDOUT)
318
+        except subprocess.CalledProcessError, exError:
319
+            print "%s: rrdtool update failed: %s" % \
320
+                        (getTimeStamp(), exError.output)
321
+            return False
322
+        updateCount += 1
323
+    ##end for
324
+
325
+    if debugOption:
326
+        print '%s added %d data points to database' % \
327
+              (getTimeStamp(), updateCount)
328
+    return True
329
+##end def
330
+
331
+def writeOutputDataFile(sData, ldData):
332
+    """Write node data items to the output data file, formatted as 
333
+       a Javascript file.  This file may then be accessed and used by
334
+       by downstream clients, for instance, in HTML documents.
335
+       Parameters:
336
+           sData - a string object containing the data to be written
337
+                   to the output data file
338
+       Returns: True if successful, False otherwise
339
+    """
340
+    if verboseDebug:
341
+        print "write output data file: %d bytes" % len(sData)
342
+
343
+    # Write file for use by html clients.  The following two
344
+    # data items are sent to the client file.
345
+    #    * The last database update date and time
346
+    #    * The data request interval
347
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
348
+                                time.localtime(ldData[-1]['time']) )
349
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
350
+           (lastUpdate, dataRequestInterval)
351
+    try:
352
+        fc = open(_SMALL_OUTPUT_FILE, "w")
353
+        fc.write(sDate)
354
+        fc.close()
355
+    except Exception, exError:
356
+        print "%s write output file failed: %s" % (getTimeStamp(), exError)
357
+        return False
358
+    return True
359
+
360
+    # Write the entire node data response to the output data file.
361
+    try:
362
+        fc = open(_OUTPUT_DATA_FILE, "w")
363
+        fc.write(sData)
364
+        fc.close()
365
+    except Exception, exError:
366
+        print "%s write output file failed: %s" % (getTimeStamp(), exError)
367
+        return False
368
+    return True
369
+## end def
370
+
371
+def setNodeStatus(updateSuccess):
372
+    """Detect if aredn node is offline or not available on
373
+       the network. After a set number of attempts to get data
374
+       from the node set a flag that the node is offline.
375
+       Parameters:
376
+           updateSuccess - a boolean that is True if data request
377
+                           successful, False otherwise
378
+       Returns: nothing
379
+    """
380
+    global failedUpdateCount, nodeOnline
381
+
382
+    if updateSuccess:
383
+        failedUpdateCount = 0
384
+        # Set status and send a message to the log if the node was
385
+        # previously offline and is now online.
386
+        if not nodeOnline:
387
+            print '%s aredn node online' % getTimeStamp()
388
+            nodeOnline = True
389
+    else:
390
+        # The last attempt failed, so update the failed attempts
391
+        # count.
392
+        failedUpdateCount += 1
393
+
394
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
395
+        # Max number of failed data requests, so set
396
+        # node status to offline.
397
+        setStatusToOffline()
398
+##end def
399
+
400
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
401
+                lower, upper, addTrend, autoScale):
402
+    """Uses rrdtool to create a graph of specified radmon data item.
403
+       Parameters:
404
+           fileName - name of file containing the graph
405
+           dataItem - data item to be graphed
406
+           gLabel - string containing a graph label for the data item
407
+           gTitle - string containing a title for the graph
408
+           gStart - beginning time of the graphed data
409
+           lower - lower bound for graph ordinate #NOT USED
410
+           upper - upper bound for graph ordinate #NOT USED
411
+           addTrend - 0, show only graph data
412
+                      1, show only a trend line
413
+                      2, show a trend line and the graph data
414
+           autoScale - if True, then use vertical axis auto scaling
415
+               (lower and upper parameters are ignored), otherwise use
416
+               lower and upper parameters to set vertical axis scale
417
+       Returns: True if successful, False otherwise
418
+    """
419
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
420
+    trendWindow = { 'end-1day': 7200,
421
+                    'end-4weeks': 172800,
422
+                    'end-12months': 604800 }
423
+ 
424
+    # Format the rrdtool graph command.
425
+
426
+    # Set chart start time, height, and width.
427
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
428
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
429
+   
430
+    # Set the range and scaling of the chart y-axis.
431
+    if lower < upper:
432
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
433
+    elif autoScale:
434
+        strCmd += "-A "
435
+    strCmd += "-Y "
436
+
437
+    # Set the chart ordinate label and chart title. 
438
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
439
+ 
440
+    # Show the data, or a moving average trend line over
441
+    # the data, or both.
442
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
443
+    if addTrend == 0:
444
+        strCmd += "LINE1:dSeries#0400ff "
445
+    elif addTrend == 1:
446
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
447
+                  % trendWindow[gStart]
448
+    elif addTrend == 2:
449
+        strCmd += "LINE1:dSeries#0400ff "
450
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
451
+                  % trendWindow[gStart]
452
+     
453
+    if verboseDebug:
454
+        print "\n%s" % strCmd # DEBUG
455
+    
456
+    # Run the formatted rrdtool command as a subprocess.
457
+    try:
458
+        result = subprocess.check_output(strCmd, \
459
+                     stderr=subprocess.STDOUT,   \
460
+                     shell=True)
461
+    except subprocess.CalledProcessError, exError:
462
+        print "rrdtool graph failed: %s" % (exError.output)
463
+        return False
464
+
465
+    if debugOption:
466
+        print "rrdtool graph: %s" % result,
467
+    return True
468
+
469
+##end def
470
+
471
+def generateGraphs():
472
+    """Generate graphs for display in html documents.
473
+       Parameters: none
474
+       Returns: nothing
475
+    """
476
+    autoScale = False
477
+
478
+    createGraph('24hr_signal', 'S', 'dBm', 
479
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
480
+    createGraph('24hr_noise', 'N', 'dBm', 
481
+                'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
482
+    createGraph('24hr_snr', 'SNR', 'dB', 
483
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
484
+    createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
485
+                'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
486
+    createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
487
+                'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
488
+    #createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
489
+    #            'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
490
+    #createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
491
+    #            'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
+
493
+    createGraph('4wk_signal', 'S', 'dBm', 
494
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
495
+    createGraph('4wk_noise', 'N', 'dBm', 
496
+                'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
497
+    createGraph('4wk_snr', 'SNR', 'dB', 
498
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
499
+    createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
500
+                'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
501
+    createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
502
+                'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
503
+    #createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
504
+    #            'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
505
+    #createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
506
+    #            'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
507
+
508
+    createGraph('12m_signal', 'S', 'dBm', 
509
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
510
+    createGraph('12m_noise', 'N', 'dBm', 
511
+                'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
512
+    createGraph('12m_snr', 'SNR', 'dB', 
513
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
514
+    createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
515
+                'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
516
+    createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
517
+                'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
518
+    #createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
519
+    #            'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
520
+    #createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
521
+    #            'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
522
+##end def
523
+
524
+def getCLarguments():
525
+    """Get command line arguments.  There are four possible arguments
526
+          -d turns on debug mode
527
+          -v turns on verbose debug mode
528
+          -t sets the aredn node query interval
529
+          -u sets the url of the aredn nodeing device
530
+       Returns: nothing
531
+    """
532
+    global debugOption, verboseDebug, dataRequestInterval, \
533
+           arednNodeUrl
534
+
535
+    index = 1
536
+    while index < len(sys.argv):
537
+        if sys.argv[index] == '-d':
538
+            debugOption = True
539
+        elif sys.argv[index] == '-v':
540
+            debugOption = True
541
+            verboseDebug = True
542
+        elif sys.argv[index] == '-p':
543
+            try:
544
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
545
+            except:
546
+                print "invalid polling period"
547
+                exit(-1)
548
+            index += 1
549
+        elif sys.argv[index] == '-u':
550
+            arednNodeUrl = sys.argv[index + 1]
551
+            index += 1
552
+        else:
553
+            cmd_name = sys.argv[0].split('/')
554
+            print "Usage: %s [-d] [-v] [-pt seconds] [-u url}" % cmd_name[-1]
555
+            exit(-1)
556
+        index += 1
557
+##end def
558
+
559
+def main():
560
+    """Handles timing of events and acts as executive routine managing
561
+       all other functions.
562
+       Parameters: none
563
+       Returns: nothing
564
+    """
565
+    global dataRequestInterval
566
+
567
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
568
+
569
+    print '%s starting up arednsig agent process' % \
570
+                  (getTimeStamp())
571
+
572
+    # last time output JSON file updated
573
+    lastDataRequestTime = -1
574
+    # last time charts generated
575
+    lastChartUpdateTime = - 1
576
+    # last time the rrdtool database updated
577
+    lastDatabaseUpdateTime = -1
578
+
579
+    ## Get command line arguments.
580
+    getCLarguments()
581
+
582
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
583
+
584
+    chartUpdateInterval = dataRequestInterval # get charts when updating database
585
+
586
+    ## Exit with error if rrdtool database does not exist.
587
+    if not os.path.exists(_RRD_FILE):
588
+        print 'rrdtool database does not exist\n' \
589
+              'use createRadmonRrd script to ' \
590
+              'create rrdtool database\n'
591
+        exit(1)
592
+ 
593
+    ## main loop
594
+    while True:
595
+
596
+        currentTime = time.time() # get current time in seconds
597
+
598
+        # Every web update interval request data from the aredn
599
+        # node and process the received data.
600
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
601
+            lastDataRequestTime = currentTime
602
+            ldData = []
603
+            result = True
604
+
605
+            # Get the data string from the device.
606
+            sData = getArednNodeData()
607
+            # If the first http request fails, try one more time.
608
+            if sData == None:
609
+                time.sleep(5)
610
+                sData = getArednNodeData()
611
+                if sData == None:
612
+                    result = False
613
+
614
+            # If successful parse the data.
615
+            if result:
616
+                result = parseNodeData(sData, ldData)
617
+           
618
+            # If parsing successful, convert the data.
619
+            if result:
620
+                result = convertData(ldData)
621
+
622
+            # If conversion successful, write data to data files.
623
+            if result:
624
+                result = updateDatabase(ldData)
625
+
626
+            if result:
627
+                writeOutputDataFile(sData, ldData)
628
+
629
+            # Set the node status to online or offline depending on the
630
+            # success or failure of the above operations.
631
+            setNodeStatus(result)
632
+
633
+
634
+        # At the chart generation interval, generate charts.
635
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
636
+            lastChartUpdateTime = currentTime
637
+            p = multiprocessing.Process(target=generateGraphs, args=())
638
+            p.start()
639
+
640
+        # Relinquish processing back to the operating system until
641
+        # the next update interval.
642
+
643
+        elapsedTime = time.time() - currentTime
644
+        if debugOption:
645
+            if result:
646
+                print "%s update successful:" % getTimeStamp(),
647
+            else:
648
+               print "%s update failed:" % getTimeStamp(),
649
+            print "%6f seconds processing time\n" % elapsedTime 
650
+        remainingTime = requestIntervalSeconds - elapsedTime
651
+        if remainingTime > 0.0:
652
+            time.sleep(remainingTime)
653
+    ## end while
654
+    return
655
+## end def
656
+
657
+if __name__ == '__main__':
658
+    try:
659
+        main()
660
+    except KeyboardInterrupt:
661
+        print '\n',
662
+        terminateAgentProcess('KeyboardInterrupt','Module')
0 663
new file mode 100755
... ...
@@ -0,0 +1,96 @@
1
+#!/usr/bin/python -u
2
+# The -u option above turns off block buffering of python output. This
3
+# assures that each error message gets individually printed to the
4
+# log file.
5
+#
6
+# Module: createArednsigRrd.py
7
+#
8
+# Description: Creates a rrdtool database for use by the weather agent to
9
+# store the data from the weather station.  The agent uses the data in the
10
+# database to generate graphic charts for display in the weather station
11
+# web page.
12
+#
13
+# Copyright 2020 Jeff Owrey
14
+#    This program is free software: you can redistribute it and/or modify
15
+#    it under the terms of the GNU General Public License as published by
16
+#    the Free Software Foundation, either version 3 of the License, or
17
+#    (at your option) any later version.
18
+#
19
+#    This program is distributed in the hope that it will be useful,
20
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
+#    GNU General Public License for more details.
23
+#
24
+#    You should have received a copy of the GNU General Public License
25
+#    along with this program.  If not, see http://www.gnu.org/license.
26
+#
27
+# Revision History
28
+#   * v10 released 11 Jan 2020 by J L Owrey
29
+#
30
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
31
+import os
32
+import time
33
+import subprocess
34
+
35
+    ### DEFINE FILE LOCATIONS ###
36
+
37
+_USER = os.environ['USER']
38
+# the RRD file that stores the data
39
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
40
+
41
+    ### DEFINE DATABASE GRANULARITY AND TIME SPAN ###
42
+
43
+_RRD_SIZE_IN_DAYS = 370 # days
44
+_1YR_RRA_STEPS_PER_DAY = 96
45
+_DATABASE_UPDATE_INTERVAL = 60
46
+
47
+def createRrdFile():
48
+    """Create the rrd file if it does not exist.
49
+       Parameters: none
50
+       Returns: True, if successful
51
+    """
52
+    if os.path.exists(_RRD_FILE):
53
+        print "aredn node database already exists"
54
+        return True
55
+
56
+     ## Calculate database size
57
+ 
58
+    heartBeat = 2 * _DATABASE_UPDATE_INTERVAL
59
+    rra1yrNumPDP =  int(round(86400 / (_1YR_RRA_STEPS_PER_DAY * \
60
+                    _DATABASE_UPDATE_INTERVAL)))
61
+    rrd24hrNumRows = int(round(86400 / _DATABASE_UPDATE_INTERVAL))
62
+    rrd1yearNumRows = _1YR_RRA_STEPS_PER_DAY * _RRD_SIZE_IN_DAYS
63
+       
64
+    ## Format rrdtool command to create RRD database
65
+     
66
+    strFmt = ("rrdtool create %s --start now-1day --step %s "
67
+              "DS:S:GAUGE:%s:U:U DS:N:GAUGE:%s:U:U DS:SNR:GAUGE:%s:U:U "
68
+              "DS:RX_MCS:GAUGE:%s:U:U DS:TX_MCS:GAUGE:%s:U:U "
69
+              "DS:RX_RATE:GAUGE:%s:U:U DS:TX_RATE:GAUGE:%s:U:U "
70
+              "RRA:LAST:0.5:1:%s RRA:LAST:0.5:%s:%s")
71
+
72
+    strCmd = strFmt % (_RRD_FILE, _DATABASE_UPDATE_INTERVAL, \
73
+                heartBeat, heartBeat, heartBeat, heartBeat,  \
74
+                heartBeat,  heartBeat, heartBeat,            \
75
+                rrd24hrNumRows, rra1yrNumPDP, rrd1yearNumRows)
76
+
77
+    ## Run the command as a shell subprocess
78
+
79
+    print "creating aredn node database...\n\n%s\n" % strCmd
80
+
81
+    try:
82
+        subprocess.check_output(strCmd, stderr=subprocess.STDOUT, \
83
+                                shell=True)
84
+    except subprocess.CalledProcessError, exError:
85
+        print "rrdtool create failed: %s" % (exError.output)
86
+        return False
87
+    return True
88
+##end def
89
+
90
+def main():
91
+    createRrdFile()
92
+## end def
93
+
94
+if __name__ == '__main__':
95
+    main()
96
+        
0 97
new file mode 100644
... ...
@@ -0,0 +1,3 @@
1
+rrdtool create /home/pi/database/arednstatData.rrd --step 60 DS:S:GAUGE:120:U:U DS:N:GAUGE:120:U:U DS:SNR:GAUGE:120:U:U DS:RX_MCS:GAUGE:120:U:U DS:TX_MCS:GAUGE:120:U:U DS:RX_RATE:GAUGE:120:U:U DS:TX_RATE:GAUGE:120:U:U RRA:LAST:0.5:1:1440 RRA:LAST:0.5:15:35520
2
+
3
+
0 4
new file mode 100644
... ...
@@ -0,0 +1,289 @@
1
+<!DOCTYPE html>
2
+<!-- Courtesy ruler for editing this file
3
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
4
+-->
5
+<html>
6
+<head>
7
+<title>Node Signal</title>
8
+<meta charset="UTF-8">
9
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+<style>
11
+body {
12
+    background-image: url("static/chalk.jpg");
13
+}
14
+h2 {
15
+    font: bold 24px arial, sans-serif;
16
+}
17
+h3 {
18
+    font: bold 18px arial, sans-serif;
19
+}
20
+h4 {
21
+    font: bold 16px arial, sans-serif;
22
+}
23
+.mainContainer {
24
+    width: 750px;
25
+    text-align: center;
26
+    margin: auto;
27
+}
28
+.datetime {
29
+    font: bold 22px arial, sans-serif;
30
+    padding: 0px;
31
+}
32
+.rowContainer {
33
+    display: table;
34
+    width: 100%;
35
+}
36
+.currentDataCell {
37
+    width: 50%;
38
+    padding: 10px;
39
+    font: bold 20px arial, sans-serif;
40
+    text-align: center;
41
+    display: table-cell;
42
+    vertical-align: middle;
43
+    /*border: 1px solid black;*/
44
+}
45
+.dataItems {
46
+    padding: 2px;
47
+    text-align: left;
48
+    line-height: 130%;
49
+    display: inline-block;
50
+    vertical-align: middle;
51
+}
52
+.chartContainer {
53
+    padding: 2px;
54
+}
55
+img.chart {
56
+    width:100%;
57
+}
58
+.notes {
59
+    font: 17px arial, sans-serif;
60
+    text-align: left;
61
+    padding: 10px;
62
+}
63
+span.chartNav {
64
+    margin: auto;
65
+}
66
+ul.chartNav {
67
+    list-style-type: none;
68
+    margin: 10px;
69
+    padding: 0;
70
+    overflow: hidden;
71
+    background-color: #bbb;
72
+    text-align: center;
73
+}
74
+li.chartNav {
75
+    display: inline-block;
76
+    font: bold 18px arial, sans-serif;
77
+    color: black;
78
+}
79
+text.chartNav:hover {
80
+    background-color: #333;
81
+    cursor: pointer;
82
+    color: white;
83
+}
84
+text.chartNav {
85
+    display: inline-block;
86
+    padding: 8px 12px;
87
+}
88
+</style>
89
+</head>
90
+
91
+<body onload="main()">
92
+
93
+<div class="mainContainer">
94
+
95
+<h2><a href="https://github.com/fractalxaos/arednstat" 
96
+style="text-decoration:none" target="_new">
97
+KA7JLO Aredn Node Signal</a></h2>
98
+<h3>Last Updated</h3>
99
+<div class="datetime">
100
+<text id="date"></text>
101
+&nbsp;&nbsp;
102
+<text id="time"></text>
103
+</div>
104
+
105
+<div class="rowContainer">
106
+<div class="currentDataCell">
107
+<div class="dataItems" style="text-align: right;">
108
+Status:<br>
109
+Page updates every:
110
+</div>
111
+<div class="dataItems">
112
+<text id="status"></text><br>
113
+<text id="period"></text> minutes
114
+</div>
115
+</div>
116
+</div>
117
+
118
+<span class="chartNav">
119
+<ul class="chartNav">
120
+<li class="chartNav">Select charts:</li>
121
+<li class="chartNav"><text class="chartNav" onclick="setChartPeriod(1)">
122
+24 hours</text></li>
123
+<li class="chartNav"><text class="chartNav" onclick="setChartPeriod(2)">
124
+4 weeks</text></li>
125
+<li class="chartNav"><text class="chartNav" onclick="setChartPeriod(3)">
126
+12 months</text></li>
127
+</ul>
128
+</span>
129
+<br>
130
+
131
+<div class="chartContainer">
132
+<img class="chart" id="snrChart">
133
+</div>
134
+
135
+<div class="chartContainer">
136
+<img class="chart" id="signalChart">
137
+</div>
138
+
139
+<div class="chartContainer">
140
+<img class="chart" id="noiseChart">
141
+</div>
142
+
143
+<div class="chartContainer">
144
+<img class="chart" id="rxrateChart">
145
+</div>
146
+
147
+<div class="chartContainer">
148
+<img class="chart" id="txrateChart">
149
+</div>
150
+
151
+<div class="notes">
152
+<b>NOTES:</b>
153
+<ul>
154
+<li>Aredn Node Signal software available at
155
+<a href="https://github.com/fractalxaos/radmon" target="_new">
156
+<i>Github.com</i></a>.</li>
157
+<li>Project sponsored by 
158
+<a href="https://willamettevalleymesh.net" TARGET="_NEW">
159
+<i>Willamette Valley Mesh Network</i></a>, Salem, Oregon.</li>
160
+<li>For more information about the amateur radio emergency
161
+ data network (AREDN) see official web site at
162
+ <a href="http://www.arednmesh.org" target="_blank">
163
+www.arednmesh.org</a>.</li>
164
+</ul>
165
+</div>
166
+</div>
167
+<br>
168
+
169
+<script>
170
+
171
+/* Global constants */
172
+
173
+var nodeDataUrl = "dynamic/nodeOnline.js";
174
+
175
+/* Global DOM objects */
176
+
177
+// Chart elements
178
+var snrChart = document.getElementById("snrChart");
179
+var signalChart = document.getElementById("signalChart");
180
+var noiseChart = document.getElementById("noiseChart");
181
+var rxrateChart = document.getElementById("rxrateChart");
182
+var txrateChart = document.getElementById("txrateChart");
183
+
184
+// Text elements
185
+var dateElmt = document.getElementById("date");    
186
+var timeElmt = document.getElementById("time"); 
187
+var statusElmt = document.getElementById("status");
188
+var periodElmt = document.getElementById("period");
189
+/* Global objects */
190
+
191
+var httpRequest = new XMLHttpRequest();
192
+
193
+/* Global variables */
194
+
195
+var graphPeriod;
196
+var graphRefreshRate = 10; // chart refresh rate in minutes
197
+
198
+function main() {
199
+    /* Register call back function to process http requests */
200
+    httpRequest.onreadystatechange = function() {
201
+        if (httpRequest.readyState == 4 && httpRequest.status == 200) {
202
+            var dataArray = JSON.parse(httpRequest.responseText);
203
+            displayData(dataArray[0]);
204
+        } else if (httpRequest.readyState == 4 && httpRequest.status == 404) {
205
+            displayOfflineStatus();
206
+        }
207
+    };
208
+    httpRequest.ontimeout = function(e) {
209
+        displayOfflineStatus();
210
+    };
211
+
212
+    getNodeData();
213
+    graphPeriod = 1;
214
+    getNodeGraphs();
215
+}
216
+
217
+function getNodeData() {
218
+    httpRequest.open("GET", nodeDataUrl, true);
219
+    httpRequest.timeout = 3000;
220
+    httpRequest.send();
221
+}
222
+
223
+function setChartPeriod(n) {
224
+    graphPeriod = n;
225
+    getNodeGraphs();   
226
+}
227
+
228
+function getNodeGraphs() {
229
+    var d = new Date;
230
+    var pfx;
231
+
232
+    switch(graphPeriod) {
233
+        case 1:
234
+            pfx = "24hr_";
235
+            break;
236
+        case 2:
237
+            pfx = "4wk_";
238
+            break;
239
+       case 3:
240
+            pfx = "12m_";
241
+            break;
242
+    }
243
+    signalChart.src = "dynamic/" + pfx + "signal.png?ver=" + d.getTime();
244
+    noiseChart.src = "dynamic/" + pfx + "noise.png?ver=" + d.getTime();
245
+    snrChart.src = "dynamic/" + pfx + "snr.png?ver=" + d.getTime();
246
+    rxrateChart.src = "dynamic/" + pfx + "rx_rate.png?ver=" + d.getTime();
247
+    txrateChart.src = "dynamic/" + pfx + "tx_rate.png?ver=" + d.getTime();
248
+}
249
+
250
+function displayData(dataItem) {
251
+    var timeStamp, date, time, hourminute;
252
+    var localDateObj,localTimeZone;
253
+
254
+    timeStamp = dataItem.date;
255
+    date = timeStamp.split(" ")[0];
256
+    time = timeStamp.split(" ")[1];
257
+    hourminute = time.split(":")[0] + ":" + time.split(":")[1];
258
+    localDate = new Date();
259
+    localTimeZone = localDate.getTimezoneOffset() / 60;
260
+    dateElmt.innerHTML = date;    
261
+    timeElmt.innerHTML = hourminute +
262
+        "  <small>(GMT+" + localTimeZone + ")</small>";    
263
+     
264
+    statusElmt.innerHTML = "Online";
265
+    statusElmt.style.color = "green";
266
+
267
+    graphRefreshRate = dataItem.period;
268
+    periodElmt.innerHTML = graphRefreshRate;
269
+    setInterval(getNodeData, 60000 * graphRefreshRate);
270
+    setInterval(getNodeGraphs, 60000 * graphRefreshRate);
271
+}
272
+
273
+function displayOfflineStatus() {
274
+    var d = new Date();
275
+    localTimeZone = d.getTimezoneOffset() / 60;
276
+    dateElmt.innerHTML = (d.getMonth() + 1) + "/" + d.getDate() + "/" +
277
+                       d.getFullYear();    
278
+    timeElmt.innerHTML = d.getHours() + ":" + d.getMinutes() +
279
+                       "  <small>(GMT+" + localTimeZone + ")</small>";
280
+    periodElmt.innerHTML = "?";    
281
+    statusElmt.innerHTML = "offline";    
282
+    statusElmt.style.color = "red";
283
+}
284
+
285
+</script>
286
+
287
+</body>
288
+</html>
289
+
0 290
new file mode 120000
... ...
@@ -0,0 +1 @@
1
+/tmp/arednsig
0 2
\ No newline at end of file
1 3
new file mode 100644
... ...
@@ -0,0 +1,7 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+  <meta http-equiv="refresh" content="0; url=./arednsig.html">
5
+  <!--<meta http-equiv="refresh" content="0; url=https://github.com/fractalxaos/radmon">-->
6
+</head>
7
+</html>
0 8
new file mode 100644
1 9
Binary files /dev/null and b/arednsig/html/static/chalk.jpg differ