Соединяем эллиптический тренажер и pygame

Привет, друзья! Случилось однажды так, что для восстановления после травмы я прикупил себе такой вот прибор.



Со своими прямыми обязанностями он справлялся вполне удовлетворительно, но было одно «но», и заключалось оно в том, что спидометр путался в показаниях, и следовательно, показывал разные результаты по пройденной дистанции. Если идти достаточно медленно, то спидометр вообще молчал. И решено было сделать свой спидометр с… ну вы поняли.


Как соединить тренажер и компьютер


Первое, с чего решено было начать — найти способ получать данные на компьютер. Промежуточным звеном решено было использовать плату Arduino.
Почему Arduino? Потому что под рукой нет ни чего другого подходящего.
При осмотре обнаружилось, что от тренажера к датчику идут два провода.


Чего вполне хватит, чтобы подключить его к пинам Arduino. Что и было сделано по вот такой вот схеме


На контакт A0 в зависимости от положения педалей, будет поступать сигнал разной величины.
В ходе экспериментов были перепробованы многие варианты подачи сигнала от микроконтроллера до компьютера, и в итоге остановился на таком варианте:
на компьютер безпрерывно подается символ «0», затем, когда сделан шаг на тренажере, подается«1». Следующий шаг — снова «0» и так по кругу.
Привожу sketch
<code class="cpp">int pin = A0; int ledPin = 13; int minSignal = 600; bool stateUp = false; bool lastState = false; bool oneStep = false; void setup() { 	pinMode(pin, INPUT); 	pinMode(ledPin, OUTPUT); 	Serial.begin(9600); } void loop() { 	int signal = analogRead(pin); 	if (signal > minSignal){ 		stateUp = true; 	} 	else{ 		stateUp = false; 	} 	if (lastState != stateUp && lastState == false){ 	       oneStep = not oneStep; 	} 	else { 	} 	lastState = stateUp; 	Serial.println(oneStep); 	digitalWrite(ledPin, oneStep); //индикатор } </code>



Игра


Что еще писать на pygame если не игру?

Идея

Эллиптический тренажер это имитация ходьбы на лыжах, поэтому это будет гонка лыжников. Каждый шаг, сделанный на тренажере делает персонаж в игре. Сперва хотелось сделать плавное передвижение\ускорение персонажа, но в итоге, решил отдать предпочтение точности.

Расчеты

Опытным путем было выяснено, что при «оптимальных» обстоятельствах один полный оборот равняется 4-м метрам. Это скорей всего не сколько проходит человек, а сколько прокручивается центральный диск. Просто примем это значение за аксиому.
На виртуальной трассе 1 метр равняется 1 пикселю. Т.е. каждый шаг перемещаем перснажа на 4 пикселя вперед.
Скорость будет высчитывать каждый шаг.
v = s /t
s = 4 м.
t — время одного шага.
*один шаг — полный оборот педалей.

Азарт

Да, будут графики и спидометр с таймером, но хочется духа соревнования.
А что, если соревноваться будешь не с кем-то а с самим собой, вчерашним? Сказано — сделано.


Сверху персонаж сегодняшний, снизу — вчерашний. Если быть более точным — персонаж прошлого забега но согласитесь, первый вариант звучит круче.

Технические детали



База данных

Естественно, раз нужно сохранять информацию о забегах, нужна БД. Я решил использовать mysql. В python использую библиотеку MySQLdb. В приложении за взаимодействие отвечает классDataManger.
Схема прилагается.


Пример кода
<code class="python">сlass DataManager:     def __init__(self):         self.time = time         self.currentTimeForLastRace = datetime.now()         self.currentTime = self.time.time()         self.speed = 0         self.db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="skirunner", charset='utf8')         self.cursor = self.db.cursor()         self.isGetLastRaceSpeeds = False         self.dataLastRace = []         self.lastRaceMinDate = datetime.now()         self.value = 0         self.lastValue = 0         self.impulse = 0         self.isRaceStart = False         self.currentRaceId = -1         self.currentDistanceId = -1         self.currentProfileId = -1      def getImpulse(self, value):         self.impulse = 0          if self.time.time() - self.currentTime > RESET_SPEED_TIME:             self.speed = 0         self.value = value         if self.value != self.lastValue:             time = self.time.time() - self.currentTime             self.impulse = POWER_IMPULSE             self.isRaceStart = True              self.speed = STEP / time # метры в секунду             self.currentTime = self.time.time()             self.lastValue = self.value          return  self.impulse      def getLastRaceDistanceAtCurrentTime(self, raceId,currentTime):         lastRaceDistance = 0         dateFormat = "%Y-%m-%d %H:%M:%S.%f"         if  not self.isGetLastRaceSpeeds:              sql = """SELECT min(date) FROM runLog WHERE race_id = %s""" % raceId             self.cursor.execute(sql)             data = self.cursor.fetchall()             for rec in data:                 self.lastRaceMinDate = datetime.strptime(rec[0],dateFormat)             sql = """SELECT distance,date FROM runLog WHERE race_id = %s ORDER BY date DESC""" % raceId             self.cursor.execute(sql)             self.dataLastRace = self.cursor.fetchall()             self.isGetLastRaceSpeeds = True          if self.isRaceStart:             time = datetime.now() - datetime.fromtimestamp(currentTime)             for rec in self.dataLastRace:                 distance, date = rec                 if time <= (datetime.strptime(date,dateFormat) - self.lastRaceMinDate):                     lastRaceDistance = distance         return  lastRaceDistance </code>



