Browse code

support for Aredn firmware version 3.20.3.0

gandolf authored on 03/31/2020 18:07:19
Showing 9 changed files
1 1
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,592 @@
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
+#   * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
37
+#     Aredn firmware version 3.20.3.0.  This agent now downloads the node's
38
+#     status page and parsed the signal data from the html.
39
+#
40
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
41
+
42
+import os
43
+import urllib2
44
+import sys
45
+import signal
46
+import subprocess
47
+import multiprocessing
48
+import time
49
+
50
+_USER = os.environ['USER']
51
+_HOSTNAME = os.uname()[1]
52
+
53
+   ### DEFAULT AREDN NODE URL ###
54
+
55
+# set url of the aredn node
56
+
57
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
58
+
59
+    ### FILE AND FOLDER LOCATIONS ###
60
+
61
+# folder for containing dynamic data objects
62
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
63
+# folder for charts and output data file
64
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
65
+# location of data output file
66
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
67
+# dummy output data file
68
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
69
+# database that stores node data
70
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
71
+
72
+    ### GLOBAL CONSTANTS ###
73
+
74
+# interval in minutes between data requests to the aredn node
75
+_DEFAULT_DATA_REQUEST_INTERVAL = 1
76
+# chart update interval in minutes
77
+_CHART_UPDATE_INTERVAL = 10
78
+
79
+# number seconds to wait for a response to HTTP request
80
+_HTTP_REQUEST_TIMEOUT = 10
81
+# max number of failed data requests allowed
82
+_MAX_FAILED_DATA_REQUESTS = 0
83
+# standard chart width in pixels
84
+_CHART_WIDTH = 600
85
+# standard chart height in pixels
86
+_CHART_HEIGHT = 150
87
+# Set this to True only if this server is intended to relay raw
88
+# node data to a mirror server.
89
+_RELAY_SERVER = False
90
+
91
+   ### GLOBAL VARIABLES ###
92
+
93
+# turn on or off of verbose debugging information
94
+debugOption = False
95
+verboseDebug = False
96
+
97
+# The following two items are used for detecting system faults
98
+# and aredn node online or offline status.
99
+
100
+# count of failed attempts to get data from aredn node
101
+failedUpdateCount = 0
102
+# detected status of aredn node device
103
+nodeOnline = True
104
+
105
+# ip address of aredn node
106
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
107
+# frequency of data requests to aredn node
108
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
109
+# chart update interval
110
+chartUpdateInterval = _CHART_UPDATE_INTERVAL
111
+# last node request time
112
+lastDataPointTime = -1
113
+
114
+  ###  PRIVATE METHODS  ###
115
+
116
+def getTimeStamp():
117
+    """
118
+    Set the error message time stamp to the local system time.
119
+    Parameters: none
120
+    Returns: string containing the time stamp
121
+    """
122
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
123
+##end def
124
+
125
+def getEpochSeconds(sTime):
126
+    """Convert the time stamp supplied in the weather data string
127
+       to seconds since 1/1/1970 00:00:00.
128
+       Parameters: 
129
+           sTime - the time stamp to be converted must be formatted
130
+                   as %m/%d/%Y %H:%M:%S
131
+       Returns: epoch seconds
132
+    """
133
+    try:
134
+        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
135
+    except Exception, exError:
136
+        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
137
+        return None
138
+    tSeconds = int(time.mktime(t_sTime))
139
+    return tSeconds
140
+##end def
141
+
142
+def setStatusToOffline():
143
+    """Set the detected status of the aredn node to
144
+       "offline" and inform downstream clients by removing input
145
+       and output data files.
146
+       Parameters: none
147
+       Returns: nothing
148
+    """
149
+    global nodeOnline
150
+
151
+    # Inform downstream clients by removing output data file.
152
+    if os.path.exists(_OUTPUT_DATA_FILE):
153
+       os.remove(_OUTPUT_DATA_FILE)
154
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
155
+       os.remove(_DUMMY_OUTPUT_FILE)
156
+    # If the aredn node was previously online, then send
157
+    # a message that we are now offline.
158
+    if nodeOnline:
159
+        print '%s aredn node offline' % getTimeStamp()
160
+    nodeOnline = False
161
+##end def
162
+
163
+def terminateAgentProcess(signal, frame):
164
+    """Send a message to log when the agent process gets killed
165
+       by the operating system.  Inform downstream clients
166
+       by removing input and output data files.
167
+       Parameters:
168
+           signal, frame - dummy parameters
169
+       Returns: nothing
170
+    """
171
+    # Inform downstream clients by removing output data file.
172
+    if os.path.exists(_OUTPUT_DATA_FILE):
173
+       os.remove(_OUTPUT_DATA_FILE)
174
+    if os.path.exists(_DUMMY_OUTPUT_FILE):
175
+       os.remove(_DUMMY_OUTPUT_FILE)
176
+    print '%s terminating arednsig agent process' % \
177
+              (getTimeStamp())
178
+    sys.exit(0)
179
+##end def
180
+
181
+  ###  PUBLIC METHODS  ###
182
+
183
+def getArednNodeData():
184
+    """Send http request to aredn node.  The response from the
185
+       node contains the node signal data as unformatted ascii text.
186
+       Parameters: none
187
+       Returns: a string containing the node signal data if successful,
188
+                or None if not successful
189
+    """
190
+    try:
191
+        conn = urllib2.urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
192
+
193
+        # Format received data into a single string.
194
+        content = ""
195
+        for line in conn:
196
+            content += line.strip()
197
+        del conn
198
+
199
+    except Exception, exError:
200
+        # If no response is received from the device, then assume that
201
+        # the device is down or unavailable over the network.  In
202
+        # that case return None to the calling function.
203
+        print "%s http error: %s" % (getTimeStamp(), exError)
204
+        return None
205
+
206
+    if verboseDebug:
207
+        print "http request successful: %d bytes" % len(content)
208
+
209
+    return content
210
+##end def
211
+
212
+def parseNodeData(sData, dData):
213
+    """Parse the node status page html from the aredn node
214
+       into its component parts.  
215
+       Parameters:
216
+           sData - the string containing the data to be parsed
217
+           dData - a dictionary object to contain the parsed data items
218
+       Returns: True if successful, False otherwise
219
+    """
220
+    try:
221
+
222
+        # Set search boundaries for signal data
223
+        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
224
+                         '<td valign=middle><nobr><big><b>'
225
+        strEndSearch = 'dB'
226
+        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
227
+        iEndIndex = sData.find(strEndSearch, iBeginIndex)
228
+
229
+        # Exception if signal data not found
230
+        if iBeginIndex == -1 or iEndIndex == -1:
231
+            raise Exception("signal data not found in status page")
232
+
233
+        # Extract signal data from html
234
+        snr = sData[iBeginIndex:iEndIndex]
235
+        snr = snr.replace(' ','')
236
+        lsnr = snr.split('/')
237
+
238
+        # Store time and signal data in dictionary object
239
+        dData['time'] = getEpochSeconds(getTimeStamp())
240
+        dData['signal'] = lsnr[0]
241
+        dData['noise'] = lsnr[1]
242
+        dData['snr'] = lsnr[2]
243
+    
244
+    except Exception, exError:
245
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
246
+        return False
247
+
248
+    if verboseDebug:
249
+        print "parse successful"
250
+    return True
251
+##end def
252
+
253
+def updateDatabase(dData):
254
+    """
255
+    Update the rrdtool database by executing an rrdtool system command.
256
+    Format the command using the data extracted from the aredn node
257
+    response.   
258
+    Parameters: dData - dictionary object containing data items to be
259
+                        written to the rr database file
260
+    Returns: True if successful, False otherwise
261
+    """
262
+    # Format the rrdtool update command.
263
+    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
264
+    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
265
+             dData['noise'], dData['snr'], '0', \
266
+             '0', '0', '0')
267
+
268
+    if verboseDebug:
269
+        print "%s" % strCmd # DEBUG
270
+
271
+    # Run the command as a subprocess.
272
+    try:
273
+        subprocess.check_output(strCmd, shell=True,  \
274
+                             stderr=subprocess.STDOUT)
275
+    except subprocess.CalledProcessError, exError:
276
+        print "%s: rrdtool update failed: %s" % \
277
+                    (getTimeStamp(), exError.output)
278
+        return False
279
+
280
+    return True
281
+##end def
282
+
283
+def writeOutputDataFile(sData, dData):
284
+    """Write node data items to the output data file, formatted as 
285
+       a Javascript file.  This file may then be accessed and used by
286
+       by downstream clients, for instance, in HTML documents.
287
+       Parameters:
288
+           sData - a string object containing the data to be written
289
+                   to the output data file
290
+       Returns: True if successful, False otherwise
291
+    """
292
+    # Write file for use by html clients.  The following two
293
+    # data items are sent to the client file.
294
+    #    * The last database update date and time
295
+    #    * The data request interval
296
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
297
+                                time.localtime(dData['time']) )
298
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
299
+           (lastUpdate, chartUpdateInterval)
300
+    try:
301
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
302
+        fc.write(sDate)
303
+        fc.close()
304
+    except Exception, exError:
305
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
306
+        return False
307
+
308
+    if _RELAY_SERVER:
309
+        # Write the entire node data response to the output data file.
310
+        try:
311
+            fc = open(_OUTPUT_DATA_FILE, "w")
312
+            fc.write(sData)
313
+            fc.close()
314
+        except Exception, exError:
315
+            print "%s write output file failed: %s" % \
316
+                  (getTimeStamp(), exError)
317
+            return False
318
+        if verboseDebug:
319
+            print "write output data file: %d bytes" % len(sData)
320
+
321
+    return True
322
+## end def
323
+
324
+def setNodeStatus(updateSuccess):
325
+    """Detect if aredn node is offline or not available on
326
+       the network. After a set number of attempts to get data
327
+       from the node set a flag that the node is offline.
328
+       Parameters:
329
+           updateSuccess - a boolean that is True if data request
330
+                           successful, False otherwise
331
+       Returns: nothing
332
+    """
333
+    global failedUpdateCount, nodeOnline
334
+
335
+    if updateSuccess:
336
+        failedUpdateCount = 0
337
+        # Set status and send a message to the log if the node was
338
+        # previously offline and is now online.
339
+        if not nodeOnline:
340
+            print '%s aredn node online' % getTimeStamp()
341
+            nodeOnline = True
342
+    else:
343
+        # The last attempt failed, so update the failed attempts
344
+        # count.
345
+        failedUpdateCount += 1
346
+
347
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
348
+        # Max number of failed data requests, so set
349
+        # node status to offline.
350
+        setStatusToOffline()
351
+##end def
352
+
353
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
354
+                lower, upper, addTrend, autoScale):
355
+    """Uses rrdtool to create a graph of specified node data item.
356
+       Parameters:
357
+           fileName - name of file containing the graph
358
+           dataItem - data item to be graphed
359
+           gLabel - string containing a graph label for the data item
360
+           gTitle - string containing a title for the graph
361
+           gStart - beginning time of the graphed data
362
+           lower - lower bound for graph ordinate #NOT USED
363
+           upper - upper bound for graph ordinate #NOT USED
364
+           addTrend - 0, show only graph data
365
+                      1, show only a trend line
366
+                      2, show a trend line and the graph data
367
+           autoScale - if True, then use vertical axis auto scaling
368
+               (lower and upper parameters are ignored), otherwise use
369
+               lower and upper parameters to set vertical axis scale
370
+       Returns: True if successful, False otherwise
371
+    """
372
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
373
+    trendWindow = { 'end-1day': 7200,
374
+                    'end-4weeks': 172800,
375
+                    'end-12months': 604800 }
376
+ 
377
+    # Format the rrdtool graph command.
378
+
379
+    # Set chart start time, height, and width.
380
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
381
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
382
+   
383
+    # Set the range and scaling of the chart y-axis.
384
+    if lower < upper:
385
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
386
+    elif autoScale:
387
+        strCmd += "-A "
388
+    strCmd += "-Y "
389
+
390
+    # Set the chart ordinate label and chart title. 
391
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
392
+ 
393
+    # Show the data, or a moving average trend line over
394
+    # the data, or both.
395
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
396
+    if addTrend == 0:
397
+        strCmd += "LINE1:dSeries#0400ff "
398
+    elif addTrend == 1:
399
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
400
+                  % trendWindow[gStart]
401
+    elif addTrend == 2:
402
+        strCmd += "LINE1:dSeries#0400ff "
403
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
404
+                  % trendWindow[gStart]
405
+     
406
+    if verboseDebug:
407
+        print "%s" % strCmd # DEBUG
408
+    
409
+    # Run the formatted rrdtool command as a subprocess.
410
+    try:
411
+        result = subprocess.check_output(strCmd, \
412
+                     stderr=subprocess.STDOUT,   \
413
+                     shell=True)
414
+    except subprocess.CalledProcessError, exError:
415
+        print "rrdtool graph failed: %s" % (exError.output)
416
+        return False
417
+
418
+    if debugOption:
419
+        print "rrdtool graph: %s\n" % result,
420
+    return True
421
+
422
+##end def
423
+
424
+def generateGraphs():
425
+    """Generate graphs for display in html documents.
426
+       Parameters: none
427
+       Returns: nothing
428
+    """
429
+    autoScale = False
430
+
431
+    # The following will force creation of charts
432
+    # of only signal strength and S/N charts.  Note that the following
433
+    # data items appear constant and do not show variation with time:
434
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
435
+    # these parameters are demonstrated to vary in time, there is no point
436
+    # in creating the charts for these data items.
437
+    createAllCharts = False
438
+
439
+    # 24 hour stock charts
440
+
441
+    createGraph('24hr_signal', 'S', 'dBm', 
442
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
443
+    createGraph('24hr_snr', 'SNR', 'dB', 
444
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
445
+
446
+    # 4 week stock charts
447
+
448
+    createGraph('4wk_signal', 'S', 'dBm', 
449
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
450
+    createGraph('4wk_snr', 'SNR', 'dB', 
451
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
452
+
453
+    # 12 month stock charts
454
+
455
+    createGraph('12m_signal', 'S', 'dBm', 
456
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
457
+    createGraph('12m_snr', 'SNR', 'dB', 
458
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
459
+
460
+    if debugOption:
461
+        #print # print a blank line to improve readability when in debug mode
462
+        pass
463
+##end def
464
+
465
+def getCLarguments():
466
+    """Get command line arguments.  There are four possible arguments
467
+          -d turns on debug mode
468
+          -v turns on verbose debug mode
469
+          -t sets the aredn node query interval
470
+          -u sets the url of the aredn nodeing device
471
+       Returns: nothing
472
+    """
473
+    global debugOption, verboseDebug, dataRequestInterval, \
474
+           arednNodeUrl
475
+
476
+    index = 1
477
+    while index < len(sys.argv):
478
+        if sys.argv[index] == '-d':
479
+            debugOption = True
480
+        elif sys.argv[index] == '-v':
481
+            debugOption = True
482
+            verboseDebug = True
483
+        elif sys.argv[index] == '-p':
484
+            try:
485
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
486
+            except:
487
+                print "invalid polling period"
488
+                exit(-1)
489
+            index += 1
490
+        elif sys.argv[index] == '-u':
491
+            arednNodeUrl = sys.argv[index + 1]
492
+            index += 1
493
+        else:
494
+            cmd_name = sys.argv[0].split('/')
495
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
496
+            exit(-1)
497
+        index += 1
498
+##end def
499
+
500
+def main():
501
+    """Handles timing of events and acts as executive routine managing
502
+       all other functions.
503
+       Parameters: none
504
+       Returns: nothing
505
+    """
506
+    global dataRequestInterval
507
+
508
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
509
+
510
+    print '%s starting up arednsig agent process' % \
511
+                  (getTimeStamp())
512
+
513
+    # last time output JSON file updated
514
+    lastDataRequestTime = -1
515
+    # last time charts generated
516
+    lastChartUpdateTime = - 1
517
+
518
+    ## Get command line arguments.
519
+    getCLarguments()
520
+
521
+    requestIntervalSeconds = dataRequestInterval * 60 # convert to seconds
522
+    chartUpdateIntervalSeconds = chartUpdateInterval * 60 # charts interval
523
+
524
+    ## Exit with error if rrdtool database does not exist.
525
+    if not os.path.exists(_RRD_FILE):
526
+        print 'rrdtool database does not exist\n' \
527
+              'use createArednsigRrd script to ' \
528
+              'create rrdtool database\n'
529
+        exit(1)
530
+ 
531
+    ## main loop
532
+    while True:
533
+
534
+        currentTime = time.time() # get current time in seconds
535
+
536
+        # Every web update interval request data from the aredn
537
+        # node and process the received data.
538
+        if currentTime - lastDataRequestTime > requestIntervalSeconds:
539
+            lastDataRequestTime = currentTime
540
+            dData = {}
541
+            result = True
542
+
543
+            # Get the data string from the device.
544
+            sData = getArednNodeData()
545
+            # If the first http request fails, try one more time.
546
+            if sData == None:
547
+                result = False
548
+
549
+            # If successful parse the data.
550
+            if result:
551
+                result = parseNodeData(sData, dData)
552
+           
553
+            # If parse successful, write data to data files.
554
+            if result:
555
+                result = updateDatabase(dData)
556
+
557
+            if result:
558
+                writeOutputDataFile(sData, dData)
559
+
560
+            # Set the node status to online or offline depending on the
561
+            # success or failure of the above operations.
562
+            setNodeStatus(result)
563
+
564
+        # At the chart generation interval, generate charts.
565
+        if currentTime - lastChartUpdateTime > chartUpdateIntervalSeconds:
566
+            lastChartUpdateTime = currentTime
567
+            p = multiprocessing.Process(target=generateGraphs, args=())
568
+            p.start()
569
+
570
+        # Relinquish processing back to the operating system until
571
+        # the next update interval.
572
+
573
+        elapsedTime = time.time() - currentTime
574
+        if debugOption:
575
+            if result:
576
+                print "%s update successful:" % getTimeStamp(),
577
+            else:
578
+                print "%s update failed:" % getTimeStamp(),
579
+            print "%6f seconds processing time\n" % elapsedTime 
580
+        remainingTime = requestIntervalSeconds - elapsedTime
581
+        if remainingTime > 0.0:
582
+            time.sleep(remainingTime)
583
+    ## end while
584
+    return
585
+## end def
586
+
587
+if __name__ == '__main__':
588
+    try:
589
+        main()
590
+    except KeyboardInterrupt:
591
+        print '\n',
592
+        terminateAgentProcess('KeyboardInterrupt','Module')
0 593
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