├── .github └── workflows │ └── test.yml ├── .gitignore ├── .stickler.yml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── get-time.py ├── set-alarm.py ├── set-countdown-timer.py └── set-time.py ├── install-legacy.sh ├── install.sh ├── library ├── .coveragerc ├── CHANGELOG.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── rv3028 │ └── __init__.py ├── setup.cfg ├── setup.py ├── tests │ ├── test_alarm.py │ ├── test_setup.py │ ├── test_time.py │ ├── test_timer.py │ └── test_unix_time.py └── tox.ini └── uninstall.sh /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python: [2.7, 3.5, 3.7, 3.9] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python }} 22 | - name: Install Dependencies 23 | run: | 24 | python -m pip install --upgrade setuptools tox 25 | - name: Run Tests 26 | working-directory: library 27 | run: | 28 | tox -e py 29 | - name: Coverage 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | working-directory: library 33 | run: | 34 | python -m pip install coveralls 35 | coveralls --service=github 36 | if: ${{ matrix.python == '3.9' }} 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | *.o 4 | *.so 5 | *.a 6 | *.py[cod] 7 | *.egg-info 8 | dist/ 9 | __pycache__ 10 | .DS_Store 11 | *.deb 12 | *.dsc 13 | *.build 14 | *.changes 15 | *.orig.* 16 | packaging/*tar.xz 17 | library/debian/ 18 | .coverage 19 | .pytest_cache 20 | .tox 21 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | flake8: 4 | python: 3 5 | max-line-length: 160 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}') 2 | LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}') 3 | 4 | .PHONY: usage install uninstall 5 | usage: 6 | @echo "Library: ${LIBRARY_NAME}" 7 | @echo "Version: ${LIBRARY_VERSION}\n" 8 | @echo "Usage: make , where target is one of:\n" 9 | @echo "install: install the library locally from source" 10 | @echo "uninstall: uninstall the local library" 11 | @echo "check: peform basic integrity checks on the codebase" 12 | @echo "python-readme: generate library/README.md from README.md + library/CHANGELOG.txt" 13 | @echo "python-wheels: build python .whl files for distribution" 14 | @echo "python-sdist: build python source distribution" 15 | @echo "python-clean: clean python build and dist directories" 16 | @echo "python-dist: build all python distribution files" 17 | @echo "python-testdeploy: build all and deploy to test PyPi" 18 | @echo "tag: tag the repository with the current version" 19 | 20 | install: 21 | ./install.sh 22 | 23 | uninstall: 24 | ./uninstall.sh 25 | 26 | check: 27 | @echo "Checking for trailing whitespace" 28 | @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO 29 | @echo "Checking for DOS line-endings" 30 | @! grep -lIUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile 31 | @echo "Checking library/CHANGELOG.txt" 32 | @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} 33 | @echo "Checking library/${LIBRARY_NAME}/__init__.py" 34 | @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" 35 | 36 | tag: 37 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 38 | 39 | python-readme: library/README.md 40 | 41 | python-license: library/LICENSE.txt 42 | 43 | library/README.md: README.md library/CHANGELOG.txt 44 | cp README.md library/README.md 45 | printf "\n# Changelog\n" >> library/README.md 46 | cat library/CHANGELOG.txt >> library/README.md 47 | 48 | library/LICENSE.txt: LICENSE 49 | cp LICENSE library/LICENSE.txt 50 | 51 | python-wheels: python-readme python-license 52 | cd library; python3 setup.py bdist_wheel 53 | cd library; python setup.py bdist_wheel 54 | 55 | python-sdist: python-readme python-license 56 | cd library; python setup.py sdist 57 | 58 | python-clean: 59 | -rm -r library/dist 60 | -rm -r library/build 61 | -rm -r library/*.egg-info 62 | 63 | python-dist: python-clean python-wheels python-sdist 64 | ls library/dist 65 | 66 | python-testdeploy: python-dist 67 | twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* 68 | 69 | python-deploy: check python-dist 70 | twine upload library/dist/* 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RV3028 Real Time Clock Breakout 2 | 3 | [![Build Status](https://shields.io/github/workflow/status/pimoroni/rv3028-python/Python%20Tests.svg)](https://github.com/pimoroni/rv3028-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/rv3028-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/rv3028-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/rv3028.svg)](https://pypi.python.org/pypi/rv3028) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/rv3028.svg)](https://pypi.python.org/pypi/rv3028) 7 | 8 | # Note 9 | 10 | This is just a Python library to interface with the RV3028 and does not install the RTC as a time source for Raspbian. 11 | 12 | In order to set up as a RTC in Raspbian you will need to add something like the following to `/boot/config.txt`: 13 | 14 | ``` 15 | dtoverlay=i2c-rtc,rv3028,backup-switchover-mode=1 16 | ``` 17 | 18 | More information is available in the `i2c-rtc` dtoverlay documentation: https://github.com/raspberrypi/linux/blob/0d72d83ec92acda1e8cbad0d4213a5ec2b3f2e1b/arch/arm/boot/dts/overlays/README#L1079 19 | 20 | # Installing 21 | 22 | Stable library from PyPi: 23 | 24 | * Just run `python3 -m pip install rv3028` 25 | 26 | Latest/development library from GitHub: 27 | 28 | * `git clone https://github.com/pimoroni/rv3028-python` 29 | * `cd rv3028-python` 30 | * `sudo ./install.sh --unstable` 31 | 32 | # Requirements 33 | 34 | This library depends upon smbus: 35 | 36 | ``` 37 | sudo apt install python-smbus # Python 2 38 | sudo apt install python3-smbus # Python 3 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /examples/get-time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rv3028 4 | import time 5 | 6 | print("""get-time.py - Gets time and date from the RTC. 7 | 8 | Press Ctrl+C to exit. 9 | 10 | """) 11 | 12 | # Create RV3028 instance 13 | rtc = rv3028.RV3028() 14 | 15 | # Switches RTC to backup battery if VCC goes below 2V 16 | # Other settings: 'switchover_disabled', 'direct_switching_mode', 'standby_mode' 17 | rtc.set_battery_switchover('level_switching_mode') 18 | 19 | try: 20 | while True: 21 | rtc_time = rtc.get_time_and_date() 22 | print("The time is: {:02d}:{:02d}:{:02d} on :{:02d}/{:02d}/{:02d}".format(rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.day, rtc_time.month, rtc_time.year)) 23 | time.sleep(1) 24 | 25 | except KeyboardInterrupt: 26 | pass 27 | -------------------------------------------------------------------------------- /examples/set-alarm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rv3028 4 | import time 5 | import datetime 6 | 7 | print("""set-time.py - Sets alarm for 5 minutes time, recurring hourly, 8 | and waits for alarm to be triggered. 9 | 10 | Press Ctrl+C to exit. 11 | 12 | """) 13 | 14 | # Create RV3028 instance 15 | rtc = rv3028.RV3028() 16 | 17 | try: 18 | rtc.clear_alarm_interrupt() 19 | current_system_time = datetime.datetime.now() 20 | rtc.set_time_and_date(current_system_time) # Set time to current system time 21 | rtc_time = rtc.get_time_and_date() 22 | 23 | print("The time is: {:02d}:{:02d}:{:02d} on :{:02d}/{:02d}/{:02d}".format(rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.day, rtc_time.month, rtc_time.year)) 24 | 25 | # Set alarm for 5 minutes time 26 | alarm_minutes = 5 27 | alarm_time = rtc_time + datetime.timedelta(minutes=alarm_minutes) 28 | rtc.set_alarm_time(alarm_time) 29 | 30 | print("Alarm set for: {:02d}:{:02d}:{:02d} on: {:02d}/{:02d}/{:02d}\n".format(alarm_time.hour, alarm_time.minute, alarm_time.second, alarm_time.day, alarm_time.month, alarm_time.year)) 31 | 32 | # Valid alarm frequencies: 'disabled_weekly', 'disabled_monthly', 'hourly_on_minute','daily_on_hour', 'daily_on_hour_and_minute', 'weekly', 33 | # 'weekly_on_minute', 'weekly_on_hour', 'weekly_on_hour_and_minute', 'monthly', 'monthly_on_minute', 'monthly_on_hour', 'monthly_on_hour_and_minute' 34 | 35 | # This alarm will recur hourly 36 | rtc.set_alarm_setting("hourly_on_minute") 37 | 38 | print(rtc.get_alarm_time()) 39 | 40 | # Poll for alarm interrupt 41 | while not rtc.get_alarm_interrupt(): 42 | print("Waiting for alarm...") 43 | time.sleep(1) 44 | 45 | rtc_time = rtc.get_time_and_date() 46 | print("\nAlarm triggered at: {:02d}:{:02d}:{:02d} on: {:02d}/{:02d}/{:02d}!".format(rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.day, rtc_time.month, rtc_time.year)) 47 | 48 | except KeyboardInterrupt: 49 | pass 50 | -------------------------------------------------------------------------------- /examples/set-countdown-timer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rv3028 4 | import time 5 | 6 | print("""set-countdown-timer.py - Set the periodic countdown timer to 5 x 1 second ticks and waits for alarm to be triggered. 7 | 8 | Press Ctrl+C to exit. 9 | 10 | """) 11 | 12 | # Create RV3028 instance 13 | rtc = rv3028.RV3028() 14 | 15 | countdown_time = 5 16 | 17 | try: 18 | rtc.stop_periodic_timer() 19 | rtc.set_periodic_timer_frequency('1Hz') # Frequency of each timer tick can take '4036Hz', '63Hz','1Hz', '0.016Hz' (1Hz == 1 second) 20 | rtc.set_periodic_timer_countdown_value(countdown_time) # Set number of timer ticks before countdown alarm is triggered 21 | rtc.clear_periodic_countdown_timer_interrupt() 22 | rtc.start_periodic_timer() # Start timer running 23 | print('Countdown timer set for {} seconds\n'.format(countdown_time)) 24 | while not rtc.get_periodic_countdown_timer_interrupt(): 25 | print('Countdown is {}'.format(rtc.get_periodic_timer_countdown_status())) # Show how many ticks are left of the countdown 26 | time.sleep(1) 27 | print('\nTimer alarm triggered!') 28 | 29 | except KeyboardInterrupt: 30 | pass 31 | -------------------------------------------------------------------------------- /examples/set-time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rv3028 4 | import time 5 | import datetime 6 | 7 | print("""set-time.py - Sets RTC with current time for your system and 8 | enables battery backup to preserve the time when power is off. 9 | 10 | Press Ctrl+C to exit. 11 | 12 | """) 13 | 14 | # Create RV3028 instance 15 | rtc = rv3028.RV3028() 16 | 17 | # Switches RTC to backup battery if VCC goes below 2V 18 | # Other settings: 'switchover_disabled', 'direct_switching_mode', 'standby_mode' 19 | rtc.set_battery_switchover('level_switching_mode') 20 | 21 | try: 22 | current_system_time = datetime.datetime.now() 23 | # Time and date may also be set as a tuple (hour, minute, second, year, month, date) 24 | rtc.set_time_and_date(current_system_time) 25 | 26 | while True: 27 | rtc_time = rtc.get_time_and_date() 28 | print("The time is: {:02d}:{:02d}:{:02d} on :{:02d}/{:02d}/{:02d}".format(rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.day, rtc_time.month, rtc_time.year)) 29 | time.sleep(1) 30 | 31 | except KeyboardInterrupt: 32 | pass 33 | -------------------------------------------------------------------------------- /install-legacy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONFIG=/boot/config.txt 4 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 5 | CONFIG_BACKUP=false 6 | APT_HAS_UPDATED=false 7 | USER_HOME=/home/$SUDO_USER 8 | RESOURCES_TOP_DIR=$USER_HOME/Pimoroni 9 | WD=`pwd` 10 | USAGE="sudo ./install.sh (--unstable)" 11 | POSITIONAL_ARGS=() 12 | FORCE=false 13 | UNSTABLE=false 14 | CODENAME=`lsb_release -sc` 15 | 16 | if [[ $CODENAME == "bullseye" ]]; then 17 | bash ./install.sh $@ 18 | exit $? 19 | fi 20 | 21 | user_check() { 22 | if [ $(id -u) -ne 0 ]; then 23 | printf "Script must be run as root. Try 'sudo ./install.sh'\n" 24 | exit 1 25 | fi 26 | } 27 | 28 | confirm() { 29 | if $FORCE; then 30 | true 31 | else 32 | read -r -p "$1 [y/N] " response < /dev/tty 33 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 34 | true 35 | else 36 | false 37 | fi 38 | fi 39 | } 40 | 41 | prompt() { 42 | read -r -p "$1 [y/N] " response < /dev/tty 43 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 44 | true 45 | else 46 | false 47 | fi 48 | } 49 | 50 | success() { 51 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 52 | } 53 | 54 | inform() { 55 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 56 | } 57 | 58 | warning() { 59 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 60 | } 61 | 62 | function do_config_backup { 63 | if [ ! $CONFIG_BACKUP == true ]; then 64 | CONFIG_BACKUP=true 65 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 66 | inform "Backing up $CONFIG to /boot/$FILENAME\n" 67 | cp $CONFIG /boot/$FILENAME 68 | mkdir -p $RESOURCES_TOP_DIR/config-backups/ 69 | cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME 70 | if [ -f "$UNINSTALLER" ]; then 71 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER 72 | fi 73 | fi 74 | } 75 | 76 | function apt_pkg_install { 77 | PACKAGES=() 78 | PACKAGES_IN=("$@") 79 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 80 | PACKAGE="${PACKAGES_IN[$i]}" 81 | if [ "$PACKAGE" == "" ]; then continue; fi 82 | printf "Checking for $PACKAGE\n" 83 | dpkg -L $PACKAGE > /dev/null 2>&1 84 | if [ "$?" == "1" ]; then 85 | PACKAGES+=("$PACKAGE") 86 | fi 87 | done 88 | PACKAGES="${PACKAGES[@]}" 89 | if ! [ "$PACKAGES" == "" ]; then 90 | echo "Installing missing packages: $PACKAGES" 91 | if [ ! $APT_HAS_UPDATED ]; then 92 | apt update 93 | APT_HAS_UPDATED=true 94 | fi 95 | apt install -y $PACKAGES 96 | if [ -f "$UNINSTALLER" ]; then 97 | echo "apt uninstall -y $PACKAGES" 98 | fi 99 | fi 100 | } 101 | 102 | while [[ $# -gt 0 ]]; do 103 | K="$1" 104 | case $K in 105 | -u|--unstable) 106 | UNSTABLE=true 107 | shift 108 | ;; 109 | -f|--force) 110 | FORCE=true 111 | shift 112 | ;; 113 | *) 114 | if [[ $1 == -* ]]; then 115 | printf "Unrecognised option: $1\n"; 116 | printf "Usage: $USAGE\n"; 117 | exit 1 118 | fi 119 | POSITIONAL_ARGS+=("$1") 120 | shift 121 | esac 122 | done 123 | 124 | user_check 125 | 126 | apt_pkg_install python-configparser 127 | 128 | CONFIG_VARS=`python - < $UNINSTALLER 165 | printf "It's recommended you run these steps manually.\n" 166 | printf "If you want to run the full script, open it in\n" 167 | printf "an editor and remove 'exit 1' from below.\n" 168 | exit 1 169 | EOF 170 | 171 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" 172 | 173 | if $UNSTABLE; then 174 | warning "Installing unstable library from source.\n\n" 175 | else 176 | printf "Installing stable library from pypi.\n\n" 177 | fi 178 | 179 | cd library 180 | 181 | printf "Installing for Python 2..\n" 182 | apt_pkg_install "${PY2_DEPS[@]}" 183 | if $UNSTABLE; then 184 | python setup.py install > /dev/null 185 | else 186 | pip install --upgrade $LIBRARY_NAME 187 | fi 188 | if [ $? -eq 0 ]; then 189 | success "Done!\n" 190 | echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER 191 | fi 192 | 193 | if [ -f "/usr/bin/python3" ]; then 194 | printf "Installing for Python 3..\n" 195 | apt_pkg_install "${PY3_DEPS[@]}" 196 | if $UNSTABLE; then 197 | python3 setup.py install > /dev/null 198 | else 199 | pip3 install --upgrade $LIBRARY_NAME 200 | fi 201 | if [ $? -eq 0 ]; then 202 | success "Done!\n" 203 | echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER 204 | fi 205 | fi 206 | 207 | cd $WD 208 | 209 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 210 | CMD="${SETUP_CMDS[$i]}" 211 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 212 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 213 | do_config_backup 214 | fi 215 | eval $CMD 216 | done 217 | 218 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 219 | CONFIG_LINE="${CONFIG_TXT[$i]}" 220 | if ! [ "$CONFIG_LINE" == "" ]; then 221 | do_config_backup 222 | inform "Adding $CONFIG_LINE to $CONFIG\n" 223 | sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 224 | if ! grep -q "^$CONFIG_LINE" $CONFIG; then 225 | printf "$CONFIG_LINE\n" >> $CONFIG 226 | fi 227 | fi 228 | done 229 | 230 | if [ -d "examples" ]; then 231 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 232 | inform "Copying examples to $RESOURCES_DIR" 233 | cp -r examples/ $RESOURCES_DIR 234 | echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER 235 | success "Done!" 236 | fi 237 | fi 238 | 239 | printf "\n" 240 | 241 | if [ -f "/usr/bin/pydoc" ]; then 242 | printf "Generating documentation.\n" 243 | pydoc -w $LIBRARY_NAME > /dev/null 244 | if [ -f "$LIBRARY_NAME.html" ]; then 245 | cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html 246 | rm -f $LIBRARY_NAME.html 247 | inform "Documentation saved to $RESOURCES_DIR/docs.html" 248 | success "Done!" 249 | else 250 | warning "Error: Failed to generate documentation." 251 | fi 252 | fi 253 | 254 | success "\nAll done!" 255 | inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" 256 | inform "Find uninstall steps in $UNINSTALLER\n" 257 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CONFIG=/boot/config.txt 3 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 4 | CONFIG_BACKUP=false 5 | APT_HAS_UPDATED=false 6 | USER_HOME=/home/$SUDO_USER 7 | RESOURCES_TOP_DIR=$USER_HOME/Pimoroni 8 | WD=`pwd` 9 | USAGE="sudo ./install.sh (--unstable)" 10 | POSITIONAL_ARGS=() 11 | FORCE=false 12 | UNSTABLE=false 13 | PYTHON="/usr/bin/python3" 14 | 15 | 16 | user_check() { 17 | if [ $(id -u) -ne 0 ]; then 18 | printf "Script must be run as root. Try 'sudo ./install.sh'\n" 19 | exit 1 20 | fi 21 | } 22 | 23 | confirm() { 24 | if $FORCE; then 25 | true 26 | else 27 | read -r -p "$1 [y/N] " response < /dev/tty 28 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 29 | true 30 | else 31 | false 32 | fi 33 | fi 34 | } 35 | 36 | prompt() { 37 | read -r -p "$1 [y/N] " response < /dev/tty 38 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 39 | true 40 | else 41 | false 42 | fi 43 | } 44 | 45 | success() { 46 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 47 | } 48 | 49 | inform() { 50 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 51 | } 52 | 53 | warning() { 54 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 55 | } 56 | 57 | function do_config_backup { 58 | if [ ! $CONFIG_BACKUP == true ]; then 59 | CONFIG_BACKUP=true 60 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 61 | inform "Backing up $CONFIG to /boot/$FILENAME\n" 62 | cp $CONFIG /boot/$FILENAME 63 | mkdir -p $RESOURCES_TOP_DIR/config-backups/ 64 | cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME 65 | if [ -f "$UNINSTALLER" ]; then 66 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER 67 | fi 68 | fi 69 | } 70 | 71 | function apt_pkg_install { 72 | PACKAGES=() 73 | PACKAGES_IN=("$@") 74 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 75 | PACKAGE="${PACKAGES_IN[$i]}" 76 | if [ "$PACKAGE" == "" ]; then continue; fi 77 | printf "Checking for $PACKAGE\n" 78 | dpkg -L $PACKAGE > /dev/null 2>&1 79 | if [ "$?" == "1" ]; then 80 | PACKAGES+=("$PACKAGE") 81 | fi 82 | done 83 | PACKAGES="${PACKAGES[@]}" 84 | if ! [ "$PACKAGES" == "" ]; then 85 | echo "Installing missing packages: $PACKAGES" 86 | if [ ! $APT_HAS_UPDATED ]; then 87 | apt update 88 | APT_HAS_UPDATED=true 89 | fi 90 | apt install -y $PACKAGES 91 | if [ -f "$UNINSTALLER" ]; then 92 | echo "apt uninstall -y $PACKAGES" 93 | fi 94 | fi 95 | } 96 | 97 | while [[ $# -gt 0 ]]; do 98 | K="$1" 99 | case $K in 100 | -u|--unstable) 101 | UNSTABLE=true 102 | shift 103 | ;; 104 | -f|--force) 105 | FORCE=true 106 | shift 107 | ;; 108 | -p|--python) 109 | PYTHON=$2 110 | shift 111 | shift 112 | ;; 113 | *) 114 | if [[ $1 == -* ]]; then 115 | printf "Unrecognised option: $1\n"; 116 | printf "Usage: $USAGE\n"; 117 | exit 1 118 | fi 119 | POSITIONAL_ARGS+=("$1") 120 | shift 121 | esac 122 | done 123 | 124 | user_check 125 | 126 | if [ ! -f "$PYTHON" ]; then 127 | printf "Python path $PYTHON not found!\n" 128 | exit 1 129 | fi 130 | 131 | PYTHON_VER=`$PYTHON --version` 132 | 133 | inform "Installing. Please wait..." 134 | 135 | $PYTHON -m pip install --upgrade configparser 136 | 137 | CONFIG_VARS=`$PYTHON - < $UNINSTALLER 174 | printf "It's recommended you run these steps manually.\n" 175 | printf "If you want to run the full script, open it in\n" 176 | printf "an editor and remove 'exit 1' from below.\n" 177 | exit 1 178 | EOF 179 | 180 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" 181 | 182 | if $UNSTABLE; then 183 | warning "Installing unstable library from source.\n\n" 184 | else 185 | printf "Installing stable library from pypi.\n\n" 186 | fi 187 | 188 | cd library 189 | 190 | printf "Installing for $PYTHON_VER...\n" 191 | apt_pkg_install "${PY3_DEPS[@]}" 192 | if $UNSTABLE; then 193 | $PYTHON setup.py install > /dev/null 194 | else 195 | $PYTHON -m pip install --upgrade $LIBRARY_NAME 196 | fi 197 | if [ $? -eq 0 ]; then 198 | success "Done!\n" 199 | echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER 200 | fi 201 | 202 | cd $WD 203 | 204 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 205 | CMD="${SETUP_CMDS[$i]}" 206 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 207 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 208 | do_config_backup 209 | fi 210 | eval $CMD 211 | done 212 | 213 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 214 | CONFIG_LINE="${CONFIG_TXT[$i]}" 215 | if ! [ "$CONFIG_LINE" == "" ]; then 216 | do_config_backup 217 | inform "Adding $CONFIG_LINE to $CONFIG\n" 218 | sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 219 | if ! grep -q "^$CONFIG_LINE" $CONFIG; then 220 | printf "$CONFIG_LINE\n" >> $CONFIG 221 | fi 222 | fi 223 | done 224 | 225 | if [ -d "examples" ]; then 226 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 227 | inform "Copying examples to $RESOURCES_DIR" 228 | cp -r examples/ $RESOURCES_DIR 229 | echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER 230 | success "Done!" 231 | fi 232 | fi 233 | 234 | printf "\n" 235 | 236 | if [ -f "/usr/bin/pydoc" ]; then 237 | printf "Generating documentation.\n" 238 | pydoc -w $LIBRARY_NAME > /dev/null 239 | if [ -f "$LIBRARY_NAME.html" ]; then 240 | cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html 241 | rm -f $LIBRARY_NAME.html 242 | inform "Documentation saved to $RESOURCES_DIR/docs.html" 243 | success "Done!" 244 | else 245 | warning "Error: Failed to generate documentation." 246 | fi 247 | fi 248 | 249 | success "\nAll done!" 250 | inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" 251 | inform "Find uninstall steps in $UNINSTALLER\n" 252 | -------------------------------------------------------------------------------- /library/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = rv3028 3 | omit = 4 | .tox/* 5 | -------------------------------------------------------------------------------- /library/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.0.5 2 | ----- 3 | 4 | * Bugfix: corrected BCD month to include Oct, Nov, Dec 5 | 6 | 0.0.4 7 | ----- 8 | 9 | * Port to i2cdevice>=0.0.6 set/get API 10 | * Cleaned up i2cdevice bindings 11 | * Corrected 12-hour and am/pm bits in bindings 12 | 13 | 0.0.3 14 | ----- 15 | 16 | * Bugfix to support days of month in the 20s 17 | 18 | 0.0.2 19 | ----- 20 | 21 | * Major bugfix to support hours past 8PM 22 | * Other minor fixes and improvements 23 | 24 | 0.0.1 25 | ----- 26 | 27 | * Initial Release 28 | -------------------------------------------------------------------------------- /library/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /library/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.txt 2 | include LICENSE.txt 3 | include README.md 4 | include setup.py 5 | recursive-include rv3028 *.py 6 | -------------------------------------------------------------------------------- /library/README.md: -------------------------------------------------------------------------------- 1 | # RV3028 Real Time Clock Breakout 2 | 3 | [![Build Status](https://shields.io/github/workflow/status/pimoroni/rv3028-python/Python%20Tests.svg)](https://github.com/pimoroni/rv3028-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/rv3028-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/rv3028-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/rv3028.svg)](https://pypi.python.org/pypi/rv3028) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/rv3028.svg)](https://pypi.python.org/pypi/rv3028) 7 | 8 | # Note 9 | 10 | This is just a Python library to interface with the RV3028 and does not install the RTC as a time source for Raspbian. 11 | 12 | In order to set up as a RTC in Raspbian you will need to add something like the following to `/boot/config.txt`: 13 | 14 | ``` 15 | dtoverlay=i2c-rtc,rv3028,backup-switchover-mode=1 16 | ``` 17 | 18 | More information is available in the `i2c-rtc` dtoverlay documentation: https://github.com/raspberrypi/linux/blob/0d72d83ec92acda1e8cbad0d4213a5ec2b3f2e1b/arch/arm/boot/dts/overlays/README#L1079 19 | 20 | # Installing 21 | 22 | Stable library from PyPi: 23 | 24 | * Just run `python3 -m pip install rv3028` 25 | 26 | Latest/development library from GitHub: 27 | 28 | * `git clone https://github.com/pimoroni/rv3028-python` 29 | * `cd rv3028-python` 30 | * `sudo ./install.sh --unstable` 31 | 32 | # Requirements 33 | 34 | This library depends upon smbus: 35 | 36 | ``` 37 | sudo apt install python-smbus # Python 2 38 | sudo apt install python3-smbus # Python 3 39 | ``` 40 | 41 | 42 | # Changelog 43 | 0.0.5 44 | ----- 45 | 46 | * Bugfix: corrected BCD month to include Oct, Nov, Dec 47 | 48 | 0.0.4 49 | ----- 50 | 51 | * Port to i2cdevice>=0.0.6 set/get API 52 | * Cleaned up i2cdevice bindings 53 | * Corrected 12-hour and am/pm bits in bindings 54 | 55 | 0.0.3 56 | ----- 57 | 58 | * Bugfix to support days of month in the 20s 59 | 60 | 0.0.2 61 | ----- 62 | 63 | * Major bugfix to support hours past 8PM 64 | * Other minor fixes and improvements 65 | 66 | 0.0.1 67 | ----- 68 | 69 | * Initial Release 70 | -------------------------------------------------------------------------------- /library/rv3028/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | from i2cdevice import Device, Register, BitField 3 | from i2cdevice.adapter import Adapter, LookupAdapter, U16ByteSwapAdapter 4 | import datetime 5 | 6 | __version__ = '0.0.5' 7 | 8 | 9 | class BCDAdapter(Adapter): 10 | 11 | def _decode(self, value): 12 | upper = ((value & 0xF0) >> 4) * 10 13 | lower = (value & 0x0F) 14 | 15 | return upper + lower 16 | 17 | def _encode(self, value): 18 | upper = (int(value / 10)) << 4 19 | lower = value % 10 20 | 21 | return upper | lower 22 | 23 | 24 | class ReverseBytesAdapter(Adapter): 25 | def __init__(self, number_of_bytes): 26 | self._number_of_bytes = number_of_bytes 27 | 28 | def _decode(self, value): 29 | result = 0 30 | for x in range(self._number_of_bytes): 31 | result <<= 8 32 | result |= value & 0xff 33 | value >>= 8 34 | return result 35 | 36 | def _encode(self, value): 37 | result = 0 38 | for x in range(self._number_of_bytes): 39 | result <<= 8 40 | result |= value & 0xff 41 | value >>= 8 42 | return result 43 | 44 | 45 | class RV3028: 46 | def __init__(self, i2c_addr=0x26, i2c_dev=None): 47 | self._i2c_addr = i2c_addr 48 | self._i2c_dev = i2c_dev 49 | self._is_setup = False 50 | 51 | self._rv3028 = Device([0x52], i2c_dev=self._i2c_dev, bit_width=8, registers=( 52 | Register('SECONDS', 0x00, fields=( 53 | BitField('seconds', 0x7F, adapter=BCDAdapter()), 54 | )), 55 | Register('MINUTES', 0x01, fields=( 56 | BitField('minutes', 0x7F, adapter=BCDAdapter()), 57 | )), 58 | Register('HOURS', 0x02, fields=( 59 | BitField('t24hours', 0b00111111, adapter=BCDAdapter()), 60 | BitField('t12hours', 0b00011111, adapter=BCDAdapter()), 61 | BitField('am_pm', 0b00100000), 62 | )), 63 | Register('WEEKDAY', 0x03, fields=( 64 | BitField('weekday', 0b00000111), 65 | )), 66 | Register('DATE', 0x04, fields=( 67 | BitField('date', 0b00111111, adapter=BCDAdapter()), 68 | )), 69 | Register('MONTH', 0x05, fields=( 70 | BitField('month', 0b00011111, adapter=BCDAdapter()), 71 | )), 72 | Register('YEAR', 0x06, fields=( 73 | BitField('year', 0xFF, adapter=BCDAdapter()), 74 | )), 75 | Register('ALARM_MINUTES', 0x07, fields=( 76 | BitField('minutes_alarm_enable', 0b10000000), 77 | BitField('minutes', 0x7F, adapter=BCDAdapter()), 78 | )), 79 | Register('ALARM_HOURS', 0x08, fields=( 80 | BitField('hours_alarm_enable', 0b10000000, adapter=BCDAdapter()), 81 | BitField('t24hours', 0b00111111, adapter=BCDAdapter()), 82 | BitField('t12hours', 0b00011111, adapter=BCDAdapter()), 83 | BitField('am_pm', 0b00100000), 84 | )), 85 | Register('ALARM_WEEKDAY', 0x09, fields=( 86 | BitField('weekday_alarm_enable', 0b10000000, adapter=BCDAdapter()), 87 | BitField('weekday', 0b00000111), 88 | BitField('date', 0b00111111) 89 | )), 90 | Register('TIMER_VALUE', 0x0A, fields=( 91 | BitField('value', 0xFF0F, adapter=U16ByteSwapAdapter()), 92 | ), bit_width=16), 93 | Register('TIMER_STATUS', 0x0C, fields=( 94 | BitField('value', 0xFF0F, adapter=U16ByteSwapAdapter()), 95 | ), bit_width=16), 96 | Register('STATUS', 0x0E, fields=( 97 | BitField('value', 0xFF), 98 | BitField('eeprom_busy_flag', 0b10000000), 99 | BitField('clock_output_interrupt_flag', 0b01000000), 100 | BitField('backup_switch_flag', 0b00100000), 101 | BitField('periodic_time_update_flag', 0b00010000), 102 | BitField('periodic_countdown_timer_flag', 0b00001000), 103 | BitField('alarm_flag', 0b00000100), 104 | BitField('external_event_flag', 0b00000010), 105 | BitField('power_on_reset_flag', 0b00000001), 106 | )), 107 | Register('CONTROL_1', 0x0F, fields=( 108 | BitField('value', 0xFF), 109 | BitField('timer_repeat', 0b10000000), 110 | BitField('weekday_date_alarm', 0b00100000), 111 | BitField('update_interrupt', 0b00010000), 112 | BitField('eeprom_memory_refresh_disable', 0b00001000), 113 | BitField('periodic_countdown_timer_enable', 0b00000100), 114 | BitField('timer_frequency_selection', 0b00000011, adapter=LookupAdapter({ 115 | '4036Hz': 0b00, # 214.14us 116 | '63Hz': 0b01, # 15.625ms 117 | '1Hz': 0b10, # 1s 118 | '0.016Hz': 0b11 # 60s 119 | })), 120 | )), 121 | Register('CONTROL_2', 0x10, fields=( 122 | BitField('value', 0xFF), 123 | BitField('timestamp_enable', 0b10000000), 124 | BitField('interrupt_controlled_output_enable', 0b01000000), 125 | BitField('periodic_time_update_interupt_enable', 0b00100000), 126 | BitField('periodic_countdown_timer_interupt_enable', 0b00010000), 127 | BitField('alarm_interupt_enable', 0b00001000), 128 | BitField('external_event_interrupt_enable', 0b00000100), 129 | BitField('select_24_12_hours', 0b00000010), 130 | BitField('reset', 0b00000001), 131 | )), 132 | Register('GENERAL_PURPOSE_STORAGE_REGISTER', 0x11, fields=( 133 | BitField('value', 0b01111111), 134 | )), 135 | Register('CLOCK_INTERRUPT_MASK', 0x12, fields=( 136 | BitField('value', 0x0F), 137 | BitField('clock_output_when_event_interrupt', 0b00001000), 138 | BitField('clock_output_when_alarm_interrupt', 0b00000100), 139 | BitField('clock_output_when_countdown_interrupt', 0b00000010), 140 | BitField('clock_output_when_periodic_interrupt', 0b00000001), 141 | )), 142 | Register('EVENT_CONTROL', 0x13, fields=( 143 | BitField('value', 0xFF), 144 | BitField('event_high_low_detection', 0b01000000), 145 | BitField('event_filtering_time', 0b00110000, adapter=LookupAdapter({ 146 | 'no_filtering': 0b00, 147 | '3.9ms': 0b01, 148 | '15.6ms': 0b10, 149 | '125ms': 0b11 150 | })), 151 | BitField('timestamp_reset', 0b00000100), 152 | BitField('timestamp_overwrite', 0b00000010), 153 | BitField('timestamp_source', 0b00000001), 154 | )), 155 | Register('TIMESTAMP_COUNT', 0x14, fields=( 156 | BitField('value', 0xFF), 157 | )), 158 | Register('TIMESTAMP_SECONDS', 0x15, fields=( 159 | BitField('seconds', 0x7F), 160 | )), 161 | Register('TIMESTAMP_MINUTES', 0x16, fields=( 162 | BitField('minutes', 0x7F), 163 | )), 164 | Register('TIMESTAMP_HOURS', 0x17, fields=( 165 | BitField('t24hours', 0b00111111), 166 | BitField('t12hours', 0b00001111), 167 | BitField('am_pm', 0x00010000), 168 | )), 169 | Register('TIMESTAMP_DATE', 0x18, fields=( 170 | BitField('date', 0b00011111), 171 | )), 172 | Register('TIMESTAMP_MONTH', 0x19, fields=( 173 | BitField('month', 0b00001111), 174 | )), 175 | Register('TIMESTAMP_YEAR', 0x1A, fields=( 176 | BitField('year', 0xFF), 177 | )), 178 | Register('UNIX_TIME', 0x1B, fields=( 179 | BitField('value', 0xFFFFFFFF, adapter=ReverseBytesAdapter(4)), 180 | ), bit_width=32), 181 | Register('USER_RAM', 0x1F, fields=( 182 | BitField('one', 0x00FF), 183 | BitField('two', 0xFF00), 184 | ), bit_width=16), 185 | Register('PASSWORD', 0x21, fields=( 186 | BitField('value', 0xFFFFFFFF), 187 | ), bit_width=32), 188 | Register('EEPROM_ADDRESS', 0x25, fields=( 189 | BitField('value', 0xFF), 190 | )), 191 | Register('EEPROM_DATA', 0x26, fields=( 192 | BitField('value', 0xFF), 193 | )), 194 | Register('EEPROM_COMMAND', 0x27, fields=( 195 | BitField('command', 0xFF, adapter=LookupAdapter({ 196 | 'first_command': 0x00, 197 | 'write_all_configuration_to_eeprom': 0x11, 198 | 'read_all_configuration_from_eeprom': 0x12, 199 | 'write_one_byte_to_eeprom_address': 0x21, 200 | 'read_one_byte_from_eeprom_address': 0x22 201 | })), 202 | )), 203 | Register('PART', 0x28, fields=( 204 | BitField('id', 0xF0), 205 | BitField('version', 0x0F) 206 | )), 207 | Register('EEPROM_PASSWORD_ENABLE', 0x30, fields=( 208 | BitField('value', 0xFF), # Write 0xFF to this register to enable EEPROM password 209 | )), 210 | Register('EEPROM_PASSWORD', 0x31, fields=( 211 | BitField('value', 0xFFFFFFFF), 212 | ), bit_width=32), 213 | Register('EEPROM_CLKOUT', 0x35, fields=( 214 | BitField('value', 0xFF), 215 | BitField('clkout_output', 0b10000000), 216 | BitField('clkout_synchronized', 0b01000000), 217 | BitField('power_on_reset_interrupt_enable', 0b00001000), 218 | BitField('clkout_frequency_selection', 0b00000111, adapter=LookupAdapter({ 219 | '32.768kHz': 0b000, 220 | '8192Hz': 0b001, 221 | '1024Hz': 0b010, 222 | '64Hz': 0b011, 223 | '32Hz': 0b100, 224 | '1Hz': 0b101, 225 | 'periodic_countdown_timer_interrupt': 0b110, 226 | 'clkout_low': 0b111, 227 | })), 228 | )), 229 | Register('EEPROM_OFFSET', 0x36, fields=( 230 | BitField('value', 0xFF), 231 | )), 232 | Register('EEPROM_BACKUP', 0x37, fields=( 233 | BitField('value', 0xFF), 234 | BitField('ee_offset', 0b10000000), 235 | BitField('backup_switchover_interrupt_enable', 0b01000000), 236 | BitField('trickle_charger_enable', 0b00100000), 237 | BitField('fast_edge_detection', 0b00010000), 238 | BitField('automatic_battery_switchover', 0b00001100, adapter=LookupAdapter({ 239 | 'switchover_disabled': 0b00, 240 | 'direct_switching_mode': 0b01, 241 | 'standby_mode': 0b10, 242 | 'level_switching_mode': 0b11 243 | })), 244 | BitField('trickle_charger_series_resistance', 0b00000011, adapter=LookupAdapter({ 245 | '1kOhm': 0b00, 246 | '3kOhm': 0b01, 247 | '6kOhm': 0b10, 248 | '11kOhm': 0b11 249 | })), 250 | )) 251 | 252 | 253 | )) 254 | self.enable_12hours = self._rv3028.get('CONTROL_2').select_24_12_hours 255 | self.alarm_frequency = { 256 | 'disabled_weekly': 0b0111, 257 | 'disabled_monthly': 0b1111, 258 | 'hourly_on_minute': 0b110, 259 | 'daily_on_hour': 0b101, 260 | 'daily_on_hour_and_minute': 0b011, 261 | 'weekly': 0b0011, 262 | 'weekly_on_minute': 0b0010, 263 | 'weekly_on_hour': 0b0001, 264 | 'weekly_on_hour_and_minute': 0b0000, 265 | 'monthly': 0b1011, 266 | 'monthly_on_minute': 0b1010, 267 | 'monthly_on_hour': 0b1001, 268 | 'monthly_on_hour_and_minute': 0b1000 269 | } 270 | 271 | def reset(self): 272 | self._rv3028.set('CONTROL_2', reset=True) 273 | time.sleep(0.01) 274 | 275 | def get_id(self): 276 | part = self._rv3028.get('PART') 277 | return part.id, part.version 278 | 279 | def get_time(self): 280 | datetime_object = self.get_time_and_date() 281 | return datetime_object.time() 282 | 283 | def set_time(self, t): 284 | if isinstance(t, datetime.datetime) or isinstance(t, datetime.time): 285 | self._rv3028.set('HOURS', t24hours=t.hour) 286 | self._rv3028.set('MINUTES', minutes=t.minute) 287 | self._rv3028.set('SECONDS', seconds=t.second) 288 | elif type(t) == tuple: 289 | self._rv3028.set('HOURS', t24hours=t[0]) 290 | self._rv3028.set('MINUTES', minutes=t[1]) 291 | self._rv3028.set('SECONDS', seconds=t[2]) 292 | else: 293 | raise TypeError('Time needs to be given as datetime.datetime object, or tuple (hour, minute, seconds) type used: {0}'.format(type(t))) 294 | 295 | def get_date(self): 296 | datetime_object = self.get_time_and_date() 297 | return datetime_object.date() 298 | 299 | def set_date(self, date): 300 | if isinstance(date, datetime.datetime) or isinstance(date, datetime.date): 301 | self._rv3028.set('YEAR', year=date.year - 2000) 302 | self._rv3028.set('MONTH', month=date.month) 303 | self._rv3028.set('DATE', date=date.day) 304 | elif type(date) == tuple: 305 | self._rv3028.set('YEAR', year=date[0] - 2000) 306 | self._rv3028.set('MONTH', month=date[1]) 307 | self._rv3028.set('DATE', date=date[2]) 308 | else: 309 | raise TypeError('Date needs to be given as datetime.datetime object, datetime.date object, or tuple (year, month, day) type used: {0}'.format(type(date))) 310 | 311 | def set_time_and_date(self, time_and_date): 312 | if isinstance(time_and_date, datetime.datetime): 313 | self.set_date(time_and_date) 314 | self.set_time(time_and_date) 315 | 316 | elif type(time_and_date) == tuple: 317 | self.set_date(time_and_date[:3]) 318 | self.set_time(time_and_date[3:]) 319 | 320 | else: 321 | raise TypeError('Time needs to be given as datetime.datetime object, or tuple (year, month, day, hour, minute, seconds) type used: {0}'.format(type(time_and_date))) 322 | 323 | def get_time_and_date(self): 324 | return datetime.datetime( 325 | self._rv3028.get('YEAR').year + 2000, 326 | self._rv3028.get('MONTH').month, 327 | self._rv3028.get('DATE').date, 328 | self._rv3028.get('HOURS').t24hours, 329 | self._rv3028.get('MINUTES').minutes, 330 | self._rv3028.get('SECONDS').seconds) 331 | 332 | def get_unix_time(self): 333 | return self._rv3028.get('UNIX_TIME').value 334 | 335 | def set_unix_time(self, value): 336 | self._rv3028.set('UNIX_TIME', value=value) 337 | 338 | def set_battery_switchover(self, value): 339 | self._rv3028.set('EEPROM_BACKUP', automatic_battery_switchover=value) 340 | 341 | def get_battery_switchover(self): 342 | return self._rv3028.get('EEPROM_BACKUP').automatic_battery_switchover 343 | 344 | def start_periodic_timer(self): 345 | self._rv3028.set('CONTROL_1', periodic_countdown_timer_enable=True) 346 | 347 | def stop_periodic_timer(self): 348 | self._rv3028.set('CONTROL_1', periodic_countdown_timer_enable=False) 349 | 350 | def get_periodic_timer_frequency(self): 351 | return self._rv3028.get('CONTROL_1').timer_frequency_selection 352 | 353 | def set_periodic_timer_frequency(self, value): 354 | self._rv3028.set('CONTROL_1', timer_frequency_selection=value) 355 | 356 | def set_periodic_timer_countdown_value(self, value): 357 | self._rv3028.set('TIMER_VALUE', value=value) 358 | 359 | def get_periodic_timer_countdown_value(self): 360 | return self._rv3028.get('TIMER_VALUE').value 361 | 362 | def get_periodic_timer_countdown_status(self): 363 | return self._rv3028.get('TIMER_STATUS').value 364 | 365 | def clear_all_interrupts(self): 366 | self._rv3028.set('STATUS', value=0) 367 | 368 | def clear_periodic_countdown_timer_interrupt(self): 369 | self._rv3028.set('STATUS', periodic_countdown_timer_flag=0) 370 | 371 | def clear_alarm_interrupt(self): 372 | self._rv3028.set('STATUS', alarm_flag=0) 373 | 374 | def get_all_interrupts(self): 375 | return self._rv3028.get('STATUS').value 376 | 377 | def get_periodic_countdown_timer_interrupt(self): 378 | return self._rv3028.get('STATUS').periodic_countdown_timer_flag 379 | 380 | def get_alarm_interrupt(self): 381 | return self._rv3028.get('STATUS').alarm_flag 382 | 383 | def wait_for_periodic_timer_interrupt(self, value): 384 | """Wait for a periodic timer countdown. 385 | 386 | The countdown period in seconds is equal to the value/timer frequency. 387 | 388 | """ 389 | self.stop_periodic_timer() 390 | self._rv3028.set('TIMER_VALUE', value=value) 391 | self._rv3028.set('STATUS', periodic_countdown_timer_flag=False) 392 | self.start_periodic_timer() 393 | while not self._rv3028.get('STATUS').periodic_countdown_timer_flag: 394 | time.sleep(0.001) 395 | 396 | def get_alarm_setting(self): 397 | setting = self._rv3028.get('ALARM_MINUTES').minutes_alarm_enable 398 | setting |= (self._rv3028.get('ALARM_HOURS').hours_alarm_enable << 1) 399 | setting |= (self._rv3028.get('ALARM_WEEKDAY').weekday_alarm_enable << 2) 400 | setting |= (self._rv3028.get('CONTROL_1').weekday_date_alarm << 3) 401 | return_value = [key for (key, value) in self.alarm_frequency.items() if value == setting] 402 | return return_value 403 | 404 | def set_alarm_setting(self, setting): 405 | self._rv3028.set('ALARM_MINUTES', minutes_alarm_enable=self.alarm_frequency.get(setting) & 0b0001) 406 | self._rv3028.set('ALARM_HOURS', hours_alarm_enable=(self.alarm_frequency.get(setting) & 0b0010) >> 1) 407 | self._rv3028.set('ALARM_WEEKDAY', weekday_alarm_enable=(self.alarm_frequency.get(setting) & 0b0100) >> 2) 408 | self._rv3028.set('CONTROL_1', weekday_date_alarm=(self.alarm_frequency.get(setting) & 0b1000) >> 3) 409 | 410 | def set_alarm_time(self, datetime_object, weekday=0): 411 | if weekday == 0: 412 | if isinstance(datetime_object, datetime.datetime): 413 | self._rv3028.set('ALARM_WEEKDAY', date=datetime_object.day) 414 | self._rv3028.set('ALARM_HOURS', t24hours=datetime_object.hour) 415 | self._rv3028.set('ALARM_MINUTES', minutes=datetime_object.minute) 416 | 417 | elif type(datetime_object) == tuple: 418 | self._rv3028.set('ALARM_WEEKDAY', date=datetime_object[0]) 419 | self._rv3028.set('ALARM_HOURS', t24hours=datetime_object[1]) 420 | self._rv3028.set('ALARM_MINUTES', minutes=datetime_object[2]) 421 | 422 | else: 423 | raise TypeError('Time needs to be given as datetime.datetime object or tuple (hour, minute, date) type used: {0}'.format(type(time))) 424 | else: 425 | if isinstance(datetime_object, datetime.datetime): 426 | self._rv3028.set('ALARM_WEEKDAY', weekday=weekday) 427 | self._rv3028.set('ALARM_HOURS', t24hours=datetime_object.hour) 428 | self._rv3028.set('ALARM_MINUTES', minutes=datetime_object.minute) 429 | 430 | elif type(datetime_object) == tuple: 431 | self._rv3028.set('ALARM_WEEKDAY', weekday=weekday) 432 | self._rv3028.set('ALARM_HOURS', t24hours=datetime_object[0]) 433 | self._rv3028.set('ALARM_MINUTES', minutes=datetime_object[1]) 434 | else: 435 | raise TypeError('Time needs to be given as datetime.datetime object or tuple (hour, minute) and a 0 > weekday int type used: {0}'.format(type(time))) 436 | 437 | def get_alarm_time(self): 438 | return datetime.time( 439 | self._rv3028.get('ALARM_HOURS').t24hours, 440 | self._rv3028.get('ALARM_MINUTES').minutes, 441 | self._rv3028.get('ALARM_WEEKDAY').weekday) 442 | 443 | 444 | if __name__ == "__main__": 445 | 446 | import smbus 447 | bus = smbus.SMBus(1) 448 | rtc = RV3028(i2c_dev=bus) 449 | print('Part ID: {0[0]} Revision: {0[1]}'.format(rtc.get_id())) 450 | -------------------------------------------------------------------------------- /library/setup.cfg: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | [metadata] 3 | name = rv3028 4 | version = 0.0.5 5 | author = Gee Bartlett 6 | author_email = gee@pimoroni.com 7 | description = Python library for the RV3028 Real Time Clock 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | keywords = Raspberry Pi 11 | url = https://www.pimoroni.com 12 | project_urls = 13 | GitHub=https://www.github.com/pimoroni/rv3028-python 14 | license = MIT 15 | # This includes the license file(s) in the wheel. 16 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 17 | license_files = LICENSE.txt 18 | classifiers = 19 | Development Status :: 4 - Beta 20 | Operating System :: POSIX :: Linux 21 | License :: OSI Approved :: MIT License 22 | Intended Audience :: Developers 23 | Programming Language :: Python :: 2.7 24 | Programming Language :: Python :: 3 25 | Topic :: Software Development 26 | Topic :: Software Development :: Libraries 27 | Topic :: System :: Hardware 28 | 29 | [options] 30 | python_requires = >= 2.7 31 | packages = rv3028 32 | install_requires = 33 | i2cdevice >= 0.0.6 34 | 35 | [flake8] 36 | exclude = 37 | .tox, 38 | .eggs, 39 | .git, 40 | __pycache__, 41 | build, 42 | dist 43 | ignore = 44 | E501 45 | 46 | [pimoroni] 47 | py2deps = 48 | py3deps = 49 | configtxt = 50 | commands = 51 | printf "Setting up i2c..\n" 52 | raspi-config nonint do_i2c 0 53 | -------------------------------------------------------------------------------- /library/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Copyright (c) 2016 Pimoroni 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from setuptools import setup, __version__ 26 | from pkg_resources import parse_version 27 | 28 | minimum_version = parse_version('30.4.0') 29 | 30 | if parse_version(__version__) < minimum_version: 31 | raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) 32 | 33 | setup() 34 | -------------------------------------------------------------------------------- /library/tests/test_alarm.py: -------------------------------------------------------------------------------- 1 | from i2cdevice import MockSMBus 2 | import datetime 3 | import pytest 4 | 5 | 6 | def test_alarm_hours_24(): 7 | import rv3028 8 | device = rv3028.RV3028(i2c_dev=MockSMBus(1, default_registers={ 9 | 0x08: 0b00100001, # 21 hours 10 | })) 11 | 12 | assert device.get_alarm_time().hour == 21 13 | 14 | 15 | def test_alarm_hours_12(): 16 | import rv3028 17 | device = rv3028.RV3028(i2c_dev=MockSMBus(1, default_registers={ 18 | 0x08: 0b00010001, # 11 hours 19 | })) 20 | 21 | assert device.get_alarm_time().hour == 11 22 | 23 | 24 | def test_set_alarm_time_tuple(): 25 | import rv3028 26 | device = rv3028.RV3028(i2c_dev=MockSMBus(1)) 27 | device.set_alarm_time((1, 12, 44)) 28 | device.set_alarm_time((12, 44), weekday=1) 29 | 30 | 31 | def test_set_alarm_time(): 32 | import rv3028 33 | device = rv3028.RV3028(i2c_dev=MockSMBus(1)) 34 | device.set_alarm_time(datetime.datetime(2020, 2, 29, 12, 44)) 35 | device.set_alarm_time(datetime.datetime(2020, 2, 29, 12, 44), weekday=1) 36 | 37 | 38 | def test_invalid_time_source(): 39 | import rv3028 40 | device = rv3028.RV3028(i2c_dev=MockSMBus(1)) 41 | with pytest.raises(TypeError): 42 | device.set_alarm_time(None) 43 | with pytest.raises(TypeError): 44 | device.set_alarm_time(None, weekday=1) 45 | -------------------------------------------------------------------------------- /library/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from i2cdevice import MockSMBus 3 | import mock 4 | 5 | 6 | def test_setup(): 7 | sys.modules['smbus'] = mock.Mock() 8 | sys.modules['smbus'].SMBus = MockSMBus 9 | import rv3028 10 | device = rv3028.RV3028() 11 | device.reset() 12 | device.get_id() 13 | device.set_time_and_date((2001, 1, 1, 1, 1, 1)) 14 | current = device.get_time_and_date() 15 | device.set_time_and_date(current) 16 | device.set_unix_time(0xFF) 17 | unix_time = device.get_unix_time() 18 | device.set_unix_time(unix_time) 19 | device.set_alarm_time((1, 1, 1, 1), 1) 20 | device.get_alarm_time() 21 | device.set_alarm_setting('disabled_monthly') 22 | device.get_alarm_setting() 23 | device.get_alarm_interrupt() 24 | device.clear_alarm_interrupt() 25 | for settings in device.alarm_frequency: 26 | device.set_alarm_setting(settings) 27 | device.get_alarm_setting() 28 | device.set_battery_switchover('level_switching_mode') 29 | device.get_battery_switchover() 30 | device.clear_all_interrupts() 31 | device.get_all_interrupts() 32 | device.stop_periodic_timer() 33 | device.set_periodic_timer_frequency('1Hz') 34 | device.get_periodic_timer_frequency() 35 | device.set_periodic_timer_countdown_value(0xFFFF) 36 | device.get_periodic_timer_countdown_value() 37 | device.get_periodic_timer_countdown_status() 38 | device.start_periodic_timer() 39 | device.clear_periodic_countdown_timer_interrupt() 40 | device.get_periodic_countdown_timer_interrupt() 41 | 42 | del device 43 | -------------------------------------------------------------------------------- /library/tests/test_time.py: -------------------------------------------------------------------------------- 1 | from i2cdevice import MockSMBus 2 | import datetime 3 | import pytest 4 | 5 | 6 | def test_time_and_date(): 7 | t = datetime.time(12, 39, 7) 8 | d = datetime.date(2020, 2, 29) 9 | smbus = MockSMBus(1) 10 | import rv3028 11 | device = rv3028.RV3028(i2c_dev=smbus) 12 | device.set_time(t) 13 | device.set_date(d) 14 | assert device.get_time() == t 15 | assert device.get_date() == d 16 | 17 | assert smbus.regs[0x02] == 0b00010010 18 | assert smbus.regs[0x05] == 0b00000010 19 | assert smbus.regs[0x06] == 0b00100000 # 2020, but we have a year offset of +2000 internally so the BCD value is just 20 20 | 21 | 22 | def test_invalid_time_source(): 23 | import rv3028 24 | device = rv3028.RV3028(i2c_dev=MockSMBus(1)) 25 | with pytest.raises(TypeError): 26 | device.set_time(None) 27 | 28 | 29 | def test_invalid_date_source(): 30 | import rv3028 31 | device = rv3028.RV3028(i2c_dev=MockSMBus(1)) 32 | with pytest.raises(TypeError): 33 | device.set_date(None) 34 | 35 | 36 | def test_invalid_datetime_source(): 37 | import rv3028 38 | device = rv3028.RV3028(i2c_dev=MockSMBus(1)) 39 | with pytest.raises(TypeError): 40 | device.set_time_and_date(None) 41 | -------------------------------------------------------------------------------- /library/tests/test_timer.py: -------------------------------------------------------------------------------- 1 | from i2cdevice import MockSMBus 2 | 3 | 4 | def test_timer_value(): 5 | import rv3028 6 | device = rv3028.RV3028(i2c_dev=MockSMBus(1, default_registers={ 7 | 0x0a: 0xff, # Bits 0-7 of TIMER_VALUE, 8 | 0x0b: 0x0f # Bits 11-8 of TIMER_VVALUE 9 | })) 10 | 11 | assert device.get_periodic_timer_countdown_value() == 0xfff 12 | 13 | 14 | def test_timer_status(): 15 | import rv3028 16 | device = rv3028.RV3028(i2c_dev=MockSMBus(1, default_registers={ 17 | 0x0c: 0xff, # Bits 0-7 of TIMER_STATUS, 18 | 0x0d: 0x0f # Bits 11-8 of TIMER_STATUS 19 | })) 20 | 21 | assert device.get_periodic_timer_countdown_status() == 0xfff 22 | -------------------------------------------------------------------------------- /library/tests/test_unix_time.py: -------------------------------------------------------------------------------- 1 | from i2cdevice import MockSMBus 2 | 3 | 4 | def test_unix_time_lsb(): 5 | import rv3028 6 | device = rv3028.RV3028(i2c_dev=MockSMBus(1, default_registers={ 7 | 0x1b: 0xff # Bits 0-7 of UNIX_TIME 8 | })) 9 | 10 | assert device.get_unix_time() == 0xff 11 | 12 | 13 | def test_unix_time_msb(): 14 | import rv3028 15 | device = rv3028.RV3028(i2c_dev=MockSMBus(1, default_registers={ 16 | 0x1b: 0x00, # Bits 0-7 of UNIX_TIME 17 | 0x1e: 0xff # Bits 24-31 of UNIX_TIME 18 | })) 19 | 20 | assert device.get_unix_time() == 0xff000000 21 | -------------------------------------------------------------------------------- /library/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,35,37,39},qa 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | commands = 7 | python setup.py install 8 | coverage run -m py.test -v -r wsx 9 | coverage report 10 | deps = 11 | mock 12 | pytest>=3.1 13 | pytest-cov 14 | 15 | [testenv:qa] 16 | commands = 17 | check-manifest --ignore tox.ini,tests*,.coveragerc 18 | python setup.py check -m -r -s 19 | flake8 --ignore E501 20 | rstcheck README.rst 21 | deps = 22 | check-manifest 23 | flake8 24 | rstcheck 25 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'` 4 | LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'` 5 | 6 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" 7 | 8 | if [ $(id -u) -ne 0 ]; then 9 | printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" 10 | exit 1 11 | fi 12 | 13 | cd library 14 | 15 | printf "Unnstalling for Python 2..\n" 16 | pip uninstall $LIBRARY_NAME 17 | 18 | if [ -f "/usr/bin/pip3" ]; then 19 | printf "Uninstalling for Python 3..\n" 20 | pip3 uninstall $LIBRARY_NAME 21 | fi 22 | 23 | cd .. 24 | 25 | printf "Done!\n" 26 | --------------------------------------------------------------------------------