Browse code

Initial release

Gandolf authored on 05/29/2021 20:13:39
Showing 17 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,93 @@
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: createPowerRrd.py
6
+#
7
+# Description: Creates a rrdtool database for use by the power agent to
8
+# store the data from the power and temperature sensors.  The agent uses
9
+# the data in the database to generate graphic charts for display in the
10
+# weather station web page.
11
+#
12
+# Copyright 2021 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 01 Jun 2021 by J L Owrey
28
+#
29
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
30
+
31
+import os
32
+import time
33
+import subprocess
34
+
35
+    ### DEFINE DATABASE FILE LOCATION ###
36
+
37
+_USER = os.environ['USER']
38
+# the file that stores the data
39
+_RRD_FILE = "/home/%s/database/powerData.rrd" % _USER
40
+
41
+   ### DEFINE DATABASE SIZE AND GRANULARITY
42
+
43
+_RRD_SIZE_IN_DAYS = 740 # days
44
+_1YR_RRA_STEPS_PER_DAY = 1440
45
+_DATABASE_UPDATE_INTERVAL = 30
46
+
47
+def createRrdFile():
48
+    """Create the rrd file if it does not exist.
49
+       Parameters: none
50
+       Returns: True, if successful
51
+    """
52
+
53
+    if os.path.exists(_RRD_FILE):
54
+        print "power database already exists"
55
+        return True
56
+
57
+     ## Calculate database size
58
+ 
59
+    heartBeat = 2 * _DATABASE_UPDATE_INTERVAL
60
+    rra1yrNumPDP =  int(round(86400 / (_1YR_RRA_STEPS_PER_DAY * \
61
+                    _DATABASE_UPDATE_INTERVAL)))
62
+    rrd24hrNumRows = int(round(86400 / _DATABASE_UPDATE_INTERVAL))
63
+    rrd1yearNumRows = _1YR_RRA_STEPS_PER_DAY * _RRD_SIZE_IN_DAYS
64
+       
65
+    strFmt = ("rrdtool create %s --start now-1day --step %s "
66
+              "DS:CUR:GAUGE:%s:U:U DS:VOLT:GAUGE:%s:U:U "
67
+              "DS:PWR:GAUGE:%s:U:U DS:BTMP:GAUGE:%s:U:U "
68
+              "DS:ATMP:GAUGE:%s:U:U "
69
+              "RRA:LAST:0.5:1:%s RRA:LAST:0.5:%s:%s")
70
+
71
+    strCmd = strFmt % (_RRD_FILE, _DATABASE_UPDATE_INTERVAL,           \
72
+                heartBeat, heartBeat, heartBeat, heartBeat, heartBeat, \
73
+                rrd24hrNumRows, rra1yrNumPDP, rrd1yearNumRows)
74
+
75
+    print "creating power database...\n\n%s\n" % strCmd
76
+
77
+    # Spawn a sub-shell and run the command
78
+    try:
79
+        subprocess.check_output(strCmd, stderr=subprocess.STDOUT, \
80
+                                shell=True)
81
+    except subprocess.CalledProcessError, exError:
82
+        print "rrdtool create failed: %s" % (exError.output)
83
+        return False
84
+    return True
85
+##end def
86
+
87
+def main():
88
+    createRrdFile()
89
+## end def
90
+
91
+if __name__ == '__main__':
92
+    main()
93
+        
0 94
new file mode 100644
... ...
@@ -0,0 +1,149 @@
1
+#!/usr/bin/python
2
+#
3
+# Module: ina260.py
4
+#
5
+# Description: This module acts as an interface between the INA260 sensor
6
+# and downstream applications that use the data.  Class methods get
7
+# current, voltage, and power data from the INA260 sensor.  It acts as a
8
+# library module that can be imported into and called from other Python
9
+# programs.
10
+#
11
+# Copyright 2021 Jeff Owrey
12
+#    This program is free software: you can redistribute it and/or modify
13
+#    it under the terms of the GNU General Public License as published by
14
+#    the Free Software Foundation, either version 3 of the License, or
15
+#    (at your option) any later version.
16
+#
17
+#    This program is distributed in the hope that it will be useful,
18
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
+#    GNU General Public License for more details.
21
+#
22
+#    You should have received a copy of the GNU General Public License
23
+#    along with this program.  If not, see http://www.gnu.org/license.
24
+#
25
+# Revision History
26
+#   * v10 released 01 June 2021 by J L Owrey; first release
27
+#
28
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
29
+
30
+# Import the I2C interface library
31
+import smbus
32
+import time
33
+
34
+# Define Device Registers
35
+CONFIG_REG = 0x0
36
+ID_REG = 0xFE
37
+CUR_REG = 0x1
38
+VOLT_REG = 0x2
39
+PWR_REG = 0x3
40
+
41
+class ina260:
42
+    # Initialize the INA260 sensor at the supplied address (default
43
+    # address is 0x40), and supplied bus (default is 1).  Creates
44
+    # a new SMBus object for each instance of this class.  Writes
45
+    # configuration data (two bytes) to the INA260 configuration
46
+    # register.
47
+    def __init__(self, sAddr=0x40, sbus=1):
48
+        # Instantiate a smbus object
49
+        self.sensorAddr = sAddr
50
+        self.bus = smbus.SMBus(sbus)
51
+        # Initialize INA260 sensor.  See the data sheet for meaning of
52
+        # each bit.  The following bytes are written to the configuration
53
+        # register
54
+        #     byte 1: 01100000
55
+        #     byte 2: 00100111
56
+        initData = [0x60, 0x27]
57
+        self.bus.write_i2c_block_data(self.sensorAddr, CONFIG_REG, initData)
58
+    ## end def
59
+
60
+    def status(self):
61
+        # Read configuration data
62
+        mfcid = self.bus.read_i2c_block_data(self.sensorAddr, ID_REG, 2)
63
+        mfcidB1 = format(mfcid[0], "08b")
64
+        mfcidB2 = format(mfcid[1], "08b")
65
+        # Read configuration data
66
+        config = self.bus.read_i2c_block_data(self.sensorAddr, CONFIG_REG, 2)
67
+        configB1 = format(config[0], "08b")
68
+        configB2 = format(config[1], "08b")
69
+        return (mfcidB1, mfcidB2, configB1, configB2)
70
+    ## end def
71
+
72
+    def getCurrent(self):
73
+        # Get the current data from the sensor.
74
+        # INA260 returns the data in two bytes formatted as follows
75
+        #        -------------------------------------------------
76
+        #    bit | b7  | b6  | b5  | b4  | b3  | b2  | b1  | b0  |
77
+        #        -------------------------------------------------
78
+        # byte 1 | d15 | d14 | d13 | d12 | d11 | d10 | d9  | d8  |
79
+        #        -------------------------------------------------
80
+        # byte 2 | d7  | d6  | d5  | d4  | d3  |  d2 | d1  | d0  |
81
+        #        -------------------------------------------------
82
+        # The current is returned in d15-d0, a two's complement,
83
+        # 16 bit number.  This means that d15 is the sign bit.        
84
+        data=self.bus.read_i2c_block_data(self.sensorAddr, CUR_REG, 2)
85
+        # Format into a 16 bit word.
86
+        bdata = data[0] << 8 | data[1]
87
+        # Convert from two's complement to integer.
88
+        # If d15 is 1, the the number is a negative two's complement
89
+        # number.  The absolute value is 2^15 - 1 minus the value
90
+        # of d14-d0 taken as a positive number.
91
+        if bdata > 0x7FFF:
92
+            bdata = -(0xFFFF - bdata) # 0xFFFF is 2^15 - 1
93
+        # Convert integer data to mAmps.
94
+        mAmps = bdata * 1.25  # LSB is 1.25 mA
95
+        return mAmps
96
+    ## end def
97
+
98
+    def getVoltage(self):
99
+        # Get the voltage data from the sensor.
100
+        # INA260 returns the data in two bytes formatted as follows
101
+        #        -------------------------------------------------
102
+        #    bit | b7  | b6  | b5  | b4  | b3  | b2  | b1  | b0  |
103
+        #        -------------------------------------------------
104
+        # byte 1 | d15 | d14 | d13 | d12 | d11 | d10 | d9  | d8  |
105
+        #        -------------------------------------------------
106
+        # byte 2 | d7  | d6  | d5  | d4  | d3  |  d2 | d1  | d0  |
107
+        #        -------------------------------------------------
108
+        # The voltage is returned in d15-d0 as an unsigned integer.
109
+        data=self.bus.read_i2c_block_data(self.sensorAddr, VOLT_REG, 2)
110
+        # Convert data to volts.
111
+        volts = (data[0] << 8 | data[1]) * 0.00125 # LSB is 1.25 mV
112
+        return volts
113
+    ## end def
114
+
115
+    def getPower(self):
116
+        # Get the wattage data from the sensor.
117
+        # INA260 returns the data in two bytes formatted as follows
118
+        #        -------------------------------------------------
119
+        #    bit | b7  | b6  | b5  | b4  | b3  | b2  | b1  | b0  |
120
+        #        -------------------------------------------------
121
+        # byte 1 | d15 | d14 | d13 | d12 | d11 | d10 | d9  | d8  |
122
+        #        -------------------------------------------------
123
+        # byte 2 | d7  | d6  | d5  | d4  | d3  |  d2 | d1  | d0  |
124
+        #        -------------------------------------------------
125
+        # The wattage is returned in d15-d0 as an unsigned integer.
126
+        data=self.bus.read_i2c_block_data(self.sensorAddr, PWR_REG, 2)
127
+        # Convert data to milliWatts. 
128
+        mW = (data[0] << 8 | data[1]) * 10.0  # LSB is 10.0 mW
129
+        return mW
130
+   ## end def
131
+## end class
132
+
133
+def test():
134
+    # Initialize the smbus and INA260 sensor.
135
+    pwr1 = ina260(0x40, 1)
136
+    # Read the INA260 configuration register and manufacturer's ID.
137
+    data = pwr1.status()
138
+    print "manufacturer ID: %s %s\nconfiguration register: %s %s\n" % data
139
+    # Print out sensor values.
140
+    while True:
141
+        print "%6.2f mA" % pwr1.getCurrent()
142
+        print "%6.2f V" % pwr1.getVoltage()
143
+        print "%6.2f mW\n" % pwr1.getPower()
144
+        time.sleep(2)
145
+## end def
146
+
147
+if __name__ == '__main__':
148
+    test()
149
+
0 150
new file mode 100644
... ...
@@ -0,0 +1,477 @@
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: nodepowerAgent.py
6
+#
7
+# Description: This module acts as an agent between the mesh network and
8
+# node power and enviromental sensors.  The agent periodically polls the
9
+# sensors and processes the data returned from the sensors, including
10
+#     - conversion of data items
11
+#     - update a round robin (rrdtool) database with the sensor data
12
+#     - periodically generate graphic charts for display in html documents
13
+#     - write the processed node status to a JSON file for use by html
14
+#       documents
15
+#
16
+# Copyright 2021 Jeff Owrey
17
+#    This program is free software: you can redistribute it and/or modify
18
+#    it under the terms of the GNU General Public License as published by
19
+#    the Free Software Foundation, either version 3 of the License, or
20
+#    (at your option) any later version.
21
+#
22
+#    This program is distributed in the hope that it will be useful,
23
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
24
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25
+#    GNU General Public License for more details.
26
+#
27
+#    You should have received a copy of the GNU General Public License
28
+#    along with this program.  If not, see http://www.gnu.org/license.
29
+#
30
+# Revision History
31
+#   * v10 released 01 June 2021 by J L Owrey; first release
32
+#
33
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
34
+
35
+# Import required python libraries.
36
+import os
37
+import sys
38
+import signal
39
+import subprocess
40
+import multiprocessing
41
+import time
42
+
43
+# Import sensor libraries.
44
+import ina260 # power sensor
45
+import tmp102 # temperature sensor
46
+
47
+    ### SENSOR BUS ADDRESSES ###
48
+
49
+# Set bus addresses of sensors
50
+_PWR_SENSOR_ADDR = 0X40
51
+_BAT_TMP_SENSOR_ADDR = 0x48
52
+_AMB_TMP_SENSOR_ADDR = 0x4B
53
+# Set bus selector
54
+_BUS_SEL = 1
55
+
56
+    ### FILE AND FOLDER LOCATIONS ###
57
+
58
+_USER = os.environ['USER']
59
+# folder for containing dynamic data objects
60
+_DOCROOT_PATH = "/home/%s/public_html/power/" % _USER
61
+# folder for charts and output data file
62
+_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
63
+# location of data output file
64
+_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/powerData.js"
65
+# database that stores node data
66
+_RRD_FILE = "/home/%s/database/powerData.rrd" % _USER
67
+
68
+    ### GLOBAL CONSTANTS ###
69
+
70
+# rrdtool database update interval in seconds
71
+_DATABASE_UPDATE_INTERVAL = 30
72
+# sensor data request interval in seconds
73
+_DEFAULT_DATA_REQUEST_INTERVAL = 2
74
+# chart update interval in seconds
75
+_CHART_UPDATE_INTERVAL = 600
76
+# standard chart width in pixels
77
+_CHART_WIDTH = 600
78
+# standard chart height in pixels
79
+_CHART_HEIGHT = 150
80
+
81
+   ### GLOBAL VARIABLES ###
82
+
83
+# turn on or off of verbose debugging information
84
+debugOption = False
85
+verboseDebug = False
86
+
87
+# frequency of data requests to sensors
88
+dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
89
+# chart update interval
90
+chartUpdateInterval = _CHART_UPDATE_INTERVAL
91
+# last node request time
92
+lastDataPointTime = -1
93
+
94
+# Define each sensor.  This also initialzes each sensor.
95
+pwr = ina260.ina260(_PWR_SENSOR_ADDR, _BUS_SEL)
96
+btmp = tmp102.tmp102(_BAT_TMP_SENSOR_ADDR, _BUS_SEL)
97
+atmp = tmp102.tmp102(_AMB_TMP_SENSOR_ADDR, _BUS_SEL)
98
+
99
+  ###  PRIVATE METHODS  ###
100
+
101
+def getTimeStamp():
102
+    """
103
+    Set the error message time stamp to the local system time.
104
+    Parameters: none
105
+    Returns: string containing the time stamp
106
+    """
107
+    return time.strftime( "%m/%d/%Y %T", time.localtime() )
108
+##end def
109
+
110
+def getEpochSeconds(sTime):
111
+    """Convert the time stamp supplied in the weather data string
112
+       to seconds since 1/1/1970 00:00:00.
113
+       Parameters: 
114
+           sTime - the time stamp to be converted must be formatted
115
+                   as %m/%d/%Y %H:%M:%S
116
+       Returns: epoch seconds
117
+    """
118
+    try:
119
+        t_sTime = time.strptime(sTime, '%m/%d/%Y %H:%M:%S')
120
+    except Exception, exError:
121
+        print '%s getEpochSeconds: %s' % (getTimeStamp(), exError)
122
+        return None
123
+    tSeconds = int(time.mktime(t_sTime))
124
+    return tSeconds
125
+##end def
126
+
127
+def terminateAgentProcess(signal, frame):
128
+    """Send a message to log when the agent process gets killed
129
+       by the operating system.  Inform downstream clients
130
+       by removing input and output data files.
131
+       Parameters:
132
+           signal, frame - dummy parameters
133
+       Returns: nothing
134
+    """
135
+    # Inform downstream clients by removing output data file.
136
+    if os.path.exists(_OUTPUT_DATA_FILE):
137
+       os.remove(_OUTPUT_DATA_FILE)
138
+    print '%s terminating npw agent process' % \
139
+              (getTimeStamp())
140
+    sys.exit(0)
141
+##end def
142
+
143
+  ###  PUBLIC METHODS  ###
144
+
145
+def getSensorData(dData):
146
+    """Poll sensors for data.
147
+       Parameters: none
148
+       Returns: a string containing the node signal data if successful,
149
+                or None if not successful
150
+    """
151
+    try:
152
+        dData["time"] = getTimeStamp()
153
+        dData["current"] = pwr.getCurrent()
154
+        dData["voltage"] = pwr.getVoltage()
155
+        dData["power"] = pwr.getPower()
156
+        dData["battemp"] = btmp.getTempC()
157
+        dData["ambtemp"] = atmp.getTempC()
158
+     
159
+    except Exception, exError:
160
+        print "%s sensor error: %s" % (getTimeStamp(), exError)
161
+        return False
162
+
163
+    return True
164
+##end def
165
+
166
+def updateDatabase(dData):
167
+    """
168
+    Update the rrdtool database by executing an rrdtool system command.
169
+    Format the command using the data extracted from the sensors.
170
+    Parameters: dData - dictionary object containing data items to be
171
+                        written to the rr database file
172
+    Returns: True if successful, False otherwise
173
+    """
174
+ 
175
+    epochTime = getEpochSeconds(dData['time'])
176
+
177
+    # Format the rrdtool update command.
178
+    strFmt = "rrdtool update %s %s:%s:%s:%s:%s:%s"
179
+    strCmd = strFmt % (_RRD_FILE, epochTime, dData['current'], \
180
+             dData['voltage'], dData['power'], dData['battemp'], \
181
+             dData['ambtemp'])
182
+
183
+    if debugOption:
184
+        print "%s" % strCmd # DEBUG
185
+
186
+    # Run the command as a subprocess.
187
+    try:
188
+        subprocess.check_output(strCmd, shell=True,  \
189
+                             stderr=subprocess.STDOUT)
190
+    except subprocess.CalledProcessError, exError:
191
+        print "%s: rrdtool update failed: %s" % \
192
+                    (getTimeStamp(), exError.output)
193
+        return False
194
+
195
+    return True
196
+##end def
197
+
198
+def writeOutputDataFile(dData):
199
+    """Write node data items to the output data file, formatted as 
200
+       a Javascript file.  This file may then be accessed and used by
201
+       by downstream clients, for instance, in HTML documents.
202
+       Parameters:
203
+           sData - a string object containing the data to be written
204
+                   to the output data file
205
+       Returns: True if successful, False otherwise
206
+    """
207
+    # Write a JSON formatted file for use by html clients.  The following
208
+    # data items are sent to the client file.
209
+    #    * The last database update date and time
210
+    #    * The data request interval
211
+    #    * The sensor values
212
+
213
+    # Create a JSON formatted string from the sensor data.
214
+    sData = "[{\"period\":\"%s\", " % \
215
+           (chartUpdateInterval)
216
+    for key in dData:
217
+        sData += '\"%s\":\"%s\", ' % (key, dData[key])
218
+    sData = sData[:-2] + '}]\n'
219
+
220
+    # Write the JSON formatted data to the output data file.
221
+    try:
222
+        fc = open(_OUTPUT_DATA_FILE, "w")
223
+        fc.write(sData)
224
+        fc.close()
225
+    except Exception, exError:
226
+        print "%s write output file failed: %s" % \
227
+              (getTimeStamp(), exError)
228
+        return False
229
+
230
+    if verboseDebug:
231
+        print sData[:-1]
232
+    if debugOption:
233
+        print "writing output data file: %d bytes" % len(sData)
234
+
235
+    return True
236
+## end def
237
+
238
+def createGraph(fileName, dataItem, gLabel, gTitle, gStart,
239
+                lower, upper, addTrend, autoScale):
240
+    """Uses rrdtool to create a graph of specified node data item.
241
+       Parameters:
242
+           fileName - name of file containing the graph
243
+           dataItem - data item to be graphed
244
+           gLabel - string containing a graph label for the data item
245
+           gTitle - string containing a title for the graph
246
+           gStart - beginning time of the graphed data
247
+           lower - lower bound for graph ordinate #NOT USED
248
+           upper - upper bound for graph ordinate #NOT USED
249
+           addTrend - 0, show only graph data
250
+                      1, show only a trend line
251
+                      2, show a trend line and the graph data
252
+           autoScale - if True, then use vertical axis auto scaling
253
+               (lower and upper parameters are ignored), otherwise use
254
+               lower and upper parameters to set vertical axis scale
255
+       Returns: True if successful, False otherwise
256
+    """
257
+    gPath = _CHARTS_DIRECTORY + fileName + ".png"
258
+    trendWindow = { 'end-1day': 7200,
259
+                    'end-4weeks': 172800,
260
+                    'end-12months': 604800 }
261
+ 
262
+    # Format the rrdtool graph command.
263
+
264
+    # Set chart start time, height, and width.
265
+    strCmd = "rrdtool graph %s -a PNG -s %s -e now -w %s -h %s " \
266
+             % (gPath, gStart, _CHART_WIDTH, _CHART_HEIGHT)
267
+   
268
+    # Set the range and scaling of the chart y-axis.
269
+    if lower < upper:
270
+        strCmd  +=  "-l %s -u %s -r " % (lower, upper)
271
+    elif autoScale:
272
+        strCmd += "-A "
273
+    strCmd += "-Y "
274
+
275
+    # Set the chart ordinate label and chart title. 
276
+    strCmd += "-v %s -t %s " % (gLabel, gTitle)
277
+ 
278
+    # Show the data, or a moving average trend line over
279
+    # the data, or both.
280
+    strCmd += "DEF:dSeries=%s:%s:LAST " % (_RRD_FILE, dataItem)
281
+    if addTrend == 0:
282
+        strCmd += "LINE1:dSeries#0400ff "
283
+    elif addTrend == 1:
284
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
285
+                  % trendWindow[gStart]
286
+    elif addTrend == 2:
287
+        strCmd += "LINE1:dSeries#0400ff "
288
+        strCmd += "CDEF:smoothed=dSeries,%s,TREND LINE3:smoothed#ff0000 " \
289
+                  % trendWindow[gStart]
290
+     
291
+    if verboseDebug:
292
+        print "%s" % strCmd # DEBUG
293
+    
294
+    # Run the formatted rrdtool command as a subprocess.
295
+    try:
296
+        result = subprocess.check_output(strCmd, \
297
+                     stderr=subprocess.STDOUT,   \
298
+                     shell=True)
299
+    except subprocess.CalledProcessError, exError:
300
+        print "rrdtool graph failed: %s" % (exError.output)
301
+        return False
302
+
303
+    if debugOption:
304
+        print "rrdtool graph: %s\n" % result,
305
+    return True
306
+
307
+##end def
308
+
309
+def generateGraphs():
310
+    """Generate graphs for display in html documents.
311
+       Parameters: none
312
+       Returns: nothing
313
+    """
314
+    autoScale = False
315
+
316
+    # 24 hour stock charts
317
+
318
+    createGraph('24hr_current', 'CUR', 'mA', 
319
+                'Current\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
320
+    createGraph('24hr_voltage', 'VOLT', 'V', 
321
+                'Voltage\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
322
+    createGraph('24hr_power', 'PWR', 'mW', 
323
+                'Power\ -\ Last\ 24\ Hours', 'end-1day', 0, 0, 2, autoScale)
324
+    createGraph('24hr_battemp', 'BTMP', 'deg\ C', 
325
+                'Battery\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \
326
+                 0, 0, 2, autoScale)
327
+    createGraph('24hr_ambtemp', 'ATMP', 'deg\ C', 
328
+                'Ambient\ Temperature\ -\ Last\ 24\ Hours', 'end-1day', \
329
+                 0, 0, 2, autoScale)
330
+
331
+    # 4 week stock charts
332
+
333
+    createGraph('4wk_current', 'CUR', 'mA', 
334
+                'Current\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
335
+    createGraph('4wk_voltage', 'VOLT', 'V', 
336
+                'Voltage\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
337
+    createGraph('4wk_power', 'PWR', 'mW', 
338
+                'Power\ -\ Last\ 4\ Weeks', 'end-4weeks', 0, 0, 2, autoScale)
339
+    createGraph('4wk_battemp', 'BTMP', 'deg\ C', 
340
+                'Battery\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \
341
+                 0, 0, 2, autoScale)
342
+    createGraph('4wk_ambtemp', 'ATMP', 'deg\ C', 
343
+                'Ambient\ Temperature\ -\ Last\ 4\ Weeks', 'end-4weeks', \
344
+                 0, 0, 2, autoScale)
345
+
346
+    # 12 month stock charts
347
+
348
+    createGraph('12m_current', 'CUR', 'mA', 
349
+                'Current\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
350
+    createGraph('12m_voltage', 'VOLT', 'V', 
351
+                'Voltage\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
352
+    createGraph('12m_power', 'PWR', 'mW', 
353
+                'Power\ -\ Past\ Year', 'end-12months', 0, 0, 2, autoScale)
354
+    createGraph('12m_battemp', 'BTMP', 'deg\ C', 
355
+                'Battery\ Temperature\ -\ Past\ Year', 'end-12months', \
356
+                 0, 0, 2, autoScale)
357
+    createGraph('12m_ambtemp', 'ATMP', 'deg\ C', 
358
+                'Ambient\ Temperature\ -\ Past\ Year', 'end-12months', \
359
+                 0, 0, 2, autoScale)
360
+##end def
361
+
362
+def getCLarguments():
363
+    """Get command line arguments.  There are three possible arguments
364
+          -d turns on debug mode
365
+          -v turns on verbose debug mode
366
+          -t sets the sensor query interval
367
+       Returns: nothing
368
+    """
369
+    global debugOption, verboseDebug, dataRequestInterval
370
+
371
+    index = 1
372
+    while index < len(sys.argv):
373
+        if sys.argv[index] == '-d':
374
+            debugOption = True
375
+        elif sys.argv[index] == '-v':
376
+            debugOption = True
377
+            verboseDebug = True
378
+        elif sys.argv[index] == '-p':
379
+            try:
380
+                dataRequestInterval = abs(int(sys.argv[index + 1]))
381
+            except:
382
+                print "invalid polling period"
383
+                exit(-1)
384
+            index += 1
385
+        else:
386
+            cmd_name = sys.argv[0].split('/')
387
+            print "Usage: %s [-d] [-v] [-p seconds]" % cmd_name[-1]
388
+            exit(-1)
389
+        index += 1
390
+##end def
391
+
392
+def main():
393
+    """Handles timing of events and acts as executive routine managing
394
+       all other functions.
395
+       Parameters: none
396
+       Returns: nothing
397
+    """
398
+    global dataRequestInterval
399
+
400
+    signal.signal(signal.SIGTERM, terminateAgentProcess)
401
+
402
+    print '%s starting up node power agent process' % \
403
+                  (getTimeStamp())
404
+
405
+    # last time output JSON file updated
406
+    lastDataRequestTime = -1
407
+    # last time charts generated
408
+    lastChartUpdateTime = - 1
409
+    # last time the rrdtool database updated
410
+    lastDatabaseUpdateTime = -1
411
+
412
+    ## Get command line arguments.
413
+    getCLarguments()
414
+
415
+    ## Exit with error if rrdtool database does not exist.
416
+    if not os.path.exists(_RRD_FILE):
417
+        print 'rrdtool database does not exist\n' \
418
+              'use createArednsigRrd script to ' \
419
+              'create rrdtool database\n'
420
+        exit(1)
421
+ 
422
+    ## main loop
423
+    while True:
424
+
425
+        currentTime = time.time() # get current time in seconds
426
+
427
+        # Every web update interval request data from the aredn
428
+        # node and process the received data.
429
+        if currentTime - lastDataRequestTime > dataRequestInterval:
430
+            lastDataRequestTime = currentTime
431
+            dData = {}
432
+            result = True
433
+
434
+            # Get the data from the sensors.
435
+            result =getSensorData(dData)
436
+ 
437
+            # If get data successful, write data to data files.
438
+            if result:
439
+                result = writeOutputDataFile(dData)
440
+
441
+            # At the rrdtool database update interval, update the database.
442
+            if currentTime - lastDatabaseUpdateTime > \
443
+                    _DATABASE_UPDATE_INTERVAL:   
444
+                lastDatabaseUpdateTime = currentTime
445
+                ## Update the round robin database with the parsed data.
446
+                if result:
447
+                    updateDatabase(dData)
448
+
449
+        # At the chart generation interval, generate charts.
450
+        if currentTime - lastChartUpdateTime > chartUpdateInterval:
451
+            lastChartUpdateTime = currentTime
452
+            p = multiprocessing.Process(target=generateGraphs, args=())
453
+            p.start()
454
+
455
+        # Relinquish processing back to the operating system until
456
+        # the next update interval.
457
+
458
+        elapsedTime = time.time() - currentTime
459
+        if debugOption:
460
+            if result:
461
+                print "%s update successful:" % getTimeStamp(),
462
+            else:
463
+                print "%s update failed:" % getTimeStamp(),
464
+            print "%6f seconds processing time\n" % elapsedTime 
465
+        remainingTime = dataRequestInterval - elapsedTime
466
+        if remainingTime > 0.0:
467
+            time.sleep(remainingTime)
468
+    ## end while
469
+    return
470
+## end def
471
+
472
+if __name__ == '__main__':
473
+    try:
474
+        main()
475
+    except KeyboardInterrupt:
476
+        print '\n',
477
+        terminateAgentProcess('KeyboardInterrupt','Module')
0 478
new file mode 100755
... ...
@@ -0,0 +1,21 @@
1
+#!/bin/bash
2
+#
3
+
4
+APP_PATH="/home/$USER/bin"
5
+LOG_PATH="/home/$USER/log"
6
+
7
+AGENT_NAME="[n]pwAgent.py"
8
+
9
+PROCESS_ID="$(ps x | awk -v a=$AGENT_NAME '$7 ~ a {print $1}')"
10
+
11
+if [ -n "$PROCESS_ID" ]; then
12
+  if [ "$1" != "-q" ]; then
13
+    printf "node power agent running [%s]\n" $PROCESS_ID
14
+  fi
15
+else
16
+  printf "starting up node agent\n"
17
+  cd $APP_PATH
18
+  $(./$AGENT_NAME >> \
19
+ $LOG_PATH/npwAgent.log 2>&1 &)
20
+fi
21
+
0 22
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="[n]pwAgent.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 node power agent [%s]\n" $PROCESS_ID
10
+  kill $PROCESS_ID
11
+else
12
+  echo node power agent not running
13
+fi
0 14
new file mode 100644
... ...
@@ -0,0 +1,129 @@
1
+#!/usr/bin/python
2
+#
3
+# Module: tmp102.py
4
+#
5
+# Description: This module acts as an interface between the TMP102 sensor
6
+# and downstream applications that use the data.  Class methods get
7
+# temperature data from the TMP102 sensor. It acts as a library module that
8
+# can be imported into and called from other Python programs.
9
+#
10
+# Copyright 2021 Jeff Owrey
11
+#    This program is free software: you can redistribute it and/or modify
12
+#    it under the terms of the GNU General Public License as published by
13
+#    the Free Software Foundation, either version 3 of the License, or
14
+#    (at your option) any later version.
15
+#
16
+#    This program is distributed in the hope that it will be useful,
17
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
+#    GNU General Public License for more details.
20
+#
21
+#    You should have received a copy of the GNU General Public License
22
+#    along with this program.  If not, see http://www.gnu.org/license.
23
+#
24
+# Revision History
25
+#   * v10 released 01 June 2021 by J L Owrey; first release
26
+#
27
+#2345678901234567890123456789012345678901234567890123456789012345678901234567890
28
+
29
+# Import the I2C interface library
30
+import smbus
31
+import time
32
+
33
+# Define constants
34
+DEGSYM = u'\xb0'
35
+
36
+# Define TMP102 Device Registers
37
+CONFIG_REG = 0x1
38
+TEMP_REG = 0x0
39
+
40
+class tmp102:
41
+
42
+    # Initialize the TMP102 sensor at the supplied address (default
43
+    # address is 0x48), and supplied bus (default is 1).  Creates
44
+    # a new SMBus object for each instance of this class.  Writes
45
+    # configuration data (two bytes) to the TMP102 configuration
46
+    # register.
47
+    def __init__(self, sAddr=0x48, sbus=1): 
48
+        # Instantiate a smbus object
49
+        self.sensorAddr = sAddr
50
+        self.bus = smbus.SMBus(sbus)
51
+        # Initialize TMP102 sensor.  See the data sheet for meaning of
52
+        # each bit.  The following bytes are written to the configuration
53
+        # register
54
+        #     byte 1: 01100000
55
+        #     byte 2: 10100000
56
+        initData = [0x60, 0xA0]
57
+        self.bus.write_i2c_block_data(self.sensorAddr, CONFIG_REG, initData)
58
+    ## end def
59
+
60
+    # Reads the configuration register (two bytes).
61
+    def status(self):
62
+        # Read configuration data
63
+        config = self.bus.read_i2c_block_data(self.sensorAddr, CONFIG_REG, 2)
64
+        configB1 = format(config[0], "08b")
65
+        configB2 = format(config[1], "08b")
66
+        return (configB1, configB2)
67
+    ## end def
68
+
69
+    # Gets the temperature in binary format and converts to degrees
70
+    # Celsius.
71
+    def getTempC(self):
72
+        # Get temperature data from the sensor.
73
+        # TMP102 returns the data in two bytes formatted as follows
74
+        #        -------------------------------------------------
75
+        #    bit | b7  | b6  | b5  | b4  | b3  | b2  | b1  | b0  |
76
+        #        -------------------------------------------------
77
+        # byte 1 | d11 | d10 | d9  | d8  | d7  | d6  | d5  | d4  |
78
+        #        -------------------------------------------------
79
+        # byte 2 | d3  | d2  | d1  | d0  | 0   |  0  |  0  |  0  |
80
+        #        -------------------------------------------------
81
+        # The temperature is returned in d11-d0, a two's complement,
82
+        # 12 bit number.  This means that d11 is the sign bit.
83
+        data=self.bus.read_i2c_block_data(self.sensorAddr, TEMP_REG, 2)
84
+        # Format into a 12 bit word.
85
+        bData = ( data[0] << 8 | data[1] ) >> 4
86
+        # Convert from two's complement to integer.
87
+        # If d11 is 1, the the number is a negative two's complement
88
+        # number.  The absolute value is 2^12 - 1 minus the value
89
+        # of d10-d0 taken as a positive number.
90
+        if bData > 0x7FF:  # all greater values are negative numbers
91
+            bData = -(0xFFF - bData)  # 0xFFF is 2^12 - 1
92
+        # convert integer data to Celsius
93
+        tempC = bData * 0.0625 # LSB is 0.0625 deg Celsius
94
+        return tempC
95
+    ## end def
96
+
97
+    def getTempF(self):
98
+        # Convert Celsius to Fahrenheit using standard formula.
99
+        tempF = (9./5.) * self.getTempC() + 32.
100
+        return tempF
101
+    ## end def
102
+## end class
103
+
104
+def testclass():
105
+    # Initialize the smbus and TMP102 sensor.
106
+    ts1 = tmp102(0x48, 1)
107
+    # Read the TMP102 configuration register.
108
+    data = ts1.status()
109
+    print "configuration register: %s %s\n" % data
110
+    # Print out sensor values.
111
+    bAl = False
112
+    while True:
113
+        tempC = ts1.getTempC()
114
+        tempF = ts1.getTempF()
115
+        if bAl:
116
+            bAl = False
117
+            print "\033[42;30m%6.2f%sC  %6.2f%sF\033[m" % \
118
+                  (tempC, DEGSYM, tempF, DEGSYM)
119
+        else:
120
+            bAl = True
121
+            print "%6.2f%sC  %6.2f%sF" % \
122
+                  (tempC, DEGSYM, tempF, DEGSYM)
123
+        time.sleep(2)
124
+    ## end while
125
+## end def
126
+
127
+if __name__ == '__main__':
128
+    testclass()
129
+
0 130
new file mode 100644
1 131
Binary files /dev/null and b/nodepower/docs/GPIO_hookup.jpg differ
2 132
new file mode 100644
3 133
Binary files /dev/null and b/nodepower/docs/Node_Power_Sensor_Hookup.pdf differ
4 134
new file mode 100644
... ...
@@ -0,0 +1,180 @@
1
+Raspbian 'stretch' - system build notes
2
+
3
+1. Copy os disk image to SD card.
4
+
5
+2. Mount the SD card to the host computer and navigate
6
+   to the boot volume of the SD card.
7
+
8
+3. If using wired Ethernet, skip to step 6.
9
+
10
+4. Edit the wpa_supplicant.conf file with your wifi ssid and
11
+   wifi password.
12
+
13
+5. Copy the wpa_supplicant.conf file to the root of the boot
14
+   volume.
15
+
16
+6. Create an empty file named "ssh" in the root of the boot
17
+   volume.
18
+
19
+7. Unmount the SD card and install in the Pi Zero.  Apply
20
+   power and boot up the Pi Zero.
21
+
22
+8. Secure shell into the Pi Zero by running
23
+       ssh pi@raspberrypi.local
24
+   The password is 'raspberry'
25
+
26
+9. Configure the following by running
27
+       sudo raspi-config
28
+   In System Options modify the following
29
+     hostname: nodepower
30
+     password: YOUR_PASSWORD
31
+   In Interface Options modify the following
32
+     I2C: ON
33
+   In Localisation Options modify the following     
34
+     Locale: en_US.UTF-8 UTF-8
35
+     Timezone: PACIFIC
36
+     Keyboard: US
37
+     WLAN: US
38
+
39
+10. If using wired Ethernet, disable wifi by adding the following
40
+    line to the end of the /boot/config.txt file
41
+      dtoverlay=disable-wifi
42
+    This helps to conserve power.
43
+
44
+11. Disable Bluetooth by adding the following line to the end of
45
+    the '/boot/config.txt' file:
46
+      dtoverlay=disable-bt
47
+    This helps to conserve power.
48
+    [Optional] Run the once off command:
49
+        sudo systemctl disable hciuart
50
+
51
+12. Set up ssh keys on client and import the public key
52
+   to the Pi Zero .ssh directory.  Create .ssh directory
53
+   on the Pi Zero.  In .ssh create a file authorized_keys
54
+   and copy the public key to it.
55
+
56
+13. Backup and then modify /etc/ssh/sshd_config as follows
57
+
58
+       #PermitRootLogin prohibit-password
59
+       PermitRootLogin no
60
+
61
+       #X11Forwarding yes
62
+       X11Forwarding no
63
+
64
+   Optionally turn off password authentication
65
+
66
+       #PasswordAuthentication yes
67
+       PasswordAuthentication no
68
+
69
+14. Setup tmpfs by backup and then modifying /etc/fstab.  Add
70
+   the following lines to the bottom of the file.
71
+
72
+       # uncomment if needed for web apps
73
+       # These changes store all non-essential logs in ram to reduce
74
+       # stress on the SD card due to frequent writes.
75
+       tmpfs /tmp tmpfs nodev,nosuid,size=20M 0 0
76
+       tmpfs /var/tmp tmpfs defaults,noatime,nosuid,size=20m 0 0
77
+       tmpfs /var/log tmpfs defaults,noatime,nosuid,mode=0755,size=20m 0 0
78
+       tmpfs /var/spool/mqueue tmpfs defaults,noatime,nosuid,mode=0700,gid=12,size=20m 0 0
79
+
80
+15. Reboot the Pi Zero by running
81
+        sudo reboot
82
+
83
+16. Run updates by running the commands
84
+        sudo apt-get update
85
+   
86
+    Optionally run all software updates
87
+        sudo apt-get upgrade
88
+        sudo reboot
89
+
90
+17. Install vim
91
+      apt-get install vim
92
+
93
+18. Optionally copy pi backup archive from the ssh client to
94
+    the /home/pi folder
95
+        scp pi.zip pi@nodepower.local:~
96
+
97
+19. Restore files and directories from backup archive by running
98
+      unzip pi.zip
99
+    Use 'mv' to move folders and files to their appropriate locations.
100
+
101
+20. Make backups of /etc/rc.local, /etc/motd. Then, acting as superuser
102
+    copy to /etc from the unzipped directory the files rc.local and motd.
103
+
104
+21. Install rrdtool
105
+       sudo apt-get install rrdtool
106
+
107
+22. Install web server
108
+
109
+    Apache2 
110
+    ======
111
+    sudo apt-get install apache2 -y
112
+    sudo a2enmod rewrite
113
+    sudo service apache2 restart
114
+
115
+    PHP
116
+    ===
117
+    sudo apt-get install php libapache2-mod-php -y
118
+    sudo service apache2 restart
119
+
120
+23. Acting as superuser, backup and then modify
121
+    /etc/apache2/mods-available/userdir.conf
122
+
123
+       # changed {date} by {name} to allow user .htacess file
124
+       #AllowOverride FileInfo AuthConfig Limit Indexes
125
+       AllowOverride All
126
+
127
+24. Enable user directories in apache2
128
+       sudo a2enmod userdir
129
+
130
+25. Acting as superuser, backup and then modify
131
+    /etc/apache2/sites-available/000-default.conf
132
+
133
+       # changed 12-06-2019 by JLO to make user pi the html document root
134
+       #DocumentRoot /var/www/html
135
+       DocumentRoot /home/pi/public_html
136
+
137
+26. Acting as superuser, backup and then modify
138
+    /etc/apache2/mods-available/php7.3.conf to allow user directories
139
+    by commenting the lines at bottom of file.  E.g.,
140
+
141
+       #<IfModule mod_userdir.c>
142
+       #    <Directory /home/*/public_html>
143
+       #        php_admin_flag engine Off
144
+       #    </Directory>
145
+       #</IfModule>
146
+
147
+27. Enable php in apache2
148
+       sudo a2enmod php7.3
149
+
150
+28. Acting as superuser, backup and then modify /etc/apache2/envvars
151
+    to create apache2 logs in tmpfs.  Add the following lines at the top
152
+    of the file
153
+
154
+       if [ ! -d /var/log/apache2 ]; then
155
+         mkdir /var/log/apache2
156
+       fi
157
+
158
+29. Acting as superuser, enable apache2 to access files the tmpfs /tmp
159
+    directory by backing up and then modifying
160
+    /lib/systemd/system/apache2.service
161
+
162
+       # changed {date} by {name} to allow apache to follow symlinks
163
+       # to the /tmp folder in tmpfs
164
+       #PrivateTmp=true
165
+       PrivateTmp=false
166
+
167
+30. Reload system deamons
168
+       sudo systemctl daemon-reload
169
+
170
+31. Restart apache2 service
171
+       sudo systemctl restart apache2
172
+
173
+32. Install i2c smbus python library by running
174
+       sudo apt-get install python-smbus
175
+
176
+33. Reboot the Pi Zero by running 'sudo reboot'.
177
+
178
+34. Test all above modifications.
179
+
180
+
0 181
new file mode 100644
1 182
Binary files /dev/null and b/nodepower/docs/datasheets/SparkFun_TMP102_Qwiic-Schematic.pdf differ
2 183
new file mode 100644
3 184
Binary files /dev/null and b/nodepower/docs/datasheets/ina260.pdf differ
4 185
new file mode 100644
5 186
Binary files /dev/null and b/nodepower/docs/datasheets/sensors_ina260_schematic.png differ
6 187
new file mode 100644
7 188
Binary files /dev/null and b/nodepower/docs/datasheets/tmp102.pdf differ
8 189
new file mode 100644
... ...
@@ -0,0 +1,6 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+  <meta http-equiv="refresh" content="0; url=./power.html">
5
+</head>
6
+</html>
0 7
new file mode 100644
... ...
@@ -0,0 +1,371 @@
1
+<!DOCTYPE html>
2
+<!-- Courtesy ruler for editing this file
3
+12345678901234567890123456789012345678901234567890123456789012345678901234567890
4
+-->
5
+<html>
6
+<head>
7
+<title>Node Power</title>
8
+<meta charset="UTF-8">
9
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+<link rel="shortcut icon" href="/favicon.png" type="image/x-icon" />
11
+<style>
12
+body {
13
+    background-image: url("static/chalk.jpg");
14
+}
15
+h2 {
16
+    font: bold 24px arial, sans-serif;
17
+}
18
+h4 {
19
+    font: bold 16px arial, sans-serif;
20
+}
21
+p {
22
+    font: normal 14px arial, sans-serif;
23
+}
24
+#mainContainer {
25
+    width: 740px;
26
+    text-align: center;
27
+    margin: auto;
28
+}
29
+#datetime {
30
+    padding: 10px;
31
+    font: bold 22px arial, sans-serif;
32
+    /*border: 1px solid black;*/
33
+}
34
+#notes {
35
+    font: 17px arial, sans-serif;
36
+    text-align: left;
37
+    padding: 10px;
38
+}
39
+.rowContainer {
40
+    display: table;
41
+    width: 100%;
42
+    /*border: 1px solid black;*/
43
+}
44
+.currentDataCell {
45
+    width: 50%;
46
+    padding: 10px;
47
+    font: bold 18px arial, sans-serif;
48
+    text-align: center;
49
+    display: table-cell;
50
+    vertical-align: middle;
51
+    /*border: 1px solid black;*/
52
+}
53
+.dataItems {
54
+    padding: 2px;
55
+    text-align: left;
56
+    line-height: 130%;
57
+    display: inline-block;
58
+    vertical-align: middle;
59
+    /*border: 1px solid black;*/
60
+}
61
+.chartContainer {
62
+    padding: 2px;
63
+    /*border: 1px solid black;*/
64
+}
65
+img.chart {
66
+    width: 100%;
67
+}
68
+span.chartNav {
69
+    margin: auto;
70
+}
71
+ul.chartNav {
72
+    list-style-type: none;
73
+    margin: 10px;
74
+    padding: 0;
75
+    overflow: hidden;
76
+    background-color: #bbb;
77
+    text-align: center;
78
+}
79
+li.chartNav {
80
+    display: inline-block;
81
+    font: bold 18px arial, sans-serif;
82
+    color: black;
83
+}
84
+text.chartNav:hover {
85
+    background-color: #333;
86
+    cursor: pointer;
87
+    color: white;
88
+}
89
+text.chartNav {
90
+    display: inline-block;
91
+    padding: 8px 12px;
92
+}
93
+</style>
94
+</head>
95
+
96
+<body onload="main()">
97
+
98
+<div id="mainContainer">
99
+
100
+<h2><a href="https://github.com/fractalxaos/ham/tree/master/nodepower" 
101
+style="text-decoration:none" target="_new">
102
+Node Power</a></h2>
103
+<h4>YOUR NODE DESCRIPTION AND CALL SIGN</h4>
104
+
105
+<div style="width:60%; text-align:left; font:14px arial, sans-serif;">
106
+This web page shows the power consumption of the above node.  Charts below
107
+provide a historical glimpse of node power consumption, battery and ambient
108
+temperatures.
109
+</div>
110
+
111
+<div id="datetime">
112
+<text id="date"></text>
113
+&nbsp;&nbsp;
114
+<text id="time"></text>
115
+</div>
116
+
117
+<div class="rowContainer">
118
+
119
+<div class="currentDataCell">
120
+<div class="dataItems">
121
+Current:<br>
122
+Voltage:<br>
123
+Power:
124
+</div>
125
+<div class="dataItems" style="width: 30%;">
126
+<text id="current"></text> mA<br>
127
+<text id="voltage"></text> V<br>
128
+<text id="power"></text> mW
129
+</div>
130
+</div>
131
+
132
+<div class="currentDataCell">
133
+<div class="dataItems">
134
+Battery Temperature:<br>
135
+Ambient Temperature:
136
+</div>
137
+<div class="dataItems"  style="width: 30%;">
138
+<text id="battemp"></text> &#8451;<br>
139
+<text id="ambtemp"></text> &#8451;<br>
140
+</div>
141
+</div>
142
+</div>
143
+
144
+<div class="rowContainer">
145
+<div class="currentDataCell">
146
+<div class="dataItems">
147
+Charts update every
148
+</div>
149
+<div class="dataItems">
150
+<text id="period"></text> minutes.<br>
151
+</div>
152
+</div>
153
+<div class="currentDataCell">
154
+<div class="dataItems">
155
+Status:
156
+</div>
157
+<div class="dataItems">
158
+<text id="status"></text><br>
159
+</div>
160
+</div>
161
+</div>
162
+
163
+<span class="chartNav">
164
+<ul class="chartNav">
165
+<li class="chartNav">Select charts:</li>
166
+<li class="chartNav"><text class="chartNav" onclick="setChartPeriod(1)">
167
+24 hours</text></li>
168
+<li class="chartNav"><text class="chartNav" onclick="setChartPeriod(2)">
169
+4 weeks</text></li>
170
+<li class="chartNav"><text class="chartNav" onclick="setChartPeriod(3)">
171
+12 months</text></li>
172
+</ul>
173
+</span>
174
+<br>
175
+
176
+<div class="chartContainer">
177
+<img class="chart" id="current_g">
178
+</div>
179
+
180
+<div class="chartContainer">
181
+<img class="chart" id="voltage_g">
182
+</div>
183
+
184
+<div class="chartContainer">
185
+<img class="chart" id="power_g">
186
+</div>
187
+
188
+<div class="chartContainer">
189
+<img class="chart" id="battemp_g">
190
+</div>
191
+
192
+<div class="chartContainer">
193
+<img class="chart" id="ambtemp_g">
194
+</div>
195
+
196
+<div id="notes">
197
+<b>NOTES:</b>
198
+<ul>
199
+<li>Node sensor project plans and software available at
200
+<a href="https://github.com/fractalxaos/ham/tree/master/nodepower" target="_new">
201
+<i>Github.com</i>
202
+</a>.</li>
203
+<li>Project plans include detailed instructions on how to use a Raspberry
204
+    Pi Zero to add power bus and battery temperature monitoring 
205
+    for your AREDN node.</li>
206
+<li>Displayed data may be delayed by as much as 2 seconds from
207
+ time of actual measurement.</li>
208
+<li>Project sponsored by
209
+ <a href=https://willamettevalleymesh.net/ TARGET="_NEW">
210
+ <i>Willamette Valley Mesh Network</i></a>, Salem, Oregon.</li>
211
+<li>Designed by Jeff Owrey, KA7JLO, 2021.</li>
212
+<li> Released under Creative Commons License.</li>
213
+</ul>
214
+</div><br><br>
215
+</div>
216
+
217
+<script>
218
+
219
+/* Global constants */
220
+
221
+var sensorDataUrl = "dynamic/powerData.js";
222
+
223
+/* Global DOM objects */
224
+
225
+// Chart Elements
226
+var current_g = document.getElementById("current_g");
227
+var voltage_g = document.getElementById("voltage_g");
228
+var power_g = document.getElementById("power_g");
229
+var battemp_g = document.getElementById("battemp_g");
230
+var ambtemp_g = document.getElementById("ambtemp_g");
231
+
232
+// Text Elements
233
+var date_t = document.getElementById("date");    
234
+var time_t = document.getElementById("time");    
235
+var current_t = document.getElementById("current");    
236
+var voltage_t = document.getElementById("voltage");    
237
+var power_t = document.getElementById("power");    
238
+var battemp_t = document.getElementById("battemp");    
239
+var ambtemp_t = document.getElementById("ambtemp");    
240
+var status_t = document.getElementById("status");
241
+var period_t = document.getElementById("period");    
242
+
243
+/* Global objects */
244
+
245
+var httpRequest = new XMLHttpRequest();
246
+
247
+/* Global variables */
248
+
249
+var graphPeriod;
250
+
251
+function main() {
252
+    // Register call back function to process client http requests
253
+    httpRequest.onreadystatechange = function() {
254
+        if (httpRequest.readyState == 4 && httpRequest.status == 200) {
255
+            var dataArray = JSON.parse(httpRequest.responseText);
256
+            displayData(dataArray[0]);
257
+        } else if (httpRequest.readyState == 4 && httpRequest.status == 404) {
258
+            displayOfflineStatus();
259
+        }
260
+    };
261
+    httpRequest.ontimeout = function(e) {
262
+        displayOfflineStatus();
263
+    };
264
+
265
+    getSensorData();
266
+    graphPeriod = 1;
267
+    getSensorGraphs();
268
+    setInterval(getSensorData, 2000);
269
+    setInterval(getSensorGraphs, 600000);
270
+}
271
+
272
+function getSensorData() {
273
+    httpRequest.open("GET", sensorDataUrl, true);
274
+    httpRequest.timeout = 3000;
275
+    httpRequest.send();
276
+}
277
+
278
+function setChartPeriod(n) {
279
+    graphPeriod = n;
280
+    getSensorGraphs();
281
+}
282
+
283
+function getSensorGraphs() {
284
+    var d = new Date;
285
+    var pfx;
286
+
287
+    switch(graphPeriod) {
288
+        case 1:
289
+            pfx = "24hr_";
290
+            break;
291
+        case 2:
292
+            pfx = "4wk_";
293
+            break;
294
+       case 3:
295
+            pfx = "12m_";
296
+            break;
297
+    }
298
+    current_g.src = "dynamic/" + pfx + "current.png?ver=" + d.getTime();
299
+    voltage_g.src = "dynamic/" + pfx + "voltage.png?ver=" + d.getTime();