Skocz do zawartości

Program do roweru stacjonarnego


roz

Pomocna odpowiedź

Hej.

W zeszłym roku (2024) rozpocząłem budowę komputera do roweru stacjonarnego. Rower złapałem na tzw. wystawce gabarytów. Nie było komputera, zasilacza. Był mechanizm od ustawiania obciążenia (obciążenie magnetyczne).

Obecnie chcę zrobić wersję trochę rozbudowaną tego komputera.

  • Płytka wykonana w JLCPCB;
  • ESP-WROOM-32 (LOLIN-32);
  • L9110 - sterowanie silnikiem od obciążenia (fabrycznie 16 poziomów obciążenia) + feedback z potencjometru (fabryczne rozwiązanie)
  • Kontaktron - obroty
  • LCD 4" ILI9486 SPI 320x480 color (biblioteka TFT_eSPI - bo z nią wyświetlacz działa bez problemu) + touch XPT2046 controller (dotyk rezystancyjny) + microSD;
  • Enkoder - do ustawień;
  • Opaska monitorująca pracę serca POLAR H7 BLE;
  • Buzzer;

Zasilanie całości 9V/2A z zasilacza wtyczkowego (tak było oryginalnie).
Rower to nieprodukowany już Kettler Golf C4. Poniżej na zdjęciu oryginalny komputer z tego roweru.
microSD na tę chwilę bez pomysłu.
Fajnie byłoby użyć obu rdzeni z ESP - pytanie czy jest sens?

Obawiam się, że polegnę, ale nie chcę się poddać.

Moje pomysły:

  • monitorowanie pracy serca + obliczanie spalonych kalorii + strefy tętna;
  • kilka programów "gotowców" ze zmiennym obciążeniem + ustawienie czasu treningu i w tym czasie rozkład obciążenia;
  • prędkość, czas jazdy (pedałowania), może dystans;
  • ręczne ustawienie obciążenia na sztywno;
  • Wifi na potrzeby ustawienia RTC z NTP;
  • użytkownicy, np. 4 i jeden "guest".

Chciałbym ten program napisać w sposób "porządny" czyli rozbicie na moduły, czyli wszystko co związane z monitorowaniem serca + kalorie w jednym pliku *.h + *.cpp; sterowanie silnikiem obciążenia + feedback drugi plik, itd. Sprawiało mi to problemy spore, gdy jakiś czas temu próbowałem - wierzę, że z Waszą pomocą dam radę.

W tym wątku będę pytał oraz w miarę możliwości wrzucał postępy..

Mile widziane pomysły już teraz.

 

image.thumb.png.670f1c2c8cfed011bee423695b9a53d0.pngimage.thumb.png.c9029fa171a366b1570e82300ace6ffa.pngimage.thumb.png.3357a2891fc8cc15656cdcf5662c2d0f.pngimage.thumb.png.efaf8a83f9a459378381ba221a92f619.pngimage.thumb.png.95de3d1e249cc287099231b28a83a19a.pngimage.thumb.png.88f39c62a64a1fab14b00d1e08dc2ee2.png

  • Lubię! 1
Link do komentarza
Share on other sites

(edytowany)

Wstępnie utworzyłem GUI.

Obszary z czerwoną ramką (ramki nie będzie w docelowym urządzeniu) są aktywne na dotyk - będzie możliwość zmiany parametru na potrzeby treningu - np. ustawić docelową ilość spalonych kalorii.

Dane są generowane na razie funkcją rand() na potrzeby testów.

Niestety Polar H7 ma uszkodzony pasek, więc na tę chwilę odpada jego użycie.

W tym tygodniu powinienem otrzymać czujnik (moduł) MAX30102 do monitorowania tętna oraz spO2.

Jeszcze sporo pracy, ale nie ukrywam - frajda jest! 🙂

image.thumb.png.2d6aaf2b9f250727e015f87bef7f3a1f.png

 

Edytowano przez roz
  • Lubię! 1
Link do komentarza
Share on other sites

Potrzebna pomoc.

W plikach hmi.h oraz hmi.cpp mam obsługę ekranu i dotyku na ekranie.

Potrzebuję z hmi.cpp po wywołaniu funkcji wysłać "coś" lub uruchomić funkcję w main.cpp, która spowoduje pauzę w programie.

O ile z main.cpp wiem jak wysłać, to w drugą stronę nie mam pojęcia.

Pomożecie?

  • Lubię! 1
Link do komentarza
Share on other sites

