diff --git a/mcu-cnc-pendant.yaml b/mcu-cnc-pendant.yaml new file mode 100644 index 0000000..0fef64c --- /dev/null +++ b/mcu-cnc-pendant.yaml @@ -0,0 +1,271 @@ +--- +# 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} + +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 + +http_request: + timeout: 1s + +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 jog_interval + - id: wheel_sent + type: float + initial_value: "0" + - id: pos + type: std::array + - id: homed + type: std::string + - id: state + type: std::string + initial_value: '"offline"' + +script: + - id: gcode + mode: queued + max_runs: 5 + parameters: + cmd: string + then: + - http_request.post: + url: ${moonraker}/printer/gcode/script + json: 'root["script"] = cmd;' + on_error: + - logger.log: + format: "gcode failed: %s" + args: [cmd.c_str()] + +interval: + # handwheel: send the counts since the last jog as one relative move + - id: jog_interval + 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); + id(gcode)->execute(str_sprintf("G91\nG1 %c%.3f F%.0f\nG90", "XYZ"[id(axis)], dist, feed)); + # DRO + - interval: 250ms + then: + - http_request.get: + url: ${moonraker}/printer/objects/query?gcode_move=gcode_position&toolhead=homed_axes&print_stats=state + capture_response: true + on_response: + - lambda: |- + json::parse_json(body, [](JsonObject root) -> bool { + JsonObject s = root["result"]["status"]; + JsonArray p = s["gcode_move"]["gcode_position"]; + for (int i = 0; i < 3; i++) id(pos)[i] = p[i]; + id(homed) = s["toolhead"]["homed_axes"].as(); + id(state) = s["print_stats"]["state"].as(); + return true; + }); + on_error: + - lambda: 'id(state) = "offline";' + +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: 'id(gcode)->execute(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: + - script.execute: + id: gcode + cmd: 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: + - http_request.post: + url: !lambda 'return std::string("${moonraker}/printer/print/") + (id(state) == "paused" ? "resume" : "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: + - http_request.post: ${moonraker}/printer/emergency_stop + +font: + - file: "gfonts://Roboto Mono@700" + id: font_big + size: 14 + - file: "gfonts://Roboto Mono" + id: font_small + size: 10 + +spi: + clk_pin: 13 + mosi_pin: 14 + +display: + - platform: st7920 + id: lcd + cs_pin: 15 + width: 128 + height: 64 + update_interval: 200ms + lambda: |- + static const char *steps[] = {"0.01", "0.1", "1"}; + it.printf(0, 0, id(font_small), "%s", id(state).c_str()); + it.printf(127, 0, id(font_small), TextAlign::TOP_RIGHT, "H:%s", id(homed).empty() ? "-" : id(homed).c_str()); + for (int i = 0; i < 3; i++) { + int y = 12 + i * 15; + Color c = COLOR_ON; + if (i == id(axis)) { + it.filled_rectangle(0, y, 128, 15); + c = COLOR_OFF; + } + it.printf(2, y, id(font_big), c, "%c", "XYZ"[i]); + it.printf(125, y, id(font_big), c, TextAlign::TOP_RIGHT, "%9.3f", id(pos)[i]); + } + it.printf(0, 54, id(font_small), "step %s mm", steps[id(step)]);