--- # 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)]);