17 Commits

Author SHA1 Message Date
suchmememanyskill
2a2fff27d6 Merge branch 'master' into dev 2024-01-19 21:10:39 +01:00
suchmememanyskill
082d66ca10 Longer watchdog timeout, refactor, use duty cycle for backlight 2024-01-19 21:05:57 +01:00
Sims
41b4bff940 Merge pull request #12 from suchmememanyskill/dev
Dev
2024-01-15 13:28:57 +01:00
suchmememanyskill
9136f4c94b Add some delay within data loop to give other processes on the core time to process 2024-01-08 21:33:01 +01:00
suchmememanyskill
50f4984231 Insert sort the fetched files, discard any extras 2024-01-07 21:07:35 +01:00
suchmememanyskill
48466cfb44 Lower data fetch task priority 2024-01-07 21:07:19 +01:00
suchmememanyskill
a7acd49d60 Make all colors somewhat worth using 2024-01-06 20:40:37 +01:00
suchmememanyskill
91920a679a Fix #10 2024-01-06 19:59:13 +01:00
suchmememanyskill
53441c86c4 Add kofi to site 2024-01-05 23:20:47 +01:00
Sims
ffdc8ae87e Merge pull request #7 from suchmememanyskill/dev
v1.1.2
2023-12-16 17:53:14 +01:00
suchmememanyskill
7c786d1e6b Don't continously call unfreeze_render_thread() 2023-12-15 19:24:55 +01:00
suchmememanyskill
34c6a5e031 Offload API request loop to core 0 2023-12-15 19:22:48 +01:00
suchmememanyskill
7a430f81c5 Change back to klipper connect screen when connection to klipper gets severed 2023-12-11 22:23:18 +01:00
Sims
230884c2cc Merge pull request #6 from suchmememanyskill/dev
V1.1.1
2023-12-03 01:25:45 +01:00
suchmememanyskill
f2d232d9eb Add visual Klipper connect retry 2023-12-02 02:01:26 +01:00
suchmememanyskill
1e3f0ab637 Lower CPU speed if screen is off 2023-12-02 01:13:40 +01:00
suchmememanyskill
e15c7e37ff update readme 2023-11-23 12:31:32 +01:00
17 changed files with 247 additions and 73 deletions

View File

@@ -18,7 +18,8 @@ lib_deps =
lvgl/lvgl@^8.3.9
https://github.com/Bodmer/TFT_eSPI.git
https://github.com/PaulStoffregen/XPT2046_Touchscreen.git
bblanchon/ArduinoJson@^6.21.3
bblanchon/ArduinoJson@^7.0.0
monitor_filters = esp32_exception_decoder
build_flags =
-DLV_CONF_PATH="../../../../src/conf/lv_conf.h"
-DUSER_SETUP_LOADED=1

View File

