init
@@ -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/
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.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)
|
||||
@@ -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
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **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.
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **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
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **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.
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **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.
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **Rysunek 5.** Schemat modułu DRV8825 |
|
||||
@@ -0,0 +1,45 @@
|
||||
# Wyświetlacz OLED 1.3" 128x64 SH1106
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **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
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **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
|
||||
|
||||
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 489 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 627 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -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
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **Rysunek 1.** Pinout serwo |
|
||||
@@ -0,0 +1,16 @@
|
||||
# UART - USB
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **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
|
||||
|
||||
|  |
|
||||
|:--:|
|
||||
| **Rysunek 2.** Pinout USB B|
|
||||
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
void uart_mode_init();
|
||||
void uart_mode_update();
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||