- zwykle wstawia się w programach statusy i je obsługuje w innym programie.

- statusy mogą być globalne a nie lokalne.

bool czekaj = false;
...
  
czekaj = true;


... 
if(czekaj)  
{
(cos zrób )
}  
  

 

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

@99teki

Tak. Gdy wszystko leży w main.cpp.

A teraz jest tak.

hmi.cpp zawiera funkcję, która po dotknięciu ekranu sprawdza, w którym miejscu ekran był dotknięty i ta funkcja ma wywołać funkcję w main.cpp.

Chociaż tak sobie myślę, że łatwiej chyba będzie napisać funkcję getPauseStatus() i sprawdzać w loop().

czyli: (najjaśniej jak potrafię)

hmi.cpp:
  
void checkTouch()
{
  if (coś tam)
  {
    pauza(); //funkcja w main.cpp
  }
}

main.cpp:
  
void setup()
{}

void loop()
{}

void pauza() //ciało funkcji w main.cpp
{
}

 

Link do komentarza
Share on other sites

Unikaj takich relacji w 2 strony. MAIN niech tworzy HMI i ewentualnie wywołuje jakieś jego metody ale HMI niech wprost nie odwołuje się do MAIN. Spróbuj
poczytać o callbackach i zrobić coś w stylu:

w main

main() {
    setHmiCallback([](const std::string& message) {
        if (message == "pauza") {
            std::cout << "Zatrzymano proces.\n";
            // Zrób coś
        }
    });
}


w hmi

std::function<void(const std::string&)> callback;

void setHmiCallback(std::function<void(const std::string&)> c) {
    callback = c;
}

if (coś) {
  callback("pauza");
}


To oczywiście zakłada, że to działa w 1 wątku. Tak naprawdę nie wiem skąd biorą się zdarzenia w HMI.

 

  • Lubię! 1
  • Pomogłeś! 1
Link do komentarza
Share on other sites

(edytowany)

Czy jestem w stanie jeszcze bardziej ustabilizować odczyt analogRead na ESP32?

Dodałem kondensator 100uF pomiędzy suwak potencjometru a GND.

Potencjometr 5kR; 3,3V na potencjometrze.

Czy już tylko średnia z tego?

[EDIT]: nie wiem czy muszę to stabilizować bardziej - to jest odczyt położenia potencjometru, który jest feedbackiem od silnika - regulacja hamulca. Jednak potrzebne do ustawienia obciążenia.

19:38:44:267 -> analogRead 1623
19:38:45:240 -> analogReadMilliVolts 1482
19:38:45:267 -> analogRead 1621
19:38:46:240 -> analogReadMilliVolts 1482
19:38:46:267 -> analogRead 1626
19:38:47:241 -> analogReadMilliVolts 1474
19:38:47:268 -> analogRead 1621
19:38:48:241 -> analogReadMilliVolts 1482
19:38:48:269 -> analogRead 1622
19:38:49:243 -> analogReadMilliVolts 1475
19:38:49:271 -> analogRead 1621
19:38:50:244 -> analogReadMilliVolts 1483
19:38:50:271 -> analogRead 1620
19:38:51:244 -> analogReadMilliVolts 1487
19:38:51:272 -> analogRead 1622
19:38:52:245 -> analogReadMilliVolts 1477
19:38:52:273 -> analogRead 1622
19:38:53:245 -> analogReadMilliVolts 1481
19:38:53:282 -> analogRead 1622
19:38:54:246 -> analogReadMilliVolts 1482
19:38:54:274 -> analogRead 1623
19:38:55:248 -> analogReadMilliVolts 1484
19:38:55:275 -> analogRead 1622
19:38:56:249 -> analogReadMilliVolts 1482
19:38:56:277 -> analogRead 1622

 

Edytowano przez roz
Link do komentarza
Share on other sites

Zgodnie z zaleceniami:

Kondensator 100nF a nie 100uF

Potencjometr wyposażyć w rezystory tak, aby minimalne napięcie przy skręceniu w lewo było przynajmniej 0.3V, a maksymalne ok. 2.5V

No i średnia z iluś tam pomiarów (według przykładów w dokumentacji 64, ja stosuję średnią ucinaną z 32 pomiarów).

Poza tym lepiej stosować funkcje z idf (dostępne w szkicach Arduino), ale tylko trochę lepiej.

Niestety ADC w ESP32 nie słynie z dokładności.

Link do komentarza
Share on other sites

(edytowany)

Zagadka.

