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

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

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


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


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

    На контакт A0 в зависимости от положения педалей, будет поступать сигнал разной величины.
    В ходе экспериментов были перепробованы многие варианты подачи сигнала от микроконтроллера до компьютера, и в итоге остановился на таком варианте:
    на компьютер безпрерывно подается символ «0», затем, когда сделан шаг на тренажере, подается «1». Следующий шаг — снова «0» и так по кругу.
    Привожу sketch
    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); //индикатор
    }
    



    Игра


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

    Идея

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

    Расчеты

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

    Азарт

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

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

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



    База данных

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

    Пример кода
    с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
    



    Графика

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

    Формы


    Для форм использовал библиотеку PyQt.
    Пример кода
    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()
    


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

    Связал события и методы
            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)
    

    И отобразил
            self.formProfile = FormProfile()
            self.formProfile.show()
    


    Графики


    Для графиков используется библиотека matplotlib.
    Тут тоже пример кода
    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()
    

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



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

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

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

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


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


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

    Комментарии 9

      +4
      Отличный апгрейд для эллипсоида! Вот бы такую штуку в серию пустить, только с приложением для смартфона/планшета.
        +1
        О, прекрасно! А я только сегодня утром думал: как бы мне приладить к министепперу torneo динамомашину, чтобы заряжать смартфон?
          +3
          На заметку разработчикам:
          — Относительно простые тренажеры (стипперы, элептические, велотренажеры) имеют встроенные «компьютеры». Эти «компьютеры» получают данные от магнитных датчиков. Как работают эти датчики описано в статье — магнит, закреплённый на тренажере, подносится к датчику и тот даёт сигнал.
          Так вот, берёте OTG переходник (кабель), старый ненужный USB кабель и планшет (можно и магнитный датчик). Немного колдовства с паяльником — достаточно соединить два нужных контакта, как в статье, и планшет сможет получать сигналы от тренажёра.
          А дальше можно писать свою программу/игру, которая будет обрабатывать эти сигналы.

          Это между прочим не «освоенная целина» — не удивлюсь, если в скором времени появятся стартапы работающие в этом направлении.
            0
            я думаю давно пора делать тренажеры для игры во всякие MMORPG
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Там датчики скорее всего точно такие же. Так что приткнуть можно.
              –3
              image
              +
              image
              =
              image
                0
                Вы это к чему?
                  +2
                  На базе такого твистера можно сделать игру наподобие Ski Free, по аналогичной технологии

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое