#!/usr/bin/python # # WB4SON NTP Clock -- Bob Beatty -- September 25, 2013 # # Based on code from Adafruit, lrvick, and LiquidCrystal # Adafriot - www.adafruit.com # lrvic - https://github.com/lrvick/raspi-hd44780/blob/master/hd44780.py # LiquidCrystal - https://github.com/arduino/Arduino/blob/master/libraries/LiquidCrystal/LiquidCrystal.cpp # from time import sleep, strftime # for sleep and string system time from subprocess import * # for run_cmd import smbus # to talk to the I2C bus import datetime # stuff needed to process date & time strings # =========================================================================== # Adafruit_I2C Class # =========================================================================== class Adafruit_I2C : @staticmethod def getPiRevision(): "Gets the version number of the Raspberry Pi board" # Courtesy quick2wire-python-api # https://github.com/quick2wire/quick2wire-python-api try: with open('/proc/cpuinfo','r') as f: for line in f: if line.startswith('Revision'): return 1 if line.rstrip()[-1] in ['1','2'] else 2 except: return 0 @staticmethod def getPiI2CBusNumber(): # Gets the I2C bus number /dev/i2c# return 1 if Adafruit_I2C.getPiRevision() > 1 else 0 def __init__(self, address, busnum=-1, debug=False): self.address = address # By default, the correct I2C bus is auto-detected using /proc/cpuinfo # Alternatively, you can hard-code the bus version below: # self.bus = smbus.SMBus(0); # Force I2C0 (early 256MB Pi's) # self.bus = smbus.SMBus(1); # Force I2C1 (512MB Pi's) self.bus = smbus.SMBus( busnum if busnum >= 0 else Adafruit_I2C.getPiI2CBusNumber()) self.debug = debug def reverseByteOrder(self, data): "Reverses the byte order of an int (16-bit) or long (32-bit) value" # Courtesy Vishal Sapre byteCount = len(hex(data)[2:].replace('L','')[::2]) val = 0 for i in range(byteCount): val = (val << 8) | (data & 0xff) data >>= 8 return val def errMsg(self): print "Error accessing 0x%02X: Check your I2C address" % self.address return -1 def write8(self, reg, value): "Writes an 8-bit value to the specified register/address" try: self.bus.write_byte_data(self.address, reg, value) if self.debug: print "I2C: Wrote 0x%02X to register 0x%02X" % (value, reg) except IOError, err: return self.errMsg() def write16(self, reg, value): "Writes a 16-bit value to the specified register/address pair" try: self.bus.write_word_data(self.address, reg, value) if self.debug: print ("I2C: Wrote 0x%02X to register pair 0x%02X,0x%02X" % (value, reg, reg+1)) except IOError, err: return self.errMsg() def writeList(self, reg, list): "Writes an array of bytes using I2C format" try: if self.debug: print "I2C: Writing list to register 0x%02X:" % reg print list self.bus.write_i2c_block_data(self.address, reg, list) except IOError, err: return self.errMsg() def readList(self, reg, length): "Read a list of bytes from the I2C device" try: results = self.bus.read_i2c_block_data(self.address, reg, length) if self.debug: print ("I2C: Device 0x%02X returned the following from reg 0x%02X" % (self.address, reg)) print results return results except IOError, err: return self.errMsg() def readU8(self, reg): "Read an unsigned byte from the I2C device" try: result = self.bus.read_byte_data(self.address, reg) if self.debug: print ("I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % (self.address, result & 0xFF, reg)) return result except IOError, err: return self.errMsg() def readS8(self, reg): "Reads a signed byte from the I2C device" try: result = self.bus.read_byte_data(self.address, reg) if result > 127: result -= 256 if self.debug: print ("I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % (self.address, result & 0xFF, reg)) return result except IOError, err: return self.errMsg() def readU16(self, reg): "Reads an unsigned 16-bit value from the I2C device" try: result = self.bus.read_word_data(self.address,reg) if (self.debug): print "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg) return result except IOError, err: return self.errMsg() def readS16(self, reg): "Reads a signed 16-bit value from the I2C device" try: result = self.bus.read_word_data(self.address,reg) if (self.debug): print "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg) return result except IOError, err: return self.errMsg() # =========================================================================== # Adafruit_CharLCDPlate Class # =========================================================================== class Adafruit_CharLCDPlate(Adafruit_I2C): # ---------------------------------------------------------------------- # Constants # Port expander registers MCP23017_IOCON_BANK0 = 0x0A # IOCON when Bank 0 active MCP23017_IOCON_BANK1 = 0x15 # IOCON when Bank 1 active # These are register addresses when in Bank 1 only: MCP23017_GPIOA = 0x09 MCP23017_IODIRB = 0x10 MCP23017_GPIOB = 0x19 # Port expander input pin definitions SELECT = 0 RIGHT = 1 DOWN = 2 UP = 3 LEFT = 4 # LED colors OFF = 0x00 RED = 0x01 GREEN = 0x02 BLUE = 0x04 YELLOW = RED + GREEN TEAL = GREEN + BLUE VIOLET = RED + BLUE WHITE = RED + GREEN + BLUE ON = RED + GREEN + BLUE # LCD Commands LCD_CLEARDISPLAY = 0x01 LCD_RETURNHOME = 0x02 LCD_ENTRYMODESET = 0x04 LCD_DISPLAYCONTROL = 0x08 LCD_CURSORSHIFT = 0x10 LCD_FUNCTIONSET = 0x20 LCD_SETCGRAMADDR = 0x40 LCD_SETDDRAMADDR = 0x80 # Flags for display on/off control LCD_DISPLAYON = 0x04 LCD_DISPLAYOFF = 0x00 LCD_CURSORON = 0x02 LCD_CURSOROFF = 0x00 LCD_BLINKON = 0x01 LCD_BLINKOFF = 0x00 # Flags for display entry mode LCD_ENTRYRIGHT = 0x00 LCD_ENTRYLEFT = 0x02 LCD_ENTRYSHIFTINCREMENT = 0x01 LCD_ENTRYSHIFTDECREMENT = 0x00 # Flags for display/cursor shift LCD_DISPLAYMOVE = 0x08 LCD_CURSORMOVE = 0x00 LCD_MOVERIGHT = 0x04 LCD_MOVELEFT = 0x00 # ---------------------------------------------------------------------- # Constructor def __init__(self, busnum=-1, addr=0x20, debug=False): self.i2c = Adafruit_I2C(addr, busnum, debug) # I2C is relatively slow. MCP output port states are cached # so we don't need to constantly poll-and-change bit states. self.porta, self.portb, self.ddrb = 0, 0, 0b00010000 # Set MCP23017 IOCON register to Bank 0 with sequential operation. # If chip is already set for Bank 0, this will just write to OLATB, # which won't seriously bother anything on the plate right now # (blue backlight LED will come on, but that's done in the next # step anyway). self.i2c.bus.write_byte_data( self.i2c.address, self.MCP23017_IOCON_BANK1, 0) # Brute force reload ALL registers to known state. This also # sets up all the input pins, pull-ups, etc. for the Pi Plate. self.i2c.bus.write_i2c_block_data( self.i2c.address, 0, [ 0b00111111, # IODIRA R+G LEDs=outputs, buttons=inputs self.ddrb , # IODIRB LCD D7=input, Blue LED=output 0b00111111, # IPOLA Invert polarity on button inputs 0b00000000, # IPOLB 0b00000000, # GPINTENA Disable interrupt-on-change 0b00000000, # GPINTENB 0b00000000, # DEFVALA 0b00000000, # DEFVALB 0b00000000, # INTCONA 0b00000000, # INTCONB 0b00000000, # IOCON 0b00000000, # IOCON 0b00111111, # GPPUA Enable pull-ups on buttons 0b00000000, # GPPUB 0b00000000, # INTFA 0b00000000, # INTFB 0b00000000, # INTCAPA 0b00000000, # INTCAPB self.porta, # GPIOA self.portb, # GPIOB self.porta, # OLATA 0 on all outputs; side effect of self.portb ]) # OLATB turning on R+G+B backlight LEDs. # Switch to Bank 1 and disable sequential operation. # From this point forward, the register addresses do NOT match # the list immediately above. Instead, use the constants defined # at the start of the class. Also, the address register will no # longer increment automatically after this -- multi-byte # operations must be broken down into single-byte calls. self.i2c.bus.write_byte_data( self.i2c.address, self.MCP23017_IOCON_BANK0, 0b10100000) self.displayshift = (self.LCD_CURSORMOVE | self.LCD_MOVERIGHT) self.displaymode = (self.LCD_ENTRYLEFT | self.LCD_ENTRYSHIFTDECREMENT) self.displaycontrol = (self.LCD_DISPLAYON | self.LCD_CURSOROFF | self.LCD_BLINKOFF) self.write(0x33) # Init self.write(0x32) # Init self.write(0x28) # 2 line 5x8 matrix self.write(self.LCD_CLEARDISPLAY) self.write(self.LCD_CURSORSHIFT | self.displayshift) self.write(self.LCD_ENTRYMODESET | self.displaymode) self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) self.write(self.LCD_RETURNHOME) # ---------------------------------------------------------------------- # Write operations # The LCD data pins (D4-D7) connect to MCP pins 12-9 (PORTB4-1), in # that order. Because this sequence is 'reversed,' a direct shift # won't work. This table remaps 4-bit data values to MCP PORTB # outputs, incorporating both the reverse and shift. flip = ( 0b00000000, 0b00010000, 0b00001000, 0b00011000, 0b00000100, 0b00010100, 0b00001100, 0b00011100, 0b00000010, 0b00010010, 0b00001010, 0b00011010, 0b00000110, 0b00010110, 0b00001110, 0b00011110 ) # Low-level 4-bit interface for LCD output. This doesn't actually # write data, just returns a byte array of the PORTB state over time. # Can concatenate the output of multiple calls (up to 8) for more # efficient batch write. def out4(self, bitmask, value): hi = bitmask | self.flip[value >> 4] lo = bitmask | self.flip[value & 0x0F] return [hi | 0b00100000, hi, lo | 0b00100000, lo] # The speed of LCD accesses is inherently limited by I2C through the # port expander. A 'well behaved program' is expected to poll the # LCD to know that a prior instruction completed. But the timing of # most instructions is a known uniform 37 mS. The enable strobe # can't even be twiddled that fast through I2C, so it's a safe bet # with these instructions to not waste time polling (which requires # several I2C transfers for reconfiguring the port direction). # The D7 pin is set as input when a potentially time-consuming # instruction has been issued (e.g. screen clear), as well as on # startup, and polling will then occur before more commands or data # are issued. pollables = ( LCD_CLEARDISPLAY, LCD_RETURNHOME ) # Write byte, list or string value to LCD def write(self, value, char_mode=False): """ Send command/data to LCD """ # If pin D7 is in input state, poll LCD busy flag until clear. if self.ddrb & 0b00010000: lo = (self.portb & 0b00000001) | 0b01000000 hi = lo | 0b00100000 # E=1 (strobe) self.i2c.bus.write_byte_data( self.i2c.address, self.MCP23017_GPIOB, lo) while True: # Strobe high (enable) self.i2c.bus.write_byte(self.i2c.address, hi) # First nybble contains busy state bits = self.i2c.bus.read_byte(self.i2c.address) # Strobe low, high, low. Second nybble (A3) is ignored. self.i2c.bus.write_i2c_block_data( self.i2c.address, self.MCP23017_GPIOB, [lo, hi, lo]) if (bits & 0b00000010) == 0: break # D7=0, not busy self.portb = lo # Polling complete, change D7 pin to output self.ddrb &= 0b11101111 self.i2c.bus.write_byte_data(self.i2c.address, self.MCP23017_IODIRB, self.ddrb) bitmask = self.portb & 0b00000001 # Mask out PORTB LCD control bits if char_mode: bitmask |= 0b10000000 # Set data bit if not a command # If string or list, iterate through multiple write ops if isinstance(value, str): last = len(value) - 1 # Last character in string data = [] # Start with blank list for i, v in enumerate(value): # For each character... # Append 4 bytes to list representing PORTB over time. # First the high 4 data bits with strobe (enable) set # and unset, then same with low 4 data bits (strobe 1/0). data.extend(self.out4(bitmask, ord(v))) # I2C block data write is limited to 32 bytes max. # If limit reached, write data so far and clear. # Also do this on last byte if not otherwise handled. if (len(data) >= 32) or (i == last): self.i2c.bus.write_i2c_block_data( self.i2c.address, self.MCP23017_GPIOB, data) self.portb = data[-1] # Save state of last byte out data = [] # Clear list for next iteration elif isinstance(value, list): # Same as above, but for list instead of string last = len(value) - 1 data = [] for i, v in enumerate(value): data.extend(self.out4(bitmask, v)) if (len(data) >= 32) or (i == last): self.i2c.bus.write_i2c_block_data( self.i2c.address, self.MCP23017_GPIOB, data) self.portb = data[-1] data = [] else: # Single byte data = self.out4(bitmask, value) self.i2c.bus.write_i2c_block_data( self.i2c.address, self.MCP23017_GPIOB, data) self.portb = data[-1] # If a poll-worthy instruction was issued, reconfigure D7 # pin as input to indicate need for polling on next call. if (not char_mode) and (value in self.pollables): self.ddrb |= 0b00010000 self.i2c.bus.write_byte_data(self.i2c.address, self.MCP23017_IODIRB, self.ddrb) # ---------------------------------------------------------------------- # Utility methods def begin(self, cols, lines): self.currline = 0 self.numlines = lines self.clear() # Puts the MCP23017 back in Bank 0 + sequential write mode so # that other code using the 'classic' library can still work. # Any code using this newer version of the library should # consider adding an atexit() handler that calls this. def stop(self): self.porta = 0b11000000 # Turn off LEDs on the way out self.portb = 0b00000001 sleep(0.0015) self.i2c.bus.write_byte_data( self.i2c.address, self.MCP23017_IOCON_BANK1, 0) self.i2c.bus.write_i2c_block_data( self.i2c.address, 0, [ 0b00111111, # IODIRA self.ddrb , # IODIRB 0b00000000, # IPOLA 0b00000000, # IPOLB 0b00000000, # GPINTENA 0b00000000, # GPINTENB 0b00000000, # DEFVALA 0b00000000, # DEFVALB 0b00000000, # INTCONA 0b00000000, # INTCONB 0b00000000, # IOCON 0b00000000, # IOCON 0b00111111, # GPPUA 0b00000000, # GPPUB 0b00000000, # INTFA 0b00000000, # INTFB 0b00000000, # INTCAPA 0b00000000, # INTCAPB self.porta, # GPIOA self.portb, # GPIOB self.porta, # OLATA self.portb ]) # OLATB def clear(self): self.write(self.LCD_CLEARDISPLAY) def home(self): self.write(self.LCD_RETURNHOME) row_offsets = ( 0x00, 0x40, 0x14, 0x54 ) def setCursor(self, col, row): if row > self.numlines: row = self.numlines - 1 elif row < 0: row = 0 self.write(self.LCD_SETDDRAMADDR | (col + self.row_offsets[row])) def display(self): """ Turn the display on (quickly) """ self.displaycontrol |= self.LCD_DISPLAYON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def noDisplay(self): """ Turn the display off (quickly) """ self.displaycontrol &= ~self.LCD_DISPLAYON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def cursor(self): """ Underline cursor on """ self.displaycontrol |= self.LCD_CURSORON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def noCursor(self): """ Underline cursor off """ self.displaycontrol &= ~self.LCD_CURSORON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def ToggleCursor(self): """ Toggles the underline cursor On/Off """ self.displaycontrol ^= self.LCD_CURSORON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def blink(self): """ Turn on the blinking cursor """ self.displaycontrol |= self.LCD_BLINKON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def noBlink(self): """ Turn off the blinking cursor """ self.displaycontrol &= ~self.LCD_BLINKON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def ToggleBlink(self): """ Toggles the blinking cursor """ self.displaycontrol ^= self.LCD_BLINKON self.write(self.LCD_DISPLAYCONTROL | self.displaycontrol) def scrollDisplayLeft(self): """ These commands scroll the display without changing the RAM """ self.displayshift = self.LCD_DISPLAYMOVE | self.LCD_MOVELEFT self.write(self.LCD_CURSORSHIFT | self.displayshift) def scrollDisplayRight(self): """ These commands scroll the display without changing the RAM """ self.displayshift = self.LCD_DISPLAYMOVE | self.LCD_MOVERIGHT self.write(self.LCD_CURSORSHIFT | self.displayshift) def leftToRight(self): """ This is for text that flows left to right """ self.displaymode |= self.LCD_ENTRYLEFT self.write(self.LCD_ENTRYMODESET | self.displaymode) def rightToLeft(self): """ This is for text that flows right to left """ self.displaymode &= ~self.LCD_ENTRYLEFT self.write(self.LCD_ENTRYMODESET | self.displaymode) def autoscroll(self): """ This will 'right justify' text from the cursor """ self.displaymode |= self.LCD_ENTRYSHIFTINCREMENT self.write(self.LCD_ENTRYMODESET | self.displaymode) def noAutoscroll(self): """ This will 'left justify' text from the cursor """ self.displaymode &= ~self.LCD_ENTRYSHIFTINCREMENT self.write(self.LCD_ENTRYMODESET | self.displaymode) def createChar(self, location, bitmap): self.write(self.LCD_SETCGRAMADDR | ((location & 7) << 3)) self.write(bitmap, True) self.write(self.LCD_SETDDRAMADDR) def message(self, text): """ Send string to LCD. Newline wraps to second line""" lines = str(text).split('\n') # Split at newline(s) for i, line in enumerate(lines): # For each substring... if i > 0: # If newline(s), self.write(0xC0) # set DDRAM address to 2nd line self.write(line, True) # Issue substring def backlight(self, color): c = ~color self.porta = (self.porta & 0b00111111) | ((c & 0b011) << 6) self.portb = (self.portb & 0b11111110) | ((c & 0b100) >> 2) # Has to be done as two writes because sequential operation is off. self.i2c.bus.write_byte_data( self.i2c.address, self.MCP23017_GPIOA, self.porta) self.i2c.bus.write_byte_data( self.i2c.address, self.MCP23017_GPIOB, self.portb) # Read state of single button def buttonPressed(self, b): return (self.i2c.readU8(self.MCP23017_GPIOA) >> b) & 1 # Read and return bitmask of combined button state def buttons(self): return self.i2c.readU8(self.MCP23017_GPIOA) & 0b11111 # =========================================================================== # Main WB4SON NTP Clock Program # =========================================================================== if __name__ == '__main__': # # setup LCD, put title and IP on screen for 10 seconds # lcd = Adafruit_CharLCDPlate() lcd.begin(16, 2) # 16 columns by 2 lines lcd.clear() lcd.message("WB4SON NTP Clock\n") # # Fetch the IP address we are using (either WLAN0 or ETH0) # (Will show "NO IP" if no interface is available # def run_cmd(cmd): p = Popen(cmd, shell=True, stdout=PIPE) output = p.communicate()[0] return output cmd = "ip addr show wlan0 | grep inet | awk '{print $2}' | cut -d/ -f1" # # wait unit there is a valid WiFi IP address or we try too many times # lcd.message("Waiting 4 WiFi") sleep (2) attempt = 0 done = 0 while (done < 1): ipaddr = run_cmd(cmd) if len(ipaddr) > 9: done = 1 attempt = attempt + 1 if attempt > 60: done = 1 if done < 1: # # Make last character on line 2 flip between + and x # so user knows we are still working # lcd.setCursor(15,1) # position to rightmost field if (attempt % 2) > 0: lcd.message("+") else: lcd.message("x") sleep (1) # # Get basic Local and UTC time on screen # lcd.clear() cleanup = 1 # signal IP stuff on screen # # We want to update the screen no more than once a second # and we would like to do that when it is between 00.000 and 00.100 # seconds (one hundred thousand microseconds). Then the rest of the time we # sleep so the OS can do other things -- but since sleeping might be longer # than we want, because the OS might be busy, we will sleep several times, # each time with half the duration, until the update window condition is met. # second_update_window_in_microseconds = 100000 while 1: now = datetime.datetime.now() utc = datetime.datetime.utcnow() microseconds_after_sec_rollover = now.microsecond if microseconds_after_sec_rollover > second_update_window_in_microseconds: # # We are outside the update window and need to sleep a bit # sleep_time = 1000000 - microseconds_after_sec_rollover sleep_time = sleep_time /2 # sleep half the remaining time else: if lcd.buttonPressed(lcd.UP): lcd.backlight(lcd.ON) if lcd.buttonPressed(lcd.DOWN): lcd.backlight(lcd.RED) if (lcd.buttonPressed(lcd.LEFT) and lcd.buttonPressed(lcd.RIGHT)): # # Both Left and Right buttons are held. Shut down Pi # lcd.clear() lcd.message("Shut down. Kill\n") lcd.message("Power in 15 sec") # 123456789012345 cmd = "sudo shutdown -h now" run_cmd(cmd) sleep(10) # give OS time to shut down if lcd.buttonPressed(lcd.SELECT): # # Select is being held # # Fetch the IP address we are using (either WLAN0 or ETH0) # (Will show "NO IP" if no interface is available # cleanup = 1 #indicate IP stuff on LCD # # valid response would be something like "192.168.1.3" which is 11 chars long # # No response will be less # lcd.clear() lcd.message("WB4SON NTP Clock\n") if len(ipaddr) < 9: # WLAN0 doesn't exist as the response is too short (Console shows "Device "WLAN0" does not exist.") cmd = "ip addr show eth0 | grep inet | awk '{print $2}' | cut -d/ -f1" ipaddr = run_cmd(cmd) if len(ipaddr) < 9: # ETH0 doesn't exist as the response is too short (Console shows "Device "ETH0" does not exist.") ipaddr = "NO IP" lcd.message('IP %s' %(ipaddr)) else: # # Select NOT held, was it held before? # if (cleanup > 0): # # We WERE just showing IP Address must put up entire clock time # cleanup = 0 lcd.clear() lcd.setCursor(0,0) lcd.message(now.strftime('%b %d %H:%M:%SE')) lcd.setCursor(0,1) lcd.message(utc.strftime('%b %d %H:%M:%SZ')) sleep_time = 500000 # 500,000 uS or 0.5 seconds else: # # Just update the time # # Mon XX HH:MM:SSZ # 0123456789012345 # sleep_time = 500000 # 500,000 uS or 0.5 seconds # # Write seconds first # lcd.setCursor(13,0) # position to seconds field lcd.message(now.strftime('%SE')) lcd.setCursor(13,1) lcd.message(now.strftime('%SZ')) # # if seconds were zero then its a new minute too # so write the minutes # if now.second == 0: lcd.setCursor(10,0) # position to minutes field lcd.message(now.strftime('%M')) lcd.setCursor(10,1) lcd.message(now.strftime('%M')) # # if minutes are zero then its a new hour too # (and new hours might be new days as well) # so write everything) # if now.minute == 0: lcd.setCursor(0,0) lcd.message(now.strftime('%b %d %H:%M:%SE')) lcd.setCursor(0,1) lcd.message(utc.strftime('%b %d %H:%M:%SZ')) # # lets go to sleep until it is time to read the clock again # sleep ((sleep_time / 1000000.0))