[EDIT]: nie było zagadki - zmęczenie bierze górę - błędy w kodzie.

Edytowano przez roz
  • Lubię! 1
Link do komentarza
Share on other sites

(edytowany)

Dlaczego kompilator czepia się, że funkcja countPulse w przerwaniu ma być static? Gdy przeniosę wszystko do sekcji "public" w klasie to nie czepia się.

void SpeedDistance::init()
{
    pinMode(REED_SWITCH, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(REED_SWITCH), countPulse, FALLING);
}

Fragment klasy:

class SpeedDistance
{
private:
    volatile unsigned long _pulseCount =0;   /// Number of pulses from reed switch
    unsigned long debounceTime=50;  // Czas eliminacji drgań styków w milisekundach
    unsigned long lastPulseTime=0; /// Time of last pulse
    // unsigned int timeDiff = 0;
    // static unsigned long counterPrev;
    // static unsigned long counterCurr;
    // float _czas_s;
    float _cadence = 0.0;                  /// Cadence in RPM
    float _maxCadence = 0.0;               /// Max cadence in RPM
    float _speed = 0.0;                    /// Speed in km/h
    float _maxSpeed = 0.0;                 /// Max speed in km/h
    float _distance = 0.0;                 /// Distance in km
    bool _paused = false;                  /// Status of counting pulses
    const float wheelCircumference = 74.0; /// Obwód koła w cm
    void IRAM_ATTR countPulse();    /// Counting pulses from reed switch

I funkcja:

void IRAM_ATTR SpeedDistance::countPulse()
{
    unsigned long currentTime = millis();
    if (currentTime - lastPulseTime > debounceTime)
    {
        _pulseCount++;
        lastPulseTime = currentTime;
    }
}

EDIT: myślałem, że z tego powodu nie działa mi przerwanie, że funkcja/zmienne są statyczne, ale nie, powód jest inny. Z forum esp32.com

gpio36 i gpio39 są "ułomne" - nie da się zrobić programowo pullup i pulldown...

image.thumb.png.dfc4167af547039b5ee73b3cfe69ea4f.png

Edytowano przez roz
Link do komentarza
Share on other sites

39 minut temu, roz napisał:

Dlaczego kompilator czepia się, że funkcja countPulse w przerwaniu ma być static?

Procedura obsługi przerwania musi być jedna dla całej klasy (czyli static), bo skąd ISR miałby wiedzieć, dane którego obiektu (instancji) ma w swoim działaniu używać.

W twoim programie przerwanie modyfikuje zmienną  _pulseCount, no ale którą jeśli mamy zadeklarowanych np. pięć liczników? 

Link do komentarza
Share on other sites

Ciąg dalszy, wystarczy leniuchowania.

W jaki sposób zbieralibyście dane o treningu? Mam taką strukturę w głowie na tę chwilę, ale mam wątpliwości co do niej.

struct UserData {
  int id; // Identyfikator użytkownika
  char date[11]; // Data w formacie "YYYY-MM-DD"
  char startTime[6]; // Godzina startu w formacie "HH:MM"
  char endTime[6]; // Godzina końca w formacie "HH:MM"
  float maxSpeed; // Maksymalna prędkość w km/h
  float avgSpeed; // Średnia prędkość w km/h
  float distance; // Dystans w km
  float avgCadence; // Średnia kadencja w RPM
  float maxCadence; // Maksymalna kadencja w RPM
  float avgHeartRate; // Średnie tętno w BPM
  float maxHeartRate; // Maksymalne tętno w BPM
  float caloriesBurned; // Ilość spalonych kalorii
  unsigned int trainingTime; // Czas treningu w minutach
};

Powyższe nadaje się (moim zdaniem) jako podsumowanie treningu po zakończeniu.

Natomiast chodzi o ciągłe rejestrowanie całego treningu. Utworzenie tablicy np. 100 elementów (jeden wiersz co minutę?) i po jej zapełnieniu zapis do pliku?

Podzielcie się pomysłami.

Dziękuję.

Ps. chcę, aby było maksymalnie 4 użytkowników - wybór profilu przed treningiem, oraz jeden typu "gość".

Link do komentarza
Share on other sites

Zmieniłbym przede wszystkim daty/godziny na time_t - krótsze, wyrównane i prostsze później w interpretacji. A przede wszystkim odporne na jakieś zmiany typu CET/CEST.

No i double a nie float albo jakiś int.

  • Lubię! 2
Link do komentarza
Share on other sites

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.