@@ -5,13 +5,13 @@
GLOBAL_CONFIG global_config = {0};
COLOR_DEF color_defs[] = {
{LV_PALETTE_BLUE, LV_PALETTE_RED},
{LV_PALETTE_GREEN, LV_PALETTE_PURPLE},
{LV_PALETTE_GREY, LV_PALETTE_CYAN},
{LV_PALETTE_YELLOW, LV_PALETTE_PINK},
{LV_PALETTE_ORANGE, LV_PALETTE_BLUE},
{LV_PALETTE_RED, LV_PALETTE_GREEN},
{LV_PALETTE_PURPLE, LV_PALETTE_GREY},
{LV_PALETTE_BLUE, 0, LV_PALETTE_RED},
{LV_PALETTE_LIME, -2, LV_PALETTE_PURPLE},
{LV_PALETTE_GREY, 0, LV_PALETTE_CYAN},
{LV_PALETTE_YELLOW, -2, LV_PALETTE_PINK},
{LV_PALETTE_ORANGE, -2, LV_PALETTE_BLUE},
{LV_PALETTE_RED, 0, LV_PALETTE_GREEN},
{LV_PALETTE_PURPLE, 0, LV_PALETTE_GREY},
};
void WriteGlobalConfig() {

View File

@@ -10,9 +10,12 @@ typedef struct _GLOBAL_CONFIG {
union {
unsigned char raw;
struct {
// Internal
bool screenCalibrated : 1;
bool wifiConfigured : 1;
bool ipConfigured : 1;
// External
bool lightMode : 1;
bool invertColors : 1;
bool rotateScreen : 1;
@@ -40,6 +43,7 @@ typedef struct _GLOBAL_CONFIG {
typedef struct _COLOR_DEF {
lv_palette_t primary_color;
short primary_color_light;
lv_palette_t secondary_color;
} COLOR_DEF;

View File

@@ -4,6 +4,7 @@
#include "../conf/global_config.h"
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <esp_task_wdt.h>
#include "macros_query.h"
const char *printer_state_messages[] = {
@@ -12,6 +13,33 @@ const char *printer_state_messages[] = {
"Printing"};
Printer printer = {0};
int klipper_request_consecutive_fail_count = 0;
char filename_buff[512] = {0};
SemaphoreHandle_t freezeRenderThreadSemaphore, freezeRequestThreadSemaphore;
const long data_update_interval = 780;
void semaphore_init(){
freezeRenderThreadSemaphore = xSemaphoreCreateMutex();
freezeRequestThreadSemaphore = xSemaphoreCreateMutex();
xSemaphoreGive(freezeRenderThreadSemaphore);
xSemaphoreGive(freezeRequestThreadSemaphore);
}
void freeze_request_thread(){
xSemaphoreTake(freezeRequestThreadSemaphore, portMAX_DELAY);
}
void unfreeze_request_thread(){
xSemaphoreGive(freezeRequestThreadSemaphore);
}
void freeze_render_thread(){
xSemaphoreTake(freezeRenderThreadSemaphore, portMAX_DELAY);
}
void unfreeze_render_thread(){
xSemaphoreGive(freezeRenderThreadSemaphore);
}
void send_gcode(bool wait, const char *gcode)
{
@@ -35,23 +63,27 @@ void send_gcode(bool wait, const char *gcode)
}
}
char filename_buff[512] = {0};
void fetch_printer_data()
{
freeze_request_thread();
char buff[256] = {};
sprintf(buff, "http://%s:%d/printer/objects/query?extruder&heater_bed&toolhead&gcode_move&virtual_sdcard&print_stats&webhooks", global_config.klipperHost, global_config.klipperPort);
HTTPClient client;
client.useHTTP10(true);
client.begin(buff);
int httpCode = client.GET();
delay(10);
if (httpCode == 200)
{
String payload = client.getString();
DynamicJsonDocument doc(4096);
deserializeJson(doc, payload);
klipper_request_consecutive_fail_count = 0;
JsonDocument doc;
deserializeJson(doc, client.getStream());
auto status = doc["result"]["status"];
bool emit_state_update = false;
int printer_state = printer.state;
delay(10);
unfreeze_request_thread();
freeze_render_thread();
if (status.containsKey("webhooks"))
{
@@ -156,29 +188,41 @@ void fetch_printer_data()
printer.state = printer_state;
lv_msg_send(DATA_PRINTER_STATE, &printer);
}
unfreeze_render_thread();
}
else
{
klipper_request_consecutive_fail_count++;
Serial.printf("Failed to fetch printer data: %d\n", httpCode);
unfreeze_request_thread();
}
}
long last_data_update = 0;
const long data_update_interval = 1500;
void data_loop()
{
if (millis() - last_data_update < data_update_interval)
return;
last_data_update = millis();
fetch_printer_data();
// Causes other threads that are trying to lock the thread to actually lock it
unfreeze_render_thread();
delay(1);
freeze_render_thread();
}
void data_loop_background(void * param){
esp_task_wdt_init(10, true);
while (true){
delay(data_update_interval);
fetch_printer_data();
}
}
TaskHandle_t background_loop;
void data_setup()
{
semaphore_init();
printer.print_filename = filename_buff;
fetch_printer_data();
macros_query_setup();
}
freeze_render_thread();
xTaskCreatePinnedToCore(data_loop_background, "data_loop_background", 5000, NULL, 0, &background_loop, 0);
}

View File

@@ -28,6 +28,7 @@ typedef struct _Printer {
} Printer;
extern Printer printer;
extern int klipper_request_consecutive_fail_count;
#define DATA_PRINTER_STATE 1
#define DATA_PRINTER_DATA 2
@@ -35,4 +36,7 @@ extern Printer printer;
void data_loop();
void data_setup();
void send_gcode(bool wait, const char* gcode);
void send_gcode(bool wait, const char* gcode);
void freeze_request_thread();
void unfreeze_request_thread();

View File

@@ -1,6 +1,7 @@
#include <list>
#include "files_query.h"
#include "../conf/global_config.h"
#include "data_setup.h"
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <HardwareSerial.h>
@@ -8,7 +9,9 @@
// Always has +1 entry with a null'd name
FILESYSTEM_FILE* last_query = NULL;
FILESYSTEM_FILE* get_files(){
FILESYSTEM_FILE* get_files(int limit){
freeze_request_thread();
if (last_query != NULL){
FILESYSTEM_FILE* current = last_query;
@@ -20,37 +23,76 @@ FILESYSTEM_FILE* get_files(){
free(last_query);
}
Serial.printf("Heap space pre-file-parse: %d bytes\n", esp_get_free_heap_size());
std::list<FILESYSTEM_FILE> files;
auto timer_request = millis();
char buff[256] = {};
sprintf(buff, "http://%s:%d/server/files/list", global_config.klipperHost, global_config.klipperPort);
HTTPClient client;
client.useHTTP10(true);
client.begin(buff);
int httpCode = client.GET();
int count = 0;
auto timer_parse = millis();
if (httpCode == 200){
String payload = client.getString();
DynamicJsonDocument doc(60000);
auto a = deserializeJson(doc, payload);
Serial.printf("JSON PARSE: %s\n", a.c_str());
JsonDocument doc;
auto parseResult = deserializeJson(doc, client.getStream());
Serial.printf("Json parse: %s\n", parseResult.c_str());
auto result = doc["result"].as<JsonArray>();
for (auto file : result){
FILESYSTEM_FILE f = {0};
const char* path = file["path"];
float modified = file["modified"];
auto file_iter = files.begin();
while (file_iter != files.end()){
if ((*file_iter).modified < modified)
break;
file_iter++;
}
// Little inefficient as it always allocates a string, even if it doesn't have to
f.name = (char*)malloc(strlen(path) + 1);
if (f.name == NULL){
Serial.println("Failed to allocate memory");
continue;
}
strcpy(f.name, path);
f.modified = file["modified"];
files.push_back(f);
count++;
f.modified = modified;
if (file_iter != files.end())
files.insert(file_iter, f);
else
files.push_back(f);
if (files.size() > limit){
auto last_entry = files.back();
if (last_entry.name != NULL)
free(last_entry.name);
files.pop_back();
}
}
}
//Serial.printf("Found %d files\n", count);
files.sort([](FILESYSTEM_FILE a, FILESYSTEM_FILE b){return a.modified < b.modified;});
files.reverse(); // TODO: Reverse is unneeded here, we can iterate backwards
size_t size = sizeof(FILESYSTEM_FILE) * (files.size() + 1);
FILESYSTEM_FILE* result = (FILESYSTEM_FILE*)malloc(size);
//Serial.printf("Allocated %d bytes\n", size);
if (result == NULL){
Serial.println("Failed to allocate memory");
for (auto file : files){
free(file.name);
}
unfreeze_request_thread();
return NULL;
}
last_query = result;
result[files.size()].name = NULL;
@@ -59,5 +101,8 @@ FILESYSTEM_FILE* get_files(){
result += 1;
}
Serial.printf("Heap space post-file-parse: %d bytes\n", esp_get_free_heap_size());
Serial.printf("Got %d files. Request took %dms, parsing took %dms\n", files.size(), timer_parse - timer_request, millis() - timer_parse);
unfreeze_request_thread();
return last_query;
}

View File

@@ -19,4 +19,4 @@ typedef struct _FILESYSTEM_FILE {
float modified;
} FILESYSTEM_FILE;
FILESYSTEM_FILE* get_files();
FILESYSTEM_FILE* get_files(int limit);

View File

@@ -15,12 +15,12 @@ static void on_state_change(void * s, lv_msg_t * m) {
String url = "http://" + String(global_config.klipperHost) + ":" + String(global_config.klipperPort) + "/printer/gcode/help";
HTTPClient client;
client.useHTTP10(true);
client.begin(url.c_str());
int httpCode = client.GET();
if (httpCode == 200){
String payload = client.getString();
DynamicJsonDocument doc(16384);
deserializeJson(doc, payload);
JsonDocument doc;
deserializeJson(doc, client.getStream());
auto result = doc["result"].as<JsonObject>();
for (int i = 0; i < macros_count; i++){

View File

@@ -84,7 +84,11 @@ void touchscreen_calibrate(bool force)
void screen_setBrightness(byte brightness)
{
analogWrite(TFT_BL, brightness);
// calculate duty, 4095 from 2 ^ 12 - 1
uint32_t duty = (4095 / 255) * brightness;
// write duty to LEDC
ledcWrite(0, duty);
}
void set_screen_brightness()
@@ -100,12 +104,20 @@ void screen_timer_wake()
lv_timer_reset(screenSleepTimer);
isScreenInSleep = false;
set_screen_brightness();
// Reset cpu freq
setCpuFrequencyMhz(CPU_FREQ_HIGH);
Serial.printf("CPU Speed: %d MHz\n", ESP.getCpuFreqMHz());
}
void screen_timer_sleep(lv_timer_t *timer)
{
screen_setBrightness(0);
isScreenInSleep = true;
// Screen is off, no need to make the cpu run fast, the user won't notice ;)
setCpuFrequencyMhz(CPU_FREQ_LOW);
Serial.printf("CPU Speed: %d MHz\n", ESP.getCpuFreqMHz());
}
void screen_timer_setup()
@@ -175,7 +187,20 @@ void screen_lv_touchRead(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
void set_color_scheme(){
lv_disp_t *dispp = lv_disp_get_default();
lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(color_defs[global_config.color_scheme].primary_color), lv_palette_main(color_defs[global_config.color_scheme].secondary_color), !global_config.lightMode, LV_FONT_DEFAULT);
lv_color_t main_color = {0};
COLOR_DEF color_def = color_defs[global_config.color_scheme];
if (color_defs[global_config.color_scheme].primary_color_light > 0){
main_color = lv_palette_lighten(color_def.primary_color, color_def.primary_color_light);
}
else if (color_defs[global_config.color_scheme].primary_color_light < 0) {
main_color = lv_palette_darken(color_def.primary_color, color_def.primary_color_light * -1);
}
else {
main_color = lv_palette_main(color_defs[global_config.color_scheme].primary_color);
}
lv_theme_t *theme = lv_theme_default_init(dispp, main_color, lv_palette_main(color_def.secondary_color), !global_config.lightMode, LV_FONT_DEFAULT);
lv_disp_set_theme(dispp, theme);
}
@@ -192,6 +217,10 @@ void screen_setup()
lv_init();
tft.init();
ledcSetup(0, 5000, 12);
ledcAttachPin(21, 0);
tft.setRotation(global_config.rotateScreen ? 3 : 1);
tft.fillScreen(TFT_BLACK);
set_screen_brightness();

View File

@@ -4,6 +4,9 @@
#ifndef _SCREEN_DRIVER_INIT
#define _SCREEN_DRIVER_INIT
#define CPU_FREQ_HIGH 240
#define CPU_FREQ_LOW 80
#include <XPT2046_Touchscreen.h>
#include <TFT_eSPI.h>

View File

@@ -25,7 +25,7 @@ void setup() {
Serial.println("Screen init done");
wifi_init();
ip_setup();
ip_init();
data_setup();
nav_style_setup();
@@ -34,6 +34,7 @@ void setup() {
void loop(){
wifi_ok();
ip_ok();
data_loop();
lv_timer_handler();
lv_task_handler();

View File

@@ -3,18 +3,22 @@
#include "lvgl.h"
#include <TFT_eSPI.h>
#include <HTTPClient.h>
#include "core/data_setup.h"
bool connect_ok = false;
lv_obj_t * ipEntry;
lv_obj_t * portEntry;
lv_obj_t * label = NULL;
void ip_init_inner();
bool verify_ip(){
HTTPClient client;
String url = "http://" + String(global_config.klipperHost) + ":" + String(global_config.klipperPort) + "/printer/info";
int httpCode;
try {
Serial.println(url);
client.setTimeout(500);
client.begin(url.c_str());
httpCode = client.GET();
return httpCode == 200;
@@ -25,17 +29,6 @@ bool verify_ip(){
}
}
bool retry_ip_verify(){
for (int i = 0; i < 5; i++){
if (verify_ip()){
return true;
}
delay(1000);
}
return false;
}
static void ta_event_cb(lv_event_t * e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * ta = lv_event_get_target(e);
@@ -53,8 +46,8 @@ static void ta_event_cb(lv_event_t * e) {
{
strcpy(global_config.klipperHost, lv_textarea_get_text(ipEntry));
global_config.klipperPort = atoi(lv_textarea_get_text(portEntry));
bool result = verify_ip();
if (result)
if (verify_ip())
{
global_config.ipConfigured = true;
WriteGlobalConfig();
@@ -67,9 +60,33 @@ static void ta_event_cb(lv_event_t * e) {
}
}
void ip_setup_inner(){
static void reset_btn_event_handler(lv_event_t * e){
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
global_config.ipConfigured = false;
ip_init_inner();
}
}
void ip_init_inner(){
lv_obj_clean(lv_scr_act());
if (global_config.ipConfigured) {
label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Connecting to Klipper");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
lv_obj_t * resetBtn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(resetBtn, reset_btn_event_handler, LV_EVENT_ALL, NULL);
lv_obj_align(resetBtn, LV_ALIGN_CENTER, 0, 40);
lv_obj_t * btnLabel = lv_label_create(resetBtn);
lv_label_set_text(btnLabel, "Reset");
lv_obj_center(btnLabel);
return;
}
lv_obj_t * keyboard = lv_keyboard_create(lv_scr_act());
label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Enter Klipper IP and Port");
@@ -95,18 +112,37 @@ void ip_setup_inner(){
lv_keyboard_set_textarea(keyboard, ipEntry);
}
void ip_setup(){
long last_data_update_ip = -10000;
const long data_update_interval_ip = 10000;
int retry_count = 0;
void ip_init(){
connect_ok = false;
retry_count = 0;
if (global_config.ipConfigured && retry_ip_verify()){
return;
}
ip_setup_inner();
ip_init_inner();
while (!connect_ok)
{
lv_timer_handler();
lv_task_handler();
if (!connect_ok && global_config.ipConfigured && (millis() - last_data_update_ip) > data_update_interval_ip){
connect_ok = verify_ip();
last_data_update_ip = millis();
retry_count++;
String retry_count_text = "Connecting to Klipper (Try " + String(retry_count + 1) + ")";
lv_label_set_text(label, retry_count_text.c_str());
}
}
}
void ip_ok(){
if (klipper_request_consecutive_fail_count > 5){
freeze_request_thread();
ip_init();
unfreeze_request_thread();
klipper_request_consecutive_fail_count = 0;
lv_msg_send(DATA_PRINTER_STATE, &printer);
}
}

View File

@@ -1 +1,2 @@
void ip_setup();
void ip_init();
void ip_ok();

View File

@@ -95,13 +95,20 @@ void print_panel_init(lv_obj_t* panel){
lv_obj_set_size(list, panel_width_margin, panel_height_margin);
lv_obj_align(list, LV_ALIGN_CENTER, 0, 0);
FILESYSTEM_FILE* files = get_files();
FILESYSTEM_FILE* files = get_files(25);
int count = 0;
while (files->name != NULL && count <= 20){
while (files != NULL && files->name != NULL && count <= 20){
lv_obj_t * btn = lv_list_add_btn(list, LV_SYMBOL_FILE, files->name);
lv_obj_add_event_cb(btn, btn_print_file_verify, LV_EVENT_CLICKED, (void*)files);
files += 1;
count++;
}
if (count <= 0){
lv_obj_del(list);
lv_obj_t * label = lv_label_create(panel);
lv_label_set_text(label, "Failed to read files.");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
}

View File

@@ -77,12 +77,11 @@ static void wifi_btn_event_handler(lv_event_t * e){
void wifi_init_inner(){
WiFi.disconnect();
lv_obj_clean(lv_scr_act());
if (global_config.wifiConfigured){
WiFi.begin(global_config.wifiSSID, global_config.wifiPassword);
lv_obj_clean(lv_scr_act());
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Connecting to WiFi");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
@@ -98,8 +97,6 @@ void wifi_init_inner(){
return;
}
lv_obj_clean(lv_scr_act());
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_label_set_text(label, "Scanning for networks...");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

View File

@@ -2,7 +2,7 @@
[![Donations](https://img.shields.io/badge/Support%20on-Ko--Fi-red)](https://ko-fi.com/suchmememanyskill)
# CYD-Klipper
An implementation of a Klipper status display on an ESP32 + screen. Uses Moonraker to fetch data.
An implementation of a wireless Klipper status display on an ESP32 + screen. Uses Moonraker to fetch data.
A simple and cheap solution to use a dedicated screen with Klipper, a 3d printing Firmware.
@@ -19,6 +19,7 @@ A ESP32-2432S028R is required to run this project. You can find out where to buy
- Move the printer
- Manage temperature
- Extrude/Retract filament
- Execute predefined gcode macros
### Install

View File

@@ -22,6 +22,7 @@
<p>An implementation of a Klipper status display on an ESP32 + screen.<br>Uses Moonraker to fetch data.</p>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/suchmememanyskill/CYD-Klipper">
<a href="https://github.com/suchmememanyskill/CYD-Klipper"><img alt="GitHub repo" src="https://img.shields.io/badge/Source-Github-blue.svg"></a>
<a href="https://ko-fi.com/suchmememanyskill"><img alt="Donate KoFi" src="https://img.shields.io/badge/Support%2FDonate%20On-Ko%20Fi-red"></a>
<section class="install">
<h3>Install</h3>
<p>Note: You may need to hold the 'BOOT' button on the device while pressing install</p>