From www.AA6E.net
Jump to: navigation, search
#!/usr/bin/env python

# File: ulog.py
# Version: 0.11
# Date: 19 Nov 2008
# Author: Martin S. Ewing

#    ulog.py captures and logs data from a DLP-IO8-G data acquisition unit.
#    Copyright (C) 2008 Martin S. Ewing
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

# To do:
# separate event logs for each device?
# filter out "unchanged" entries?
# gzip old days' logs?

import time,serial,sys,os

INTERVAL = 30.0            # seconds between samples
DLP_DEV = "/dev/ttyUSB0"    # A/D device

# DLP-IO8-G codes, ports 1 - 8
DO_HI = ['1','2','3','4','5','6','7','8'] # set dig out high
DO_LO = ['Q','W','E','R','T','Y','U','I'] # set dig out low
DI    = ['A','S','D','F','G','H','J','K'] # read dig in
AI    = ['Z','X','C','V','B','N','M',','] # read analog in
TI    = ['9','0','-','=','O','P','[',']'] # read temp in
DLPASC= '`' # set ascii mode
DLPBIN= '\\' # set binary mode
DLPF  = 'L' # set fahrenheit
DLPC  = ';' # set celsius
DLPING= "'" # ping

# Store events (pumps on/off, etc) in separate file when they occur.
# Construct file names using current date.

date_string = time.strftime("%Y%m%d")
date_string_old = date_string
LOGFORMAT = "logs/ulog%s.log"
EVTFORMAT = "logs/uevt%s.log"
PENLOGFORMAT = "logs/pending/ulog%s.log"
LOGDIR = "logs"
REPTDIR = "reports"
PENDIR = "logs/pending" # where to put completed day's log
LOGBLEN = 128           # log buffer length
EVTBLEN = 0             # event buffer length
NTRY=3                  # Number of retries allowed for temps.
T_RETRY= 2.0            # wait time if retrying. (exptl)
T0 = 0.1                # spacing between temp. commands (exptl)
T1 = 0.1                # spacing between channels (exptl)
TEMP_INVAL = 39.99      # Temp. to use in case of wild reading (can't occur naturally)
NEG_MASK = -1 << 16     # for temp. sign correction

# Check validity of Dig. Input and invert sense.
def dio_decode(raw, id):
    if not raw in [chr(0), chr(1)]:
        print "ulog: invalid digital input [%s]: %d" \
            % (id, ord(raw))
        raw = chr(0)
    return 1 - ord(raw)

