Skip to content

Commit

Permalink
Add buttons, clock, rtc, speaker, scheduler
Browse files Browse the repository at this point in the history
This is a major jump in functionality. We get:
* A working display
* A scheduler supporting callbacks of varying trigger durations
* Working buttons with min/max press windows
* Working speakers with a simple beep only
* RTC clock support
* A basic clock implementation, with flashing colon
  • Loading branch information
malcolmholmes committed Sep 12, 2021
1 parent b82c7c2 commit 503f1f4
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 43 deletions.
58 changes: 58 additions & 0 deletions buttons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from machine import Pin
import time



STATE_UNPRESSED=1
STATE_PRESSED=2

class Buttons:
PINS = {
1: 2,
2: 17,
3: 15,
}
class Button:

class Callback:
def __init__(self, callback, min=0, max=-1):
self.callback = callback
self.min = min
self.max = max

def __init__(self, number):
self.pin = Pin(Buttons.PINS[number], Pin.IN, Pin.PULL_UP)
self.number = number
self.state = STATE_UNPRESSED
self.callbacks = []
self.pressed_time = None

def add_callback(self, callback, min=0, max=-1):
callbackObj = self.Callback(callback, min, max)
self.callbacks.append(callbackObj)
return callbackObj

def __init__(self, scheduler):
self.buttons = []
scheduler.schedule("button-press", 1, self.millis_callback)

def add_button(self, number):
button = Buttons.Button(number)
self.buttons.append(button)
return button

def millis_callback(self, t):
for button in self.buttons:
if len(button.callbacks)>0:
if button.state == STATE_UNPRESSED and button.pin.value() == 0:
button.state = STATE_PRESSED
button.pressed = time.ticks_ms()
elif button.state == STATE_PRESSED and button.pin.value() == 1:
button.state = STATE_UNPRESSED
tm = time.ticks_ms()
press_duration = time.ticks_diff(tm, button.pressed)
print("Button %d pressed for %dms" %(button.number, press_duration))
for callback in button.callbacks:
if callback.min < press_duration and (callback.max==-1 or press_duration <= callback.max):
callback.callback(t)
button.pressed = None
20 changes: 20 additions & 0 deletions clock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import time

class Clock:
def __init__(self, scheduler, display, rtc):
self.display = display
self.rtc = rtc
scheduler.schedule("clock-second", 1000, self.secs_callback)
scheduler.schedule("clock-minute", 60000, self.mins_callback)

def secs_callback(self, t):
t = time.time()
if t%2==0:
self.display.show_char(":", pos=10)
else:
self.display.show_char(" :", pos=10)

def mins_callback(self, t):
t = self.rtc.get_time()
now = "%02d:%02d" % (t[3], t[4])
self.display.show_text(now)
92 changes: 63 additions & 29 deletions display.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import time
from machine import Pin, Timer
import random

class Display:
def __init__(self):
def __init__(self, scheduler):
self.a0 = Pin(16, Pin.OUT)
self.a1 = Pin(18, Pin.OUT)
self.a2 = Pin(22, Pin.OUT)
Expand All @@ -11,47 +12,79 @@ def __init__(self):
self.clk = Pin(10, Pin.OUT)
self.le = Pin(12, Pin.OUT)

self.CS_cnt = 0
self.row = 0
self.count = 0
self.leds = [[0] * 32 for i in range(0,8)]
self.leds_changed = False
self.disp_offset = 2
self.initialise_fonts()
self.initialise_icons()

def start(self):
self.timer = Timer(period=1, callback=self.repeating_timer_callback_ms)

def repeating_timer_callback_ms(self, timer):
self.CS_cnt = (self.CS_cnt+1)%8
scheduler.schedule("enable-leds", 1, self.enable_leds)

led_row = self.leds[self.CS_cnt]
for col in range(32):
self.clk.value(0)
self.sdi.value(led_row[col])
self.clk.value(1)
def enable_leds(self, t):
self.count+=1
self.row = (self.row+1)%8
led_row = self.leds[self.row]
if True:
for col in range(32):
self.clk.value(0)
self.sdi.value(led_row[col])
self.clk.value(1)
self.le.value(1)
self.le.value(0)
self.leds_changed = False

self.le.value(1)
self.le.value(0)
self.a0.value(1 if self.CS_cnt&0x01 else 0)
self.a1.value(1 if self.CS_cnt&0x02 else 0)
self.a2.value(1 if self.CS_cnt&0x04 else 0)
self.a0.value(1 if self.row&0x01 else 0)
self.a1.value(1 if self.row&0x02 else 0)
self.a2.value(1 if self.row&0x04 else 0)

def clear(self, x=0, y=0, w=22, h=7):
for yy in range(y, y+h):
for xx in range(x, x+w):
self.leds[yy][xx]=0

def show(self, pos, character):
def show_char(self, character, pos):
pos+=self.disp_offset # Plus the offset of the status indicator
char = self.ziku[character]
for row in range(1,8):
byte = char.rows[row-1]
for col in range(0, char.width):
self.leds[row][pos+col] = (byte >> col) % 2
self.leds_changed = True

