This commit is contained in:
2026-06-25 18:42:40 +02:00
parent 5a6201331d
commit 0aefeeb959
42 changed files with 1965 additions and 2 deletions
+48
View File
@@ -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/
+10
View File
@@ -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"
]
}
+44
View File
@@ -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"
}
]
}
+137 -2
View File
@@ -1,3 +1,138 @@
# cnc_draw_machine
# CNC Drawing Machine Firmware
Maszyna do rysowania CNC
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.5x2.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.5x2.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)
+108
View File
@@ -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 ~15 Ω (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 |
+45
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+14
View File
@@ -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 |
+16
View File
@@ -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|
View File
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <Arduino.h>
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();
+126
View File
@@ -0,0 +1,126 @@
#pragma once
#include <Arduino.h>
// ── 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;
};
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <Arduino.h>
#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();
+5
View File
@@ -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);
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <Arduino.h>
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();
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
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);
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <Arduino.h>
#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();
+4
View File
@@ -0,0 +1,4 @@
#pragma once
void uart_mode_init();
void uart_mode_update();
+10
View File
@@ -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();
+46
View File
@@ -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 <Foo.h>
#include <Bar.h>
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
+25
View File
@@ -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
+57
View File
@@ -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;
}
+293
View File
@@ -0,0 +1,293 @@
#include "display.h"
#include "config.h"
#include <U8g2lib.h>
#include <Wire.h>
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();
}
+65
View File
@@ -0,0 +1,65 @@
#include "gcode_parser.h"
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
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;
}
+34
View File
@@ -0,0 +1,34 @@
#include <Arduino.h>
#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();
}
}
+170
View File
@@ -0,0 +1,170 @@
#include "motion.h"
#include "config.h"
#include <AccelStepper.h>
// 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;
}
+37
View File
@@ -0,0 +1,37 @@
#include "pen.h"
#include "config.h"
#include <ESP32Servo.h>
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;
}
+93
View File
@@ -0,0 +1,93 @@
#include "sd_manager.h"
#include <SD.h>
#include <SPI.h>
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;
}
+107
View File
@@ -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;
}
}
}
+396
View File
@@ -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 <string.h>
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;
}
}
}
+11
View File
@@ -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