Skip to content

Commit ed5d9db

Browse files
committed
Initial commit
1 parent ad7bebc commit ed5d9db

File tree

3 files changed

+197
-1
lines changed

3 files changed

+197
-1
lines changed

README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
# HamTrack
2-
Hamster tracking with a Raspberry Pi
2+
`HamTrack` is a tracking software that tracks your Hamster's nightly running activity. It measures revolutions of your Hamster's wheel using a reed switch connected to a GPIO pin. After a session is complete, it writes the data into a MySQL database and sends a Firebase Cloud message
3+
4+
# Requirements
5+
`HamTrack` requires the python modules `rpi.gpio`, `pyfcm`, `peewee`, and `mysqlclient`
6+
7+
pip install rpi.gpio pyfcm peewee mysqlclient
8+
9+
# Getting started
10+
Adjust the configuration, in particular your wheel's circumference, mysql connection parameters, etc., and comment out features you don't need, like the Firebase Cloud messaging.

hamtrack.py

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import os
2+
import time
3+
from datetime import datetime
4+
import logging
5+
from logging.config import fileConfig
6+
7+
import RPi.GPIO as GPIO
8+
9+
from peewee import *
10+
from playhouse.shortcuts import RetryOperationalError
11+
12+
from pyfcm import FCMNotification
13+
14+
# wheel circumference in cm
15+
# e.g., for 2*r = 28cm, d = 2*pi*r
16+
HAMSTER_WHEEL_CIRCUMFERENCE = 88
17+
# min time between revolutions in ms
18+
HAMSTER_DEBOUNCE = 250
19+
# session timeout in s
20+
HAMSTER_SESSION_TIMEOUT = 60
21+
22+
# GPIO init
23+
GPIO_CHANNEL = 10
24+
GPIO.setmode(GPIO.BCM)
25+
GPIO.setup(GPIO_CHANNEL, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
26+
27+
# FCM stuff
28+
push_service = FCMNotification(api_key="<FCM API KEY>")
29+
30+
# setup logging
31+
logging.config.fileConfig(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'logging_config.ini'))
32+
logger = logging.getLogger(__name__)
33+
34+
# SQL stuff
35+
SQL_HOST = '<SQL_HOST>'
36+
SQL_DB = '<SQL_DB>'
37+
SQL_USER = '<SQL_USER>'
38+
SQL_PASSWORD = '<SQL_PASSWORD>'
39+
mysql_db = MySQLDatabase(SQL_DB, host=SQL_HOST, user=SQL_USER, passwd=SQL_PASSWORD)
40+
41+
# Fallback file - written when sql fails
42+
FALLBACK_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fallback.log')
43+
44+
# SQL table structure
45+
class BaseModel(Model):
46+
"""A base model that will use our MySQL database"""
47+
class Meta:
48+
database = mysql_db
49+
50+
class Hamstersession(BaseModel):
51+
start = DateTimeField(unique=True, index=True)
52+
circumference = FloatField()
53+
duration = FloatField()
54+
distance = FloatField()
55+
56+
# Create table if it doesn't exist
57+
if not Hamstersession.table_exists():
58+
Hamstersession.create_table()
59+
print('Table Hamstersession created')
60+
61+
62+
# no. of revolutions
63+
revolutions = 0
64+
# timestamp
65+
last_revo = 0
66+
session_start = 0
67+
68+
def post_notification(data_message):
69+
result = push_service.notify_topic_subscribers(
70+
topic_name = "news",
71+
data_message = data_message
72+
)
73+
74+
75+
def finish_session(session_end):
76+
wstart = datetime.fromtimestamp(session_start)
77+
wcircumference = HAMSTER_WHEEL_CIRCUMFERENCE
78+
wduration = session_end - session_start
79+
wdistance = revolutions * HAMSTER_WHEEL_CIRCUMFERENCE
80+
81+
logger.info('Session finished')
82+
logger.info('Start: {0}'.format(wstart))
83+
logger.info('Duration: {0}s'.format(wduration))
84+
logger.info('Distance: {0}m'.format(wdistance/100.0))
85+
logger.info('Revolutions: {0}'.format(revolutions))
86+
87+
data_message = {
88+
"event": "session_finished",
89+
"start": int(session_start*1000),
90+
"duration": "{0:.1f}".format(wduration/60.0),
91+
"distance": "{0:.1f}".format(wdistance/100.0),
92+
"revolutions": revolutions
93+
}
94+
post_notification(data_message=data_message)
95+
96+
try:
97+
#mysql_db.connect()
98+
mysql_db.get_conn().ping(True)
99+
hamstersession = Hamstersession.create(
100+
start = wstart,
101+
circumference = wcircumference,
102+
duration = wduration,
103+
distance = wdistance)
104+
hamstersession.save()
105+
mysql_db.close()
106+
except Exception as e:
107+
logger.error('MySQL error: {0}'.format(e))
108+
with open(FALLBACK_FILE, 'a') as fd:
109+
fd.write('start: {0}\n'.format(wstart))
110+
fd.write('circumference: {0}\n'.format(wcircumference))
111+
fd.write('duration: {0}\n'.format(wduration))
112+
fd.write('distance: {0}\n'.format(wdistance))
113+
114+
115+
116+
def start_session(session_start):
117+
data_message = {
118+
"event": "session_started",
119+
"start": int(session_start*1000)
120+
}
121+
post_notification(data_message=data_message)
122+
123+
124+
logger.info('HamTrack initialized')
125+
# main loop
126+
try:
127+
while 1:
128+
channel = GPIO.wait_for_edge(GPIO_CHANNEL,
129+
GPIO.RISING,
130+
timeout=HAMSTER_SESSION_TIMEOUT*1000)
131+
now = time.time()
132+
if channel is None:
133+
# Timeout: End session
134+
if revolutions >= 5:
135+
finish_session(now - HAMSTER_SESSION_TIMEOUT)
136+
if revolutions < 5 and session_start != 0:
137+
logger.info('Session aborted - no activity')
138+
revolutions = 0
139+
session_start = 0
140+
else:
141+
# Debounce
142+
if now - last_revo > HAMSTER_DEBOUNCE/1000.0:
143+
# Revolution detected
144+
logger.debug('Revolution detected. dt={0}'.format(now-last_revo))
145+
# Start new session if none is running
146+
last_revo = now
147+
if session_start == 0:
148+
session_start = now
149+
logger.info('Session started')
150+
else:
151+
revolutions += 1
152+
if revolutions == 5:
153+
start_session(session_start)
154+
155+
# except RuntimeError as e:
156+
# syslog.syslog(e)
157+
except KeyboardInterrupt:
158+
pass
159+
160+
GPIO.cleanup()

logging_config.ini

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[loggers]
2+
keys=root
3+
4+
[handlers]
5+
keys=stream_handler
6+
7+
[formatters]
8+
keys=formatter
9+
10+
[logger_root]
11+
level=DEBUG
12+
#level=INFO
13+
handlers=stream_handler
14+
15+
[handler_stream_handler]
16+
#class=StreamHandler
17+
#class=systemd.journal.JournalHandler
18+
class=logging.handlers.SysLogHandler
19+
level=DEBUG
20+
formatter=formatter
21+
#args=(sys.stderr,)
22+
#args=(DEBUG,)
23+
args=('/dev/log',)
24+
25+
[formatter_formatter]
26+
#format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s
27+
#format=%(name)-12s %(levelname)-8s %(message)s
28+
format=%(levelname)-8s %(message)s

0 commit comments

Comments
 (0)