def show_text(self, text, pos=0):
i=0
while i<len(text):
if text[i:i+2] in self.ziku:
c=text[i:i+2]
i+=2
else:
c=text[i]
i+=1
char = self.ziku[c]
self.show_char(c, pos)
width = self.ziku[c].width
pos+=width+1

def show_icon(self, name):
icon = self.Icons[name]
for w in range(icon.width):
self.leds[icon.y][icon.x+w]=1
self.leds_changed = True

def hide_icon(self, name):
icon = self.Icons[name]
for w in range(icon.width):
self.leds[icon.y][icon.x+w]=0
self.leds_changed = True

def backlight_on(self):
self.leds[0][2]=1
self.leds[0][5]=1

def backlight_off(self):
self.leds[0][2]=0
self.leds[0][5]=0

def print(self):
for row in range(0,8):
Expand Down Expand Up @@ -96,13 +129,13 @@ def initialise_icons(self):
"CountUp": self.Icon(0,5, width=2),
"Hourly": self.Icon(0,6, width=2),
"AutoLight": self.Icon(0,7, width=2),
"Mon": self.Icon(3,0, width=3),
"Tue": self.Icon(6,0, width=3),
"Wed": self.Icon(9,0, width=3),
"Thur": self.Icon(12,0, width=3),
"Fri": self.Icon(15,0, width=3),
"Sat": self.Icon(18,0, width=3),
"Sun": self.Icon(21,0, width=3),
"Mon": self.Icon(3,0, width=2),
"Tue": self.Icon(6,0, width=2),
"Wed": self.Icon(9,0, width=2),
"Thur": self.Icon(12,0, width=2),
"Fri": self.Icon(15,0, width=2),
"Sat": self.Icon(18,0, width=2),
"Sun": self.Icon(21,0, width=2),
}
# Derived from c code created by yufu on 2021/1/23.
# Modulus method: negative code, reverse, line by line, 4X7 font
Expand Down Expand Up @@ -130,13 +163,14 @@ def initialise_fonts(self):
"N": self.Character(width=4, rows=[0x09,0x09,0x0B,0x0D,0x09,0x09,0x09]),
"P": self.Character(width=4, rows=[0x07,0x09,0x09,0x07,0x01,0x01,0x01]),
"U": self.Character(width=4, rows=[0x09,0x09,0x09,0x09,0x09,0x09,0x06]),
":": self.Character(width=4, rows=[0x00,0x03,0x03,0x00,0x03,0x03,0x00]), #2×7
":": self.Character(width=2, rows=[0x00,0x03,0x03,0x00,0x03,0x03,0x00]), #2×7
" :": self.Character(width=2, rows=[0x00,0x00,0x00,0x00,0x00,0x00,0x00]), # colon width space
"°C": self.Character(width=4, rows=[0x01,0x0C,0x12,0x02,0x02,0x12,0x0C]), # celcuis 5×7
"°F": self.Character(width=4, rows=[0x01,0x1E,0x02,0x1E,0x02,0x02,0x02]), # farenheit
" ": self.Character(width=4, rows=[0x00,0x00,0x00,0x00,0x00,0x00,0x00]), # space
"Y": self.Character(width=4, rows=[0x1F,0x04,0x04,0x04,0x04,0x04,0x04]), # 5*7
".": self.Character(width=4, rows=[0x00,0x00,0x00,0x00,0x00,0x00,0x01]), # 1×7
"-": self.Character(width=4, rows=[0x00,0x00,0x00,0x03,0x00,0x00,0x00]), # 2×7
".": self.Character(width=1, rows=[0x00,0x00,0x00,0x00,0x00,0x00,0x01]), # 1×7
"-": self.Character(width=2, rows=[0x00,0x00,0x00,0x03,0x00,0x00,0x00]), # 2×7
"M": self.Character(width=4, rows=[0x00,0x11,0x1B,0x15,0x11,0x11,0x11,0x11]), # 5×7
"/": self.Character(width=4, rows=[0x00,0x04,0x04,0x02,0x02,0x02,0x01,0x01]), # 3×7
"°C2": self.Character(width=4, rows=[0x00,0x01,0x0C,0x12,0x02,0x02,0x12,0x0C]), # 5×7
Expand Down
146 changes: 146 additions & 0 deletions ds3231_port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# FROM HERE: https://github.com/peterhinch/micropython-samples/tree/master/DS3231

# ds3231_port.py Portable driver for DS3231 precison real time clock.
# Adapted from WiPy driver at https://github.com/scudderfish/uDS3231

# Author: Peter Hinch
# Copyright Peter Hinch 2018 Released under the MIT license.

import utime
import machine
import sys
DS3231_I2C_ADDR = 104

try:
rtc = machine.RTC()
except:
print('Warning: machine module does not support the RTC.')
rtc = None

def bcd2dec(bcd):
return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f))

def dec2bcd(dec):
tens, units = divmod(dec, 10)
return (tens << 4) + units

def tobytes(num):
return num.to_bytes(1, 'little')