Графика

Как можно увидеть из скриншота выше, графика примитивная, но не няшность это тут главное. Для ее реализации была использована библиотека pygame. О работе с которой я уже писал.

Формы



Для форм использовал библиотеку PyQt.
Пример кода
<code class="python">class FormProfile(QMainWindow):      def __init__(self):         super(QMainWindow, self).__init__()         uic.loadUi('%s/ui/frm_profile.ui' % DIR, self)         self.cb_profile_load()         self.te_newProfile.hide()         self.bt_addProfile.hide()         self.bt_cancel.hide()         self.lb_add.hide()         self.move(QDesktopWidget().availableGeometry().center() - self.frameGeometry().center())          self.connect(self.bt_ok, SIGNAL("clicked()"), self.bt_ok_clicked)         self.connect(self.bt_new, SIGNAL("clicked()"), self.bt_new_clicked)         self.connect(self.bt_addProfile,SIGNAL("clicked()"), self.bt_addProfile_clicked)         self.connect(self.bt_cancel, SIGNAL("clicked()"), self.bt_cancel_clicked)         self.connect(self.bt_graph, SIGNAL("clicked()"), self.bt_graph_clicked)      def bt_ok_clicked(self):         self.profileId = self.cb_profile.itemData(self.cb_profile.currentIndex()).toString()         self.formDistance = FormDistance(self.profileId)         self.formDistance.show()         self.hide() </code>


Мне очень понравился процесс разработки окон. Не сложнее, чем в MS studio.
Формы создал в приложении Qt 4 Creator.
Импортировал их в код
<code class="python">uic.loadUi('%s/ui/frm_profile.ui' % DIR, self) </code>

Связал события и методы
<code class="python">        self.connect(self.bt_ok, SIGNAL("clicked()"), self.bt_ok_clicked)         self.connect(self.bt_new, SIGNAL("clicked()"), self.bt_new_clicked)         self.connect(self.bt_addProfile,SIGNAL("clicked()"), self.bt_addProfile_clicked)         self.connect(self.bt_cancel, SIGNAL("clicked()"), self.bt_cancel_clicked)         self.connect(self.bt_graph, SIGNAL("clicked()"), self.bt_graph_clicked) </code>

И отобразил
<code class="python">        self.formProfile = FormProfile()         self.formProfile.show() </code>


Графики



Для графиков используется библиотека matplotlib.
Тут тоже пример кода
<code class="python">import matplotlib.pyplot as plt     def bt_averageSpeed_clicked(self):         ...         plt.plot_date(dates, values,'b')         plt.plot_date(dates, values,'bo')         averageSpeed = len(values) > 0 and (lambda: sum(values) / len(values)) or (lambda: 0)         plt.xlabel(u"Средняя-средняя скорость= %.2f м/с или %.2f км/ч" % (float(averageSpeed()),float(averageSpeed()) / 1000 * 3600))         plt.ylabel(u"Средняя скорость (м/с)")         plt.title(u"График скоростей профиля %s" % dm.getProfileNameById(self.profileId))         plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%d/%m/%y'))         plt.gcf().autofmt_xdate()         plt.grid(True)         plt.show() </code>

Хотелось бы заметить, что для отображения кириллицы, нужно подключить поддерживающие шрифты.
<code class="python">from matplotlib import rc    font = {'family': 'Droid Sans',         'weight': 'normal',         'size': 14}         rc('font', **font) </code>



Чтение данных с arduino

Для этой цели использовал библиотеку serial.
Следующий код запускается в отдельном потоке.
<code class="python">def getDataFromSimulator():     global valueFromSimulator, isRunnig     ser = serial.Serial('/dev/ttyACM0', 9600)     while isRunnig:        value =  ser.readline()        try:            valueFromSimulator = int(value)        except:            pass </code>

Переменная valueFromSimulator в другом потоке используется только для считывания.
Запуск двух потоков.
<code class="python">t1 = threading.Thread(target=main,args = (self.profileId,self.distanceId)) t2 = threading.Thread(target=getDataFromSimulator) t2.start() t1.start() </code>

Видеодемонстрация плохого качества


Как и заказывали.


Буду рад замечаниям, критике и предложениям.
Все исходники тут

Источник: habrahabr.ru/post/217891/