diff --git a/.gitignore b/.gitignore index e257658..a6ad0e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,18 @@ +# Ignoruj cały folder z buildami +#build/*.elf +#build/*.map +.pio + +# MacOS +.DS_Store + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + # ---> C++ # Prerequisites *.d @@ -32,3 +47,36 @@ *.out *.app +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +#build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..490998c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,44 @@ +// AUTOMATICALLY GENERATED FILE. PLEASE DO NOT MODIFY IT MANUALLY +// +// PlatformIO Debugging Solution +// +// Documentation: https://docs.platformio.org/en/latest/plus/debugging.html +// Configuration: https://docs.platformio.org/en/latest/projectconf/sections/env/options/debug/index.html + +{ + "version": "0.2.0", + "configurations": [ + { + "type": "platformio-debug", + "request": "launch", + "name": "PIO Debug", + "executable": "/Users/lklich/Programowanie/cnc_draw_machine/.pio/build/esp32wroom/firmware.elf", + "projectEnvName": "esp32wroom", + "toolchainBinDir": "/Users/lklich/.platformio/packages/toolchain-xtensa-esp32/bin", + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": { + "type": "PlatformIO", + "task": "Pre-Debug" + } + }, + { + "type": "platformio-debug", + "request": "launch", + "name": "PIO Debug (skip Pre-Debug)", + "executable": "/Users/lklich/Programowanie/cnc_draw_machine/.pio/build/esp32wroom/firmware.elf", + "projectEnvName": "esp32wroom", + "toolchainBinDir": "/Users/lklich/.platformio/packages/toolchain-xtensa-esp32/bin", + "internalConsoleOptions": "openOnSessionStart" + }, + { + "type": "platformio-debug", + "request": "launch", + "name": "PIO Debug (without uploading)", + "executable": "/Users/lklich/Programowanie/cnc_draw_machine/.pio/build/esp32wroom/firmware.elf", + "projectEnvName": "esp32wroom", + "toolchainBinDir": "/Users/lklich/.platformio/packages/toolchain-xtensa-esp32/bin", + "internalConsoleOptions": "openOnSessionStart", + "loadMode": "manual" + } + ] +} diff --git a/README.md b/README.md index 64c0c65..7ffb69c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,138 @@ -# cnc_draw_machine +# CNC Drawing Machine Firmware -Maszyna do rysowania CNC \ No newline at end of file +Projekt jest maszyną CNC do rysowania z dwoma silnikami NEMA17 sterowanymi przez DRV8825 na płytce CNC shield, ESP32 WROOM, serwomechanizm do pisaka, wyświetlacz LCD I2C 2x16, karta SD, 3 przyciski (Up/Down/OK). Celem jest kompletny firmware: przeglądanie plików GCode na karcie SD, drukowanie z podglądem postępu, regulacja prędkości podczas druku, tryb komend przez UART. + +# Działanie + +* Start normalny: lista plików .gcode/.nc z karty SD, Up/Down przewija, OK rozpoczyna druk +* Podczas druku: Up/Down zmienia prędkość (0.5x–2.0x), OK → dialog przerwania +* Boot z wciśniętym OK (2s): tryb UART — wysyłaj GCode przez monitor (G1 X50 Y30 F1000, G28, M3/M5) + +# Elementy + +* Moduł deweloperski ESP32 +* Dwa silniki krokowyme NEMA17 +* Sterownik A4988 DRV8825, +* Sterownik silnika krokowego DRV8825 +* Serwo do opuszczania pisaka. +* Moduł karty SD +* LCD I2C 2x16 +* 3x przycisk Up/Down/OK + +## include/config.h parametry dostosowalne przez użytkownika + +* WORKSPACE_WIDTH_MM, WORKSPACE_HEIGHT_MM — pole robocze +* STEPS_PER_MM_X, STEPS_PER_MM_Y — zależne od mikrostepowania DRV8825 +* SPEED_DEFAULT_MM_S, SPEED_MIN_MM_S, SPEED_MAX_MM_S — prędkości +* SPEED_MULT_MIN/MAX/STEP — zakres regulatora prędkości (0.5x–2.0x) +* ACCEL_MM_S2 — przyspieszenie AccelStepper +* SERVO_ANGLE_UP, SERVO_ANGLE_DOWN — kąty pisaka (nacisk) +* SERVO_SETTLE_MS — czas oczekiwania po ruchu serwa + +### Wszystkie numery pinów GPIO + +* LCD_I2C_ADDR, LCD_COLS, LCD_ROWS +* SD_MAX_FILES, FILENAME_MAX_LEN +* DISPLAY_UPDATE_MS, BUTTON_DEBOUNCE_MS, BOOT_HOLD_MS +* Enum AppState i struct GCodeLine, PrintStatus + +### Przypisanie pinów (domyślne, zmieniane w config.h) + +Sygnał GPIO + +* X STEP 26 +* X DIR 27 +* Y STEP 14 +* Y DIR 12 +* STEPPER ENABLE 13 +* SERVO 25 +* SD CS 5 +* LCD SDA 21 +* LCD SCL 22 +* BTN UP 32 +* BTN DOWN 33 +* BTN OK 4 + +Uwaga: GPIO12 to strapping pin — DRV8825 DIR ma wysoką impedancję przy braku sygnału, więc nie powinien przeciągać pinu podczas bootu. Jeśli problemy — zmienić na inny pin. + +## Do skonfigurowania przed użyciem w config.h: + +* STEPS_PER_MM_X/Y — zależy od mikrostepowania DRV8825 (jumperów na shield) +* SERVO_ANGLE_DOWN — kąt nacisku pisaka (kalibracja mechaniczna) +* PIN_* — jeśli CNC shield używa innych pinów niż domyślne +* LCD_I2C_ADDR — sprawdzić 0x27 lub 0x3F (zależy od modułu LCD) + + + + +include/config.h + +PIN_LIMIT_X = 34, PIN_LIMIT_Y = 35 (GPIO input-only, idealne dla krańcówek) +HOMING_DIR_X = -1, HOMING_DIR_Y = -1 — silniki jadą w kierunku ujemnym do lewego górnego rogu +HOMING_SPEED_MM_S = 20, HOMING_BACKOFF_MM = 2 — cofnięcie po wyzwoleniu +Pole robocze: 297 × 210 mm (A4 poziomo) + + +otowe. Podsumowanie geometrii w kodzie: + + +Fizyczna maszyna Pole robocze (GCode) +───────────────── ─────────────────────────────── +Prawy górny (HOME) ────→ (0,0) = lewy górny = START +Lewy górny ────→ (297, 0) = prawy górny +Prawy dolny ────→ (0, 210) = lewy dolny +Lewy dolny ────→ (297, 210) = prawy dolny = koniec +Sekwencja homingu: silniki jadą w kierunku HOMING_DIR = +1 (dodatni krok AccelSteppera) → po wyzwoleniu obu krańcówek = pozycja (0,0) → maszyna gotowa do drukowania od lewego górnego rogu. + +Jeśli przy homingu silnik jedzie w złą stronę (od krańcówki zamiast do niej): zmień HOMING_DIR_X lub HOMING_DIR_Y na -1 w include/config.h. + +Kompilacja OK. Co zostało dodane: + +Nowy stan AppState::ERROR — wyświetla błąd na LCD, OK wraca do listy plików. + +Wykrywane błędy: + +Sytuacja Komunikat +Nie można otworzyć pliku Blad odczytu SD / Nie otwarto pliku +Plik istnieje ale jest pusty Blad pliku / Plik jest pusty +Karta SD odłączona w trakcie druku Blad karty SD / Utracono plik +Współrzędne GCode poza polem roboczym Blad GCode / Poza polem rob. +Zachowanie: po każdym błędzie silniki są zatrzymywane i pisak unoszony (abortPrint()), a wciśnięcie OK przywraca listę plików (z ponownym odczytem z karty SD, gdyby karta była wymieniona). + + +ESP32 ma wbudowane WiFi — dodajemy asynchroniczny serwer HTTP, który serwuje stronę WWW do zarządzania maszyną bez fizycznego dostępu: upload plików .gcode, przeglądanie listy, start/stop druku, podgląd postępu. Eliminuje konieczność wyjmowania karty SD. + +Najważniejszy wybór: ESPAsyncWebServer (nie wbudowany WebServer.h), bo callbacks HTTP działają w osobnym tasku FreeRTOS (Core 0), dzięki czemu motion_run() w loop() nie jest nigdy blokowany przez request HTTP ani upload. + +Co zostało zaimplementowane +Serwer HTTP na porcie 80 (ESPAsyncWebServer — nie blokuje loop() ani silników): + +URL Metoda Funkcja +/ GET Strona WWW (PROGMEM) +/api/status GET JSON: stan, plik, postęp, prędkość +/api/files GET JSON: lista plików .gcode z SD +/api/print POST Start druku — file=nazwa.gcode +/api/stop POST Zatrzymanie druku +/api/upload POST Upload pliku .gcode na kartę SD +Strona WWW (działa z telefonu/laptopa, offline, bez CDN): + +Status druku z paskiem postępu, aktualną prędkością +Lista plików z przyciskami "Drukuj" +Upload z paskiem postępu +Przycisk "Zatrzymaj druk" +Polling co 1 sekundę +Sekwencja startu: + +Próba połączenia z WIFI_SSID (10s timeout) +Sukces → LCD "WiFi STA: 192.168.x.x" +Brak sieci → fallback AP mode → LCD "WiFi AP: 192.168.4.1" +Konfiguracja — zmień w include/config.h: + + +#define WIFI_SSID "NazwaSieci" +#define WIFI_PASSWORD "Haslo" + + +1. [Moduł sterownika silników](docs/MotoExpander.md) + +[Wersje systemu, poprawki, etc.](docs/versions.md) \ No newline at end of file diff --git a/docs/MotorExpander.md b/docs/MotorExpander.md new file mode 100644 index 0000000..3630d06 --- /dev/null +++ b/docs/MotorExpander.md @@ -0,0 +1,108 @@ +# DRV8825/A4988 42 moduł + +On-board DIP switch, you can easily adjust the drive segments. Terminal power connector for easy connection of drive power. Compatible with a 12/24V drive scheme. +Highly integrated expansion board, small size, stable performance. Suitable for 42 stepper motor drives, 3D printers, and DIY. + +## Specification + +* Logic voltage: 5V +* Input voltage: 12-30V +* Ports: Digital +* Interface: Direction, enable, speed +* Size: 42*42*10mm +* Fixed: 3mm +* Pitch: 35.56*35.56mm (1400*1400mil) +* Applicable module: A4988, DRV8825 + +| ![Opis](img/stepdriver.png) | +|:--:| +| **Rysunek 1.** DRV8825/A4988 42 Stepper Motor Driver Expansion Board | + +## Rozdzielczość + +Rozmiar kroku wybierany jest za pomocą wejść MS1, MS2, MS3. Możliwe ustawienia pokazane zostały w tabeli poniżej. Wejścia MS1, MS2 oraz MS3 mają wewnętrzny rezystor pull-down (100kOm). + +* MS1 MS2 MS3 Rozdzielczość +* niski niski niski Pełny krok +* wysoki niski niski 1/2 kroku +* niski wysoki niski 1/4 kroku +* wysoki wysoki niski 1/8 kroku +* wysoki wysoki wysoki 1/16 kroku + + +# A4988 - sterownik silnika krokowego + +Sterownik silnika krokowego oparty o układ A4988. Zasilany napięciem z zakresu 8 - 35 V, przy maksymalnym poborze prądu 2 A na cewkę. Maksymalna rozdzielczość: 1/16 kroku. Moduł pozwala na przepływ do 1 A prądu bez radiatora. + +| ![Opis](img/00148_4a.png) | +|:--:| +| **Rysunek 2.** Sterownik silnika DRV8825 | + + +* Napięcie zasilania silnika: 8 V - 35 V +* Prąd: maks. 1 A na cewkę ( przy zastosowaniu chłodzenia do 2 A) +* Napięcie zasilania części logicznej: 3 V - 5,5 V +* Praca w 5 różnych trybach: pełny krok, 1/2, 1/4, 1/8 oraz 1/16 kroku +* Możliwość regulacji prądu pobieranego przez silnik za pomocą potencjometru +* Ochrona przed przegrzaniem układu +* Moduł jest w pełni kompatybilny ze poprzednikiem: A4983 + +| ![Opis](img/stepper-motor-nema17.jpg) | +|:--:| +| **Rysunek 3.** Silnik NEMA17 | + +NEMA17 to silnik bipolarny: 4 przewody = 2 cewki + +* Cewka 1: przewód A+ i A- → złącze 1A / 1B +* Cewka 2: przewód B+ i B- → złącze 2A / 2B + +Kolejność w obrębie cewki (A+ vs A-) decyduje tylko o kierunku obrotu — można odwrócić. + +Jak zidentyfikować pary cewek multimetrem: ustaw multimetr na pomiar rezystancji (Ω): + +* Przewody tej samej cewki → rezystancja ~1–5 Ω (ciągłość) +* Przewody różnych cewek → brak ciągłości (OL / ∞) +* Znajdź 2 pary → każda para to jedna cewka. + +Typowe kolory przewodów NEMA17 + +Różni producenci używają różnych kolorów — nie polegaj na kolorach, zawsze mierz. + +### Producent / standard + +* Cewka Cewka 2 +* Typowy chiński NEMA17 Czarny / ZielonyCzerwony / Niebieski +* Creality / EnderNiebieski / CzerwonyCzarny / Zielony +* Reprap standardCzarny / ZielonyCzerwony / Niebieski + +### Podłączenie do modułu DRV8825/A4988 Expansion Board + +Na module szukaj oznaczeń: +* B2 B1 A1 A2 + +lub + +* 2B 2A 1A 1B + +Podłącz: + +* Para 1 (cewka 1) → A1 + A2 (lub 1A + 1B) +* Para 2 (cewka 2) → B1 + B2 (lub 2A + 2B) + +Jeśli silnik po podłączeniu drży / nie kręci się: jest zamieniony jeden przewód w parze — zamień miejscami jeden z dwóch przewodów jednej cewki. Nie trzeba nic przeliczać — to tylko zmiana polaryzacji cewki. + +Podsumowanie: Złącze śrubowe CNC Shield i piny expansion board mają tę samą logikę (2×cewka), ale fizyczna kolejność pinów może się różnić. Zidentyfikuj pary multimetrem i podłącz — działa. + +## Regulacja - Ograniczenie prądu + +Układem można sterować silnikami o nominalnym napięciu niższym niż wymagane 8 V. W tym celu należy ograniczyć maksymalny pobór prądu za pomocą potencjometru, tak aby nie przekroczyć dopuszczalnego natężenia silnika. Np. dla silnika o rezystancji 5 Ω na cewkę i poborze prądu 1 A, nominalne napięcie zasilania to 5 V. Zasilając napięciem 12 V należy ograniczyć prąd, tak aby nie przekroczył wartości 1 A. + +| ![Opis](img/PinoutNema.png) | +|:--:| +| **Rysunek 4.** Podłączenie silnika NEMA17 | + +Regulator krokowy A4988 pozwala na aktywne ograniczenie prądu przy pomocy potencjometru. Jednym ze sposobów wprowadzenia ograniczenia jest ustawienie sterownika w tryb pełnego kroku oraz pomiar prądu przepływającego przez jedną cewkę bez podawania sygnału na wejście STEP. Zmierzony prąd to 70% ustawionego limitu (obie cewki są zawsze włączone i ograniczone do 70% w trybie pełnego kroku). Innym sposobem jest pomiar napięcia na wyprowadzeniu REF (oznaczonego kółkiem na płytce drukowanej) oraz obliczenie aktualnego limitu (rezystory pomiarowe mają wartość 0.05Ω). Więcej szczegółów w dokumentacji układu A4988. + +| ![Opis](img/stepdiagram.jpg) | +|:--:| +| **Rysunek 5.** Schemat modułu DRV8825 | \ No newline at end of file diff --git a/docs/display.md b/docs/display.md new file mode 100644 index 0000000..2eb90f9 --- /dev/null +++ b/docs/display.md @@ -0,0 +1,45 @@ +# Wyświetlacz OLED 1.3" 128x64 SH1106 + +| ![Opis](img/disp.png) | +|:--:| +| **Rysunek 1.** Wyświetlacz - front | + +## Specyfikacja wyświetlacza + +* Napięcie pracy: od 3,3 V do 5,0 V +* Sterownik: SH1106 (dokumentacja) +* Komunikacja: I2C +* Typ wyświetlacza: OLED +* Przekątna: 1,3" +* Rozdzielczość: 128 x 64 px +* Kąt widzenia: powyżej 160° +* Temperatura pracy: od -20°C do 70°C +* Wymiary: 35 x 33 mm + +| ![Opis](img/disp2.png) | +|:--:| +| **Rysunek 2.** Wyświetlacz - tył | + + +## Pinout + +* PIN SYMBOL OPIS +* 1 VCC Napięcie zasilania od 3,3 V do 5,0 V. +* 2 GND Masa układu. +* 3 SCL Linia zegarowa I2C. +* 4 SDA Linia danych I2C. + +------------ + + +## Layouty ekranów + +Font domyślny: u8g2_font_6x10_tf) - siatka bazowych linii Y (6x10 font, 13px spacing): + +* Y=10 linia 1 (nagłówek) +* Y=12 separator poziomy +* Y=24 linia 2 +* Y=36 linia 3 +* Y=48 linia 4 +* Y=60 linia 5 + diff --git a/docs/img/00148_4a.png b/docs/img/00148_4a.png new file mode 100644 index 0000000..be7a665 Binary files /dev/null and b/docs/img/00148_4a.png differ diff --git a/docs/img/PinoutNema.png b/docs/img/PinoutNema.png new file mode 100644 index 0000000..22f085c Binary files /dev/null and b/docs/img/PinoutNema.png differ diff --git a/docs/img/disp.png b/docs/img/disp.png new file mode 100644 index 0000000..4a26ab5 Binary files /dev/null and b/docs/img/disp.png differ diff --git a/docs/img/disp2.png b/docs/img/disp2.png new file mode 100644 index 0000000..927c07e Binary files /dev/null and b/docs/img/disp2.png differ diff --git a/docs/img/sd1.png b/docs/img/sd1.png new file mode 100644 index 0000000..35e6a80 Binary files /dev/null and b/docs/img/sd1.png differ diff --git a/docs/img/servo.png b/docs/img/servo.png new file mode 100644 index 0000000..9011500 Binary files /dev/null and b/docs/img/servo.png differ diff --git a/docs/img/srvpinout.png b/docs/img/srvpinout.png new file mode 100644 index 0000000..09e12ef Binary files /dev/null and b/docs/img/srvpinout.png differ diff --git a/docs/img/stepdiagram.jpg b/docs/img/stepdiagram.jpg new file mode 100644 index 0000000..ea35d37 Binary files /dev/null and b/docs/img/stepdiagram.jpg differ diff --git a/docs/img/stepdriver.png b/docs/img/stepdriver.png new file mode 100644 index 0000000..a575e34 Binary files /dev/null and b/docs/img/stepdriver.png differ diff --git a/docs/img/stepper-motor-nema17.jpg b/docs/img/stepper-motor-nema17.jpg new file mode 100644 index 0000000..c464905 Binary files /dev/null and b/docs/img/stepper-motor-nema17.jpg differ diff --git a/docs/img/usb-microb-plug-connector-4.jpg.webp b/docs/img/usb-microb-plug-connector-4.jpg.webp new file mode 100644 index 0000000..342dbad Binary files /dev/null and b/docs/img/usb-microb-plug-connector-4.jpg.webp differ diff --git a/docs/img/usbb.jpg.webp b/docs/img/usbb.jpg.webp new file mode 100644 index 0000000..545e8ab Binary files /dev/null and b/docs/img/usbb.jpg.webp differ diff --git a/docs/servo.md b/docs/servo.md new file mode 100644 index 0000000..e172640 --- /dev/null +++ b/docs/servo.md @@ -0,0 +1,14 @@ +# Serwo Serwo SG-90 - micro - 180 + +## Dane techniczne + +* Parametry dla napięcia 4,8 V: +* Moment: 1,8 kg*cm (0,18 Nm) +* Prędkość: 0,1 s/60° +* Zakres ruchu: od 0° do 180° +* Wymiary: 22 x 11,5 x 27 mm +* Masa: 9 g + +| ![Opis](img/srvpinout.png) | +|:--:| +| **Rysunek 1.** Pinout serwo | \ No newline at end of file diff --git a/docs/uart.md b/docs/uart.md new file mode 100644 index 0000000..f1ab6a2 --- /dev/null +++ b/docs/uart.md @@ -0,0 +1,16 @@ +# UART - USB + +| ![Opis](img/usb-microb-plug-connector-4.jpg.webp) | +|:--:| +| **Rysunek 1.** Pinout USB micro | + +1. VBUS Red +5 V +2. D− White Data− +3. D+ Green Data+ +4. ID N/A: not connected +5. GND Black Signal ground + +| ![Opis](img/usbb.jpg.webp) | +|:--:| +| **Rysunek 2.** Pinout USB B| + diff --git a/docs/versions.md b/docs/versions.md new file mode 100644 index 0000000..e69de29 diff --git a/include/buttons.h b/include/buttons.h new file mode 100644 index 0000000..666603c --- /dev/null +++ b/include/buttons.h @@ -0,0 +1,13 @@ +#pragma once +#include + +enum class ButtonEvent : uint8_t { + NONE, + UP_PRESS, + DOWN_PRESS, + OK_PRESS +}; + +void buttons_init(); +ButtonEvent buttons_update(); +bool buttons_check_ok_held_at_boot(); diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..c030325 --- /dev/null +++ b/include/config.h @@ -0,0 +1,126 @@ +#pragma once +#include + +// ── Wersja firmware ────────────────────────────────────────────────────────── +#define FIRMWARE_VERSION "1.0" +#define FIRMWARE_AUTHOR "Leszek Klich" + +// ── Pole robocze (A4 poziomo) ───────────────────────────────────────────────── +constexpr float WORKSPACE_WIDTH_MM = 297.0f; +constexpr float WORKSPACE_HEIGHT_MM = 210.0f; + +// ── Ruch / silniki krokowe ─────────────────────────────────────────────────── +// DRV8825 z 1/16 mikrostepowaniem (MS1=0, MS2=0, MS3=1): 200*16 / (20T*2mm) = 80 krok/mm +constexpr float STEPS_PER_MM_X = 80.0f; +constexpr float STEPS_PER_MM_Y = 80.0f; + +constexpr float SPEED_DEFAULT_MM_S = 60.0f; +constexpr float SPEED_MIN_MM_S = 10.0f; +constexpr float SPEED_MAX_MM_S = 150.0f; +constexpr float ACCEL_MM_S2 = 500.0f; + +// ── Homing ─────────────────────────────────────────────────────────────────── +// Krańcówki wyzwalają się gdy głowica jest w PRAWYM GÓRNYM rogu KARTKI. +// W układzie pola roboczego to jest pozycja (0,0) = lewy górny róg rysowania. +// Po homingu: (0,0) = pozycja domowa, X rośnie w prawo, Y rośnie w dół. +// +// HOMING_DIR: kierunek jazdy do krańcówek (+1 = dodatni krok AccelSteppera). +// Wartość domyślna +1 oznacza że dodatni kierunek silnika jedzie ku krańcówce. +// Jeśli silnik kręci się w złą stronę: zmień na -1 lub zamień kabel DIR. +constexpr int8_t HOMING_DIR_X = +1; +constexpr int8_t HOMING_DIR_Y = +1; +constexpr float HOMING_SPEED_MM_S = 20.0f; +constexpr float HOMING_BACKOFF_MM = 2.0f; // cofnięcie po wyzwoleniu [mm] + +// Mnożnik prędkości zmieniany przyciskami podczas drukowania +constexpr float SPEED_MULT_MIN = 0.5f; +constexpr float SPEED_MULT_MAX = 2.0f; +constexpr float SPEED_MULT_STEP = 0.1f; +constexpr float SPEED_MULT_DEFAULT = 1.0f; + +// ── Servo / pisak ──────────────────────────────────────────────────────────── +constexpr int SERVO_ANGLE_UP = 90; // pisak uniesiony +constexpr int SERVO_ANGLE_DOWN = 50; // pisak na papierze (nacisk) +constexpr int SERVO_SETTLE_MS = 150; // czas opadnięcia serwa [ms] + +// ── Piny GPIO ──────────────────────────────────────────────────────────────── +constexpr uint8_t PIN_X_STEP = 26; +constexpr uint8_t PIN_X_DIR = 27; +constexpr uint8_t PIN_Y_STEP = 14; +constexpr uint8_t PIN_Y_DIR = 15; // GPIO15 — bezpieczny zamiennik (GPIO16 niedostępny) +constexpr uint8_t PIN_STEPPER_EN = 13; // aktywny LOW + +// Wyłączniki krańcowe (INPUT_PULLUP, aktywny LOW — wyzwolony = LOW) +// MIN = pozycja domowa (0,0), używany podczas homingu +// MAX = koniec pola roboczego, tylko zabezpieczenie przed najazdem +constexpr uint8_t PIN_LIMIT_X_MIN = 36; // tylko wejście — prawa strona = dom X (VP) +constexpr uint8_t PIN_LIMIT_X_MAX = 34; // tylko wejście — lewa strona = koniec X +constexpr uint8_t PIN_LIMIT_Y_MIN = 39; // tylko wejście — strona domowa osi Y (VN) +constexpr uint8_t PIN_LIMIT_Y_MAX = 35; // tylko wejście — koniec osi Y + +constexpr uint8_t PIN_SERVO = 25; + +constexpr uint8_t PIN_SD_CS = 5; // SPI CS (MOSI=23, MISO=19, SCK=18) + +// Przyciski podciągnięte do VCC (INPUT_PULLUP), aktywne LOW +constexpr uint8_t PIN_BTN_UP = 32; +constexpr uint8_t PIN_BTN_DOWN = 33; +constexpr uint8_t PIN_BTN_OK = 4; + +// ── OLED SH1106 1.3" 128x64 I2C ───────────────────────────────────────────── +// SDA=21, SCL=22 (domyślne Wire dla ESP32) +constexpr uint8_t OLED_I2C_ADDR = 0x3C; // alternatywnie 0x3D +constexpr uint8_t OLED_W = 128; +constexpr uint8_t OLED_H = 64; +constexpr uint8_t DISPLAY_LINE_CHARS = 21; // max znaków w linii (font 6x10) + +// ── Karta SD ───────────────────────────────────────────────────────────────── +constexpr uint8_t SD_MAX_FILES = 20; +constexpr uint8_t FILENAME_MAX_LEN = 64; + +// ── Timery UI ──────────────────────────────────────────────────────────────── +constexpr uint32_t DISPLAY_UPDATE_MS = 200; +constexpr uint32_t BUTTON_DEBOUNCE_MS = 50; +constexpr uint32_t BOOT_HOLD_MS = 2000; +constexpr uint32_t STEPPER_IDLE_TIMEOUT_MS = 5000; // auto-wyłączenie silników po bezczynności [ms], 0=wyłącz + +// ── Stany aplikacji ─────────────────────────────────────────────────────────── +enum class AppState : uint8_t { + MAIN_MENU, + FILE_LIST, + PRINTING, + CONFIRM_STOP, + UART_MODE, + AXIS_TEST, + ERROR +}; + +constexpr float TEST_STEP_MM = 1.0f; // krok na jedno naciśnięcie w trybie testowym [mm] +constexpr float TEST_SPEED_MM_S = 5.0f; // prędkość w trybie testowym [mm/s] + +// ── Komendy GCode ──────────────────────────────────────────────────────────── +enum class GCodeCmd : uint8_t { + NONE, + G0, + G1, + G28, + M3, + M5, + UNKNOWN +}; + +struct GCodeLine { + GCodeCmd cmd; + bool hasX, hasY, hasF; + float x, y; + float f; // feedrate w mm/min +}; + +// ── Status drukowania ───────────────────────────────────────────────────────── +struct PrintStatus { + char filename[FILENAME_MAX_LEN]; + uint32_t totalLines; + uint32_t currentLine; + float speedMultiplier; + bool isPrinting; +}; diff --git a/include/display.h b/include/display.h new file mode 100644 index 0000000..8e312c1 --- /dev/null +++ b/include/display.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include "config.h" + +void display_init(); +void display_show_splash(); +void display_show_homing(); +void display_show_main_menu(uint8_t selected); +void display_show_axis_test(char axis, float posX, float posY, bool limitHit); +void display_show_file_list(const char files[][FILENAME_MAX_LEN], uint8_t count, uint8_t selected); +void display_show_printing(const char* filename, uint8_t percent, float speedMult); +void display_show_confirm_stop(bool yesSelected); +void display_show_uart_mode(); +void display_show_error(const char* line1, const char* line2); +void display_force_update(); diff --git a/include/gcode_parser.h b/include/gcode_parser.h new file mode 100644 index 0000000..9e64f0d --- /dev/null +++ b/include/gcode_parser.h @@ -0,0 +1,5 @@ +#pragma once +#include "config.h" + +bool gcode_parse_line(const char* line, GCodeLine& out); +bool gcode_extract_param(const char* line, char code, float& out); diff --git a/include/motion.h b/include/motion.h new file mode 100644 index 0000000..bef5ef8 --- /dev/null +++ b/include/motion.h @@ -0,0 +1,16 @@ +#pragma once +#include + +void motion_init(); +void motion_move_to(float x_mm, float y_mm, float feedrate_mm_per_min); +bool motion_run(); +bool motion_is_idle(); +void motion_wait_idle(); +void motion_home(); +void motion_set_speed_multiplier(float mult); +float motion_get_speed_multiplier(); +void motion_enable(); +void motion_disable(); +float motion_get_x_mm(); +float motion_get_y_mm(); +bool motion_any_limit_triggered(); diff --git a/include/pen.h b/include/pen.h new file mode 100644 index 0000000..0ac704e --- /dev/null +++ b/include/pen.h @@ -0,0 +1,9 @@ +#pragma once +#include + +void pen_init(); +void pen_up(); +void pen_down(); +bool pen_is_down(); +void pen_set_down_angle(uint8_t angle); +void pen_set_up_angle(uint8_t angle); diff --git a/include/sd_manager.h b/include/sd_manager.h new file mode 100644 index 0000000..eb06db7 --- /dev/null +++ b/include/sd_manager.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include "config.h" + +bool sd_init(); +uint8_t sd_list_files(char files[][FILENAME_MAX_LEN], uint8_t maxCount); +bool sd_open_file(const char* filename); +uint32_t sd_count_lines(); +bool sd_read_line(char* buf, uint16_t bufLen); +void sd_close_file(); +bool sd_is_file_open(); diff --git a/include/uart_mode.h b/include/uart_mode.h new file mode 100644 index 0000000..0022e49 --- /dev/null +++ b/include/uart_mode.h @@ -0,0 +1,4 @@ +#pragma once + +void uart_mode_init(); +void uart_mode_update(); diff --git a/include/ui.h b/include/ui.h new file mode 100644 index 0000000..0a0c2f4 --- /dev/null +++ b/include/ui.h @@ -0,0 +1,10 @@ +#pragma once +#include "config.h" + +void ui_init(); +void ui_init_uart_mode(); +void ui_update(); +void ui_on_motion_idle(); +void ui_set_error(const char* line1, const char* line2); +AppState ui_get_state(); +const PrintStatus& ui_get_print_status(); diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..edfb8fe --- /dev/null +++ b/platformio.ini @@ -0,0 +1,25 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32wroom] +platform = espressif32 +framework = arduino +;board = upesy_wroom +;board = esp32doit-devkit-v1 +board = esp32dev +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +;upload_speed = 115200 +;upload_flags = --no-stub + +lib_deps = + waspinator/AccelStepper @ ^1.64.0 + madhephaestus/ESP32Servo @ ^3.0.5 + olikraus/U8g2 @ ^2.35.17 diff --git a/src/buttons.cpp b/src/buttons.cpp new file mode 100644 index 0000000..8a13af2 --- /dev/null +++ b/src/buttons.cpp @@ -0,0 +1,57 @@ +#include "buttons.h" +#include "config.h" + +struct BtnState { + uint8_t lastLevel; + uint32_t lastChangeMs; + bool consumed; +}; + +static BtnState s_up = {HIGH, 0, false}; +static BtnState s_down = {HIGH, 0, false}; +static BtnState s_ok = {HIGH, 0, false}; + +static ButtonEvent checkBtn(BtnState& s, uint8_t pin, ButtonEvent ev) { + uint8_t level = digitalRead(pin); + uint32_t now = millis(); + + if (level != s.lastLevel) { + s.lastLevel = level; + s.lastChangeMs = now; + s.consumed = false; + } + + if (!s.consumed && + (now - s.lastChangeMs) >= BUTTON_DEBOUNCE_MS && + s.lastLevel == LOW) { + s.consumed = true; + return ev; + } + return ButtonEvent::NONE; +} + +void buttons_init() { + pinMode(PIN_BTN_UP, INPUT_PULLUP); + pinMode(PIN_BTN_DOWN, INPUT_PULLUP); + pinMode(PIN_BTN_OK, INPUT_PULLUP); +} + +ButtonEvent buttons_update() { + ButtonEvent ev; + ev = checkBtn(s_up, PIN_BTN_UP, ButtonEvent::UP_PRESS); + if (ev != ButtonEvent::NONE) return ev; + ev = checkBtn(s_down, PIN_BTN_DOWN, ButtonEvent::DOWN_PRESS); + if (ev != ButtonEvent::NONE) return ev; + ev = checkBtn(s_ok, PIN_BTN_OK, ButtonEvent::OK_PRESS); + return ev; +} + +bool buttons_check_ok_held_at_boot() { + if (digitalRead(PIN_BTN_OK) != LOW) return false; + uint32_t start = millis(); + while (millis() - start < BOOT_HOLD_MS) { + if (digitalRead(PIN_BTN_OK) != LOW) return false; + delay(10); + } + return true; +} diff --git a/src/display.cpp b/src/display.cpp new file mode 100644 index 0000000..ad93253 --- /dev/null +++ b/src/display.cpp @@ -0,0 +1,293 @@ +#include "display.h" +#include "config.h" +#include +#include + +static U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); +static uint32_t s_lastUpdateMs = 0; + +static bool throttled() { + return (millis() - s_lastUpdateMs) < DISPLAY_UPDATE_MS; +} + +static void markUpdated() { + s_lastUpdateMs = millis(); +} + +static void drawSep(uint8_t y) { + u8g2.drawHLine(0, y, OLED_W); +} + +// Truncate src to maxChars and copy to dst (dst must have maxChars+1 bytes) +static void trunc(const char* src, char* dst, uint8_t maxChars) { + strncpy(dst, src, maxChars); + dst[maxChars] = '\0'; +} + +// Draw text centered horizontally at top-position y +static void drawCentered(uint8_t y, const char* str) { + uint8_t w = u8g2.getStrWidth(str); + u8g2.drawStr((OLED_W - w) / 2, y, str); +} + +// ── Publiczne API ───────────────────────────────────────────────────────────── + +void display_init() { + Wire.begin(); + u8g2.begin(); + u8g2.setFontPosTop(); // Y = górna krawędź znaku + u8g2.setContrast(200); + u8g2.clearBuffer(); + u8g2.sendBuffer(); +} + +void display_force_update() { + s_lastUpdateMs = 0; +} + +void display_show_splash() { + u8g2.clearBuffer(); + + // Tytuł — większy font, wycentrowany + u8g2.setFont(u8g2_font_8x13_tf); + drawCentered(4, "CNC Draw"); + + // Wersja — mniejszy font + u8g2.setFont(u8g2_font_6x10_tf); + drawCentered(21, "v." FIRMWARE_VERSION); + + drawSep(35); + + drawCentered(40, FIRMWARE_AUTHOR); + + u8g2.sendBuffer(); + delay(2000); + markUpdated(); +} + +void display_show_homing() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + drawCentered(0, "Kalibracja..."); + drawSep(13); + drawCentered(22, "Szukam pozycji 0,0"); + u8g2.setFont(u8g2_font_5x7_tf); + drawCentered(40, "Prosze czekac..."); + u8g2.sendBuffer(); + markUpdated(); +} + +void display_show_main_menu(uint8_t selected) { + if (throttled()) return; + markUpdated(); + + static const char* const items[3] = {"Drukowanie", "Tryb UART", "Test osi"}; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + + drawCentered(0, "CNC Draw - Menu"); + drawSep(12); + + for (uint8_t i = 0; i < 3; i++) { + uint8_t yTop = 14 + i * 17; + if (i == selected) { + u8g2.setDrawColor(1); + u8g2.drawBox(0, yTop - 1, OLED_W, 13); + u8g2.setDrawColor(0); + u8g2.drawStr(4, yTop, items[i]); + u8g2.setDrawColor(1); + } else { + u8g2.drawStr(4, yTop, items[i]); + } + } + + u8g2.sendBuffer(); +} + +void display_show_axis_test(char axis, float posX, float posY, bool limitHit) { + if (throttled()) return; + markUpdated(); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + + if (limitHit) { + drawCentered(0, "! KRANC OW KA !"); + drawSep(12); + drawCentered(20, "Cofanie..."); + u8g2.setFont(u8g2_font_5x7_tf); + drawCentered(38, "czekaj"); + } else { + char hdr[22]; + snprintf(hdr, sizeof(hdr), "Test osi: %c", axis); + u8g2.drawStr(0, 0, hdr); + drawSep(12); + + char buf[22]; + snprintf(buf, sizeof(buf), "X: %5.1f mm", posX); + u8g2.drawStr(0, 14, buf); + snprintf(buf, sizeof(buf), "Y: %5.1f mm", posY); + u8g2.drawStr(0, 27, buf); + + drawSep(42); + u8g2.setFont(u8g2_font_5x7_tf); + u8g2.drawStr(0, 46, "UP/DOWN = ruch"); + u8g2.drawStr(0, 55, "OK = zmien os"); + } + + u8g2.sendBuffer(); +} + +void display_show_file_list(const char files[][FILENAME_MAX_LEN], uint8_t count, uint8_t selected) { + if (throttled()) return; + markUpdated(); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + + if (count == 0) { + drawCentered(16, "Brak plikow"); + drawCentered(30, ".gcode na SD"); + u8g2.sendBuffer(); + return; + } + + // Nagłówek z licznikiem + char hdr[22]; + snprintf(hdr, sizeof(hdr), "Pliki [%d/%d]", selected + 1, count); + u8g2.drawStr(0, 0, hdr); + drawSep(12); + + // 4 wpisy plików (Y = 14, 27, 40, 53) + uint8_t scrollOffset = (selected / 4) * 4; + for (uint8_t i = 0; i < 4 && (scrollOffset + i) < count; i++) { + uint8_t idx = scrollOffset + i; + uint8_t yTop = 14 + i * 13; + + char fname[22]; + trunc(files[idx], fname, 21); + + if (idx == selected) { + // Wybrany element — odwrócone tło + u8g2.setDrawColor(1); + u8g2.drawBox(0, yTop - 1, OLED_W, 13); + u8g2.setDrawColor(0); + u8g2.drawStr(3, yTop, fname); + u8g2.setDrawColor(1); + } else { + u8g2.drawStr(3, yTop, fname); + } + } + + u8g2.sendBuffer(); +} + +void display_show_printing(const char* filename, uint8_t percent, float speedMult) { + if (throttled()) return; + markUpdated(); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + + // Nagłówek: "DRUKOWANIE" + procent + u8g2.drawStr(0, 0, "DRUKOWANIE"); + char pct[8]; + snprintf(pct, sizeof(pct), "%d%%", percent); + uint8_t pw = u8g2.getStrWidth(pct); + u8g2.drawStr(OLED_W - pw, 0, pct); + drawSep(12); + + // Nazwa pliku (max 21 znaków) + char fname[22]; + trunc(filename, fname, 21); + u8g2.drawStr(0, 14, fname); + + // Pasek postępu (ramka + wypełnienie) + u8g2.drawFrame(0, 28, OLED_W, 10); + uint8_t fill = (uint8_t)((uint32_t)(OLED_W - 2) * percent / 100); + if (fill > 0) u8g2.drawBox(1, 29, fill, 8); + + // Prędkość + int mi = (int)(speedMult * 10); + char spd[22]; + snprintf(spd, sizeof(spd), "Predkosc: %d.%dx", mi / 10, mi % 10); + u8g2.drawStr(0, 42, spd); + + u8g2.sendBuffer(); +} + +void display_show_confirm_stop(bool yesSelected) { + if (throttled()) return; + markUpdated(); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + + drawCentered(2, "Przerwac druk?"); + drawSep(14); + + // Przycisk TAK (lewy) + if (yesSelected) { + u8g2.drawBox(8, 26, 50, 18); + u8g2.setDrawColor(0); + u8g2.drawStr(21, 30, "TAK"); + u8g2.setDrawColor(1); + } else { + u8g2.drawFrame(8, 26, 50, 18); + u8g2.drawStr(21, 30, "TAK"); + } + + // Przycisk NIE (prawy) + if (!yesSelected) { + u8g2.drawBox(70, 26, 50, 18); + u8g2.setDrawColor(0); + u8g2.drawStr(83, 30, "NIE"); + u8g2.setDrawColor(1); + } else { + u8g2.drawFrame(70, 26, 50, 18); + u8g2.drawStr(83, 30, "NIE"); + } + + // Podpowiedź + u8g2.setFont(u8g2_font_5x7_tf); + drawCentered(52, "UP/DOWN=wybor OK=ok"); + + u8g2.sendBuffer(); +} + +void display_show_uart_mode() { + if (throttled()) return; + markUpdated(); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + + drawCentered(0, "TRYB UART"); + drawSep(12); + + u8g2.drawStr(4, 18, "Wysylaj komendy GCode"); + u8g2.drawStr(4, 31, "przez port szeregowy"); + u8g2.drawStr(4, 44, "115200 baud"); + + u8g2.sendBuffer(); +} + +void display_show_error(const char* line1, const char* line2) { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tf); + + u8g2.drawStr(0, 0, "! BLAD"); + drawSep(12); + + if (line1) u8g2.drawStr(0, 15, line1); + if (line2) u8g2.drawStr(0, 28, line2); + + drawSep(42); + u8g2.setFont(u8g2_font_5x7_tf); + drawCentered(46, "Nacisnij OK"); + drawCentered(55, "aby kontynuowac"); + + u8g2.sendBuffer(); + markUpdated(); +} diff --git a/src/gcode_parser.cpp b/src/gcode_parser.cpp new file mode 100644 index 0000000..3a1d443 --- /dev/null +++ b/src/gcode_parser.cpp @@ -0,0 +1,65 @@ +#include "gcode_parser.h" +#include +#include +#include + +bool gcode_extract_param(const char* line, char code, float& out) { + char upper = (char)toupper((unsigned char)code); + for (const char* p = line; *p; p++) { + if (toupper((unsigned char)*p) == upper) { + char* end; + float val = strtof(p + 1, &end); + if (end != p + 1) { + out = val; + return true; + } + } + } + return false; +} + +bool gcode_parse_line(const char* line, GCodeLine& out) { + out = {GCodeCmd::NONE, false, false, false, 0, 0, 0}; + + // Skip leading whitespace + while (*line == ' ' || *line == '\t') line++; + + // Skip empty lines and comments + if (*line == '\0' || *line == ';' || *line == '(' || *line == '%') return false; + + // Strip inline comment — work on a local copy + static char buf[128]; + strncpy(buf, line, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + char* comment = strchr(buf, ';'); + if (comment) *comment = '\0'; + comment = strchr(buf, '('); + if (comment) *comment = '\0'; + + // Convert to uppercase in-place + for (char* p = buf; *p; p++) *p = (char)toupper((unsigned char)*p); + + const char* src = buf; + + // Detect command — order matters (G28 before G2x, M05 before M5 etc.) + if (strstr(src, "G28")) out.cmd = GCodeCmd::G28; + else if (strstr(src, "G0")) out.cmd = GCodeCmd::G0; + else if (strstr(src, "G1")) out.cmd = GCodeCmd::G1; + else if (strstr(src, "M3")) out.cmd = GCodeCmd::M3; + else if (strstr(src, "M5")) out.cmd = GCodeCmd::M5; + else { + // Line has no recognized command — might be modal (X Y only) + // Check if it has coordinates at all + float dummy; + if (!gcode_extract_param(src, 'X', dummy) && !gcode_extract_param(src, 'Y', dummy)) { + return false; + } + out.cmd = GCodeCmd::NONE; // caller must apply last modal cmd + } + + out.hasX = gcode_extract_param(src, 'X', out.x); + out.hasY = gcode_extract_param(src, 'Y', out.y); + out.hasF = gcode_extract_param(src, 'F', out.f); + + return true; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a3030ac --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,34 @@ +#include +#include "config.h" +#include "buttons.h" +#include "display.h" +#include "sd_manager.h" +#include "motion.h" +#include "pen.h" +#include "ui.h" +#include "uart_mode.h" + +void setup() { + Serial.begin(115200); + + buttons_init(); + display_init(); + display_show_splash(); + + motion_init(); + pen_init(); + + display_show_homing(); + motion_home(); + + ui_init(); // startuje w MAIN_MENU +} + +void loop() { + motion_run(); + ui_update(); + + if (ui_get_state() == AppState::PRINTING && motion_is_idle()) { + ui_on_motion_idle(); + } +} diff --git a/src/motion.cpp b/src/motion.cpp new file mode 100644 index 0000000..94c6845 --- /dev/null +++ b/src/motion.cpp @@ -0,0 +1,170 @@ +#include "motion.h" +#include "config.h" +#include + +// Kinematyka KARTEZJAŃSKA: +// stepperX → oś X (lewo/prawo) +// stepperY → oś Y (góra/dół) +// Każdy silnik steruje tylko swoją osią niezależnie. + +static AccelStepper s_stepperX(AccelStepper::DRIVER, PIN_X_STEP, PIN_X_DIR); +static AccelStepper s_stepperY(AccelStepper::DRIVER, PIN_Y_STEP, PIN_Y_DIR); +static float s_speedMult = SPEED_MULT_DEFAULT; +static float s_lastBaseFeedMmS = SPEED_DEFAULT_MM_S; +static uint32_t s_idleSinceMs = 0; +static bool s_autoDisabled = false; + +static float clampSpeed(float s) { + if (s < SPEED_MIN_MM_S) return SPEED_MIN_MM_S; + if (s > SPEED_MAX_MM_S) return SPEED_MAX_MM_S; + return s; +} + +static void applySpeed(float baseFeedMmS) { + float speed = clampSpeed(baseFeedMmS * s_speedMult); + s_stepperX.setMaxSpeed(speed * STEPS_PER_MM_X); + s_stepperY.setMaxSpeed(speed * STEPS_PER_MM_Y); +} + +// ── Homing pomocniczy (jeden silnik) ────────────────────────────────────────── + +static void homeAxisSingle(AccelStepper& stepper, int8_t dir, + uint8_t limitPin, float workspaceMm, float stepsMm) { + long safeTravel = (long)((workspaceMm + 50.0f) * stepsMm) * dir; + stepper.move(safeTravel); + while (digitalRead(limitPin) != LOW) { + if (!stepper.run()) break; + } + stepper.setCurrentPosition(0); + + long backoff = (long)(HOMING_BACKOFF_MM * stepsMm) * (-dir); + stepper.move(backoff); + while (stepper.run()) {} + stepper.setCurrentPosition(0); +} + +// ── Publiczne API ───────────────────────────────────────────────────────────── + +void motion_init() { + pinMode(PIN_STEPPER_EN, OUTPUT); + pinMode(PIN_LIMIT_X_MIN, INPUT_PULLUP); + pinMode(PIN_LIMIT_X_MAX, INPUT_PULLUP); + pinMode(PIN_LIMIT_Y_MIN, INPUT_PULLUP); + pinMode(PIN_LIMIT_Y_MAX, INPUT_PULLUP); + motion_enable(); + + s_stepperX.setMaxSpeed(SPEED_DEFAULT_MM_S * STEPS_PER_MM_X); + s_stepperX.setAcceleration(ACCEL_MM_S2 * STEPS_PER_MM_X); + s_stepperY.setMaxSpeed(SPEED_DEFAULT_MM_S * STEPS_PER_MM_Y); + s_stepperY.setAcceleration(ACCEL_MM_S2 * STEPS_PER_MM_Y); +} + +void motion_move_to(float x_mm, float y_mm, float feedrate_mm_per_min) { + if (s_autoDisabled) { + motion_enable(); + s_autoDisabled = false; + } + s_idleSinceMs = 0; + s_lastBaseFeedMmS = feedrate_mm_per_min / 60.0f; + applySpeed(s_lastBaseFeedMmS); + + // Kartezjański: każdy silnik steruje tylko swoją osią + long sx = (long)(x_mm * STEPS_PER_MM_X) * -HOMING_DIR_X; + long sy = (long)(y_mm * STEPS_PER_MM_Y) * -HOMING_DIR_Y; + s_stepperX.moveTo(sx); + s_stepperY.moveTo(sy); +} + +bool motion_run() { + float sx = s_stepperX.speed(); + float sy = s_stepperY.speed(); + + // Zabezpieczenia krańcówek X + if (digitalRead(PIN_LIMIT_X_MIN) == LOW && (float)HOMING_DIR_X * sx > 0) + s_stepperX.setCurrentPosition(s_stepperX.currentPosition()); + if (digitalRead(PIN_LIMIT_X_MAX) == LOW && (float)HOMING_DIR_X * sx < 0) + s_stepperX.setCurrentPosition(s_stepperX.currentPosition()); + + // Zabezpieczenia krańcówek Y + if (digitalRead(PIN_LIMIT_Y_MIN) == LOW && (float)HOMING_DIR_Y * sy > 0) + s_stepperY.setCurrentPosition(s_stepperY.currentPosition()); + if (digitalRead(PIN_LIMIT_Y_MAX) == LOW && (float)HOMING_DIR_Y * sy < 0) + s_stepperY.setCurrentPosition(s_stepperY.currentPosition()); + + bool rx = s_stepperX.run(); + bool ry = s_stepperY.run(); + bool moving = rx || ry; + + if (moving) { + s_idleSinceMs = 0; + s_autoDisabled = false; + } else if (STEPPER_IDLE_TIMEOUT_MS > 0 && !s_autoDisabled) { + if (s_idleSinceMs == 0) { + s_idleSinceMs = millis(); + } else if (millis() - s_idleSinceMs >= STEPPER_IDLE_TIMEOUT_MS) { + motion_disable(); + s_autoDisabled = true; + } + } + + return moving; +} + +bool motion_is_idle() { + return !s_stepperX.isRunning() && !s_stepperY.isRunning(); +} + +void motion_wait_idle() { + while (!motion_is_idle()) motion_run(); +} + +// Homing kartezjański: każda oś niezależnie +void motion_home() { + s_stepperX.setMaxSpeed(HOMING_SPEED_MM_S * STEPS_PER_MM_X); + s_stepperX.setAcceleration(ACCEL_MM_S2 * STEPS_PER_MM_X); + s_stepperY.setMaxSpeed(HOMING_SPEED_MM_S * STEPS_PER_MM_Y); + s_stepperY.setAcceleration(ACCEL_MM_S2 * STEPS_PER_MM_Y); + + homeAxisSingle(s_stepperX, HOMING_DIR_X, PIN_LIMIT_X_MIN, + WORKSPACE_WIDTH_MM, STEPS_PER_MM_X); + homeAxisSingle(s_stepperY, HOMING_DIR_Y, PIN_LIMIT_Y_MIN, + WORKSPACE_HEIGHT_MM, STEPS_PER_MM_Y); + + applySpeed(s_lastBaseFeedMmS > 0 ? s_lastBaseFeedMmS : SPEED_DEFAULT_MM_S); + s_stepperX.setAcceleration(ACCEL_MM_S2 * STEPS_PER_MM_X); + s_stepperY.setAcceleration(ACCEL_MM_S2 * STEPS_PER_MM_Y); +} + +void motion_set_speed_multiplier(float mult) { + if (mult < SPEED_MULT_MIN) mult = SPEED_MULT_MIN; + if (mult > SPEED_MULT_MAX) mult = SPEED_MULT_MAX; + s_speedMult = mult; + applySpeed(s_lastBaseFeedMmS); +} + +float motion_get_speed_multiplier() { + return s_speedMult; +} + +void motion_enable() { + digitalWrite(PIN_STEPPER_EN, LOW); +} + +void motion_disable() { + digitalWrite(PIN_STEPPER_EN, HIGH); +} + +float motion_get_x_mm() { + return (float)s_stepperX.currentPosition() / STEPS_PER_MM_X * (-HOMING_DIR_X); +} + +float motion_get_y_mm() { + return (float)s_stepperY.currentPosition() / STEPS_PER_MM_Y * (-HOMING_DIR_Y); +} + +bool motion_any_limit_triggered() { + return digitalRead(PIN_LIMIT_X_MIN) == LOW || + digitalRead(PIN_LIMIT_X_MAX) == LOW || + digitalRead(PIN_LIMIT_Y_MIN) == LOW || + digitalRead(PIN_LIMIT_Y_MAX) == LOW; +} diff --git a/src/pen.cpp b/src/pen.cpp new file mode 100644 index 0000000..5f8e077 --- /dev/null +++ b/src/pen.cpp @@ -0,0 +1,37 @@ +#include "pen.h" +#include "config.h" +#include + +static Servo s_servo; +static bool s_penDown = false; +static uint8_t s_angleUp = SERVO_ANGLE_UP; +static uint8_t s_angleDown = SERVO_ANGLE_DOWN; + +void pen_init() { + s_servo.attach(PIN_SERVO); + pen_up(); +} + +void pen_up() { + s_servo.write(s_angleUp); + delay(SERVO_SETTLE_MS); + s_penDown = false; +} + +void pen_down() { + s_servo.write(s_angleDown); + delay(SERVO_SETTLE_MS); + s_penDown = true; +} + +bool pen_is_down() { + return s_penDown; +} + +void pen_set_down_angle(uint8_t angle) { + s_angleDown = angle; +} + +void pen_set_up_angle(uint8_t angle) { + s_angleUp = angle; +} diff --git a/src/sd_manager.cpp b/src/sd_manager.cpp new file mode 100644 index 0000000..e3f39c3 --- /dev/null +++ b/src/sd_manager.cpp @@ -0,0 +1,93 @@ +#include "sd_manager.h" +#include +#include + +static File s_file; +static bool s_fileOpen = false; + +static bool hasGcodeExtension(const char* name) { + const char* dot = strrchr(name, '.'); + if (!dot) return false; + // case-insensitive compare + char ext[8] = {}; + for (int i = 0; i < 7 && dot[i + 1]; i++) { + ext[i] = (char)tolower((unsigned char)dot[i + 1]); + } + return strcmp(ext, "gcode") == 0 || strcmp(ext, "nc") == 0; +} + +bool sd_init() { + return SD.begin(PIN_SD_CS); +} + +uint8_t sd_list_files(char files[][FILENAME_MAX_LEN], uint8_t maxCount) { + File root = SD.open("/"); + if (!root) return 0; + + uint8_t count = 0; + while (count < maxCount) { + File entry = root.openNextFile(); + if (!entry) break; + if (!entry.isDirectory()) { + const char* name = entry.name(); + if (hasGcodeExtension(name)) { + strncpy(files[count], name, FILENAME_MAX_LEN - 1); + files[count][FILENAME_MAX_LEN - 1] = '\0'; + count++; + } + } + entry.close(); + } + root.close(); + return count; +} + +bool sd_open_file(const char* filename) { + if (s_fileOpen) sd_close_file(); + + // Build full path + char path[FILENAME_MAX_LEN + 2]; + snprintf(path, sizeof(path), "/%s", filename); + + s_file = SD.open(path, FILE_READ); + s_fileOpen = (bool)s_file; + return s_fileOpen; +} + +uint32_t sd_count_lines() { + if (!s_fileOpen) return 0; + s_file.seek(0); + uint32_t lines = 0; + while (s_file.available()) { + char c = (char)s_file.read(); + if (c == '\n') lines++; + } + // Count last line if file doesn't end with newline + if (s_file.size() > 0) { + s_file.seek(s_file.size() - 1); + char last = (char)s_file.read(); + if (last != '\n') lines++; + } + s_file.seek(0); + return lines; +} + +bool sd_read_line(char* buf, uint16_t bufLen) { + if (!s_fileOpen || !s_file.available()) return false; + uint16_t n = s_file.readBytesUntil('\n', buf, bufLen - 1); + buf[n] = '\0'; + // Strip carriage return + if (n > 0 && buf[n - 1] == '\r') buf[n - 1] = '\0'; + return true; +} + +void sd_close_file() { + if (s_fileOpen) { + s_file.close(); + s_fileOpen = false; + } +} + +bool sd_is_file_open() { + return s_fileOpen; +} diff --git a/src/uart_mode.cpp b/src/uart_mode.cpp new file mode 100644 index 0000000..f74d397 --- /dev/null +++ b/src/uart_mode.cpp @@ -0,0 +1,107 @@ +#include "uart_mode.h" +#include "config.h" +#include "motion.h" +#include "pen.h" +#include "gcode_parser.h" + +static char s_lineBuf[128]; +static uint8_t s_lineLen = 0; +static float s_lastX = 0.0f; +static float s_lastY = 0.0f; +static float s_lastFeed = SPEED_DEFAULT_MM_S * 60.0f; +static GCodeCmd s_lastGCmd = GCodeCmd::G0; +static bool s_waitingIdle = false; // waiting for motion to finish before sending "ok" + +void uart_mode_init() { + motion_enable(); + pen_up(); + motion_home(); + Serial.println("UART MODE READY"); + Serial.println("Wysylaj komendy GCode (G0,G1,G28,M3,M5)"); +} + +static void executeGCode(const char* line) { + GCodeLine gl; + if (!gcode_parse_line(line, gl)) { + Serial.println("ok"); + return; + } + + GCodeCmd cmd = gl.cmd; + if (cmd == GCodeCmd::NONE) { + cmd = s_lastGCmd; + } else if (cmd == GCodeCmd::G0 || cmd == GCodeCmd::G1) { + s_lastGCmd = cmd; + } + + float x = gl.hasX ? gl.x : s_lastX; + float y = gl.hasY ? gl.y : s_lastY; + float f = gl.hasF ? gl.f : s_lastFeed; + + s_lastX = x; + s_lastY = y; + s_lastFeed = f; + + switch (cmd) { + case GCodeCmd::G0: + pen_up(); + motion_move_to(x, y, SPEED_MAX_MM_S * 60.0f); + s_waitingIdle = true; + break; + + case GCodeCmd::G1: + pen_down(); + motion_move_to(x, y, f); + s_waitingIdle = true; + break; + + case GCodeCmd::G28: + pen_up(); + motion_home(); + Serial.println("ok"); + break; + + case GCodeCmd::M3: + pen_down(); + Serial.println("ok"); + break; + + case GCodeCmd::M5: + pen_up(); + Serial.println("ok"); + break; + + default: + Serial.println("ok"); + break; + } +} + +void uart_mode_update() { + // Always advance steppers + motion_run(); + + // Send "ok" once motion finishes + if (s_waitingIdle && motion_is_idle()) { + s_waitingIdle = false; + Serial.println("ok"); + } + + // Accumulate incoming bytes into line buffer + while (Serial.available()) { + char c = (char)Serial.read(); + if (c == '\n' || c == '\r') { + if (s_lineLen > 0) { + s_lineBuf[s_lineLen] = '\0'; + s_lineLen = 0; + if (!s_waitingIdle) { + executeGCode(s_lineBuf); + } else { + Serial.println("error: busy"); + } + } + } else if (s_lineLen < sizeof(s_lineBuf) - 1) { + s_lineBuf[s_lineLen++] = c; + } + } +} diff --git a/src/ui.cpp b/src/ui.cpp new file mode 100644 index 0000000..bed4cf8 --- /dev/null +++ b/src/ui.cpp @@ -0,0 +1,396 @@ +#include "ui.h" +#include "buttons.h" +#include "display.h" +#include "sd_manager.h" +#include "motion.h" +#include "pen.h" +#include "gcode_parser.h" +#include "uart_mode.h" +#include + +static AppState s_state = AppState::MAIN_MENU; +static PrintStatus s_status = {}; + +// Menu startowe +static uint8_t s_menuSelected = 0; +static bool s_sdInited = false; + +// File list +static char s_files[SD_MAX_FILES][FILENAME_MAX_LEN]; +static uint8_t s_fileCount = 0; +static uint8_t s_selectedFile = 0; + +// Print state +static float s_lastX = 0.0f; +static float s_lastY = 0.0f; +static float s_lastFeed = SPEED_DEFAULT_MM_S * 60.0f; +static GCodeCmd s_lastGCmd = GCodeCmd::G0; + +// Confirm dialog +static bool s_confirmYes = true; + +// Error state +static char s_errLine1[DISPLAY_LINE_CHARS + 1]; +static char s_errLine2[DISPLAY_LINE_CHARS + 1]; + +// Axis test +static char s_testAxis = 'X'; +static uint8_t s_lastLimitMask = 0xFF; // 0xFF = nieznany (wymusi log przy starcie) + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +static uint8_t limitMask() { + return ((digitalRead(PIN_LIMIT_X_MIN) == LOW) ? 0x01 : 0) | + ((digitalRead(PIN_LIMIT_X_MAX) == LOW) ? 0x02 : 0) | + ((digitalRead(PIN_LIMIT_Y_MIN) == LOW) ? 0x04 : 0) | + ((digitalRead(PIN_LIMIT_Y_MAX) == LOW) ? 0x08 : 0); +} + +static void logAxisTest(const char* event, char axis, float posX, float posY) { + uint8_t lm = limitMask(); + Serial.printf("[TestOsi] Os:%c %s X=%.1f Y=%.1f XMIN=%d XMAX=%d YMIN=%d YMAX=%d\n", + axis, event, + (double)posX, (double)posY, + (lm & 0x01) ? 1 : 0, + (lm & 0x02) ? 1 : 0, + (lm & 0x04) ? 1 : 0, + (lm & 0x08) ? 1 : 0); +} + +static void loadFileList() { + s_fileCount = sd_list_files(s_files, SD_MAX_FILES); + s_selectedFile = 0; +} + +static void abortPrint() { + pen_up(); + motion_disable(); + sd_close_file(); + s_status.isPrinting = false; +} + +static void stopPrinting() { + abortPrint(); + motion_home(); + s_state = AppState::MAIN_MENU; + display_force_update(); + display_show_main_menu(s_menuSelected); +} + +static void startPrinting() { + const char* fname = s_files[s_selectedFile]; + + if (!sd_open_file(fname)) { + ui_set_error("Blad odczytu SD", "Nie otwarto pliku"); + return; + } + + uint32_t lines = sd_count_lines(); + if (lines == 0) { + sd_close_file(); + ui_set_error("Blad pliku", "Plik jest pusty"); + return; + } + + strncpy(s_status.filename, fname, FILENAME_MAX_LEN - 1); + s_status.filename[FILENAME_MAX_LEN - 1] = '\0'; + s_status.totalLines = lines; + s_status.currentLine = 0; + s_status.speedMultiplier = SPEED_MULT_DEFAULT; + s_status.isPrinting = true; + + s_lastX = 0.0f; + s_lastY = 0.0f; + s_lastFeed = SPEED_DEFAULT_MM_S * 60.0f; + s_lastGCmd = GCodeCmd::G0; + + motion_enable(); + pen_up(); + + s_state = AppState::PRINTING; + display_force_update(); +} + +// ── Public API ──────────────────────────────────────────────────────────────── + +void ui_set_error(const char* line1, const char* line2) { + abortPrint(); + strncpy(s_errLine1, line1 ? line1 : "", DISPLAY_LINE_CHARS); + strncpy(s_errLine2, line2 ? line2 : "", DISPLAY_LINE_CHARS); + s_errLine1[DISPLAY_LINE_CHARS] = '\0'; + s_errLine2[DISPLAY_LINE_CHARS] = '\0'; + s_state = AppState::ERROR; + display_show_error(s_errLine1, s_errLine2); +} + +void ui_init() { + s_state = AppState::MAIN_MENU; + s_menuSelected = 0; + s_sdInited = false; + display_force_update(); + display_show_main_menu(0); +} + +void ui_init_uart_mode() { + s_state = AppState::UART_MODE; + display_force_update(); +} + +AppState ui_get_state() { + return s_state; +} + +const PrintStatus& ui_get_print_status() { + return s_status; +} + +void ui_update() { + ButtonEvent ev = buttons_update(); + + switch (s_state) { + + // ── Menu startowe ────────────────────────────────────────────────────────── + case AppState::MAIN_MENU: + if (ev == ButtonEvent::UP_PRESS) { + s_menuSelected = (s_menuSelected + 2) % 3; + display_force_update(); + } else if (ev == ButtonEvent::DOWN_PRESS) { + s_menuSelected = (s_menuSelected + 1) % 3; + display_force_update(); + } else if (ev == ButtonEvent::OK_PRESS) { + if (s_menuSelected == 0) { + // Drukowanie → inicjuj SD jeśli potrzeba, przejdź do listy + if (!s_sdInited) { + if (!sd_init()) { + ui_set_error("Blad SD!", "Wloz karte SD"); + break; + } + s_sdInited = true; + } + loadFileList(); + s_state = AppState::FILE_LIST; + display_force_update(); + display_show_file_list(s_files, s_fileCount, s_selectedFile); + } else if (s_menuSelected == 1) { + // Tryb UART + uart_mode_init(); + s_state = AppState::UART_MODE; + display_force_update(); + } else { + // Test osi + s_testAxis = 'X'; + motion_enable(); + pen_up(); + s_state = AppState::AXIS_TEST; + display_force_update(); + } + } + display_show_main_menu(s_menuSelected); + break; + + // ── Lista plików ─────────────────────────────────────────────────────────── + case AppState::FILE_LIST: + if (s_fileCount == 0) { + display_show_file_list(s_files, s_fileCount, s_selectedFile); + break; + } + if (ev == ButtonEvent::UP_PRESS) { + if (s_selectedFile > 0) s_selectedFile--; + else s_selectedFile = s_fileCount - 1; + display_force_update(); + } else if (ev == ButtonEvent::DOWN_PRESS) { + if (s_selectedFile < s_fileCount - 1) s_selectedFile++; + else s_selectedFile = 0; + display_force_update(); + } else if (ev == ButtonEvent::OK_PRESS) { + startPrinting(); + break; + } + display_show_file_list(s_files, s_fileCount, s_selectedFile); + break; + + // ── Drukowanie ───────────────────────────────────────────────────────────── + case AppState::PRINTING: { + if (ev == ButtonEvent::UP_PRESS) { + float m = s_status.speedMultiplier + SPEED_MULT_STEP; + if (m > SPEED_MULT_MAX) m = SPEED_MULT_MAX; + s_status.speedMultiplier = m; + motion_set_speed_multiplier(m); + display_force_update(); + } else if (ev == ButtonEvent::DOWN_PRESS) { + float m = s_status.speedMultiplier - SPEED_MULT_STEP; + if (m < SPEED_MULT_MIN) m = SPEED_MULT_MIN; + s_status.speedMultiplier = m; + motion_set_speed_multiplier(m); + display_force_update(); + } else if (ev == ButtonEvent::OK_PRESS) { + s_confirmYes = true; + s_state = AppState::CONFIRM_STOP; + display_force_update(); + display_show_confirm_stop(s_confirmYes); + break; + } + uint8_t pct = (s_status.totalLines > 0) + ? (uint8_t)((s_status.currentLine * 100UL) / s_status.totalLines) : 0; + display_show_printing(s_status.filename, pct, s_status.speedMultiplier); + break; + } + + // ── Potwierdzenie zatrzymania ────────────────────────────────────────────── + case AppState::CONFIRM_STOP: + if (ev == ButtonEvent::UP_PRESS || ev == ButtonEvent::DOWN_PRESS) { + s_confirmYes = !s_confirmYes; + display_force_update(); + } else if (ev == ButtonEvent::OK_PRESS) { + if (s_confirmYes) { + stopPrinting(); + } else { + s_state = AppState::PRINTING; + display_force_update(); + } + break; + } + display_show_confirm_stop(s_confirmYes); + break; + + // ── Tryb UART ────────────────────────────────────────────────────────────── + case AppState::UART_MODE: + uart_mode_update(); + display_show_uart_mode(); + break; + + // ── Test osi ─────────────────────────────────────────────────────────────── + case AppState::AXIS_TEST: { + bool limitActive = motion_any_limit_triggered(); + float posX = motion_get_x_mm(); + float posY = motion_get_y_mm(); + + // Log zmiany stanu krańcówek + uint8_t lm = limitMask(); + if (lm != s_lastLimitMask) { + s_lastLimitMask = lm; + logAxisTest(lm ? "KRANC!" : "OK ", s_testAxis, posX, posY); + } + + // Automatyczne cofanie gdy krańcówka wyzwolona i silniki stoją + if (limitActive && motion_is_idle()) { + const float backoff = 3.0f; + float nx = posX; + float ny = posY; + if (digitalRead(PIN_LIMIT_X_MIN) == LOW) nx += backoff; + if (digitalRead(PIN_LIMIT_X_MAX) == LOW) nx -= backoff; + if (digitalRead(PIN_LIMIT_Y_MIN) == LOW) ny += backoff; + if (digitalRead(PIN_LIMIT_Y_MAX) == LOW) ny -= backoff; + if (nx < 0.0f) nx = 0.0f; + if (nx > WORKSPACE_WIDTH_MM) nx = WORKSPACE_WIDTH_MM; + if (ny < 0.0f) ny = 0.0f; + if (ny > WORKSPACE_HEIGHT_MM) ny = WORKSPACE_HEIGHT_MM; + motion_enable(); + motion_move_to(nx, ny, TEST_SPEED_MM_S * 60.0f); + logAxisTest("COFANIE", s_testAxis, nx, ny); + display_force_update(); + } + + // Przyciski aktywne tylko gdy silniki stoją i żadna krańcówka nie wyzwolona + if (!limitActive && motion_is_idle()) { + if (ev == ButtonEvent::UP_PRESS) { + float nx = posX + (s_testAxis == 'X' ? TEST_STEP_MM : 0.0f); + float ny = posY + (s_testAxis == 'Y' ? TEST_STEP_MM : 0.0f); + if (nx > WORKSPACE_WIDTH_MM) nx = WORKSPACE_WIDTH_MM; + if (ny > WORKSPACE_HEIGHT_MM) ny = WORKSPACE_HEIGHT_MM; + motion_move_to(nx, ny, TEST_SPEED_MM_S * 60.0f); + logAxisTest("GORA ", s_testAxis, nx, ny); + display_force_update(); + } else if (ev == ButtonEvent::DOWN_PRESS) { + float nx = posX - (s_testAxis == 'X' ? TEST_STEP_MM : 0.0f); + float ny = posY - (s_testAxis == 'Y' ? TEST_STEP_MM : 0.0f); + if (nx < 0.0f) nx = 0.0f; + if (ny < 0.0f) ny = 0.0f; + motion_move_to(nx, ny, TEST_SPEED_MM_S * 60.0f); + logAxisTest("DOL ", s_testAxis, nx, ny); + display_force_update(); + } else if (ev == ButtonEvent::OK_PRESS) { + s_testAxis = (s_testAxis == 'X') ? 'Y' : 'X'; + logAxisTest("ZMIANA", s_testAxis, posX, posY); + display_force_update(); + } + } + + display_show_axis_test(s_testAxis, posX, posY, limitActive); + break; + } + + // ── Błąd ────────────────────────────────────────────────────────────────── + case AppState::ERROR: + display_show_error(s_errLine1, s_errLine2); + if (ev == ButtonEvent::OK_PRESS) { + s_state = AppState::MAIN_MENU; + display_force_update(); + display_show_main_menu(s_menuSelected); + } + break; + } +} + +void ui_on_motion_idle() { + if (s_state != AppState::PRINTING) return; + + static char lineBuf[128]; + + while (true) { + if (!sd_read_line(lineBuf, sizeof(lineBuf))) { + if (!sd_is_file_open()) { + ui_set_error("Blad karty SD", "Utracono plik"); + } else { + stopPrinting(); + } + return; + } + s_status.currentLine++; + + GCodeLine gl; + if (!gcode_parse_line(lineBuf, gl)) continue; + + GCodeCmd cmd = gl.cmd; + if (cmd == GCodeCmd::NONE) { + cmd = s_lastGCmd; + } else if (cmd == GCodeCmd::G0 || cmd == GCodeCmd::G1) { + s_lastGCmd = cmd; + } + + float x = gl.hasX ? gl.x : s_lastX; + float y = gl.hasY ? gl.y : s_lastY; + float f = gl.hasF ? gl.f : s_lastFeed; + + if (x < 0 || x > WORKSPACE_WIDTH_MM || y < 0 || y > WORKSPACE_HEIGHT_MM) { + ui_set_error("Blad GCode", "Poza polem rob."); + return; + } + + s_lastX = x; + s_lastY = y; + s_lastFeed = f; + + switch (cmd) { + case GCodeCmd::G0: + pen_up(); + motion_move_to(x, y, SPEED_MAX_MM_S * 60.0f); + return; + case GCodeCmd::G1: + pen_down(); + motion_move_to(x, y, f); + return; + case GCodeCmd::G28: + motion_home(); + return; + case GCodeCmd::M3: + pen_down(); + continue; + case GCodeCmd::M5: + pen_up(); + continue; + default: + continue; + } + } +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html