# return the time delay from now until next integral number of IVL seconds.
def until_next_interval(IVL):
    next_int = (IVL * (1.00+(time.time()//IVL))) + 0.001
    return next_int-time.time()

#### REAL ENTRY ####

# Open DLP-IO8-G device which appears as DLP_DEV
# Assume we have appropriate privilege.
try:
    ser = serial.Serial(DLP_DEV, 115200, timeout=1)
except serial.serialutil.SerialException:
    print "ulog: Can't open DLP-IO8-G on %s, terminating." % DLP_DEV
    sys.exit()

ser.write(DLPBIN)        # ensure binary mode

logfd = open(LOGFORMAT % date_string, 'a', LOGBLEN) # Open/append log file.
evtfd = open(EVTFORMAT % date_string, 'a', EVTBLEN) # record discrete events

hwc_state_old = -1        # hot water prev state
furn_state_old = -1        # furnace prev state
z1_state_old = -1        # zone 1
z2_state_old = -1        # zone 2
z3_state_old = -1        # zone 3
time_last_event = time.time()    # time of previous event (float sec) 

timestring = time.asctime()
print >>evtfd,      "%s --- Logging (re)started ---" % timestring
print >>sys.stderr, "%s --- Logging (re)started ---" % timestring

# start out on INTERVAL boundary.
time.sleep(until_next_interval(INTERVAL))

try:
  while (True):
    date_string = time.strftime("%Y%m%d")    # NB: date may have changed
    if date_string <> date_string_old:    # yes, new date
        logfd.close()            # close old logs, start new
        evtfd.close()
        print >>sys.stderr, "Date change, was %s, now %s" % \
            (date_string_old, date_string)
        old_name = LOGFORMAT % date_string_old        # move to
        new_name = PENLOGFORMAT % date_string_old    # pending dir.
        try:
            print >>sys.stderr, "Renaming %s to %s" % \
                (old_name, new_name)
            os.rename(old_name,new_name)    # awaiting report gen.
        except OSError:
            print >>sys.stderr, "Rename failed."
            pass        # if can't rename, leave it be.
        logsdir = os.listdir("logs")    # take care of uevt* files
        for x in logsdir:
            if x[:4] == "uevt":    # i.e. uevt*
                # Move to reports directory
                print >>sys.stderr, "Renaming %s" % x
                try:
                    os.rename(LOGDIR+"/"+x, REPTDIR+"/"+x)
                except OSError:
                    print >>sys.stderr, "Rename failed."
                    pass
        os.system("sudo ntpdate 1.pool.ntp.org")    # set clock
                                # since ntpd NG.
        date_string_old = date_string
        logfd = open(LOGFORMAT % date_string, 'a', LOGBLEN)
        evtfd = open(EVTFORMAT % date_string, 'a', EVTBLEN)    
    time_string = time.strftime("%H%M%S")
    time_now = time.time()

    # Read all inputs
    # Port 1 = Temperature (HW)
    for i in range(NTRY):    # Allow retries in case of blown reading
        ser.write(TI[0])
        time.sleep(T0)    # slowdown -> more reliable?
        t_raw = ser.read(2)
        # This temperature is always > 0...
        temp_hw = 0.0625 * (ord(t_raw[1]) << 8 | ord(t_raw[0]))
        if temp_hw < 60.:    # i.e., normal reading
            break
        time.sleep(T_RETRY)    # Try waiting a little
        ermsg = "%s HW Temp retry" % time_string
        print >>evtfd, ermsg
        print >>sys.stderr, ermsg
    if temp_hw > 60. : temp_hw = TEMP_INVAL  # could be disconnected probe?
    time.sleep(T1)        # slow down inter-port

    # Port 2 = HW Circulator on/off
    ser.write(DI[1])
    hwc_raw = ser.read(1)
    hwc_state = dio_decode(hwc_raw, "HW Circulator")
    if hwc_state <> hwc_state_old:    # an event is happening
        hwc_state_old = hwc_state
        dtime = (time_now - time_last_event) / 60.
        time_last_event = time_now
        print >>evtfd, "%s hwc=%d (%.1f)" % \
            (time_string,hwc_state,dtime)
    time.sleep(T1)        # slow down inter-port

    # Port 3 = Furnace burner on/off
    ser.write(DI[2])
    furn_raw = ser.read(1)
    furn_state = dio_decode(furn_raw, "Furnace")
    if furn_state <> furn_state_old:    # an event
        furn_state_old = furn_state
        dtime = (time_now - time_last_event) / 60.
        time_last_event = time_now
        print >>evtfd, "%s furnace=%d (%.1f)" % \
            (time_string,furn_state,dtime)
    time.sleep(T1)        # slow down inter-port

    # Port 4 = Zone 1 circulator (downstairs, south)
    ser.write(DI[3])
    z1_raw = ser.read(1)
    z1_state = dio_decode(z1_raw, "Zone 1")
    if z1_state <> z1_state_old:
        z1_state_old = z1_state
        dtime = (time_now - time_last_event) / 60.
        time_last_event = time_now
        print >>evtfd, "%s Zone1=%d (%.1f)" % \
            (time_string,z1_state,dtime)
    time.sleep(T1)        # slow down inter-port

    # Port 5 = Zone 2 circulator (downstairs, north)
    ser.write(DI[4])
    z2_raw = ser.read(1)
    z2_state = dio_decode(z2_raw, "Zone 2")
    if z2_state <> z2_state_old:
        z2_state_old = z2_state
        dtime = (time_now - time_last_event) / 60.
        time_last_event = time_now
        print >>evtfd, "%s Zone2=%d (%.1f)" % \
            (time_string,z2_state,dtime)
    time.sleep(T1)        # slow down inter-port

    # Port 6 = Zone 3 circulator (upstairs)
    ser.write(DI[5])
    z3_raw = ser.read(1)
    z3_state = dio_decode(z3_raw, "Zone 3")
    if z3_state <> z3_state_old:
        z3_state_old = z3_state
        dtime = (time_now - time_last_event) / 60.
        time_last_event = time_now
        print >>evtfd, "%s Zone3=%d (%.1f)" % \
            (time_string,z3_state,dtime)
    time.sleep(T1)        # slow down inter-port

        # Port 7 = Temperature (Ambient)
    for i in range(NTRY):
        ser.write(TI[6])
        time.sleep(T0)    # slowdown -> more reliable?
        ta_raw = ser.read(2)
        # This temperature can be < 0... must check sign
        rawt = ord(ta_raw[1]) << 8 | ord (ta_raw[0])
        if rawt & 0x8000:               # Check sign bit
            rawt = NEG_MASK | rawt      # making full precision neg. int.
        tempa_hw = 0.0625 * rawt
        if tempa_hw < 60.:    # normal condition
            break        # valid temp, probably
        time.sleep(T_RETRY)
        ermsg = "%s Amb. Temp. retry" % time_string
        print >>evtfd, ermsg
        print >>sys.stderr, ermsg
        if tempa_hw > 60. : tempa_hw = TEMP_INVAL

    # Print and log result
    logitem = "%s %.2f %d %d %d %d %d %.2f" % (time_string, temp_hw, 
        hwc_state, furn_state, z1_state, z2_state, z3_state, tempa_hw)
    print logitem
    print >>logfd, logitem
     # wait till next integral mult. of INTERVAL secs.
    time.sleep(until_next_interval(INTERVAL))

except KeyboardInterrupt:        # e.g., from kill -SIGINT <proc>
    pass                # graceful termination

# Close up files and terminate.
logfd.close()
evtfd.close()
ser.close()