mirror of
https://github.com/randybb/esphome-configs.git
synced 2026-10-14 08:35:12 +02:00
Compare commits
3
Commits
3f9a8c3117
...
db57574497
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db57574497 | ||
|
|
fd7a4c98b5 | ||
|
|
2e8b678072 |
No files matched your search
+28781
File diff suppressed because it is too large.
Load diff
+86576
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,148 @@
|
||||
#pragma once
|
||||
// Moonraker client for mcu-cnc-pendant on its own FreeRTOS task: esp_http_client blocks
|
||||
// (DNS is not bounded by timeout_ms), so the main loop only queues commands and reads
|
||||
// the last polled status. An unreachable Klipper shows as "offline" instead of starving
|
||||
// the loop into the task watchdog.
|
||||
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "esp_http_client.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace moonraker {
|
||||
|
||||
struct Status {
|
||||
float pos[3]{};
|
||||
std::string homed;
|
||||
std::string state{"offline"};
|
||||
};
|
||||
|
||||
class Client {
|
||||
public:
|
||||
void start(const char *base) {
|
||||
this->base_ = base;
|
||||
xTaskCreate(task, "moonraker", 6144, this, 1, nullptr);
|
||||
}
|
||||
|
||||
// Run G-code; dropped when Klipper is not keeping up.
|
||||
void gcode(const std::string &script) {
|
||||
JsonDocument doc;
|
||||
doc["script"] = script;
|
||||
std::string body;
|
||||
serializeJson(doc, body);
|
||||
this->post("/printer/gcode/script", body);
|
||||
}
|
||||
|
||||
// urgent: throw away everything queued and send this first (emergency stop)
|
||||
void post(const std::string &path, const std::string &body = "", bool urgent = false) {
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
if (urgent) {
|
||||
this->queue_.clear();
|
||||
} else if (this->queue_.size() >= MAX_QUEUE) {
|
||||
return;
|
||||
}
|
||||
this->queue_.emplace_back(path, body);
|
||||
}
|
||||
|
||||
Status status() {
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
return this->status_;
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr size_t MAX_QUEUE = 8;
|
||||
static constexpr uint32_t POLL_MS = 250;
|
||||
static constexpr int TIMEOUT_MS = 1000;
|
||||
|
||||
static void task(void *arg) {
|
||||
auto *self = static_cast<Client *>(arg);
|
||||
uint32_t last_poll = 0;
|
||||
for (;;) {
|
||||
std::pair<std::string, std::string> cmd;
|
||||
bool have_cmd = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(self->mutex_);
|
||||
if (!self->queue_.empty()) {
|
||||
cmd = std::move(self->queue_.front());
|
||||
self->queue_.pop_front();
|
||||
have_cmd = true;
|
||||
}
|
||||
}
|
||||
if (have_cmd) {
|
||||
int code = self->request(cmd.first, &cmd.second, nullptr);
|
||||
if (code != 200)
|
||||
ESP_LOGW("moonraker", "POST %s failed (%d): %s", cmd.first.c_str(), code, cmd.second.c_str());
|
||||
continue; // drain commands before the next poll
|
||||
}
|
||||
if (esphome::millis() - last_poll >= POLL_MS) {
|
||||
last_poll = esphome::millis();
|
||||
self->poll();
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
|
||||
void poll() {
|
||||
std::string body;
|
||||
Status s;
|
||||
if (this->request("/printer/objects/query?gcode_move=gcode_position&toolhead=homed_axes&print_stats=state",
|
||||
nullptr, &body) == 200) {
|
||||
auto doc = esphome::json::parse_json(body);
|
||||
JsonObject st = doc["result"]["status"];
|
||||
JsonArray p = st["gcode_move"]["gcode_position"];
|
||||
for (int i = 0; i < 3; i++)
|
||||
s.pos[i] = p[i];
|
||||
s.homed = st["toolhead"]["homed_axes"].as<std::string>();
|
||||
s.state = st["print_stats"]["state"].as<std::string>();
|
||||
} else {
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
s.pos[0] = this->status_.pos[0]; // keep the last position, only flag offline
|
||||
s.pos[1] = this->status_.pos[1];
|
||||
s.pos[2] = this->status_.pos[2];
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(this->mutex_);
|
||||
this->status_ = std::move(s);
|
||||
}
|
||||
|
||||
// POST when body is set, GET otherwise; returns the HTTP status or -1
|
||||
int request(const std::string &path, const std::string *body, std::string *out) {
|
||||
std::string url = this->base_ + path;
|
||||
esp_http_client_config_t cfg{};
|
||||
cfg.url = url.c_str();
|
||||
cfg.timeout_ms = TIMEOUT_MS;
|
||||
cfg.method = body ? HTTP_METHOD_POST : HTTP_METHOD_GET;
|
||||
esp_http_client_handle_t c = esp_http_client_init(&cfg);
|
||||
if (c == nullptr)
|
||||
return -1;
|
||||
int code = -1;
|
||||
int len = body ? body->size() : 0;
|
||||
if (len > 0)
|
||||
esp_http_client_set_header(c, "Content-Type", "application/json");
|
||||
if (esp_http_client_open(c, len) == ESP_OK && (len == 0 || esp_http_client_write(c, body->data(), len) == len)) {
|
||||
esp_http_client_fetch_headers(c);
|
||||
code = esp_http_client_get_status_code(c);
|
||||
char buf[256];
|
||||
int n;
|
||||
while (out != nullptr && (n = esp_http_client_read(c, buf, sizeof(buf))) > 0)
|
||||
out->append(buf, n);
|
||||
}
|
||||
esp_http_client_cleanup(c);
|
||||
return code;
|
||||
}
|
||||
|
||||
std::string base_;
|
||||
std::mutex mutex_;
|
||||
std::deque<std::pair<std::string, std::string>> queue_;
|
||||
Status status_;
|
||||
};
|
||||
|
||||
inline Client client;
|
||||
|
||||
} // namespace moonraker
|
||||
@@ -0,0 +1,235 @@
|
||||
---
|
||||
# Hardware: M5Stack M5Stamp S3 in the ESPendant case (github.com/positron96/ESP32_CNC_Pendant),
|
||||
# talks to Klipper over Moonraker HTTP API
|
||||
# Pinout:
|
||||
# GPIO0 Stamp button (boot) -> stamp_button (binary_sensor gpio) (inverted)
|
||||
# GPIO1 Handwheel A -> handwheel (sensor rotary_encoder pin_a)
|
||||
# GPIO2 Handwheel B -> handwheel (sensor rotary_encoder pin_b)
|
||||
# GPIO4 Axis encoder A -> axis_encoder (sensor rotary_encoder pin_a)
|
||||
# GPIO5 Axis encoder B -> axis_encoder (sensor rotary_encoder pin_b)
|
||||
# GPIO6 Axis encoder push -> axis_button (binary_sensor gpio) (inverted)
|
||||
# GPIO7 Step encoder A -> step_encoder (sensor rotary_encoder pin_a)
|
||||
# GPIO8 Step encoder B -> step_encoder (sensor rotary_encoder pin_b)
|
||||
# GPIO9 Step encoder push -> step_button (binary_sensor gpio) (inverted)
|
||||
# GPIO10 BT1 -> bt1 (binary_sensor gpio) (inverted)
|
||||
# GPIO11 BT2 -> bt2 (binary_sensor gpio) (inverted)
|
||||
# GPIO12 BT3 -> bt3 (binary_sensor gpio) (inverted)
|
||||
# GPIO13 LCD E (SCK) -> spi (spi clk_pin)
|
||||
# GPIO14 LCD R/W (MOSI) -> spi (spi mosi_pin)
|
||||
# GPIO15 LCD RS (CS) -> lcd (display st7920 cs_pin)
|
||||
# GPIO21 RGB LED -> status_rgb (light esp32_rmt_led_strip)
|
||||
# GPIO39-44 free (BT4-BT6 later)
|
||||
# LCD RST: RC to 3.3 V, PSB to GND (serial mode)
|
||||
substitutions:
|
||||
device: cnc-pendant
|
||||
name: CNC Pendant
|
||||
area: Workshop
|
||||
comment: Klipper CNC pendant
|
||||
moonraker: http://klipper.local:7125
|
||||
max_feed: "3000" # mm/min, handwheel jog feed cap
|
||||
|
||||
esphome:
|
||||
name: mcu-${device}
|
||||
friendly_name: ${name}
|
||||
area: ${area}
|
||||
comment: ${comment}
|
||||
includes:
|
||||
- include/moonraker.h
|
||||
on_boot:
|
||||
- lambda: 'moonraker::client.start("${moonraker}");'
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_size: 8MB
|
||||
toolchain: esp-idf
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
packages:
|
||||
common: !include common/common.yaml
|
||||
# ipv6_only: !include common/ipv6_only.yaml
|
||||
status_led: !include
|
||||
file: common/status_led.yaml
|
||||
vars:
|
||||
light_id: status_rgb
|
||||
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: status_rgb
|
||||
name: LED
|
||||
pin: 21
|
||||
num_leds: 1
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
restore_mode: ALWAYS_OFF
|
||||
|
||||
# only links esp_http_client for include/moonraker.h, which runs requests on its own task
|
||||
http_request:
|
||||
|
||||
globals:
|
||||
- id: axis
|
||||
type: int
|
||||
initial_value: "0" # 0 X, 1 Y, 2 Z
|
||||
- id: step
|
||||
type: int
|
||||
initial_value: "1" # index into steps[] in the jog interval
|
||||
- id: wheel_sent
|
||||
type: float
|
||||
initial_value: "0"
|
||||
|
||||
interval:
|
||||
# handwheel: send the counts since the last jog as one relative move
|
||||
- interval: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
static const float steps[] = {0.01f, 0.1f, 1.0f};
|
||||
float delta = id(handwheel).state - id(wheel_sent);
|
||||
if (delta == 0 || std::isnan(delta)) return;
|
||||
id(wheel_sent) = id(handwheel).state;
|
||||
float dist = delta * steps[id(step)];
|
||||
// feed so the move takes ~100 ms, i.e. about one interval
|
||||
// ponytail: capped feed lets fast spins queue moves that keep running after the wheel stops; drop excess counts if that bites
|
||||
float feed = std::min(std::max(std::fabs(dist) * 600.0f, 60.0f), ${max_feed}.0f);
|
||||
moonraker::client.gcode(str_sprintf("G91\nG1 %c%.3f F%.0f\nG90", "XYZ"[id(axis)], dist, feed));
|
||||
|
||||
sensor:
|
||||
- platform: rotary_encoder
|
||||
id: handwheel
|
||||
pin_a:
|
||||
number: 1
|
||||
mode: INPUT_PULLUP
|
||||
pin_b:
|
||||
number: 2
|
||||
mode: INPUT_PULLUP
|
||||
resolution: 1 # calibration: counts per detent, flip pin_a/pin_b if direction is wrong
|
||||
- platform: rotary_encoder
|
||||
id: axis_encoder
|
||||
pin_a:
|
||||
number: 4
|
||||
mode: INPUT_PULLUP
|
||||
pin_b:
|
||||
number: 5
|
||||
mode: INPUT_PULLUP
|
||||
on_clockwise:
|
||||
- lambda: 'id(axis) = std::min(id(axis) + 1, 2);'
|
||||
on_anticlockwise:
|
||||
- lambda: 'id(axis) = std::max(id(axis) - 1, 0);'
|
||||
- platform: rotary_encoder
|
||||
id: step_encoder
|
||||
pin_a:
|
||||
number: 7
|
||||
mode: INPUT_PULLUP
|
||||
pin_b:
|
||||
number: 8
|
||||
mode: INPUT_PULLUP
|
||||
on_clockwise:
|
||||
- lambda: 'id(step) = std::min(id(step) + 1, 2);'
|
||||
on_anticlockwise:
|
||||
- lambda: 'id(step) = std::max(id(step) - 1, 0);'
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: stamp_button
|
||||
name: Stamp Button
|
||||
pin:
|
||||
number: 0
|
||||
inverted: true
|
||||
# axis push: zero the selected axis in work coordinates
|
||||
- platform: gpio
|
||||
id: axis_button
|
||||
name: Axis Button
|
||||
pin:
|
||||
number: 6
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
filters:
|
||||
- delayed_on_off: 20ms
|
||||
on_press:
|
||||
- lambda: 'moonraker::client.gcode(str_sprintf("G92 %c0", "XYZ"[id(axis)]));'
|
||||
- platform: gpio
|
||||
id: step_button
|
||||
name: Step Button
|
||||
pin:
|
||||
number: 9
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
filters:
|
||||
- delayed_on_off: 20ms
|
||||
# BT1: home all
|
||||
- platform: gpio
|
||||
id: bt1
|
||||
name: BT1
|
||||
pin:
|
||||
number: 10
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
filters:
|
||||
- delayed_on_off: 20ms
|
||||
on_press:
|
||||
- lambda: 'moonraker::client.gcode("G28");'
|
||||
# BT2: pause / resume
|
||||
- platform: gpio
|
||||
id: bt2
|
||||
name: BT2
|
||||
pin:
|
||||
number: 11
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
filters:
|
||||
- delayed_on_off: 20ms
|
||||
on_press:
|
||||
- lambda: 'moonraker::client.post(moonraker::client.status().state == "paused" ? "/printer/print/resume" : "/printer/print/pause");'
|
||||
# BT3: hold 1 s for emergency stop (Klipper needs FIRMWARE_RESTART afterwards)
|
||||
- platform: gpio
|
||||
id: bt3
|
||||
name: BT3
|
||||
pin:
|
||||
number: 12
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
filters:
|
||||
- delayed_on_off: 20ms
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at least 1s
|
||||
then:
|
||||
- lambda: 'moonraker::client.post("/printer/emergency_stop", "", true);'
|
||||
|
||||
font:
|
||||
# X11 misc-fixed bitmap fonts (as in u8g2 / Marlin), 6 px wide: axis letter + "-123.456" fits 64 px
|
||||
- file: assets/fonts/6x13.bdf
|
||||
id: font_big
|
||||
- file: assets/fonts/6x10.bdf
|
||||
id: font_small
|
||||
|
||||
spi:
|
||||
clk_pin: 13
|
||||
mosi_pin: 14
|
||||
|
||||
display:
|
||||
- platform: st7920
|
||||
id: lcd
|
||||
cs_pin:
|
||||
number: 15
|
||||
inverted: true # ST7920 RS/CS is active high
|
||||
data_rate: 1MHz # default 200kHz takes ~220 ms per frame; drop back if the picture garbles
|
||||
width: 128
|
||||
height: 64
|
||||
rotation: 90 # mounted portrait, 270 if upside down
|
||||
update_interval: 200ms
|
||||
lambda: |-
|
||||
static const char *steps[] = {"0.01", "0.1", "1"};
|
||||
auto s = moonraker::client.status();
|
||||
it.printf(0, 0, id(font_small), "%s", s.state.c_str());
|
||||
it.printf(0, 10, id(font_small), "H:%s", s.homed.empty() ? "-" : s.homed.c_str());
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int y = 22 + i * 14;
|
||||
Color c = COLOR_ON;
|
||||
if (i == id(axis)) {
|
||||
it.filled_rectangle(0, y, 64, 13);
|
||||
c = COLOR_OFF;
|
||||
}
|
||||
it.printf(1, y + 1, id(font_big), c, "%c", "XYZ"[i]);
|
||||
it.printf(63, y + 1, id(font_big), c, TextAlign::TOP_RIGHT, "%.3f", s.pos[i]);
|
||||
}
|
||||
it.printf(0, 118, id(font_small), "%s mm", steps[id(step)]);
|
||||
+5
-6
@@ -4,8 +4,8 @@
|
||||
# P0.06 UART0 TX -> logger (J-Link VCOM)
|
||||
# P0.13 LED 1 (active low) -> ANT+ Connected (a configured device is received)
|
||||
# P0.14 LED 2 (active low) -> ANT+ Unknown Device (an unconfigured one is in range)
|
||||
# P1.01 UART1 TX -> mcu-nrf-dk UART1 RX (P1.01), packet_transport; plus GND
|
||||
# P1.02 UART1 RX (unused)
|
||||
# P1.01 UART1 RX <- mcu-nrf-dk UART1 TX (P1.02), packet_transport
|
||||
# P1.02 UART1 TX -> mcu-nrf-dk UART1 RX (P1.01), packet_transport; plus GND
|
||||
# Radio: ANT+ receiver (RadiANT), no network of its own; values go to mcu-nrf-dk
|
||||
# ANT+ devices:
|
||||
# 655730 Garmin HRM-Pro heart rate
|
||||
@@ -147,10 +147,9 @@ ant_plus:
|
||||
uart:
|
||||
id: link
|
||||
port: UART1
|
||||
tx_pin: P1.01
|
||||
# unused, but must be set: the fork otherwise keeps the board's default RX, which on
|
||||
# the DK is P1.01 too, and the UART hears its own transmissions
|
||||
rx_pin: P1.02
|
||||
# the DK's default UART1 pins, same on both boards, wires crossed
|
||||
tx_pin: P1.02
|
||||
rx_pin: P1.01
|
||||
baud_rate: 115200
|
||||
|
||||
packet_transport:
|
||||
|
||||
+4
-1
@@ -9,7 +9,8 @@
|
||||
# P0.16 LED 4 (active low) -> LED 4 (light binary)
|
||||
# P0.24 Button 3 (active low) -> Button 3 (binary_sensor gpio)
|
||||
# P0.25 Button 4 (active low) -> Button 4 (binary_sensor gpio)
|
||||
# P1.01 UART1 RX <- mcu-nrf-ant UART1 TX (P1.01), packet_transport; plus GND
|
||||
# P1.01 UART1 RX <- mcu-nrf-ant UART1 TX (P1.02), packet_transport; plus GND
|
||||
# P1.02 UART1 TX -> mcu-nrf-ant UART1 RX (P1.01), packet_transport
|
||||
# ANT+ values (Garmin HRM-Pro, Concept2 PM5, Garmin Tempe, Garmin Varia RTL515) come from mcu-nrf-ant
|
||||
esphome:
|
||||
name: mcu-nrf-dk
|
||||
@@ -41,7 +42,9 @@ api:
|
||||
uart:
|
||||
id: ant_link
|
||||
port: UART1
|
||||
# the DK's default UART1 pins, same on both boards, wires crossed
|
||||
rx_pin: P1.01
|
||||
tx_pin: P1.02
|
||||
baud_rate: 115200
|
||||
|
||||
packet_transport:
|
||||
|
||||
Reference in new issue
Block a user