Browse code

add version for FW v3.20

Gandolf authored on 10/03/2020 23:02:19
Showing 11 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,32 @@
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
+
13
+#NODE_URL="http://localnode:8080/cgi-bin/signal.json"
14
+NODE_URL="http://192.168.1.30:8080/cgi-bin/status"
15
+
16
+# Changing the value overides the default data request interval hardcoded
17
+# in the agent script.  '60' is the default. Value is in seconds.
18
+POLLING_INTERVAL="60"
19
+
20
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
21
+
22
+if [ -n "$PROCESS_ID" ]; then
23
+  if [ "$1" != "-q" ]; then
24
+    printf "arednsig agent running [%s]\n" $PROCESS_ID
25
+  fi
26
+else
27
+  printf "starting up arednsig agent\n"
28
+  cd $APP_PATH
29
+  $(./$AGENT_NAME -u $NODE_URL -p $POLLING_INTERVAL >> \
30
+ $LOG_PATH/arednsigAgent.log 2>&1 &)
31
+fi
32
+
0 33
new file mode 100755
... ...
@@ -0,0 +1,13 @@
1
+#!/bin/bash
2
+# Stop the radmon agent process and clean up environment.
3
+
4
+AGENT_NAME="[a]rednsigAgent.py"
5
+
6
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
7
+
8
+if [ -n "$PROCESS_ID" ]; then
9
+  printf "killing arednsig agent [%s]\n" $PROCESS_ID
10
+  kill $PROCESS_ID
11
+else
12
+  echo arednsig agent not running
13
+fi
0 14
new file mode 100755
... ...
@@ -0,0 +1,590 @@
1
+#!/usr/bin/python2 -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
+
52
+   ### DEFAULT AREDN NODE URL ###
53
+
54
+# set url of the aredn node
55
+
56
+_DEFAULT_AREDN_NODE_URL = "http://localnode:8080/cgi-bin/status"
57
+
58
+    ### FILE AND FOLDER LOCATIONS ###
59
+
60
+# folder for containing dynamic data objects
61
+_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
62
+# folder for charts and output data file
63
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
64
+# location of data output file
65
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigOutputData.js"
66
+# dummy output data file
67
+_DUMMY_OUTPUT_FILE = _DOCROOT_PATH + "dynamic/nodeOnline.js"
68
+# database that stores node data
69
+_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
70
+
71
+    ### GLOBAL CONSTANTS ###
72
+
73
+# interval in seconds between data requests to the aredn node
74
+_DEFAULT_DATA_REQUEST_INTERVAL = 60
75
+# chart update interval in minutes
76
+_CHART_UPDATE_INTERVAL = 600
77
+
78
+# number seconds to wait for a response to HTTP request
79
+_HTTP_REQUEST_TIMEOUT = 10
80
+# max number of failed data requests allowed
81
+_MAX_FAILED_DATA_REQUESTS = 0
82
+# standard chart width in pixels
83
+_CHART_WIDTH = 600
84
+# standard chart height in pixels
85
+_CHART_HEIGHT = 150
86
+# Set this to True only if this server is intended to relay raw
87
+# node data to a mirror server.
88
+_RELAY_SERVER = False
89
+
90
+   ### GLOBAL VARIABLES ###
91
+
92
+# turn on or off of verbose debugging information
93
+debugOption = False
94
+verboseDebug = False
95
+
96
+# The following two items are used for detecting system faults
97
+# and aredn node online or offline status.
98
+
99
+# count of failed attempts to get data from aredn node
100
+failedUpdateCount = 0
101
+# detected status of aredn node device
102
+nodeOnline = True
103
+
104
+# ip address of aredn node
105
+arednNodeUrl = _DEFAULT_AREDN_NODE_URL
106
+# frequency of data requests to aredn node
107
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
108
+# chart update interval
109
+chartUpdateInterval = _CHART_UPDATE_INTERVAL
110
+# last node request time
111
+lastDataPointTime = -1
112
+
113
+  ###  PRIVATE METHODS  ###
114
+
115
+def getTimeStamp():
116
+    """
117
+    Set the error message time stamp to the local system time.
118
+    Parameters: none
119
+    Returns: string containing the time stamp
120
+    """
121
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
122
+##end def
123
+
124
+def getEpochSeconds(sTime):
125
+    """Convert the time stamp supplied in the weather data string
126
+       to seconds since 1/1/1970 00:00:00.
127
+       Parameters: 
128
+           sTime - the time stamp to be converted must be formatted
129
+                   as %m/%d/%Y %H:%M:%S
130
+       Returns: epoch seconds
131
+    """
132
+    try:
133
+        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
134
+    except Exception, exError:
135
+        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
136
+        return None
137
+    tSeconds = int(time.mktime(t_sTime))
138
+    return tSeconds
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, dData):
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
+    
220
+
221
+    try:
222
+        strBeginSearch = '<nobr>Signal/Noise/Ratio</nobr></th>' \
223
+                         '<td valign=middle><nobr><big><b>'
224
+        strEndSearch = 'dB'
225
+
226
+        iBeginIndex = sData.find(strBeginSearch) + len(strBeginSearch)
227
+        iEndIndex = sData.find(strEndSearch, iBeginIndex)
228
+
229
+        if iBeginIndex == -1 or iEndIndex == -1:
230
+            raise Exception("signal data not found in status page")
231
+
232
+        snr = sData[iBeginIndex:iEndIndex]
233
+        snr = snr.replace(' ','')
234
+        lsnr = snr.split('/')
235
+
236
+        dData['time'] = getEpochSeconds(getTimeStamp())
237
+
238
+        dData['signal'] = lsnr[0]
239
+        dData['noise'] = lsnr[1]
240
+        dData['snr'] = lsnr[2]
241
+    
242
+    except Exception, exError:
243
+        print "%s parse failed: %s" % (getTimeStamp(), exError)
244
+        return False
245
+
246
+    if verboseDebug:
247
+        print "parse successful"
248
+    return True
249
+##end def
250
+
251
+def updateDatabase(dData):
252
+    """
253
+    Update the rrdtool database by executing an rrdtool system command.
254
+    Format the command using the data extracted from the aredn node
255
+    response.   
256
+    Parameters: dData - dictionary object containing data items to be
257
+                        written to the rr database file
258
+    Returns: True if successful, False otherwise
259
+    """
260
+    # Format the rrdtool update command.
261
+    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s:%s:%s"
262
+    strCmd = strFmt % (_RRD_FILE, dData['time'], dData['signal'], \
263
+             dData['noise'], dData['snr'], '0', \
264
+             '0', '0', '0')
265
+
266
+    if verboseDebug:
267
+        print "%s" % strCmd # DEBUG
268
+
269
+    # Run the command as a subprocess.
270
+    try:
271
+        subprocess.check_output(strCmd, shell=True,  \
272
+                             stderr=subprocess.STDOUT)
273
+    except subprocess.CalledProcessError, exError:
274
+        print "%s: rrdtool update failed: %s" % \
275
+                    (getTimeStamp(), exError.output)
276
+        return False
277
+
278
+    return True
279
+##end def
280
+
281
+def writeOutputDataFile(sData, dData):
282
+    """Write node data items to the output data file, formatted as 
283
+       a Javascript file.  This file may then be accessed and used by
284
+       by downstream clients, for instance, in HTML documents.
285
+       Parameters:
286
+           sData - a string object containing the data to be written
287
+                   to the output data file
288
+       Returns: True if successful, False otherwise
289
+    """
290
+    # Write file for use by html clients.  The following two
291
+    # data items are sent to the client file.
292
+    #    * The last database update date and time
293
+    #    * The data request interval
294
+    lastUpdate = time.strftime( "%m.%d.%Y %T", 
295
+                                time.localtime(dData['time']) )
296
+    sDate = "[{\"date\":\"%s\",\"period\":\"%s\"}]" % \
297
+           (lastUpdate, chartUpdateInterval)
298
+    try:
299
+        fc = open(_DUMMY_OUTPUT_FILE, "w")
300
+        fc.write(sDate)
301
+        fc.close()
302
+    except Exception, exError:
303
+        print "%s write node file failed: %s" % (getTimeStamp(), exError)
304
+        return False
305
+
306
+    if _RELAY_SERVER:
307
+        # Write the entire node data response to the output data file.
308
+        try:
309
+            fc = open(_OUTPUT_DATA_FILE, "w")
310
+            fc.write(sData)
311
+            fc.close()
312
+        except Exception, exError:
313
+            print "%s write output file failed: %s" % \
314
+                  (getTimeStamp(), exError)
315
+            return False
316
+        if verboseDebug:
317
+            print "write output data file: %d bytes" % len(sData)
318
+
319
+    return True
320
+## end def
321
+
322
+def setNodeStatus(updateSuccess):
323
+    """Detect if aredn node is offline or not available on
324
+       the network. After a set number of attempts to get data
325
+       from the node set a flag that the node is offline.
326
+       Parameters:
327
+           updateSuccess - a boolean that is True if data request
328
+                           successful, False otherwise
329
+       Returns: nothing
330
+    """
331
+    global failedUpdateCount, nodeOnline
332
+
333
+    if updateSuccess:
334
+        failedUpdateCount = 0
335
+        # Set status and send a message to the log if the node was
336
+        # previously offline and is now online.
337
+        if not nodeOnline:
338
+            print '%s aredn node online' % getTimeStamp()
339
+            nodeOnline = True
340
+    else:
341
+        # The last attempt failed, so update the failed attempts
342
+        # count.
343
+        failedUpdateCount += 1
344
+
345
+    if failedUpdateCount > _MAX_FAILED_DATA_REQUESTS:
346
+        # Max number of failed data requests, so set
347
+        # node status to offline.
348
+        setStatusToOffline()
349
+##end def
350
+
351
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
352
+                lower, upper, addTrend, autoScale):
353
+    """Uses rrdtool to create a graph of specified node data item.
354
+       Parameters:
355
+           fileName - name of file containing the graph
356
+           dataItem - data item to be graphed
357
+           gLabel - string containing a graph label for the data item
358
+           gTitle - string containing a title for the graph
359
+           gStart - beginning time of the graphed data
360
+           lower - lower bound for graph ordinate #NOT USED
361
+           upper - upper bound for graph ordinate #NOT USED
362
+           addTrend - 0, show only graph data
363
+                      1, show only a trend line
364
+                      2, show a trend line and the graph data
365
+           autoScale - if True, then use vertical axis auto scaling
366
+               (lower and upper parameters are ignored), otherwise use
367
+               lower and upper parameters to set vertical axis scale
368
+       Returns: True if successful, False otherwise
369
+    """
370
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
371
+    trendWindow = { 'end-1day': 7200,
372
+                    'end-4weeks': 172800,
373
+                    'end-12months': 604800 }
374
+ 
375
+    # Format the rrdtool graph command.
376
+
377
+    # Set chart start time, height, and width.
378
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
379
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
380
+   
381
+    # Set the range and scaling of the chart y-axis.
382
+    if lower < upper:
383
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
384
+    elif autoScale:
385
+        strCmd += "-A "
386
+    strCmd += "-Y "
387
+
388
+    # Set the chart ordinate label and chart title. 
389
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
390
+ 
391
+    # Show the data, or a moving average trend line over
392
+    # the data, or both.
393
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
394
+    if addTrend == 0:
395
+        strCmd += "LINE1:dSeries#0400ff "
396
+    elif addTrend == 1:
397
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
398
+                  % trendWindow[gStart]
399
+    elif addTrend == 2:
400
+        strCmd += "LINE1:dSeries#0400ff "
401
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
402
+                  % trendWindow[gStart]
403
+     
404
+    if verboseDebug:
405
+        print "%s" % strCmd # DEBUG
406
+    
407
+    # Run the formatted rrdtool command as a subprocess.
408
+    try:
409
+        result = subprocess.check_output(strCmd, \
410
+                     stderr=subprocess.STDOUT,   \
411
+                     shell=True)
412
+    except subprocess.CalledProcessError, exError:
413
+        print "rrdtool graph failed: %s" % (exError.output)
414
+        return False
415
+
416
+    if debugOption:
417
+        print "rrdtool graph: %s\n" % result,
418
+    return True
419
+
420
+##end def
421
+
422
+def generateGraphs():
423
+    """Generate graphs for display in html documents.
424
+       Parameters: none
425
+       Returns: nothing
426
+    """
427
+    autoScale = False
428
+
429
+    # The following will force creation of charts
430
+    # of only signal strength and S/N charts.  Note that the following
431
+    # data items appear constant and do not show variation with time:
432
+    # noise level, rx mcs, rx rate, tx mcs, tx rate.  Therefore, until
433
+    # these parameters are demonstrated to vary in time, there is no point
434
+    # in creating the charts for these data items.
435
+    createAllCharts = False
436
+
437
+    # 24 hour stock charts
438
+
439
+    createGraph('24hr_signal', 'S', 'dBm', 
440
+                'RSSI\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
441
+    createGraph('24hr_snr', 'SNR', 'dB', 
442
+                'SNR\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
443
+
444
+    # 4 week stock charts
445
+
446
+    createGraph('4wk_signal', 'S', 'dBm', 
447
+                'RSSI\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
448
+    createGraph('4wk_snr', 'SNR', 'dB', 
449
+                'SNR\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
450
+
451
+    # 12 month stock charts
452
+
453
+    createGraph('12m_signal', 'S', 'dBm', 
454
+                'RSSI\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
455
+    createGraph('12m_snr', 'SNR', 'dB', 
456
+                'SNR\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
457
+
458
+    if debugOption:
459
+        #print # print a blank line to improve readability when in debug mode
460
+        pass
461
+##end def
462
+
463
+def getCLarguments():
464
+    """Get command line arguments.  There are four possible arguments
465
+          -d turns on debug mode
466
+          -v turns on verbose debug mode
467
+          -t sets the aredn node query interval
468
+          -u sets the url of the aredn nodeing device
469
+       Returns: nothing
470
+    """
471
+    global debugOption, verboseDebug, dataRequestInterval, \
472
+           arednNodeUrl
473
+
474
+    index = 1
475
+    while index < len(sys.argv):
476
+        if sys.argv[index] == '-d':
477
+            debugOption = True
478
+        elif sys.argv[index] == '-v':
479
+            debugOption = True
480
+            verboseDebug = True
481
+        elif sys.argv[index] == '-p':
482
+            try:
483
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
484
+            except:
485
+                print "invalid polling period"
486
+                exit(-1)
487
+            index += 1
488
+        elif sys.argv[index] == '-u':
489
+            arednNodeUrl = sys.argv[index + 1]
490
+            index += 1
491
+        else:
492
+            cmd_name = sys.argv[0].split('/')
493
+            print "Usage: %s [-d] [-v] [-p seconds] [-u url]" % cmd_name[-1]
494
+            exit(-1)
495
+        index += 1
496
+##end def
497
+
498
+def main():
499
+    """Handles timing of events and acts as executive routine managing
500
+       all other functions.
501
+       Parameters: none
502
+       Returns: nothing
503
+    """
504
+    global dataRequestInterval
505
+
506
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
507
+
508
+    print '%s starting up arednsig agent process' % \
509
+                  (getTimeStamp())
510
+
511
+    # last time output JSON file updated
512
+    lastDataRequestTime = -1
513
+    # last time charts generated
514
+    lastChartUpdateTime = - 1
515
+    # last time the rrdtool database updated
516
+    lastDatabaseUpdateTime = -1
517
+
518
+    ## Get command line arguments.
519
+    getCLarguments()
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 > dataRequestInterval:
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
+
562
+        # At the chart generation interval, generate charts.
563
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
564
+            lastChartUpdateTime = currentTime
565
+            p = multiprocessing.Process(target=generateGraphs, args=())
566
+            p.start()
567
+
568
+        # Relinquish processing back to the operating system until
569
+        # the next update interval.
570
+
571
+        elapsedTime = time.time() - currentTime
572
+        if debugOption:
573
+            if result:
574
+                print "%s update successful:" % getTimeStamp(),
575
+            else:
576
+                print "%s update failed:" % getTimeStamp(),
577
+            print "%6f seconds processing time\n" % elapsedTime 
578
+        remainingTime = dataRequestInterval - elapsedTime
579
+        if remainingTime > 0.0:
580
+            time.sleep(remainingTime)
581
+    ## end while
582
+    return
583
+## end def
584
+
585
+if __name__ == '__main__':
586
+    try:
587
+        main()
588
+    except KeyboardInterrupt:
589
+        print '\n',
590
+        terminateAgentProcess('KeyboardInterrupt','Module')
0 591
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
1 18
Binary files /dev/null and b/arednsig/aredn_fw_3_20_3_0/doc/arednsig_320_installation.odt differ
2 19
new file mode 100644
3 20
Binary files /dev/null and b/arednsig/aredn_fw_3_20_3_0/doc/arednsig_320_installation.pdf differ
4 21
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