class DS3231:
def __init__(self, i2c):
self.ds3231 = i2c
self.timebuf = bytearray(7)
if DS3231_I2C_ADDR not in self.ds3231.scan():
raise RuntimeError("DS3231 not found on I2C bus at %d" % DS3231_I2C_ADDR)

def get_time(self, set_rtc=False):
if set_rtc:
self.await_transition() # For accuracy set RTC immediately after a seconds transition
else:
self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf) # don't wait
return self.convert(set_rtc)

def convert(self, set_rtc=False): # Return a tuple in localtime() format (less yday)
data = self.timebuf
ss = bcd2dec(data[0])
mm = bcd2dec(data[1])
if data[2] & 0x40:
hh = bcd2dec(data[2] & 0x1f)
if data[2] & 0x20:
hh += 12
else:
hh = bcd2dec(data[2])
wday = data[3]
DD = bcd2dec(data[4])
MM = bcd2dec(data[5] & 0x1f)
YY = bcd2dec(data[6])
if data[5] & 0x80:
YY += 2000
else:
YY += 1900
# Time from DS3231 in time.localtime() format (less yday)
result = YY, MM, DD, hh, mm, ss, wday -1, 0
if set_rtc:
if rtc is None:
# Best we can do is to set local time
secs = utime.mktime(result)
utime.localtime(secs)
else:
rtc.datetime((YY, MM, DD, wday, hh, mm, ss, 0))
return result

def save_time(self):
(YY, MM, mday, hh, mm, ss, wday, yday) = utime.localtime() # Based on RTC
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 0, tobytes(dec2bcd(ss)))
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 1, tobytes(dec2bcd(mm)))
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 2, tobytes(dec2bcd(hh))) # Sets to 24hr mode
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 3, tobytes(dec2bcd(wday + 1))) # 1 == Monday, 7 == Sunday
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 4, tobytes(dec2bcd(mday))) # Day of month
if YY >= 2000:
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM) | 0b10000000)) # Century bit
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-2000)))
else:
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 5, tobytes(dec2bcd(MM)))
self.ds3231.writeto_mem(DS3231_I2C_ADDR, 6, tobytes(dec2bcd(YY-1900)))

# Wait until DS3231 seconds value changes before reading and returning data
def await_transition(self):
self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf)
ss = self.timebuf[0]
while ss == self.timebuf[0]:
self.ds3231.readfrom_mem_into(DS3231_I2C_ADDR, 0, self.timebuf)
return self.timebuf

# Test hardware RTC against DS3231. Default runtime 10 min. Return amount
# by which DS3231 clock leads RTC in PPM or seconds per year.
# Precision is achieved by starting and ending the measurement on DS3231
# one-seond boundaries and using ticks_ms() to time the RTC.
# For a 10 minute measurement +-1ms corresponds to 1.7ppm or 53s/yr. Longer
# runtimes improve this, but the DS3231 is "only" good for +-2ppm over 0-40C.
def rtc_test(self, runtime=600, ppm=False, verbose=True):
if rtc is None:
raise RuntimeError('machine.RTC does not exist')
verbose and print('Waiting {} minutes for result'.format(runtime//60))
factor = 1_000_000 if ppm else 114_155_200 # seconds per year

self.await_transition() # Start on transition of DS3231. Record time in .timebuf
t = utime.ticks_ms() # Get system time now
ss = rtc.datetime()[6] # Seconds from system RTC
while ss == rtc.datetime()[6]:
pass
ds = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC
ds3231_start = utime.mktime(self.convert()) # Time when transition occurred
t = rtc.datetime()
rtc_start = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0

utime.sleep(runtime) # Wait a while (precision doesn't matter)

self.await_transition() # of DS3231 and record the time
t = utime.ticks_ms() # and get system time now
ss = rtc.datetime()[6] # Seconds from system RTC
while ss == rtc.datetime()[6]:
pass
de = utime.ticks_diff(utime.ticks_ms(), t) # ms to transition of RTC
ds3231_end = utime.mktime(self.convert()) # Time when transition occurred
t = rtc.datetime()
rtc_end = utime.mktime((t[0], t[1], t[2], t[4], t[5], t[6], t[3] - 1, 0)) # y m d h m s wday 0

d_rtc = 1000 * (rtc_end - rtc_start) + de - ds # ms recorded by RTC
d_ds3231 = 1000 * (ds3231_end - ds3231_start) # ms recorded by DS3231
ratio = (d_ds3231 - d_rtc) / d_ds3231
ppm = ratio * 1_000_000
verbose and print('DS3231 leads RTC by {:4.1f}ppm {:4.1f}mins/yr'.format(ppm, ppm*1.903))
return ratio * factor


def _twos_complement(self, input_value: int, num_bits: int) -> int:
mask = 2 ** (num_bits - 1)
return -(input_value & mask) + (input_value & ~mask)


def get_temperature(self):
t = self.ds3231.readfrom_mem(DS3231_I2C_ADDR, 0x11, 2)
i = t[0] << 8 | t[1]
return self._twos_complement(i >> 6, 10) * 0.25

Loading

0 comments on commit 503f1f4

Please sign in to comment.