Browse code

support for Aredn FW v3.19.3.0

gandolf authored on 03/31/2020 17:38:41
Showing 18 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,27 @@
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
+AGENT_NAME="[a]rednsigAgent.py"
12
+NODE_URL="http://localnode:8080/cgi-bin/signal.json"
13
+
14
+POLLING_INTERVAL="60"
15
+
16
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
17
+
18
+if [ -n "$PROCESS_ID" ]; then
19
+  if [ "$1" != "-q" ]; then
20
+    printf "arednsig agent running [%s]\n" $PROCESS_ID
21
+  fi
22
+else
23
+  printf "starting up arednsig agent\n"
24
+  cd $APP_PATH
25
+  $(./$AGENT_NAME -u $NODE_URL -p $POLLING_INTERVAL >> \
26
+ $LOG_PATH/arednsigAgent.log 2>&1 &)
27
+fi
0 28
new file mode 100755
... ...
@@ -0,0 +1,14 @@
1
+#!/bin/bash
2
+# Stop the radmon agent process and clean up environment.
3
+
4
+
5
+AGENT_NAME="[a]rednsigAgent.py"
6
+
7
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
8
+
9
+if [ -n "$PROCESS_ID" ]; then
10
+  printf "killing arednsig agent [%s]\n" $PROCESS_ID
11
+  kill $PROCESS_ID
12
+else
13
+  echo arednsig agent not running
14
+fi
0 15
new file mode 100755
... ...
@@ -0,0 +1,701 @@
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 mest 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 node status 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
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#
37
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
+
39
+import os
40
+import urllib2
41
+import sys
42
+import signal
43
+import subprocess
44
+import multiprocessing
45
+import time
46
+import json
47
+
48
+_USER = os.environ['USER']
49
+_HOSTNAME = os.uname()[1]
50
+
51
+   ### DEFAULT AREDN NODE URL ###
52
+
53
+# set url of the aredn node
54
+
55
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/signal.json"
56
+
57
+    ### FILE AND FOLDER LOCATIONS ###
58
+
59
+# folder for containing dynamic data objects
60
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
61
+# folder for charts and output data file
62
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
+# location of data output file
64
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
65
+# dummy output data file
66
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
67
+# database that stores node data
68
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
69
+
70
+    ### GLOBAL CONSTANTS ###
71
+
72
+# max number of failed data requests allowed
73
+_MAX_FAILED_DATA_REQUESTS = 0
74
+# interval in minutes between data requests to the aredn node
75
+_DEFAULT_DATA_REQUEST_INTERVAL = 60
76
+# number seconds to wait for a response to HTTP request
77
+_HTTP_REQUEST_TIMEOUT = 10
78
+# standard chart width in pixels
79
+_CHART_WIDTH = 600
80
+# standard chart height in pixels
81
+_CHART_HEIGHT = 150
82
+# Set this to True only if this server is intended to relay raw
83
+# node data to a mirror server.
84
+_RELAY_SERVER = False
85
+
86
+   ### GLOBAL VARIABLES ###
87
+
88
+# turn on or off of verbose debugging information
89
+debugOption = False
90
+verboseDebug = False
91
+
92
+# The following two items are used for detecting system faults
93
+# and aredn node online or offline status.
94
+
95
+# count of failed attempts to get data from aredn node
96
+failedUpdateCount = 0
97
+# detected status of aredn node device
98
+nodeOnline = True
99
+
100
+# ip address of aredn node
101
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
102
+# frequency of data requests to aredn node
103
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
104
+# last node request time
105
+lastDataPointTime = -1
106
+
107
+  ###  PRIVATE METHODS  ###
108
+
109
+def getTimeStamp():
110
+    """
111
+    Set the error message time stamp to the local system time.
112
+    Parameters: none
113
+    Returns: string containing the time stamp
114
+    """
115
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
116
+##end def
117
+
118
+def getLastDataPointTime():
119
+    """
120
+    Get the timestamp of the most recent update to the round robin
121
+    database.
122
+    Parameters: none
123
+    Returns: string epoch time stamp of the last rrd update
124
+    """
125
+    strCmd = "rrdtool lastupdate %s" % \
126
+             (_RRD_FILE)
127
+
128
+    # Run the command as a subprocess.
129
+    try:
130
+        result = subprocess.check_output(strCmd, shell=True,  \
131
+                             stderr=subprocess.STDOUT)
132
+    except subprocess.CalledProcessError, exError:
133
+        print "%s: rrdtool update failed: %s" % \
134
+                    (getTimeStamp(), exError.output)
135
+        return None
136
+
137
+    # Return just the epoch time stamp of the last rrd update
138
+    return int((result.split('\n')[-2]).split(':')[0])
139
+##end def
140
+
141
+def setStatusToOffline():
142
+    """Set the detected status of the aredn node to
143
+       "offline" and inform downstream clients by removing input
144
+       and output data files.
145
+       Parameters: none
146
+       Returns: nothing
147
+    """
148
+    global nodeOnline
149
+
150
+    # Inform downstream clients by removing output data file.
151
+    if os.path.exists(_OUTPUT_DATA_FILE):
152
+       os.remove(_OUTPUT_DATA_FILE)
153
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
154
+       os.remove(_DUMMY_OUTPUT_FILE)
155
+    # If the aredn node was previously online, then send
156
+    # a message that we are now offline.
157
+    if nodeOnline:
158
+        print '%s aredn node offline' % getTimeStamp()
159
+    nodeOnline = False
160
+##end def
161
+
162
+def terminateAgentProcess(signal, frame):
163
+    """Send a message to log when the agent process gets killed
164
+       by the operating system.  Inform downstream clients
165
+       by removing input and output data files.
166
+       Parameters:
167
+           signal, frame - dummy parameters
168
+       Returns: nothing
169
+    """
170
+    # Inform downstream clients by removing output data file.
171
+    if os.path.exists(_OUTPUT_DATA_FILE):
172
+       os.remove(_OUTPUT_DATA_FILE)
173
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
174
+       os.remove(_DUMMY_OUTPUT_FILE)
175
+    print '%s terminating arednsig agent process' % \
176
+              (getTimeStamp())
177
+    sys.exit(0)
178
+##end def
179
+
180
+  ###  PUBLIC METHODS  ###
181
+
182
+def getArednNodeData():
183
+    """Send http request to aredn node.  The response from the
184
+       node contains the node signal data as unformatted ascii text.
185
+       Parameters: none
186
+       Returns: a string containing the node signal data if successful,
187
+                or None if not successful
188
+    """
189
+    try:
190
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
191
+
192
+        # Format received data into a single string.
193
+        content = ""
194
+        for line in conn:
195
+            content += line.strip()
196
+        del conn
197
+
198
+    except Exception, exError:
199
+        # If no response is received from the device, then assume that
200
+        # the device is down or unavailable over the network.  In
201
+        # that case return None to the calling function.
202
+        print "%s http error: %s" % (getTimeStamp(), exError)
203
+        return None
204
+
205
+    if verboseDebug:
206
+        print "http request successful: %d bytes" % len(content)
207
+
208
+    return content
209
+##end def
210
+
211
+def parseNodeData(sData, ldData):
212
+    """Parse the node  signal data JSON string from the aredn node
213
+       into its component parts.  
214
+       Parameters:
215
+           sData - the string containing the data to be parsed
216
+           dData - a dictionary object to contain the parsed data items
217
+       Returns: True if successful, False otherwise
218
+    """
219
+    iTrail = int(dataRequestInterval)
220
+
221
+    try:
222
+        ldTmp = json.loads(sData[1:-1])
223
+        ldTmp = ldTmp[-iTrail:]
224
+        if len(ldTmp) != iTrail:
225
+            #raise Exception("truncated list")
226
+            pass
227
+    except Exception, exError:
228
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
229
+        return False
230
+    
231
+    del ldData[:]
232
+    for item in ldTmp:
233
+        ldData.append(item)
234
+
235
+    if verboseDebug:
236
+        print "parse successful: %d data points" % len(ldData)
237
+    return True
238
+##end def
239
+
240
+def convertData(ldData):
241
+    """Convert individual node signal data items as necessary.
242
+       Parameters:
243
+           dData - a dictionary object containing the node signal data
244
+       Returns: True if successful, False otherwise
245
+    """
246
+    # parse example string
247
+    # {u'tx_mcs': u'15', u'rx_mcs': u'15', u'm': 47,
248
+    #  u'label': u'01/10/2020 22:17:01', u'rx_rate': u'130',
249
+    #  u'y': [-48, -95], u'x': 1578694621000, u'tx_rate': u'130'}
250
+    #
251
+    index = 0
252
+    while index < len(ldData):
253
+        item = ldData[index]
254
+        try:
255
+            item['time'] = int(item.pop('x')) / 1000
256
+            item['signal'] = int(item['y'][0])
257
+            item['noise'] = int(item['y'][1])
258
+            item['snr'] = int(item.pop('m'))
259
+            item['rx_mcs'] = int(item.pop('rx_mcs'))
260
+            item['tx_mcs'] = int(item.pop('tx_mcs'))
261
+            item['rx_rate'] = int(item.pop('rx_rate'))
262
+            item['tx_rate'] = int(item.pop('tx_rate'))
263
+            item.pop('y')
264
+            item.pop('label')
265
+        except Exception, exError:
266
+            print "%s convert data item failed: %s" % (getTimeStamp(), exError)
267
+            print "discarding %s" % item
268
+            #return False
269
+            ldData.pop(index)
270
+        else:
271
+            index += 1
272
+    ##end for
273
+
274
+    if len(ldData) > 0:
275
+        if verboseDebug:
276
+            print "convert data successful"
277
+        return True
278
+    else:
279
+        print "convert data failed"
280
+        return False
281
+##end def
282
+
283
+def updateDatabase(ldData):
284
+    """
285
+    Update the rrdtool database by executing an rrdtool system command.
286
+    Format the command using the data extracted from the aredn node
287
+    response.   
288
+    Parameters: dData - dictionary object containing data items to be
289
+                        written to the rr database file
290
+    Returns: True if successful, False otherwise
291
+    """
292
+    updateCount = 0
293
+    lastDataPointTime = getLastDataPointTime()
294
+
295
+    if verboseDebug:
296
+         print "updating database..."
297
+
298
+    for item in ldData:
299
+
300
+        if item['time'] <= lastDataPointTime:
301
+            if verboseDebug:
302
+                print "%s invalid timestamp: discarding data" % \
303
+                      (getTimeStamp())
304
+            continue
305
+
306
+        # Format the rrdtool update command.
307
+        strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
308
+        strCmd = strFmt % (_RRD_FILE, item['time'], item['signal'], \
309
+                 item['noise'], item['snr'], item['rx_mcs'], \
310
+                 item['tx_mcs'], item['rx_rate'], item['tx_rate'])
311
+
312
+        if verboseDebug:
313
+            print "%s" % strCmd # DEBUG
314
+
315
+        # Run the command as a subprocess.
316
+        try:
317
+            subprocess.check_output(strCmd, shell=True,  \
318
+                                 stderr=subprocess.STDOUT)
319
+        except subprocess.CalledProcessError, exError:
320
+            print "%s: rrdtool update failed: %s" % \
321
+                        (getTimeStamp(), exError.output)
322
+            return False
323
+        updateCount += 1
324
+    ##end for
325
+
326
+    if debugOption:
327
+        print '%s added %d data points to database' % \
328
+              (getTimeStamp(), updateCount)
329
+    return True
330
+##end def
331
+
332
+def writeOutputDataFile(sData, ldData):
333
+    """Write node data items to the output data file, formatted as 
334
+       a Javascript file.  This file may then be accessed and used by
335
+       by downstream clients, for instance, in HTML documents.
336
+       Parameters:
337
+           sData - a string object containing the data to be written
338
+                   to the output data file
339
+       Returns: True if successful, False otherwise
340
+    """
341
+    # Write file for use by html clients.  The following two
342
+    # data items are sent to the client file.
343
+    #    * The last database update date and time
344
+    #    * The data request interval
345
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
346
+                                time.localtime(ldData[-1]['time']) )
347
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
348
+           (lastUpdate, dataRequestInterval)
349
+    try:
350
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
351
+        fc.write(sDate)
352
+        fc.close()
353
+    except Exception, exError:
354
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
355
+        return False
356
+
357
+    if _RELAY_SERVER:
358
+        # Write the entire node data response to the output data file.
359
+        try:
360
+            fc = open(_OUTPUT_DATA_FILE, "w")
361
+            fc.write(sData)
362
+            fc.close()
363
+        except Exception, exError:
364
+            print "%s write output file failed: %s" % \
365
+                  (getTimeStamp(), exError)
366
+            return False
367
+        if verboseDebug:
368
+            print "write output data file: %d bytes" % len(sData)
369
+
370
+    return True
371
+## end def
372
+
373
+def setNodeStatus(updateSuccess):
374
+    """Detect if aredn node is offline or not available on
375
+       the network. After a set number of attempts to get data
376
+       from the node set a flag that the node is offline.
377
+       Parameters:
378
+           updateSuccess - a boolean that is True if data request
379
+                           successful, False otherwise
380
+       Returns: nothing
381
+    """
382
+    global failedUpdateCount, nodeOnline
383
+
384
+    if updateSuccess:
385
+        failedUpdateCount = 0
386
+        # Set status and send a message to the log if the node was
387
+        # previously offline and is now online.
388
+        if not nodeOnline:
389
+            print '%s aredn node online' % getTimeStamp()
390
+            nodeOnline = True
391
+    else:
392
+        # The last attempt failed, so update the failed attempts
393
+        # count.
394
+        failedUpdateCount += 1
395
+
396
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
397
+        # Max number of failed data requests, so set
398
+        # node status to offline.
399
+        setStatusToOffline()
400
+##end def
401
+
402
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
403
+                lower, upper, addTrend, autoScale):
404
+    """Uses rrdtool to create a graph of specified node data item.
405
+       Parameters:
406
+           fileName - name of file containing the graph
407
+           dataItem - data item to be graphed
408
+           gLabel - string containing a graph label for the data item
409
+           gTitle - string containing a title for the graph
410
+           gStart - beginning time of the graphed data
411
+           lower - lower bound for graph ordinate #NOT USED
412
+           upper - upper bound for graph ordinate #NOT USED
413
+           addTrend - 0, show only graph data
414
+                      1, show only a trend line
415
+                      2, show a trend line and the graph data
416
+           autoScale - if True, then use vertical axis auto scaling
417
+               (lower and upper parameters are ignored), otherwise use
418
+               lower and upper parameters to set vertical axis scale
419
+       Returns: True if successful, False otherwise
420
+    """
421
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
422
+    trendWindow = { 'end-1day': 7200,
423
+                    'end-4weeks': 172800,
424
+                    'end-12months': 604800 }
425
+ 
426
+    # Format the rrdtool graph command.
427
+
428
+    # Set chart start time, height, and width.
429
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
430
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
431
+   
432
+    # Set the range and scaling of the chart y-axis.
433
+    if lower < upper:
434
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
435
+    elif autoScale:
436
+        strCmd += "-A "
437
+    strCmd += "-Y "
438
+
439
+    # Set the chart ordinate label and chart title. 
440
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
441
+ 
442
+    # Show the data, or a moving average trend line over
443
+    # the data, or both.
444
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
445
+    if addTrend == 0:
446
+        strCmd += "LINE1:dSeries#0400ff "
447
+    elif addTrend == 1:
448
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
449
+                  % trendWindow[gStart]
450
+    elif addTrend == 2:
451
+        strCmd += "LINE1:dSeries#0400ff "
452
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
453
+                  % trendWindow[gStart]
454
+     
455
+    if verboseDebug:
456
+        print "%s" % strCmd # DEBUG
457
+    
458
+    # Run the formatted rrdtool command as a subprocess.
459
+    try:
460
+        result = subprocess.check_output(strCmd, \
461
+                     stderr=subprocess.STDOUT,   \
462
+                     shell=True)
463
+    except subprocess.CalledProcessError, exError:
464
+        print "rrdtool graph failed: %s" % (exError.output)
465
+        return False
466
+
467
+    if debugOption:
468
+        print "rrdtool graph: %s\n" % result,
469
+    return True
470
+
471
+##end def
472
+
473
+def generateGraphs():
474
+    """Generate graphs for display in html documents.
475
+       Parameters: none
476
+       Returns: nothing
477
+    """
478
+    autoScale = False
479
+
480
+    # The following will force creation of charts
481
+    # of only signal strength and S/N charts.  Note that the following
482
+    # data items appear constant and do not show variation with time:
483
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
484
+    # these parameters are demonstrated to vary in time, there is no point
485
+    # in creating the charts for these data items.
486
+    createAllCharts = False
487
+
488
+    # 24 hour stock charts
489
+
490
+    createGraph('24hr_signal', 'S', 'dBm', 
491
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
492
+    createGraph('24hr_snr', 'SNR', 'dB', 
493
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
494
+
495
+    if createAllCharts:
496
+        createGraph('24hr_noise', 'N', 'dBm', 
497
+                    'Noise\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
498
+                    autoScale)
499
+        createGraph('24hr_rx_rate', 'RX_RATE', 'Mbps',
500
+                    'Rx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
501
+                    autoScale)
502
+        createGraph('24hr_tx_rate', 'TX_RATE', 'Mbps',
503
+                    'Tx\ Rate\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
504
+                    autoScale)
505
+        createGraph('24hr_rx_mcs', 'RX_MCS', 'Index',
506
+                    'Rx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
507
+                     autoScale)
508
+        createGraph('24hr_tx_mcs', 'TX_MCS', 'Index',
509
+                    'Tx\ MCS\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2,
510
+                     autoScale)
511
+
512
+    # 4 week stock charts
513
+
514
+    createGraph('4wk_signal', 'S', 'dBm', 
515
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
516
+    createGraph('4wk_snr', 'SNR', 'dB', 
517
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
518
+
519
+    if createAllCharts:
520
+        createGraph('4wk_noise', 'N', 'dBm', 
521
+                    'Noise\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
522
+                    autoScale)
523
+        createGraph('4wk_rx_rate', 'RX_RATE', 'Mbps',
524
+                    'Rx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
525
+                    autoScale)
526
+        createGraph('4wk_tx_rate', 'TX_RATE', 'Mbps',
527
+                    'Tx\ Rate\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
528
+                    autoScale)
529
+        createGraph('4wk_rx_mcs', 'RX_MCS', 'Index',
530
+                    'Rx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
531
+                    autoScale)
532
+        createGraph('4wk_tx_mcs', 'TX_MCS', 'Index',
533
+                    'Tx\ MCS\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2,
534
+                    autoScale)
535
+
536
+    # 12 month stock charts
537
+
538
+    createGraph('12m_signal', 'S', 'dBm', 
539
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
540
+    createGraph('12m_snr', 'SNR', 'dB', 
541
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
542
+
543
+    if createAllCharts:
544
+        createGraph('12m_noise', 'N', 'dBm', 
545
+                    'Noise\ -\ Past\ Year', 'end-12months', 0, 0, 2,
546
+                    autoScale)
547
+        createGraph('12m_rx_rate', 'RX_RATE', 'Mbps',
548
+                    'Rx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
549
+                    autoScale)
550
+        createGraph('12m_tx_rate', 'TX_RATE', 'Mbps',
551
+                    'Tx\ Rate\ -\ Past\ Year', 'end-12months', 0, 0, 2,
552
+                    autoScale)
553
+        createGraph('12m_rx_mcs', 'RX_MCS', 'Index',
554
+                    'Rx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
555
+                    autoScale)
556
+        createGraph('12m_tx_mcs', 'TX_MCS', 'Index',
557
+                    'Tx\ MCS\ -\ Past\ Year', 'end-12months', 0, 0, 2,
558
+                    autoScale)
559
+    if debugOption:
560
+        #print # print a blank line to improve readability when in debug mode
561
+        pass
562
+##end def
563
+
564
+def getCLarguments():
565
+    """Get command line arguments.  There are four possible arguments
566
+          -d turns on debug mode
567
+          -v turns on verbose debug mode
568
+          -t sets the aredn node query interval
569
+          -u sets the url of the aredn nodeing device
570
+       Returns: nothing
571
+    """
572
+    global debugOption, verboseDebug, dataRequestInterval, \
573
+           arednNodeUrl
574
+
575
+    index = 1
576
+    while index < len(sys.argv):
577
+        if sys.argv[index] == '-d':
578
+            debugOption = True
579
+        elif sys.argv[index] == '-v':
580
+            debugOption = True
581
+            verboseDebug = True
582
+        elif sys.argv[index] == '-p':
583
+            try:
584
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
585
+            except:
586
+                print "invalid polling period"
587
+                exit(-1)
588
+            index += 1
589
+        elif sys.argv[index] == '-u':
590
+            arednNodeUrl = sys.argv[index + 1]
591
+            index += 1
592
+        else:
593
+            cmd_name = sys.argv[0].split('/')
594
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
595
+            exit(-1)
596
+        index += 1
597
+##end def
598
+
599
+def main():
600
+    """Handles timing of events and acts as executive routine managing
601
+       all other functions.
602
+       Parameters: none
603
+       Returns: nothing
604
+    """
605
+    global dataRequestInterval
606
+
607
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
608
+
609
+    print '%s starting up arednsig agent process' % \
610
+                  (getTimeStamp())
611
+
612
+    # last time output JSON file updated
613
+    lastDataRequestTime = -1
614
+    # last time charts generated
615
+    lastChartUpdateTime = - 1
616
+    # last time the rrdtool database updated
617
+    lastDatabaseUpdateTime = -1
618
+
619
+    ## Get command line arguments.
620
+    getCLarguments()
621
+
622
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
623
+    chartUpdateInterval = dataRequestInterval # get charts interval
624
+
625
+    ## Exit with error if rrdtool database does not exist.
626
+    if not os.path.exists(_RRD_FILE):
627
+        print 'rrdtool database does not exist\n' \
628
+              'use createArednsigRrd script to ' \
629
+              'create rrdtool database\n'
630
+        exit(1)
631
+ 
632
+    ## main loop
633
+    while True:
634
+
635
+        currentTime = time.time() # get current time in seconds
636
+
637
+        # Every web update interval request data from the aredn
638
+        # node and process the received data.
639
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
640
+            lastDataRequestTime = currentTime
641
+            ldData = []
642
+            result = True
643
+
644
+            # Get the data string from the device.
645
+            sData = getArednNodeData()
646
+            # If the first http request fails, try one more time.
647
+            if sData == None:
648
+                time.sleep(5)
649
+                sData = getArednNodeData()
650
+                if sData == None:
651
+                    result = False
652
+
653
+            # If successful parse the data.
654
+            if result:
655
+                result = parseNodeData(sData, ldData)
656
+           
657
+            # If parsing successful, convert the data.
658
+            if result:
659
+                result = convertData(ldData)
660
+
661
+            # If conversion successful, write data to data files.
662
+            if result:
663
+                result = updateDatabase(ldData)
664
+
665
+            if result:
666
+                writeOutputDataFile(sData, ldData)
667
+
668
+            # Set the node status to online or offline depending on the
669
+            # success or failure of the above operations.
670
+            setNodeStatus(result)
671
+
672
+
673
+        # At the chart generation interval, generate charts.
674
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
675
+            lastChartUpdateTime = currentTime
676
+            p = multiprocessing.Process(target=generateGraphs, args=())
677
+            p.start()
678
+
679
+        # Relinquish processing back to the operating system until
680
+        # the next update interval.
681
+
682
+        elapsedTime = time.time() - currentTime
683
+        if debugOption:
684
+            if result:
685
+                print "%s update successful:" % getTimeStamp(),
686
+            else:
687
+                print "%s update failed:" % getTimeStamp(),
688
+            print "%6f seconds processing time\n" % elapsedTime 
689
+        remainingTime = requestIntervalSeconds - elapsedTime
690
+        if remainingTime > 0.0:
691
+            time.sleep(remainingTime)
692
+    ## end while
693
+    return
694
+## end def
695
+
696
+if __name__ == '__main__':
697
+    try:
698
+        main()
699
+    except KeyboardInterrupt:
700
+        print '\n',
701
+        terminateAgentProcess('KeyboardInterrupt','Module')
0 702
new file mode 100755
... ...
@@ -0,0 +1,89 @@
1
+#!/usr/bin/python -u
2
+## The -u option above turns off block buffering of python output. This assures
3
+## that each error message gets individually printed to the log file.
4
+#
5
+# Module: createArednsigRrd.py
6
+#
7
+# Description: Creates a rrdtool database for use by the weather agent to
8
+# store the data from the weather station.  The agent uses the data in the
9
+# database to generate graphic charts for display in the weather station
10
+# web page.
11
+#
12
+# Copyright 2020 Jeff Owrey
13
+#    This program is free software: you can redistribute it and/or modify
14
+#    it under the terms of the GNU General Public License as published by
15
+#    the Free Software Foundation, either version 3 of the License, or
16
+#    (at your option) any later version.
17
+#
18
+#    This program is distributed in the hope that it will be useful,
19
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21
+#    GNU General Public License for more details.
22
+#
23
+#    You should have received a copy of the GNU General Public License
24
+#    along with this program.  If not, see http://www.gnu.org/license.
25
+#
26
+# Revision History
27
+#   * v10 released 11 Jan 2020 by J L Owrey
28
+#
29
+import os
30
+import time
31
+import subprocess
32
+
33
+    ### DEFINE FILE LOCATIONS ###
34
+
35
+_USER = os.environ['USER']
36
+# the file that stores the data
37
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
38
+_RRD_SIZE_IN_DAYS = 370 # days
39
+_1YR_RRA_STEPS_PER_DAY = 96
40
+_DATABASE_UPDATE_INTERVAL = 60
41
+
42
+def createRrdFile():
43
+    """Create the rrd file if it does not exist.
44
+       Parameters: none
45
+       Returns: True, if successful
46
+    """
47
+
48
+    if os.path.exists(_RRD_FILE):
49
+        print "aredn node database already exists"
50
+        return True
51
+
52
+     ## Calculate database size
53
+ 
54
+    heartBeat = 2 * _DATABASE_UPDATE_INTERVAL
55
+    rra1yrNumPDP =  int(round(86400 / (_1YR_RRA_STEPS_PER_DAY * \
56
+                    _DATABASE_UPDATE_INTERVAL)))
57
+    rrd24hrNumRows = int(round(86400 / _DATABASE_UPDATE_INTERVAL))
58
+    rrd1yearNumRows = _1YR_RRA_STEPS_PER_DAY * _RRD_SIZE_IN_DAYS
59
+       
60
+    strFmt = ("rrdtool create %s --start now-1day --step %s "
61
+              "DS:S:GAUGE:%s:U:U DS:N:GAUGE:%s:U:U DS:SNR:GAUGE:%s:U:U "
62
+              "DS:RX_MCS:GAUGE:%s:U:U DS:TX_MCS:GAUGE:%s:U:U "
63
+              "DS:RX_RATE:GAUGE:%s:U:U DS:TX_RATE:GAUGE:%s:U:U "
64
+              "RRA:LAST:0.5:1:%s RRA:LAST:0.5:%s:%s")
65
+
66
+    strCmd = strFmt % (_RRD_FILE, _DATABASE_UPDATE_INTERVAL, \
67
+                heartBeat, heartBeat, heartBeat, heartBeat,  \
68
+                heartBeat,  heartBeat, heartBeat,            \
69
+                rrd24hrNumRows, rra1yrNumPDP, rrd1yearNumRows)
70
+
71
+    print "creating aredn node database...\n\n%s\n" % strCmd
72
+
73
+    # Spawn a sub-shell and run the command
74
+    try:
75
+        subprocess.check_output(strCmd, stderr=subprocess.STDOUT, \
76
+                                shell=True)
77
+    except subprocess.CalledProcessError, exError:
78
+        print "rrdtool create failed: %s" % (exError.output)
79
+        return False
80
+    return True
81
+##end def
82
+
83
+def main():
84
+    createRrdFile()
85
+## end def
86
+
87
+if __name__ == '__main__':
88
+    main()
89
+        
0 90
new file mode 100755
... ...
@@ -0,0 +1,16 @@
1
+#!/bin/sh
2
+#
3
+# Create a directory in the temporary file system for arednsig dynamic
4
+# data.  Set ownership and permissions to allow the Apache www-data user
5
+# read and write access to this folder.
6
+mkdir /tmp/arednsig
7
+sudo chown :www-data /tmp/arednsig
8
+chmod g+w /tmp/arednsig
9
+
10
+# Uncomment the following line if you choose to mount the dynamic
11
+# folder to the folder created above.
12
+#sudo mount --bind /tmp/arednsig  /home/pi/public_html/arednsig/dynamic
13
+
14
+# Start arednsig agent
15
+(sleep 5; /home/pi/bin/ardstart;) &
16
+
0 17
new file mode 100644
... ...
@@ -0,0 +1,323 @@
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
+    /*border: 1px solid black;*/
28
+}
29
+.datetime {
30
+    font: bold 22px arial, sans-serif;
31
+    padding: 0px;
32
+}
33
+.rowContainer {
34
+    display: table;
35
+    width: 100%;
36
+}
37
+.currentDataCell {
38
+    width: 50%;
39
+    padding: 10px;
40
+    font: bold 20px arial, sans-serif;
41
+    text-align: center;
42
+    display: table-cell;
43
+    vertical-align: middle;
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.chartSelector {
64
+    margin: auto;
65
+}
66
+ul.selectorElement {
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.selectorElement {
75
+    display: inline-block;
76
+    font: bold 18px arial, sans-serif;
77
+    color: black;
78
+}
79
+span.selectorElement:hover {
80
+    background-color: #333;
81
+    cursor: pointer;
82
+    color: white;
83
+}
84
+span.selectorElement {
85
+    display: inline-block;
86
+    padding: 8px 12px;
87
+}
88
+#iframe_a {
89
+    border:none;
90
+    width:100%;
91
+    height:450px;
92
+}
93
+</style>
94
+</head>
95
+
96
+<body onload="main()">
97
+
98
+<div class="mainContainer">
99
+<h2><a href="https://github.com/fractalxaos/ham/tree/master/arednsig" 
100
+  style="text-decoration:none" target="_new">
101
+KA7JLO AREDN<sup>&#174;</sup> Node Signal</a></h2>
102
+<h3>Last Updated</h3>
103
+<div class="datetime">
104
+<span id="date"></span>
105
+&nbsp;&nbsp;
106
+<span id="time"></span>
107
+</div>
108
+
109
+<div class="rowContainer">
110
+<div class="currentDataCell">
111
+<div class="dataItems" style="text-align: center;">
112
+Status: <span id="status"></span><br>
113
+Data updates every: <span id="period"></span> minutes
114
+</div>
115
+
116
+</div>
117
+</div>
118
+
119
+<span class="chartSelectors">
120
+<ul class="selectorElement">
121
+<li class="selectorElement">Select charts:</li>
122
+<li class="selectorElement"><span class="selectorElement"
123
+ onclick="setChartPeriod(1)">24 hours</span></li>
124
+<li class="selectorElement"><span class="selectorElement"
125
+ onclick="setChartPeriod(2)">4 weeks</span></li>
126
+<li class="selectorElement"><span class="selectorElement"
127
+ onclick="setChartPeriod(3)">12 months</span></li>
128
+<li class="selectorElement"><span class="selectorElement"
129
+ onclick="setChartPeriod(0)">Custom…</span></li>
130
+</ul>
131
+</span>
132
+
133
+<div class="rowContainer" id="customChartsContainer" style="display:none;">
134
+<div class="currentDataCell">
135
+<form id="fmDateSelector" action="arednsig.php" method="post"
136
+ target="iframe_a">
137
+<label for="beginDate">Begin Date: </label>
138
+<input id="beginDate" name="beginDate" type="date" value="mm/dd/yyyy" />
139
+<label for="endDate">End Date: </label>
140
+<input id="endDate" name="endDate" type="date" value="mm/dd/yyyy" />
141
+<br><br>
142
+<input type="button" onclick="getCustomCharts()" value="Get Charts">
143
+</form>
144
+<span id="errorMsg"></span><br>
145
+<iframe id="iframe_a" name="iframe_a"></iframe>
146
+</div>
147
+</div>
148
+
149
+<br>
150
+
151
+<div class="rowContainer" id="stockChartsContainer">
152
+<div class="chartContainer">
153
+<img class="chart" id="signalChart">
154
+</div>
155
+<div class="chartContainer">
156
+<img class="chart" id="snrChart">
157
+</div>
158
+</div>
159
+
160
+<div class="notes">
161
+<b>NOTES:</b>
162
+<ul>
163
+<li>Aredn Node Signal software available at
164
+<a href="https://github.com/fractalxaos/ham/tree/master/arednsig"
165
+ target="_new">
166
+<i>Github.com</i></a>.</li>
167
+<li>Project sponsored by 
168
+<a href="https://willamettevalleymesh.net" TARGET="_NEW">
169
+<i>Willamette Valley Mesh Network</i></a>, Salem, Oregon.</li>
170
+<li>For more information about the amateur radio emergency
171
+ data network (AREDN) see official web site at
172
+ <a href="http://www.arednmesh.org" target="_blank">
173
+www.arednmesh.org</a>.</li>
174
+</ul>
175
+</div>
176
+</div>
177
+<br>
178
+
179
+<script>
180
+
181
+/* Global constants */
182
+
183
+var nodeDataUrl = "dynamic/nodeOnline.js";
184
+
185
+/* Global DOM objects */
186
+
187
+// Chart elements
188
+var signalChart = document.getElementById("signalChart");
189
+var snrChart = document.getElementById("snrChart");
190
+
191
+// Text elements
192
+var dateElmt = document.getElementById("date");    
193
+var timeElmt = document.getElementById("time"); 
194
+var statusElmt = document.getElementById("status");
195
+var periodElmt = document.getElementById("period");
196
+
197
+// Document elements
198
+var customChartsContainer = document.getElementById("customChartsContainer");
199
+var stockChartsContainer = document.getElementById("stockChartsContainer");
200
+var fmDateSelector = document.getElementById("fmDateSelector");
201
+var errorMsg = document.getElementById("errorMsg");
202
+
203
+/* Global objects */
204
+
205
+var httpRequest = new XMLHttpRequest();
206
+
207
+/* Global variables */
208
+
209
+var chartPeriod = 1;
210
+var chartRefreshRate = 0; // chart refresh rate in minutes
211
+
212
+function main() {
213
+    /* Register call back function to process http requests */
214
+    httpRequest.onreadystatechange = function() {
215
+        if (httpRequest.readyState == 4 && httpRequest.status == 200) {
216
+            var dataArray = JSON.parse(httpRequest.responseText);
217
+            displayData(dataArray[0]);
218
+        } else if (httpRequest.readyState == 4 && httpRequest.status == 404) {
219
+            displayOfflineStatus();
220
+        }
221
+    };
222
+    httpRequest.ontimeout = function(e) {
223
+        displayOfflineStatus();
224
+    };
225
+
226
+    initializeDateSelector();
227
+    getNodeData();
228
+    getNodeCharts();
229
+}
230
+
231
+function getNodeData() {
232
+    httpRequest.open("GET", nodeDataUrl, true);
233
+    httpRequest.timeout = 3000;
234
+    httpRequest.send();
235
+}
236
+
237
+function setChartPeriod(n) {
238
+    chartPeriod = n;
239
+    if (n == 0) {
240
+        customChartsContainer.style.display = "block";
241
+        stockChartsContainer.style.display = "none";
242
+    } else {
243
+        customChartsContainer.style.display = "none";
244
+        stockChartsContainer.style.display = "block";
245
+    getNodeCharts();   
246
+    }
247
+}
248
+
249
+function getNodeCharts() {
250
+    var d = new Date;
251
+    var pfx;
252
+
253
+    switch(chartPeriod) {
254
+        case 1:
255
+            pfx = "24hr_";
256
+            break;
257
+        case 2:
258
+            pfx = "4wk_";
259
+            break;
260
+       case 3:
261
+            pfx = "12m_";
262
+            break;
263
+    }
264
+    signalChart.src = "dynamic/" + pfx + "signal.png?ver=" + d.getTime();
265
+    snrChart.src = "dynamic/" + pfx + "snr.png?ver=" + d.getTime();
266
+}
267
+
268
+function displayData(dataItem) {
269
+    var timeStamp, date, time, hourminute;
270
+    var localDateObj,localTimeZone;
271
+
272
+    timeStamp = dataItem.date;
273
+    date = timeStamp.split(" ")[0];
274
+    time = timeStamp.split(" ")[1];
275
+    hourminute = time.split(":")[0] + ":" + time.split(":")[1];
276
+    localDate = new Date();
277
+    localTimeZone = localDate.getTimezoneOffset() / 60;
278
+    dateElmt.innerHTML = date;    
279
+    timeElmt.innerHTML = hourminute +
280
+                         "  <small>(GMT+" + localTimeZone + ")</small>";    
281
+     
282
+    statusElmt.innerHTML = "Online";
283
+    statusElmt.style.color = "green";
284
+
285
+    chartRefreshRate = dataItem.period;
286
+    periodElmt.innerHTML = chartRefreshRate;
287
+    setInterval(getNodeData, 60000 * chartRefreshRate);
288
+    setInterval(getNodeCharts, 60000 * chartRefreshRate);
289
+}
290
+
291
+function displayOfflineStatus() {
292
+    var d = new Date();
293
+    localTimeZone = d.getTimezoneOffset() / 60;
294
+    dateElmt.innerHTML = (d.getMonth() + 1) + "/" + d.getDate() + "/" +
295
+                          d.getFullYear();    
296
+    timeElmt.innerHTML = d.getHours() + ":" + d.getMinutes() +
297
+                         "  <small>(GMT+" + localTimeZone + ")</small>";
298
+    periodElmt.innerHTML = "?";    
299
+    statusElmt.innerHTML = "offline";    
300
+    statusElmt.style.color = "red";
301
+}
302
+
303
+function initializeDateSelector() {
304
+    var d = new Date();
305
+
306
+    var dEnd = new Date(d.getFullYear(),
307
+               d.getMonth(), d.getDate() - 0);
308
+
309
+    var dBegin = new Date(d.getFullYear(),
310
+               d.getMonth(), d.getDate() - 1);
311
+
312
+    document.getElementById("beginDate").valueAsDate = dBegin;
313
+    document.getElementById("endDate").valueAsDate = dEnd;
314
+}
315
+
316
+function getCustomCharts() {
317
+    fmDateSelector.submit();
318
+}
319
+</script>
320
+
321
+</body>
322
+</html>
323
+
0 324
new file mode 100644
... ...
@@ -0,0 +1,205 @@
1
+<html>
2
+<!-- Courtsey ruler
3
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
4
+-->
5
+<head>
6
+<style>
7
+p {
8
+    font: 14px ariel, sans serif;
9
+}
10
+#errorMsg {
11
+    font:bold 18px arial,sans-serif;
12
+    color:red;
13
+    text-align:center;
14
+}
15
+.chartContainer {
16
+    padding: 2px;
17
+}
18
+img.chart {
19
+    width:100%;
20
+}
21
+</style>
22
+</head>
23
+<body>
24
+
25
+<?php
26
+/*
27
+ Script: arednsig.php
28
+
29
+ Description: This scripts generates on the server charts showing
30
+ signal data spanning the period supplied by the user.  The script
31
+ does the following:
32
+    - converts user supplied dates to  epoch time
33
+    - gets the times of the first and last data point in the round
34
+      robin database (RRD)
35
+    - from above validates user supplied begin and end dates
36
+    - creates charts of the specified period
37
+
38
+ Copyright 2020 Jeff Owrey
39
+    This program is free software: you can redistribute it and/or modify
40
+    it under the terms of the GNU General Public License as published by
41
+    the Free Software Foundation, either version 3 of the License, or
42
+    (at your option) any later version.
43
+
44
+    This program is distributed in the hope that it will be useful,
45
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
46
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
47
+    GNU General Public License for more details.
48
+
49
+    You should have received a copy of the GNU General Public License
50
+    along with this program.  If not, see http://www.gnu.org/license.
51
+
52
+ Revision History
53
+   * v20 released 18 Jan 2020 by J L Owrey; first release
54
+*/
55
+
56
+# Define global constants
57
+
58
+# round robin database file
59
+define("_RRD_FILE", str_replace("public_html/arednsig/arednsig.php",
60
+                                "database/arednsigData.rrd",
61
+                                $_SERVER["SCRIPT_FILENAME"]));
62
+# charts html directory
63
+define("_CHART_DIRECTORY", str_replace("arednsig.php",
64
+                                       "dynamic/",
65
+                                       $_SERVER["SCRIPT_FILENAME"]));
66
+# standard chart width in pixels
67
+define("_CHART_WIDTH", 600);
68
+# standard chart height in pixels
69
+define("_CHART_HEIGHT", 150);
70
+# debug mode
71
+define("_DEBUG", false);
72
+
73
+# Set error handling modes.
74
+error_reporting(E_ALL);
75
+
76
+# Get user supplied chart begin and end dates.
77
+$beginDate = $_POST["beginDate"];
78
+$endDate =  $_POST["endDate"];
79
+
80
+# Convert the user supplied dates to epoch time stamps.
81
+$beginDateEp = strtotime($beginDate);
82
+$endDateEp = strtotime($endDate);
83
+
84
+# Get the time stamp of the earliest data point in the RRD file.
85
+$cmd = sprintf("rrdtool first %s --rraindex 1", _RRD_FILE);
86
+$firstDP = shell_exec($cmd);
87
+
88
+# Get the time stamp of the latest data point in the RRD file.
89
+$cmd = sprintf("rrdtool last %s", _RRD_FILE);
90
+$lastDP = shell_exec($cmd);
91
+
92
+# Determine validity of user supplied dates.  User supplied begin
93
+# date must be less than user supplied end date.  Furthermore both
94
+# dates must be within the range of dates stored in the RRD.
95
+if ($beginDateEp > $endDateEp) {
96
+    echo "<p id=\"errorMsg\">" .
97
+         "End date must be after begin date.</p>";
98
+} elseif ($beginDateEp < $firstDP || $endDateEp > $lastDP) {
99
+    echo "<p id=\"errorMsg\">" .
100
+          "Date range must be between " .
101
+          date('m / d / Y', $firstDP) . " and " . 
102
+          date('m / d / Y', $lastDP) . ".</p>";
103
+} else {
104
+    # Generate charts from validated user supplied dates.
105
+    if (_DEBUG) {
106
+        echo "<p>Date range: " . $beginDateEp . " thru " .
107
+              $endDateEp . "</p>";
108
+    }
109
+    createChart('custom_signal', 'S', 'dBm', 
110
+                'RSSI', $beginDateEp, $endDateEp,
111
+                 0, 0, 2, false);
112
+    createChart('custom_snr', 'SNR', 'dBm', 
113
+                'S/N', $beginDateEp, $endDateEp,
114
+                 0, 0, 2, false);
115
+    # Send html commands to client browser.
116
+    echo "<div class=\"chartContainer\">" .
117
+         "<img class=\"chart\" src=\"dynamic/custom_signal.png\">" .
118
+         "</div>";
119
+    echo "<div class=\"chartContainer\">" .
120
+         "<img class=\"chart\" src=\"dynamic/custom_snr.png\">" .
121
+         "</div>";
122
+}
123
+
124
+function createChart($chartFile, $dataItem, $label, $title, $begin,
125
+                     $end, $lower, $upper, $addTrend, $autoScale) {
126
+    /*
127
+    Uses rrdtool to create a chart of specified aredn node data item.
128
+    Parameters:
129
+       fileName - name of the created chart file
130
+       dataItem - data item to be charted
131
+       label - string containing a label for the item to be charted
132
+       title - string containing a title for the chart
133
+       begin - beginning time of the chart data
134
+       end   - ending time of the data to be charted
135
+       lower - lower bound for chart ordinate #NOT USED
136
+       upper - upper bound for chart ordinate #NOT USED
137
+       addTrend - 0, show only chart data
138
+                  1, show only a trend line
139
+                  2, show a trend line and the chart data
140
+       autoScale - if True, then use vertical axis auto scaling
141
+           (lower and upper parameters are ignored), otherwise use
142
+           lower and upper parameters to set vertical axis scale
143
+    Returns: True if successful, False otherwise
144
+    */
145
+
146
+    # Define path on server to chart files.
147
+    $chartPath = _CHART_DIRECTORY . $chartFile . ".png";
148
+
149
+    # Format the rrdtool chart command.
150
+
151
+    # Set chart file name, start time, end time, height, and width.
152
+    $cmdfmt = "rrdtool graph %s -a PNG -s %s -e %s -w %s -h %s ";
153
+    $cmd = sprintf($cmdfmt, $chartPath, $begin, $end, _CHART_WIDTH,
154
+                   _CHART_HEIGHT);
155
+    $cmdfmt = "-l %s -u %s -r ";
156
+
157
+    # Set upper and lower ordinate bounds.
158
+    if ($lower < $upper) {
159
+        $cmd .= sprintf($cmdfmt, $lower, $upper);
160
+    } elseif ($autoScale) {
161
+        $cmd .= "-A ";
162
+    }
163
+    $cmd .= "-Y ";
164
+
165
+    # Set the chart ordinate label and chart title. 
166
+    $cmdfmt = "-v %s -t %s ";
167
+    $cmd .= sprintf($cmdfmt, $label, $title);
168
+   
169
+    # Define moving average window width.
170
+    $trendWindow = floor(($end - $begin) / 12);
171
+        
172
+    # Show the data, or a moving average trend line over
173
+    # the data, or both.
174
+    $cmdfmt = "DEF:dSeries=%s:%s:LAST ";
175
+    $cmd .= sprintf($cmdfmt, _RRD_FILE, $dataItem);
176
+    if ($addTrend == 0) {
177
+        $cmd .= "LINE1:dSeries#0400ff ";
178
+    } elseif ($addTrend == 1) {
179
+        $cmdfmt = "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 ";
180
+        $cmd .= sprintf($cmdfmt, $trendWindow);
181
+    } elseif ($addTrend == 2) {
182
+        $cmd .= "LINE1:dSeries#0400ff ";
183
+        $cmdfmt = "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 ";
184
+        #$cmdfmt = "CDEF:smoothed=dSeries,%s,XYZZY LINE3:smoothed#ff0000 ";
185
+        $cmd .=  sprintf($cmdfmt, $trendWindow);
186
+    }
187
+     
188
+    # Execute the formatted rrdtool command in the shell. The rrdtool
189
+    # command will complete execution before the html image tags get
190
+    # sent to the browser.  This assures that the charts are available
191
+    # when the client browser executes the html code that loads the
192
+    # charts into the document displayed by the client browser.
193
+    if (_DEBUG) {
194
+        echo "<p>chart command:<br>" . $cmd . "</p>";
195
+    }
196
+    $result = shell_exec($cmd . " 2>&1");
197
+    if (_DEBUG) {
198
+        echo "<p>result:<br>" . $result . "</p>";
199
+    }
200
+}
201
+
202
+?>
203
+
204
+</body>
205
+</html>
0 206
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/aredn_fw_3_19_3_0/html/static/chalk.jpg differ
2 10
new file mode 100755
... ...
@@ -0,0 +1,28 @@
1
+#!/bin/bash
2
+#
3
+# The aredn node 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
+AGENT_NAME="[a]rednsigAgent.py"
12
+NODE_URL="http://localnode:8080/cgi-bin/signal.json"
13
+
14
+POLLING_INTERVAL="1"
15
+
16
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
17
+
18
+if [ -n "$PROCESS_ID" ]; then
19
+  if [ "$1" != "-q" ]; then
20
+    printf "arednsig agent running [%s]\n" $PROCESS_ID
21
+  fi
22
+else
23
+  printf "starting up arednsig agent\n"
24
+  cd $APP_PATH
25
+  $(./$AGENT_NAME -u $NODE_URL -p $POLLING_INTERVAL >> \
26
+ $LOG_PATH/arednsigAgent.log 2>&1 &)
27
+fi
28
+
0 29
new file mode 100755
... ...
@@ -0,0 +1,16 @@
1
+#!/bin/bash
2
+# Stop the radmon agent process and clean up environment.
3
+
4
+
5
+AGENT_NAME="[a]rednsigAgent.py"
6
+#AGENT_NAME="[a]rednsigMirrorAgent.py"
7
+
8
+#AGENT_NAME="[a]rednsigMirrorAgent.py"
9
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
10
+
11
+if [ -n "$PROCESS_ID" ]; then
12
+  printf "killing arednsig agent [%s]\n" $PROCESS_ID
13
+  kill $PROCESS_ID
14
+else
15
+  echo arednsig agent not running
16
+fi
0 17
new file mode 100755
... ...
@@ -0,0 +1,589 @@
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 mest 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 node status 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
+#   * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
34
+#     powers on and signal data memory is empty.  Data points with N/A data
35
+#     are discarded.
36
+#
37
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
38
+
39
+import os
40
+import urllib2
41
+import sys
42
+import signal
43
+import subprocess
44
+import multiprocessing
45
+import time
46
+
47
+_USER = os.environ['USER']
48
+_HOSTNAME = os.uname()[1]
49
+
50
+   ### DEFAULT AREDN NODE URL ###
51
+
52
+# set url of the aredn node
53
+
54
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
55
+
56
+    ### FILE AND FOLDER LOCATIONS ###
57
+
58
+# folder for containing dynamic data objects
59
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
60
+# folder for charts and output data file
61
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
62
+# location of data output file
63
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
64
+# dummy output data file
65
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
66
+# database that stores node data
67
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
68
+
69
+    ### GLOBAL CONSTANTS ###
70
+
71
+# interval in minutes between data requests to the aredn node
72
+_DEFAULT_DATA_REQUEST_INTERVAL = 1
73
+# chart update interval in minutes
74
+_CHART_UPDATE_INTERVAL = 10
75
+
76
+# number seconds to wait for a response to HTTP request
77
+_HTTP_REQUEST_TIMEOUT = 10
78
+# max number of failed data requests allowed
79
+_MAX_FAILED_DATA_REQUESTS = 0
80
+# standard chart width in pixels
81
+_CHART_WIDTH = 600
82
+# standard chart height in pixels
83
+_CHART_HEIGHT = 150
84
+# Set this to True only if this server is intended to relay raw
85
+# node data to a mirror server.
86
+_RELAY_SERVER = False
87
+
88
+   ### GLOBAL VARIABLES ###
89
+
90
+# turn on or off of verbose debugging information
91
+debugOption = False
92
+verboseDebug = False
93
+
94
+# The following two items are used for detecting system faults
95
+# and aredn node online or offline status.
96
+
97
+# count of failed attempts to get data from aredn node
98
+failedUpdateCount = 0
99
+# detected status of aredn node device
100
+nodeOnline = True
101
+
102
+# ip address of aredn node
103
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
104
+# frequency of data requests to aredn node
105
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
106
+# chart update interval
107
+chartUpdateInterval = _CHART_UPDATE_INTERVAL
108
+# last node request time
109
+lastDataPointTime = -1
110
+
111
+  ###  PRIVATE METHODS  ###
112
+
113
+def getTimeStamp():
114
+    """
115
+    Set the error message time stamp to the local system time.
116
+    Parameters: none
117
+    Returns: string containing the time stamp
118
+    """
119
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
120
+##end def
121
+
122
+def getEpochSeconds(sTime):
123
+    """Convert the time stamp supplied in the weather data string
124
+       to seconds since 1/1/1970 00:00:00.
125
+       Parameters: 
126
+           sTime - the time stamp to be converted must be formatted
127
+                   as %m/%d/%Y %H:%M:%S
128
+       Returns: epoch seconds
129
+    """
130
+    try:
131
+        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
132
+    except Exception, exError:
133
+        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
134
+        return None
135
+    tSeconds = int(time.mktime(t_sTime))
136
+    return tSeconds
137
+##end def
138
+
139
+def setStatusToOffline():
140
+    """Set the detected status of the aredn node to
141
+       "offline" and inform downstream clients by removing input
142
+       and output data files.
143
+       Parameters: none
144
+       Returns: nothing
145
+    """
146
+    global nodeOnline
147
+
148
+    # Inform downstream clients by removing output data file.
149
+    if os.path.exists(_OUTPUT_DATA_FILE):
150
+       os.remove(_OUTPUT_DATA_FILE)
151
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
152
+       os.remove(_DUMMY_OUTPUT_FILE)
153
+    # If the aredn node was previously online, then send
154
+    # a message that we are now offline.
155
+    if nodeOnline:
156
+        print '%s aredn node offline' % getTimeStamp()
157
+    nodeOnline = False
158
+##end def
159
+
160
+def terminateAgentProcess(signal, frame):
161
+    """Send a message to log when the agent process gets killed
162
+       by the operating system.  Inform downstream clients
163
+       by removing input and output data files.
164
+       Parameters:
165
+           signal, frame - dummy parameters
166
+       Returns: nothing
167
+    """
168
+    # Inform downstream clients by removing output data file.
169
+    if os.path.exists(_OUTPUT_DATA_FILE):
170
+       os.remove(_OUTPUT_DATA_FILE)
171
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
172
+       os.remove(_DUMMY_OUTPUT_FILE)
173
+    print '%s terminating arednsig agent process' % \
174
+              (getTimeStamp())
175
+    sys.exit(0)
176
+##end def
177
+
178
+  ###  PUBLIC METHODS  ###
179
+
180
+def getArednNodeData():
181
+    """Send http request to aredn node.  The response from the
182
+       node contains the node signal data as unformatted ascii text.
183
+       Parameters: none
184
+       Returns: a string containing the node signal data if successful,
185
+                or None if not successful
186
+    """
187
+    try:
188
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
189
+
190
+        # Format received data into a single string.
191
+        content = ""
192
+        for line in conn:
193
+            content += line.strip()
194
+        del conn
195
+
196
+    except Exception, exError:
197
+        # If no response is received from the device, then assume that
198
+        # the device is down or unavailable over the network.  In
199
+        # that case return None to the calling function.
200
+        print "%s http error: %s" % (getTimeStamp(), exError)
201
+        return None
202
+
203
+    if verboseDebug:
204
+        print "http request successful: %d bytes" % len(content)
205
+
206
+    return content
207
+##end def
208
+
209
+def parseNodeData(sData, dData):
210
+    """Parse the node status page html from the aredn node
211
+       into its component parts.  
212
+       Parameters:
213
+           sData - the string containing the data to be parsed
214
+           dData - a dictionary object to contain the parsed data items
215
+       Returns: True if successful, False otherwise
216
+    """
217
+    try:
218
+
219
+        # Set search boundaries for signal data
220
+        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
221
+                         '<td valign=middle><nobr><big><b>'
222
+        strEndSearch = 'dB'
223
+        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
224
+        iEndIndex = sData.find(strEndSearch, iBeginIndex)
225
+
226
+        # Exception if signal data not found
227
+        if iBeginIndex == -1 or iEndIndex == -1:
228
+            raise Exception("signal data not found in status page")
229
+
230
+        # Extract signal data from html
231
+        snr = sData[iBeginIndex:iEndIndex]
232
+        snr = snr.replace(' ','')
233
+        lsnr = snr.split('/')
234
+
235
+        # Store time and signal data in dictionary object
236
+        dData['time'] = getEpochSeconds(getTimeStamp())
237
+        dData['signal'] = lsnr[0]
238
+        dData['noise'] = lsnr[1]
239
+        dData['snr'] = lsnr[2]
240
+    
241
+    except Exception, exError:
242
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
243
+        return False
244
+
245
+    if verboseDebug:
246
+        print "parse successful"
247
+    return True
248
+##end def
249
+
250
+def updateDatabase(dData):
251
+    """
252
+    Update the rrdtool database by executing an rrdtool system command.
253
+    Format the command using the data extracted from the aredn node
254
+    response.   
255
+    Parameters: dData - dictionary object containing data items to be
256
+                        written to the rr database file
257
+    Returns: True if successful, False otherwise
258
+    """
259
+    # Format the rrdtool update command.
260
+    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
261
+    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
262
+             dData['noise'], dData['snr'], '0', \
263
+             '0', '0', '0')
264
+
265
+    if verboseDebug:
266
+        print "%s" % strCmd # DEBUG
267
+
268
+    # Run the command as a subprocess.
269
+    try:
270
+        subprocess.check_output(strCmd, shell=True,  \
271
+                             stderr=subprocess.STDOUT)
272
+    except subprocess.CalledProcessError, exError:
273
+        print "%s: rrdtool update failed: %s" % \
274
+                    (getTimeStamp(), exError.output)
275
+        return False
276
+
277
+    return True
278
+##end def
279
+
280
+def writeOutputDataFile(sData, dData):
281
+    """Write node data items to the output data file, formatted as 
282
+       a Javascript file.  This file may then be accessed and used by
283
+       by downstream clients, for instance, in HTML documents.
284
+       Parameters:
285
+           sData - a string object containing the data to be written
286
+                   to the output data file
287
+       Returns: True if successful, False otherwise
288
+    """
289
+    # Write file for use by html clients.  The following two
290
+    # data items are sent to the client file.
291
+    #    * The last database update date and time
292
+    #    * The data request interval
293
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
294
+                                time.localtime(dData['time']) )
295
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
296
+           (lastUpdate, chartUpdateInterval)
297
+    try:
298
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
299
+        fc.write(sDate)
300
+        fc.close()
301
+    except Exception, exError:
302
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
303
+        return False
304
+
305
+    if _RELAY_SERVER:
306
+        # Write the entire node data response to the output data file.
307
+        try:
308
+            fc = open(_OUTPUT_DATA_FILE, "w")
309
+            fc.write(sData)
310
+            fc.close()
311
+        except Exception, exError:
312
+            print "%s write output file failed: %s" % \
313
+                  (getTimeStamp(), exError)
314
+            return False
315
+        if verboseDebug:
316
+            print "write output data file: %d bytes" % len(sData)
317
+
318
+    return True
319
+## end def
320
+
321
+def setNodeStatus(updateSuccess):
322
+    """Detect if aredn node is offline or not available on
323
+       the network. After a set number of attempts to get data
324
+       from the node set a flag that the node is offline.
325
+       Parameters:
326
+           updateSuccess - a boolean that is True if data request
327
+                           successful, False otherwise
328
+       Returns: nothing
329
+    """
330
+    global failedUpdateCount, nodeOnline
331
+
332
+    if updateSuccess:
333
+        failedUpdateCount = 0
334
+        # Set status and send a message to the log if the node was
335
+        # previously offline and is now online.
336
+        if not nodeOnline:
337
+            print '%s aredn node online' % getTimeStamp()
338
+            nodeOnline = True
339
+    else:
340
+        # The last attempt failed, so update the failed attempts
341
+        # count.
342
+        failedUpdateCount += 1
343
+
344
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
345
+        # Max number of failed data requests, so set
346
+        # node status to offline.
347
+        setStatusToOffline()
348
+##end def
349
+
350
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
351
+                lower, upper, addTrend, autoScale):
352
+    """Uses rrdtool to create a graph of specified node data item.
353
+       Parameters:
354
+           fileName - name of file containing the graph
355
+           dataItem - data item to be graphed
356
+           gLabel - string containing a graph label for the data item
357
+           gTitle - string containing a title for the graph
358
+           gStart - beginning time of the graphed data
359
+           lower - lower bound for graph ordinate #NOT USED
360
+           upper - upper bound for graph ordinate #NOT USED
361
+           addTrend - 0, show only graph data
362
+                      1, show only a trend line
363
+                      2, show a trend line and the graph data
364
+           autoScale - if True, then use vertical axis auto scaling
365
+               (lower and upper parameters are ignored), otherwise use
366
+               lower and upper parameters to set vertical axis scale
367
+       Returns: True if successful, False otherwise
368
+    """
369
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
370
+    trendWindow = { 'end-1day': 7200,
371
+                    'end-4weeks': 172800,
372
+                    'end-12months': 604800 }
373
+ 
374
+    # Format the rrdtool graph command.
375
+
376
+    # Set chart start time, height, and width.
377
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
378
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
379
+   
380
+    # Set the range and scaling of the chart y-axis.
381
+    if lower < upper:
382
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
383
+    elif autoScale:
384
+        strCmd += "-A "
385
+    strCmd += "-Y "
386
+
387
+    # Set the chart ordinate label and chart title. 
388
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
389
+ 
390
+    # Show the data, or a moving average trend line over
391
+    # the data, or both.
392
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
393
+    if addTrend == 0:
394
+        strCmd += "LINE1:dSeries#0400ff "
395
+    elif addTrend == 1:
396
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
397
+                  % trendWindow[gStart]
398
+    elif addTrend == 2:
399
+        strCmd += "LINE1:dSeries#0400ff "
400
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
401
+                  % trendWindow[gStart]
402
+     
403
+    if verboseDebug:
404
+        print "%s" % strCmd # DEBUG
405
+    
406
+    # Run the formatted rrdtool command as a subprocess.
407
+    try:
408
+        result = subprocess.check_output(strCmd, \
409
+                     stderr=subprocess.STDOUT,   \
410
+                     shell=True)
411
+    except subprocess.CalledProcessError, exError:
412
+        print "rrdtool graph failed: %s" % (exError.output)
413
+        return False
414
+
415
+    if debugOption:
416
+        print "rrdtool graph: %s\n" % result,
417
+    return True
418
+
419
+##end def
420
+
421
+def generateGraphs():
422
+    """Generate graphs for display in html documents.
423
+       Parameters: none
424
+       Returns: nothing
425
+    """
426
+    autoScale = False
427
+
428
+    # The following will force creation of charts
429
+    # of only signal strength and S/N charts.  Note that the following
430
+    # data items appear constant and do not show variation with time:
431
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
432
+    # these parameters are demonstrated to vary in time, there is no point
433
+    # in creating the charts for these data items.
434
+    createAllCharts = False
435
+
436
+    # 24 hour stock charts
437
+
438
+    createGraph('24hr_signal', 'S', 'dBm', 
439
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
440
+    createGraph('24hr_snr', 'SNR', 'dB', 
441
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
442
+
443
+    # 4 week stock charts
444
+
445
+    createGraph('4wk_signal', 'S', 'dBm', 
446
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
447
+    createGraph('4wk_snr', 'SNR', 'dB', 
448
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
449
+
450
+    # 12 month stock charts
451
+
452
+    createGraph('12m_signal', 'S', 'dBm', 
453
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
454
+    createGraph('12m_snr', 'SNR', 'dB', 
455
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
456
+
457
+    if debugOption:
458
+        #print # print a blank line to improve readability when in debug mode
459
+        pass
460
+##end def
461
+
462
+def getCLarguments():
463
+    """Get command line arguments.  There are four possible arguments
464
+          -d turns on debug mode
465
+          -v turns on verbose debug mode
466
+          -t sets the aredn node query interval
467
+          -u sets the url of the aredn nodeing device
468
+       Returns: nothing
469
+    """
470
+    global debugOption, verboseDebug, dataRequestInterval, \
471
+           arednNodeUrl
472
+
473
+    index = 1
474
+    while index < len(sys.argv):
475
+        if sys.argv[index] == '-d':
476
+            debugOption = True
477
+        elif sys.argv[index] == '-v':
478
+            debugOption = True
479
+            verboseDebug = True
480
+        elif sys.argv[index] == '-p':
481
+            try:
482
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
483
+            except:
484
+                print "invalid polling period"
485
+                exit(-1)
486
+            index += 1
487
+        elif sys.argv[index] == '-u':
488
+            arednNodeUrl = sys.argv[index + 1]
489
+            index += 1
490
+        else:
491
+            cmd_name = sys.argv[0].split('/')
492
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
493
+            exit(-1)
494
+        index += 1
495
+##end def
496
+
497
+def main():
498
+    """Handles timing of events and acts as executive routine managing
499
+       all other functions.
500
+       Parameters: none
501
+       Returns: nothing
502
+    """
503
+    global dataRequestInterval
504
+
505
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
506
+
507
+    print '%s starting up arednsig agent process' % \
508
+                  (getTimeStamp())
509
+
510
+    # last time output JSON file updated
511
+    lastDataRequestTime = -1
512
+    # last time charts generated
513
+    lastChartUpdateTime = - 1
514
+
515
+    ## Get command line arguments.
516
+    getCLarguments()
517
+
518
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
519
+    chartUpdateIntervalSeconds = chartUpdateInterval * 60 # charts interval
520
+
521
+    ## Exit with error if rrdtool database does not exist.
522
+    if not os.path.exists(_RRD_FILE):
523
+        print 'rrdtool database does not exist\n' \
524
+              'use createArednsigRrd script to ' \
525
+              'create rrdtool database\n'
526
+        exit(1)
527
+ 
528
+    ## main loop
529
+    while True:
530
+
531
+        currentTime = time.time() # get current time in seconds
532
+
533
+        # Every web update interval request data from the aredn
534
+        # node and process the received data.
535
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
536
+            lastDataRequestTime = currentTime
537
+            dData = {}
538
+            result = True
539
+
540
+            # Get the data string from the device.
541
+            sData = getArednNodeData()
542
+            # If the first http request fails, try one more time.
543
+            if sData == None:
544
+                result = False
545
+
546
+            # If successful parse the data.
547
+            if result:
548
+                result = parseNodeData(sData, dData)
549
+           
550
+            # If parse successful, write data to data files.
551
+            if result:
552
+                result = updateDatabase(dData)
553
+
554
+            if result:
555
+                writeOutputDataFile(sData, dData)
556
+
557
+            # Set the node status to online or offline depending on the
558
+            # success or failure of the above operations.
559
+            setNodeStatus(result)
560
+
561
+        # At the chart generation interval, generate charts.
562
+        if currentTime - lastChartUpdateTime > chartUpdateIntervalSeconds:
563
+            lastChartUpdateTime = currentTime
564
+            p = multiprocessing.Process(target=generateGraphs, args=())
565
+            p.start()
566
+
567
+        # Relinquish processing back to the operating system until
568
+        # the next update interval.
569
+
570
+        elapsedTime = time.time() - currentTime
571
+        if debugOption:
572
+            if result:
573
+                print "%s update successful:" % getTimeStamp(),
574
+            else:
575
+                print "%s update failed:" % getTimeStamp(),
576
+            print "%6f seconds processing time\n" % elapsedTime 
577
+        remainingTime = requestIntervalSeconds - elapsedTime
578
+        if remainingTime > 0.0:
579
+            time.sleep(remainingTime)
580
+    ## end while
581
+    return
582
+## end def
583
+
584
+if __name__ == '__main__':
585
+    try:
586
+        main()
587
+    except KeyboardInterrupt:
588
+        print '\n',
589
+        terminateAgentProcess('KeyboardInterrupt','Module')
0 590
new file mode 100755
... ...
@@ -0,0 +1,89 @@
1
+#!/usr/bin/python -u
2
+## The -u option above turns off block buffering of python output. This assures
3
+## that each error message gets individually printed to the log file.
4
+#
5
+# Module: createArednsigRrd.py
6
+#
7
+# Description: Creates a rrdtool database for use by the weather agent to
8
+# store the data from the weather station.  The agent uses the data in the
9
+# database to generate graphic charts for display in the weather station
10
+# web page.
11
+#
12
+# Copyright 2020 Jeff Owrey
13
+#    This program is free software: you can redistribute it and/or modify
14
+#    it under the terms of the GNU General Public License as published by
15
+#    the Free Software Foundation, either version 3 of the License, or
16
+#    (at your option) any later version.
17
+#
18
+#    This program is distributed in the hope that it will be useful,
19
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21
+#    GNU General Public License for more details.
22
+#
23
+#    You should have received a copy of the GNU General Public License
24
+#    along with this program.  If not, see http://www.gnu.org/license.
25
+#
26
+# Revision History
27
+#   * v10 released 11 Jan 2020 by J L Owrey
28
+#
29
+import os
30
+import time
31
+import subprocess
32
+
33
+    ### DEFINE FILE LOCATIONS ###
34
+
35
+_USER = os.environ['USER']
36
+# the file that stores the data
37
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
38
+_RRD_SIZE_IN_DAYS = 370 # days
39
+_1YR_RRA_STEPS_PER_DAY = 96
40
+_DATABASE_UPDATE_INTERVAL = 60
41
+
42
+def createRrdFile():
43
+    """Create the rrd file if it does not exist.
44
+       Parameters: none
45
+       Returns: True, if successful
46
+    """
47
+
48
+    if os.path.exists(_RRD_FILE):
49
+        print "aredn node database already exists"
50
+        return True
51
+
52
+     ## Calculate database size
53
+ 
54
+    heartBeat = 2 * _DATABASE_UPDATE_INTERVAL
55
+    rra1yrNumPDP =  int(round(86400 / (_1YR_RRA_STEPS_PER_DAY * \
56
+                    _DATABASE_UPDATE_INTERVAL)))
57
+    rrd24hrNumRows = int(round(86400 / _DATABASE_UPDATE_INTERVAL))
58
+    rrd1yearNumRows = _1YR_RRA_STEPS_PER_DAY * _RRD_SIZE_IN_DAYS
59
+       
60
+    strFmt = ("rrdtool create %s --start now-1day --step %s "
61
+              "DS:S:GAUGE:%s:U:U DS:N:GAUGE:%s:U:U DS:SNR:GAUGE:%s:U:U "
62
+              "DS:RX_MCS:GAUGE:%s:U:U DS:TX_MCS:GAUGE:%s:U:U "
63
+              "DS:RX_RATE:GAUGE:%s:U:U DS:TX_RATE:GAUGE:%s:U:U "
64
+              "RRA:LAST:0.5:1:%s RRA:LAST:0.5:%s:%s")
65
+
66
+    strCmd = strFmt % (_RRD_FILE, _DATABASE_UPDATE_INTERVAL, \
67
+                heartBeat, heartBeat, heartBeat, heartBeat,  \
68
+                heartBeat,  heartBeat, heartBeat,            \
69
+                rrd24hrNumRows, rra1yrNumPDP, rrd1yearNumRows)
70
+
71
+    print "creating aredn node database...\n\n%s\n" % strCmd
72
+
73
+    # Spawn a sub-shell and run the command
74
+    try:
75
+        subprocess.check_output(strCmd, stderr=subprocess.STDOUT, \
76
+                                shell=True)
77
+    except subprocess.CalledProcessError, exError:
78
+        print "rrdtool create failed: %s" % (exError.output)
79
+        return False
80
+    return True
81
+##end def
82
+
83
+def main():
84
+    createRrdFile()
85
+## end def
86
+
87
+if __name__ == '__main__':
88
+    main()
89
+        
0 90
new file mode 100755
... ...
@@ -0,0 +1,16 @@
1
+#!/bin/sh
2
+#
3
+# Create a directory in the temporary file system for arednsig dynamic
4
+# data.  Set ownership and permissions to allow the Apache www-data user
5
+# read and write access to this folder.
6
+mkdir /tmp/arednsig
7
+sudo chown :www-data /tmp/arednsig
8
+chmod g+w /tmp/arednsig
9
+
10
+# Uncomment the following line if you choose to mount the dynamic
11
+# folder to the folder created above.
12
+#sudo mount --bind /tmp/arednsig  /home/pi/public_html/arednsig/dynamic
13
+
14
+# Start arednsig agent
15
+(sleep 5; /home/pi/bin/ardstart;) &
16
+
0 17
new file mode 100644
... ...
@@ -0,0 +1,323 @@
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
+    /*border: 1px solid black;*/
28
+}
29
+.datetime {
30
+    font: bold 22px arial, sans-serif;
31
+    padding: 0px;
32
+}
33
+.rowContainer {
34
+    display: table;
35
+    width: 100%;
36
+}
37
+.currentDataCell {
38
+    width: 50%;
39
+    padding: 10px;
40
+    font: bold 20px arial, sans-serif;
41
+    text-align: center;
42
+    display: table-cell;
43
+    vertical-align: middle;
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.chartSelector {
64
+    margin: auto;
65
+}
66
+ul.selectorElement {
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.selectorElement {
75
+    display: inline-block;
76
+    font: bold 18px arial, sans-serif;
77
+    color: black;
78
+}
79
+span.selectorElement:hover {
80
+    background-color: #333;
81
+    cursor: pointer;
82
+    color: white;
83
+}
84
+span.selectorElement {
85
+    display: inline-block;
86
+    padding: 8px 12px;
87
+}
88
+#iframe_a {
89
+    border:none;
90
+    width:100%;
91
+    height:450px;
92
+}
93
+</style>
94
+</head>
95
+
96
+<body onload="main()">
97
+
98
+<div class="mainContainer">
99
+<h2><a href="https://github.com/fractalxaos/ham/tree/master/arednsig" 
100
+  style="text-decoration:none" target="_new">
101
+KA7JLO AREDN<sup>&#174;</sup> Node Signal</a></h2>
102
+<h3>Last Updated</h3>
103
+<div class="datetime">
104
+<span id="date"></span>
105
+&nbsp;&nbsp;
106
+<span id="time"></span>
107
+</div>
108
+
109
+<div class="rowContainer">
110
+<div class="currentDataCell">
111
+<div class="dataItems" style="text-align: center;">
112
+Status: <span id="status"></span><br>
113
+Data updates every: <span id="period"></span> minutes
114
+</div>
115
+
116
+</div>
117
+</div>
118
+
119
+<span class="chartSelectors">
120
+<ul class="selectorElement">
121
+<li class="selectorElement">Select charts:</li>
122
+<li class="selectorElement"><span class="selectorElement"
123
+ onclick="setChartPeriod(1)">24 hours</span></li>
124
+<li class="selectorElement"><span class="selectorElement"
125
+ onclick="setChartPeriod(2)">4 weeks</span></li>
126
+<li class="selectorElement"><span class="selectorElement"
127
+ onclick="setChartPeriod(3)">12 months</span></li>
128
+<li class="selectorElement"><span class="selectorElement"
129
+ onclick="setChartPeriod(0)">Custom…</span></li>
130
+</ul>
131
+</span>
132
+
133
+<div class="rowContainer" id="customChartsContainer" style="display:none;">
134
+<div class="currentDataCell">
135
+<form id="fmDateSelector" action="arednsig.php" method="post"
136
+ target="iframe_a">
137
+<label for="beginDate">Begin Date: </label>
138
+<input id="beginDate" name="beginDate" type="date" value="mm/dd/yyyy" />
139
+<label for="endDate">End Date: </label>
140
+<input id="endDate" name="endDate" type="date" value="mm/dd/yyyy" />
141
+<br><br>
142
+<input type="button" onclick="getCustomCharts()" value="Get Charts">
143
+</form>
144
+<span id="errorMsg"></span><br>
145
+<iframe id="iframe_a" name="iframe_a"></iframe>
146
+</div>
147
+</div>
148
+
149
+<br>
150
+
151
+<div class="rowContainer" id="stockChartsContainer">
152
+<div class="chartContainer">
153
+<img class="chart" id="signalChart">
154
+</div>
155
+<div class="chartContainer">
156
+<img class="chart" id="snrChart">
157
+</div>
158
+</div>
159
+
160
+<div class="notes">
161
+<b>NOTES:</b>
162
+<ul>
163
+<li>Aredn Node Signal software available at
164
+<a href="https://github.com/fractalxaos/ham/tree/master/arednsig"
165
+ target="_new">
166
+<i>Github.com</i></a>.</li>
167
+<li>Project sponsored by 
168
+<a href="https://willamettevalleymesh.net" TARGET="_NEW">
169
+<i>Willamette Valley Mesh Network</i></a>, Salem, Oregon.</li>
170
+<li>For more information about the amateur radio emergency
171
+ data network (AREDN) see official web site at
172
+ <a href="http://www.arednmesh.org" target="_blank">
173
+www.arednmesh.org</a>.</li>
174
+</ul>
175
+</div>
176
+</div>
177
+<br>
178
+
179
+<script>
180
+
181
+/* Global constants */
182
+
183
+var nodeDataUrl = "dynamic/nodeOnline.js";
184
+
185
+/* Global DOM objects */
186
+
187
+// Chart elements
188
+var signalChart = document.getElementById("signalChart");
189
+var snrChart = document.getElementById("snrChart");
190
+
191
+// Text elements
192
+var dateElmt = document.getElementById("date");    
193
+var timeElmt = document.getElementById("time"); 
194
+var statusElmt = document.getElementById("status");
195
+var periodElmt = document.getElementById("period");
196
+
197
+// Document elements
198
+var customChartsContainer = document.getElementById("customChartsContainer");
199
+var stockChartsContainer = document.getElementById("stockChartsContainer");
200
+var fmDateSelector = document.getElementById("fmDateSelector");
201
+var errorMsg = document.getElementById("errorMsg");
202
+
203
+/* Global objects */
204
+
205
+var httpRequest = new XMLHttpRequest();
206
+
207
+/* Global variables */
208
+
209
+var chartPeriod = 1;
210
+var chartRefreshRate = 0; // chart refresh rate in minutes
211
+
212
+function main() {
213
+    /* Register call back function to process http requests */
214
+    httpRequest.onreadystatechange = function() {
215
+        if (httpRequest.readyState == 4 && httpRequest.status == 200) {
216
+            var dataArray = JSON.parse(httpRequest.responseText);
217
+            displayData(dataArray[0]);
218
+        } else if (httpRequest.readyState == 4 && httpRequest.status == 404) {
219
+            displayOfflineStatus();
220
+        }
221
+    };
222
+    httpRequest.ontimeout = function(e) {
223
+        displayOfflineStatus();
224
+    };
225
+
226
+    initializeDateSelector();
227
+    getNodeData();
228
+    getNodeCharts();
229
+}
230
+
231
+function getNodeData() {
232
+    httpRequest.open("GET", nodeDataUrl, true);
233
+    httpRequest.timeout = 3000;
234
+    httpRequest.send();
235
+}
236
+
237
+function setChartPeriod(n) {
238
+    chartPeriod = n;
239
+    if (n == 0) {
240
+        customChartsContainer.style.display = "block";
241
+        stockChartsContainer.style.display = "none";
242
+    } else {
243
+        customChartsContainer.style.display = "none";
244
+        stockChartsContainer.style.display = "block";
245
+    getNodeCharts();   
246
+    }
247
+}
248
+
249
+function getNodeCharts() {
250
+    var d = new Date;
251
+    var pfx;
252
+
253
+    switch(chartPeriod) {
254
+        case 1:
255
+            pfx = "24hr_";
256
+            break;
257
+        case 2:
258
+            pfx = "4wk_";
259
+            break;
260
+       case 3:
261
+            pfx = "12m_";
262
+            break;
263
+    }
264
+    signalChart.src = "dynamic/" + pfx + "signal.png?ver=" + d.getTime();
265
+    snrChart.src = "dynamic/" + pfx + "snr.png?ver=" + d.getTime();
266
+}
267
+
268
+function displayData(dataItem) {
269
+    var timeStamp, date, time, hourminute;
270
+    var localDateObj,localTimeZone;
271
+
272
+    timeStamp = dataItem.date;
273
+    date = timeStamp.split(" ")[0];
274
+    time = timeStamp.split(" ")[1];
275
+    hourminute = time.split(":")[0] + ":" + time.split(":")[1];
276
+    localDate = new Date();
277
+    localTimeZone = localDate.getTimezoneOffset() / 60;
278
+    dateElmt.innerHTML = date;    
279
+    timeElmt.innerHTML = hourminute +
280
+                         "  <small>(GMT+" + localTimeZone + ")</small>";    
281
+     
282
+    statusElmt.innerHTML = "Online";
283
+    statusElmt.style.color = "green";
284
+
285
+    chartRefreshRate = dataItem.period;
286
+    periodElmt.innerHTML = chartRefreshRate;
287
+    setInterval(getNodeData, 60000 * chartRefreshRate);
288
+    setInterval(getNodeCharts, 60000 * chartRefreshRate);
289
+}
290
+
291
+function displayOfflineStatus() {
292
+    var d = new Date();
293
+    localTimeZone = d.getTimezoneOffset() / 60;
294
+    dateElmt.innerHTML = (d.getMonth() + 1) + "/" + d.getDate() + "/" +
295
+                          d.getFullYear();    
296
+    timeElmt.innerHTML = d.getHours() + ":" + d.getMinutes() +
297
+                         "  <small>(GMT+" + localTimeZone + ")</small>";
298
+    periodElmt.innerHTML = "?";    
299
+    statusElmt.innerHTML = "offline";    
300
+    statusElmt.style.color = "red";
301
+}
302
+
303
+function initializeDateSelector() {
304
+    var d = new Date();
305
+
306
+    var dEnd = new Date(d.getFullYear(),
307
+               d.getMonth(), d.getDate() - 0);
308
+
309
+    var dBegin = new Date(d.getFullYear(),
310
+               d.getMonth(), d.getDate() - 1);
311
+
312
+    document.getElementById("beginDate").valueAsDate = dBegin;
313
+    document.getElementById("endDate").valueAsDate = dEnd;
314
+}
315
+
316
+function getCustomCharts() {
317
+    fmDateSelector.submit();
318
+}
319
+</script>
320
+
321
+</body>
322
+</html>
323
+
0 324
new file mode 100644
... ...
@@ -0,0 +1,205 @@
1
+<html>
2
+<!-- Courtsey ruler
3
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
4
+-->
5
+<head>
6
+<style>
7
+p {
8
+    font: 14px ariel, sans serif;
9
+}
10
+#errorMsg {
11
+    font:bold 18px arial,sans-serif;
12
+    color:red;
13
+    text-align:center;
14
+}
15
+.chartContainer {
16
+    padding: 2px;
17
+}
18
+img.chart {
19
+    width:100%;
20
+}
21
+</style>
22
+</head>
23
+<body>
24
+
25
+<?php
26
+/*
27
+ Script: arednsig.php
28
+
29
+ Description: This scripts generates on the server charts showing
30
+ signal data spanning the period supplied by the user.  The script
31
+ does the following:
32
+    - converts user supplied dates to  epoch time
33
+    - gets the times of the first and last data point in the round
34
+      robin database (RRD)
35
+    - from above validates user supplied begin and end dates
36
+    - creates charts of the specified period
37
+
38
+ Copyright 2020 Jeff Owrey
39
+    This program is free software: you can redistribute it and/or modify
40
+    it under the terms of the GNU General Public License as published by
41
+    the Free Software Foundation, either version 3 of the License, or
42
+    (at your option) any later version.
43
+
44
+    This program is distributed in the hope that it will be useful,
45
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
46
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
47
+    GNU General Public License for more details.
48
+
49
+    You should have received a copy of the GNU General Public License
50
+    along with this program.  If not, see http://www.gnu.org/license.
51
+
52
+ Revision History
53
+   * v20 released 18 Jan 2020 by J L Owrey; first release
54
+*/
55
+
56
+# Define global constants
57
+
58
+# round robin database file
59
+define("_RRD_FILE", str_replace("public_html/arednsig/arednsig.php",
60
+                                "database/arednsigData.rrd",
61
+                                $_SERVER["SCRIPT_FILENAME"]));
62
+# charts html directory
63
+define("_CHART_DIRECTORY", str_replace("arednsig.php",
64
+                                       "dynamic/",
65
+                                       $_SERVER["SCRIPT_FILENAME"]));
66
+# standard chart width in pixels
67
+define("_CHART_WIDTH", 600);
68
+# standard chart height in pixels
69
+define("_CHART_HEIGHT", 150);
70
+# debug mode
71
+define("_DEBUG", false);
72
+
73
+# Set error handling modes.
74
+error_reporting(E_ALL);
75
+
76
+# Get user supplied chart begin and end dates.
77
+$beginDate = $_POST["beginDate"];
78
+$endDate =  $_POST["endDate"];
79
+
80
+# Convert the user supplied dates to epoch time stamps.
81
+$beginDateEp = strtotime($beginDate);
82
+$endDateEp = strtotime($endDate);
83
+
84
+# Get the time stamp of the earliest data point in the RRD file.
85
+$cmd = sprintf("rrdtool first %s --rraindex 1", _RRD_FILE);
86
+$firstDP = shell_exec($cmd);
87
+
88
+# Get the time stamp of the latest data point in the RRD file.
89
+$cmd = sprintf("rrdtool last %s", _RRD_FILE);
90
+$lastDP = shell_exec($cmd);
91
+
92
+# Determine validity of user supplied dates.  User supplied begin
93
+# date must be less than user supplied end date.  Furthermore both
94
+# dates must be within the range of dates stored in the RRD.
95
+if ($beginDateEp > $endDateEp) {
96
+    echo "<p id=\"errorMsg\">" .
97
+         "End date must be after begin date.</p>";
98
+} elseif ($beginDateEp < $firstDP || $endDateEp > $lastDP) {
99
+    echo "<p id=\"errorMsg\">" .
100
+          "Date range must be between " .
101
+          date('m / d / Y', $firstDP) . " and " . 
102
+          date('m / d / Y', $lastDP) . ".</p>";
103
+} else {
104
+    # Generate charts from validated user supplied dates.
105
+    if (_DEBUG) {
106
+        echo "<p>Date range: " . $beginDateEp . " thru " .
107
+              $endDateEp . "</p>";
108
+    }
109
+    createChart('custom_signal', 'S', 'dBm', 
110
+                'RSSI', $beginDateEp, $endDateEp,
111
+                 0, 0, 2, false);
112
+    createChart('custom_snr', 'SNR', 'dBm', 
113
+                'S/N', $beginDateEp, $endDateEp,
114
+                 0, 0, 2, false);
115
+    # Send html commands to client browser.
116
+    echo "<div class=\"chartContainer\">" .
117
+         "<img class=\"chart\" src=\"dynamic/custom_signal.png\">" .
118
+         "</div>";
119
+    echo "<div class=\"chartContainer\">" .
120
+         "<img class=\"chart\" src=\"dynamic/custom_snr.png\">" .
121
+         "</div>";
122
+}
123
+
124
+function createChart($chartFile, $dataItem, $label, $title, $begin,
125
+                     $end, $lower, $upper, $addTrend, $autoScale) {
126
+    /*
127
+    Uses rrdtool to create a chart of specified aredn node data item.
128
+    Parameters:
129
+       fileName - name of the created chart file
130
+       dataItem - data item to be charted
131
+       label - string containing a label for the item to be charted
132
+       title - string containing a title for the chart
133
+       begin - beginning time of the chart data
134
+       end   - ending time of the data to be charted
135
+       lower - lower bound for chart ordinate #NOT USED
136
+       upper - upper bound for chart ordinate #NOT USED
137
+       addTrend - 0, show only chart data
138
+                  1, show only a trend line
139
+                  2, show a trend line and the chart data
140
+       autoScale - if True, then use vertical axis auto scaling
141
+           (lower and upper parameters are ignored), otherwise use
142
+           lower and upper parameters to set vertical axis scale
143
+    Returns: True if successful, False otherwise
144
+    */
145
+
146
+    # Define path on server to chart files.
147
+    $chartPath = _CHART_DIRECTORY . $chartFile . ".png";
148
+
149
+    # Format the rrdtool chart command.
150
+
151
+    # Set chart file name, start time, end time, height, and width.
152
+    $cmdfmt = "rrdtool graph %s -a PNG -s %s -e %s -w %s -h %s ";
153
+    $cmd = sprintf($cmdfmt, $chartPath, $begin, $end, _CHART_WIDTH,
154
+                   _CHART_HEIGHT);
155
+    $cmdfmt = "-l %s -u %s -r ";
156
+
157
+    # Set upper and lower ordinate bounds.
158
+    if ($lower < $upper) {
159
+        $cmd .= sprintf($cmdfmt, $lower, $upper);
160
+    } elseif ($autoScale) {
161
+        $cmd .= "-A ";
162
+    }
163
+    $cmd .= "-Y ";
164
+
165
+    # Set the chart ordinate label and chart title. 
166
+    $cmdfmt = "-v %s -t %s ";
167
+    $cmd .= sprintf($cmdfmt, $label, $title);
168
+   
169
+    # Define moving average window width.
170
+    $trendWindow = floor(($end - $begin) / 12);
171
+        
172
+    # Show the data, or a moving average trend line over
173
+    # the data, or both.
174
+    $cmdfmt = "DEF:dSeries=%s:%s:LAST ";
175
+    $cmd .= sprintf($cmdfmt, _RRD_FILE, $dataItem);
176
+    if ($addTrend == 0) {
177
+        $cmd .= "LINE1:dSeries#0400ff ";
178
+    } elseif ($addTrend == 1) {
179
+        $cmdfmt = "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 ";
180
+        $cmd .= sprintf($cmdfmt, $trendWindow);
181
+    } elseif ($addTrend == 2) {
182
+        $cmd .= "LINE1:dSeries#0400ff ";
183
+        $cmdfmt = "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 ";
184
+        #$cmdfmt = "CDEF:smoothed=dSeries,%s,XYZZY LINE3:smoothed#ff0000 ";
185
+        $cmd .=  sprintf($cmdfmt, $trendWindow);
186
+    }
187
+     
188
+    # Execute the formatted rrdtool command in the shell. The rrdtool
189
+    # command will complete execution before the html image tags get
190
+    # sent to the browser.  This assures that the charts are available
191
+    # when the client browser executes the html code that loads the
192
+    # charts into the document displayed by the client browser.
193
+    if (_DEBUG) {
194
+        echo "<p>chart command:<br>" . $cmd . "</p>";
195
+    }
196
+    $result = shell_exec($cmd . " 2>&1");
197
+    if (_DEBUG) {
198
+        echo "<p>result:<br>" . $result . "</p>";
199
+    }
200
+}
201
+
202
+?>
203
+
204
+</body>
205
+</html>
0 206
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/aredn_fw_3_20_3_0/html/static/chalk.jpg differ