1077 lines
32 KiB
YAML
1077 lines
32 KiB
YAML
#:########################################################################################:#
|
|
# TITLE: ASTRONOMICAL CLOCK STEPPER
|
|
# zorruno.com layout v1.1 2026
|
|
#:########################################################################################:#
|
|
# REPO:
|
|
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-astroclockstepper.yaml
|
|
#:########################################################################################:#
|
|
# VERSIONS:
|
|
# V1.2 2026-06-22 Added DHCP IPv4 address flash sequence on onboard LED after WiFi connects
|
|
# V1.1 2026-06-22 Added onboard D1 Mini LED flash on each motor step
|
|
# V1.0 2026-06-22 Initial Version
|
|
#:########################################################################################:#
|
|
# HARDWARE:
|
|
# ESP8266 D1 Mini Clone
|
|
# 5V 28BYJ-48 Stepper Motor
|
|
# Keyestudio / ULN2003 style 5V stepper driver board
|
|
#
|
|
# Stepper Driver:
|
|
# - IN1: GPIO14 / D5
|
|
# - IN2: GPIO12 / D6
|
|
# - IN3: GPIO13 / D7
|
|
# - IN4: GPIO15 / D8
|
|
# - VCC: 5V motor supply
|
|
# - GND: Common GND with ESP8266
|
|
#
|
|
# Physical Buttons:
|
|
# - Fast Forward: GPIO5 / D1
|
|
# - Slow Forward: GPIO4 / D2
|
|
# - Fast Reverse: GPIO0 / D3
|
|
# - Slow Reverse: GPIO16 / D0
|
|
#
|
|
# Button board:
|
|
# - Uses built-in 103 pull-up resistors, 10k ohm
|
|
# - Buttons should pull the GPIO pin to GND when pressed
|
|
# - Pull-ups must go to 3.3V, not 5V
|
|
#
|
|
# Onboard Step LED:
|
|
# - GPIO2 / D4
|
|
# - Active-low on most D1 Mini boards
|
|
#:########################################################################################:#
|
|
# OPERATION NOTES:
|
|
# - Normal rotation starts automatically after boot.
|
|
# - Default rotation is one full revolution every 365.2422 days.
|
|
# - Default step count is 4096 steps per full revolution.
|
|
# - Normal movement is extremely slow, about one step every 2.14 hours.
|
|
# - Motor coils are powered only while stepping, then released.
|
|
# - No holding torque is used.
|
|
# - Web template switches mimic held physical buttons.
|
|
# - Manual movement only applies while a physical button or web switch is held/on.
|
|
# - Position counter wraps back to zero after one full rotation.
|
|
# - Onboard D1 Mini LED flashes briefly every time a motor step is issued.
|
|
# - After WiFi connects, the onboard LED flashes the DHCP IPv4 address.
|
|
# - IPv4 flash sequence is decimal digit based.
|
|
# - A digit of 0 is represented as 10 flashes.
|
|
#:########################################################################################:#
|
|
# OFFLINE NOTES:
|
|
# a) Home Assistant OFFLINE, but Network ONLINE
|
|
# - Device remains controllable via local web portal
|
|
# - HA entities unavailable until HA returns
|
|
# b) WiFi/Network OFFLINE
|
|
# - No HA API or web control available
|
|
# - Physical buttons continue to work
|
|
# - Normal motor timing continues from device uptime
|
|
# - Fallback AP / captive portal should become available from network_common_dhcp.yaml
|
|
# c) Device power loss / reboot
|
|
# - V1 does not restore exact astronomical position after reboot
|
|
# - Re-align manually using the web or physical button controls
|
|
#:########################################################################################:#
|
|
|
|
|
|
#:########################################################################################:#
|
|
# SUBSTITUTIONS: Specific device variable substitutions
|
|
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
|
|
#:########################################################################################:#
|
|
substitutions:
|
|
# Device Naming
|
|
device_name: "esp-astroclockstepper1"
|
|
friendly_name: "Astronomical Clock Stepper"
|
|
description_comment: "Astronomical Clock Stepper :: ESP8266 D1 Mini with 28BYJ-48 Stepper Motor (Layout V1.1)"
|
|
device_area: "Standalone"
|
|
|
|
# Project Naming
|
|
project_name: "zorruno.Astronomical Clock Stepper"
|
|
project_version: "v1.2"
|
|
|
|
# Passwords & Secrets (Unfortunately, you cannot use substitutions inside secret names)
|
|
api_key: !secret esp-api_key
|
|
ota_pass: !secret esp-ota_pass
|
|
|
|
# Device Settings
|
|
log_level: "INFO"
|
|
sensor_update_interval: "1s"
|
|
calculated_sensor_update_interval: "60s"
|
|
|
|
# Astronomical Clock Settings
|
|
rotation_days: "365.2422"
|
|
steps_per_rotation: "4096"
|
|
normal_direction_name: "Clockwise"
|
|
|
|
# Use 1 for normal direction.
|
|
# Use -1 if the motor rotates the wrong way.
|
|
normal_direction_multiplier: "1"
|
|
|
|
# Manual Movement Speeds
|
|
manual_fast_rotation_seconds: "30"
|
|
manual_slow_rotation_seconds: "300"
|
|
|
|
# Coil Release
|
|
coil_release_ms: "500"
|
|
|
|
# Step LED
|
|
onboard_step_led_pin: GPIO2
|
|
step_led_flash_ms: "25"
|
|
|
|
# DHCP IP Address LED Flash Settings
|
|
ip_flash_enabled: "true"
|
|
ip_flash_repeat_count: "5"
|
|
ip_flash_zero_digit_blinks: "10"
|
|
ip_flash_loop_interval: "25ms"
|
|
|
|
# Suggested human-countable timing
|
|
ip_flash_on_ms: "200"
|
|
ip_flash_between_flash_gap_ms: "450"
|
|
ip_flash_digit_gap_ms: "1500"
|
|
ip_flash_octet_gap_ms: "3500"
|
|
ip_flash_repeat_gap_ms: "8000"
|
|
|
|
# Stepper Output Pins
|
|
stepper_in1_pin: GPIO14
|
|
stepper_in2_pin: GPIO12
|
|
stepper_in3_pin: GPIO13
|
|
stepper_in4_pin: GPIO15
|
|
|
|
# Physical Button Input Pins
|
|
button_fast_forward_pin: GPIO5
|
|
button_slow_forward_pin: GPIO4
|
|
button_fast_reverse_pin: GPIO0
|
|
button_slow_reverse_pin: GPIO16
|
|
|
|
# Entity Naming
|
|
web_fast_forward_name: "Web Fast Forward"
|
|
web_slow_forward_name: "Web Slow Forward"
|
|
web_fast_reverse_name: "Web Fast Reverse"
|
|
web_slow_reverse_name: "Web Slow Reverse"
|
|
|
|
physical_fast_forward_name: "Physical Fast Forward"
|
|
physical_slow_forward_name: "Physical Slow Forward"
|
|
physical_fast_reverse_name: "Physical Fast Reverse"
|
|
physical_slow_reverse_name: "Physical Slow Reverse"
|
|
|
|
zero_position_name: "Zero Position Counter"
|
|
replay_ip_flash_name: "Replay IP Address Flash"
|
|
|
|
|
|
#:########################################################################################:#
|
|
# PACKAGES: Included Common Packages
|
|
# https://esphome.io/components/packages.html
|
|
#:########################################################################################:#
|
|
packages:
|
|
#### WIFI, Network (DHCP/IPV6 etc), Fallback AP, Safemode ####
|
|
common_wifi: !include
|
|
file: common/network_common_dhcp.yaml
|
|
vars:
|
|
local_device_name: "${device_name}"
|
|
local_ota_pass: "${ota_pass}"
|
|
|
|
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
|
|
common_api: !include
|
|
file: common/api_common.yaml
|
|
#file: common/api_common_noencryption.yaml
|
|
vars:
|
|
local_api_key: "${api_key}"
|
|
|
|
#### MQTT ####
|
|
#common_mqtt: !include
|
|
# file: common/mqtt_common.yaml
|
|
# vars:
|
|
# local_device_name: "${device_name}"
|
|
|
|
#### WEB PORTAL ####
|
|
#common_webportal: !include common/webportal_common.yaml
|
|
common_webportal: !include common/webportal_common_nopasswd.yaml
|
|
|
|
#### SNTP (Only use if you want/need accurate timeclocks) ####
|
|
#common_sntp: !include common/sntp_common.yaml
|
|
|
|
#### DIAGNOSTICS Sensors ####
|
|
diag_basic: !include common/include_basic_diag_sensors.yaml
|
|
#diag_more: !include common/include_more_diag_sensors.yaml
|
|
#diag_debug: !include common/include_debug_diag_sensors.yaml
|
|
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
|
|
|
|
|
|
#:########################################################################################:#
|
|
# ESPHOME:
|
|
# https://esphome.io/components/esphome.html
|
|
#:########################################################################################:#
|
|
esphome:
|
|
name: "${device_name}"
|
|
friendly_name: "${friendly_name}"
|
|
comment: "${description_comment}"
|
|
area: "${device_area}"
|
|
project:
|
|
name: "${project_name}"
|
|
version: "${project_version}"
|
|
|
|
on_boot:
|
|
priority: -100
|
|
then:
|
|
- output.turn_off: onboard_step_led
|
|
- logger.log:
|
|
level: INFO
|
|
format: "Astronomical Clock Stepper started. Normal rotation is %s days per full revolution."
|
|
args:
|
|
- '"${rotation_days}"'
|
|
|
|
# Wait for WiFi/DHCP, then flash the assigned IPv4 address.
|
|
# This does not stop the main stepper scheduler from running.
|
|
- wait_until:
|
|
condition:
|
|
wifi.connected:
|
|
- delay: 3s
|
|
- script.execute: start_ip_flash_script
|
|
|
|
|
|
#:########################################################################################:#
|
|
# ESP PLATFORM AND FRAMEWORK:
|
|
# https://esphome.io/components/esp8266.html
|
|
#:########################################################################################:#
|
|
esp8266:
|
|
board: d1_mini
|
|
restore_from_flash: true
|
|
|
|
preferences:
|
|
flash_write_interval: 5min
|
|
|
|
mdns:
|
|
disabled: false
|
|
|
|
|
|
#:########################################################################################:#
|
|
# LOGGING:
|
|
# https://esphome.io/components/logger.html
|
|
#:########################################################################################:#
|
|
logger:
|
|
level: "${log_level}"
|
|
baud_rate: 0
|
|
|
|
|
|
#:########################################################################################:#
|
|
# GLOBALS:
|
|
# Internal position, timing, mode, held-button state, and IP flash state
|
|
# https://esphome.io/components/globals.html
|
|
#:########################################################################################:#
|
|
globals:
|
|
- id: current_position_step
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: current_phase_index
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: last_loop_ms
|
|
type: uint32_t
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: last_step_ms
|
|
type: uint32_t
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: step_accumulator_ms
|
|
type: double
|
|
restore_value: no
|
|
initial_value: "0.0"
|
|
|
|
- id: coils_are_on
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: step_led_is_on
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: step_led_started_ms
|
|
type: uint32_t
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
# 0 = Normal
|
|
# 1 = Fast Forward
|
|
# 2 = Slow Forward
|
|
# 3 = Fast Reverse
|
|
# 4 = Slow Reverse
|
|
- id: current_motion_mode
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
# Physical Button States
|
|
- id: physical_fast_forward_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: physical_slow_forward_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: physical_fast_reverse_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: physical_slow_reverse_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
# Web Button States
|
|
- id: web_fast_forward_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: web_slow_forward_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: web_fast_reverse_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: web_slow_reverse_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
# IP Address Flash State
|
|
- id: ip_flash_active
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: ip_flash_led_is_on
|
|
type: bool
|
|
restore_value: no
|
|
initial_value: "false"
|
|
|
|
- id: ip_flash_next_action_ms
|
|
type: uint32_t
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_repeats_completed
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_octet_index
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_digit_index
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_blinks_completed
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_octet_0
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_octet_1
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_octet_2
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
- id: ip_flash_octet_3
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
|
|
#:########################################################################################:#
|
|
# OUTPUT:
|
|
# Stepper driver GPIO outputs and onboard step LED
|
|
# https://esphome.io/components/output/gpio.html
|
|
#:########################################################################################:#
|
|
output:
|
|
- platform: gpio
|
|
id: stepper_in1
|
|
pin: ${stepper_in1_pin}
|
|
|
|
- platform: gpio
|
|
id: stepper_in2
|
|
pin: ${stepper_in2_pin}
|
|
|
|
- platform: gpio
|
|
id: stepper_in3
|
|
pin: ${stepper_in3_pin}
|
|
|
|
- platform: gpio
|
|
id: stepper_in4
|
|
pin: ${stepper_in4_pin}
|
|
|
|
# D1 Mini onboard LED GPIO2 / D4 and active-low
|
|
- platform: gpio
|
|
id: onboard_step_led
|
|
pin:
|
|
number: ${onboard_step_led_pin}
|
|
inverted: true
|
|
|
|
|
|
#:########################################################################################:#
|
|
# SCRIPT:
|
|
# Utility scripts
|
|
# https://esphome.io/components/script.html
|
|
#:########################################################################################:#
|
|
script:
|
|
- id: start_ip_flash_script
|
|
mode: restart
|
|
then:
|
|
- lambda: |-
|
|
const bool IP_FLASH_ENABLED = ${ip_flash_enabled};
|
|
|
|
if (!IP_FLASH_ENABLED) {
|
|
ESP_LOGI("ip_flash", "IP flash is disabled.");
|
|
return;
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
ESP_LOGW("ip_flash", "WiFi is not connected. Cannot flash IP address.");
|
|
return;
|
|
}
|
|
|
|
IPAddress ip = WiFi.localIP();
|
|
|
|
if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0) {
|
|
ESP_LOGW("ip_flash", "Invalid IPv4 address 0.0.0.0. Cannot flash IP address.");
|
|
return;
|
|
}
|
|
|
|
id(ip_flash_octet_0) = ip[0];
|
|
id(ip_flash_octet_1) = ip[1];
|
|
id(ip_flash_octet_2) = ip[2];
|
|
id(ip_flash_octet_3) = ip[3];
|
|
|
|
id(ip_flash_repeats_completed) = 0;
|
|
id(ip_flash_octet_index) = 0;
|
|
id(ip_flash_digit_index) = 0;
|
|
id(ip_flash_blinks_completed) = 0;
|
|
id(ip_flash_led_is_on) = false;
|
|
id(ip_flash_next_action_ms) = millis();
|
|
id(ip_flash_active) = true;
|
|
|
|
// Keep the IP flash sequence visually clean.
|
|
id(step_led_is_on) = false;
|
|
id(onboard_step_led).turn_off();
|
|
|
|
ESP_LOGI(
|
|
"ip_flash",
|
|
"Flashing IPv4 address %u.%u.%u.%u on onboard LED. Repeats: %d",
|
|
ip[0],
|
|
ip[1],
|
|
ip[2],
|
|
ip[3],
|
|
${ip_flash_repeat_count}
|
|
);
|
|
|
|
|
|
#:########################################################################################:#
|
|
# SWITCH:
|
|
# Web held-button controls
|
|
# https://esphome.io/components/switch/template.html
|
|
#:########################################################################################:#
|
|
switch:
|
|
- platform: template
|
|
name: "${web_fast_forward_name}"
|
|
id: web_fast_forward_switch
|
|
icon: mdi:fast-forward
|
|
optimistic: true
|
|
restore_mode: ALWAYS_OFF
|
|
turn_on_action:
|
|
- switch.turn_off: web_slow_forward_switch
|
|
- switch.turn_off: web_fast_reverse_switch
|
|
- switch.turn_off: web_slow_reverse_switch
|
|
- globals.set:
|
|
id: web_fast_forward_active
|
|
value: "true"
|
|
turn_off_action:
|
|
- globals.set:
|
|
id: web_fast_forward_active
|
|
value: "false"
|
|
|
|
- platform: template
|
|
name: "${web_slow_forward_name}"
|
|
id: web_slow_forward_switch
|
|
icon: mdi:play-speed
|
|
optimistic: true
|
|
restore_mode: ALWAYS_OFF
|
|
turn_on_action:
|
|
- switch.turn_off: web_fast_forward_switch
|
|
- switch.turn_off: web_fast_reverse_switch
|
|
- switch.turn_off: web_slow_reverse_switch
|
|
- globals.set:
|
|
id: web_slow_forward_active
|
|
value: "true"
|
|
turn_off_action:
|
|
- globals.set:
|
|
id: web_slow_forward_active
|
|
value: "false"
|
|
|
|
- platform: template
|
|
name: "${web_fast_reverse_name}"
|
|
id: web_fast_reverse_switch
|
|
icon: mdi:rewind
|
|
optimistic: true
|
|
restore_mode: ALWAYS_OFF
|
|
turn_on_action:
|
|
- switch.turn_off: web_fast_forward_switch
|
|
- switch.turn_off: web_slow_forward_switch
|
|
- switch.turn_off: web_slow_reverse_switch
|
|
- globals.set:
|
|
id: web_fast_reverse_active
|
|
value: "true"
|
|
turn_off_action:
|
|
- globals.set:
|
|
id: web_fast_reverse_active
|
|
value: "false"
|
|
|
|
- platform: template
|
|
name: "${web_slow_reverse_name}"
|
|
id: web_slow_reverse_switch
|
|
icon: mdi:rewind-outline
|
|
optimistic: true
|
|
restore_mode: ALWAYS_OFF
|
|
turn_on_action:
|
|
- switch.turn_off: web_fast_forward_switch
|
|
- switch.turn_off: web_slow_forward_switch
|
|
- switch.turn_off: web_fast_reverse_switch
|
|
- globals.set:
|
|
id: web_slow_reverse_active
|
|
value: "true"
|
|
turn_off_action:
|
|
- globals.set:
|
|
id: web_slow_reverse_active
|
|
value: "false"
|
|
|
|
|
|
#:########################################################################################:#
|
|
# BINARY SENSORS:
|
|
# Physical button inputs using external 103 / 10k pull-ups
|
|
# https://esphome.io/components/binary_sensor/gpio.html
|
|
#:########################################################################################:#
|
|
binary_sensor:
|
|
- platform: gpio
|
|
name: "${physical_fast_forward_name}"
|
|
id: physical_fast_forward_button
|
|
pin:
|
|
number: ${button_fast_forward_pin}
|
|
mode: INPUT
|
|
inverted: true
|
|
filters:
|
|
- delayed_on: 20ms
|
|
- delayed_off: 20ms
|
|
on_press:
|
|
- globals.set:
|
|
id: physical_fast_forward_active
|
|
value: "true"
|
|
on_release:
|
|
- globals.set:
|
|
id: physical_fast_forward_active
|
|
value: "false"
|
|
|
|
- platform: gpio
|
|
name: "${physical_slow_forward_name}"
|
|
id: physical_slow_forward_button
|
|
pin:
|
|
number: ${button_slow_forward_pin}
|
|
mode: INPUT
|
|
inverted: true
|
|
filters:
|
|
- delayed_on: 20ms
|
|
- delayed_off: 20ms
|
|
on_press:
|
|
- globals.set:
|
|
id: physical_slow_forward_active
|
|
value: "true"
|
|
on_release:
|
|
- globals.set:
|
|
id: physical_slow_forward_active
|
|
value: "false"
|
|
|
|
- platform: gpio
|
|
name: "${physical_fast_reverse_name}"
|
|
id: physical_fast_reverse_button
|
|
pin:
|
|
number: ${button_fast_reverse_pin}
|
|
mode: INPUT
|
|
inverted: true
|
|
filters:
|
|
- delayed_on: 20ms
|
|
- delayed_off: 20ms
|
|
on_press:
|
|
- globals.set:
|
|
id: physical_fast_reverse_active
|
|
value: "true"
|
|
on_release:
|
|
- globals.set:
|
|
id: physical_fast_reverse_active
|
|
value: "false"
|
|
|
|
- platform: gpio
|
|
name: "${physical_slow_reverse_name}"
|
|
id: physical_slow_reverse_button
|
|
pin:
|
|
number: ${button_slow_reverse_pin}
|
|
mode: INPUT
|
|
inverted: true
|
|
filters:
|
|
- delayed_on: 20ms
|
|
- delayed_off: 20ms
|
|
on_press:
|
|
- globals.set:
|
|
id: physical_slow_reverse_active
|
|
value: "true"
|
|
on_release:
|
|
- globals.set:
|
|
id: physical_slow_reverse_active
|
|
value: "false"
|
|
|
|
|
|
#:########################################################################################:#
|
|
# BUTTON:
|
|
# Utility buttons
|
|
# https://esphome.io/components/button/template.html
|
|
#:########################################################################################:#
|
|
button:
|
|
- platform: template
|
|
name: "${zero_position_name}"
|
|
id: zero_position_counter_button
|
|
icon: mdi:counter
|
|
on_press:
|
|
- globals.set:
|
|
id: current_position_step
|
|
value: "0"
|
|
- globals.set:
|
|
id: current_phase_index
|
|
value: "0"
|
|
- globals.set:
|
|
id: step_accumulator_ms
|
|
value: "0.0"
|
|
- logger.log:
|
|
level: INFO
|
|
format: "Position counter zeroed."
|
|
|
|
- platform: template
|
|
name: "${replay_ip_flash_name}"
|
|
id: replay_ip_flash_button
|
|
icon: mdi:ip-network
|
|
on_press:
|
|
- script.execute: start_ip_flash_script
|
|
|
|
|
|
#:########################################################################################:#
|
|
# SENSOR:
|
|
# Position and calculated movement sensors
|
|
# https://esphome.io/components/sensor/template.html
|
|
#:########################################################################################:#
|
|
sensor:
|
|
- platform: template
|
|
name: "Position Days"
|
|
id: position_days_sensor
|
|
icon: mdi:calendar-clock
|
|
unit_of_measurement: "days"
|
|
accuracy_decimals: 4
|
|
update_interval: "${sensor_update_interval}"
|
|
lambda: |-
|
|
return ((double) id(current_position_step) * ${rotation_days}) / ${steps_per_rotation};
|
|
|
|
- platform: template
|
|
name: "Position Step"
|
|
id: position_step_sensor
|
|
icon: mdi:counter
|
|
accuracy_decimals: 0
|
|
update_interval: "${sensor_update_interval}"
|
|
lambda: |-
|
|
return id(current_position_step);
|
|
|
|
- platform: template
|
|
name: "Days Per Step"
|
|
id: days_per_step_sensor
|
|
icon: mdi:calendar-arrow-right
|
|
unit_of_measurement: "days/step"
|
|
accuracy_decimals: 8
|
|
update_interval: "${calculated_sensor_update_interval}"
|
|
lambda: |-
|
|
return ${rotation_days} / ${steps_per_rotation};
|
|
|
|
- platform: template
|
|
name: "Hours Per Step"
|
|
id: hours_per_step_sensor
|
|
icon: mdi:timer-outline
|
|
unit_of_measurement: "h/step"
|
|
accuracy_decimals: 8
|
|
update_interval: "${calculated_sensor_update_interval}"
|
|
lambda: |-
|
|
return (${rotation_days} * 24.0) / ${steps_per_rotation};
|
|
|
|
- platform: template
|
|
name: "Normal Step Interval"
|
|
id: normal_step_interval_sensor
|
|
icon: mdi:timer-cog-outline
|
|
unit_of_measurement: "s/step"
|
|
accuracy_decimals: 4
|
|
update_interval: "${calculated_sensor_update_interval}"
|
|
lambda: |-
|
|
return (${rotation_days} * 24.0 * 60.0 * 60.0) / ${steps_per_rotation};
|
|
|
|
|
|
#:########################################################################################:#
|
|
# TEXT SENSOR:
|
|
# Human-readable status sensors
|
|
# https://esphome.io/components/text_sensor/template.html
|
|
#:########################################################################################:#
|
|
text_sensor:
|
|
- platform: template
|
|
name: "Motion Mode"
|
|
id: motion_mode_text_sensor
|
|
icon: mdi:rotate-3d-variant
|
|
update_interval: "${sensor_update_interval}"
|
|
lambda: |-
|
|
switch (id(current_motion_mode)) {
|
|
case 1:
|
|
return {"Fast Forward"};
|
|
case 2:
|
|
return {"Slow Forward"};
|
|
case 3:
|
|
return {"Fast Reverse"};
|
|
case 4:
|
|
return {"Slow Reverse"};
|
|
case 0:
|
|
default:
|
|
return {"Normal"};
|
|
}
|
|
|
|
- platform: template
|
|
name: "Normal Direction"
|
|
id: normal_direction_text_sensor
|
|
icon: mdi:rotate-right
|
|
update_interval: "${calculated_sensor_update_interval}"
|
|
lambda: |-
|
|
return {"${normal_direction_name}"};
|
|
|
|
|
|
#:########################################################################################:#
|
|
# INTERVAL:
|
|
# Stepper scheduler and IP flash scheduler
|
|
# https://esphome.io/components/interval.html
|
|
#:########################################################################################:#
|
|
interval:
|
|
- interval: 5ms
|
|
then:
|
|
- lambda: |-
|
|
const int STEPS_PER_ROTATION = ${steps_per_rotation};
|
|
const int NORMAL_DIRECTION_MULTIPLIER = ${normal_direction_multiplier};
|
|
|
|
const double NORMAL_STEP_INTERVAL_MS =
|
|
(${rotation_days} * 24.0 * 60.0 * 60.0 * 1000.0) / ${steps_per_rotation};
|
|
|
|
const double FAST_STEP_INTERVAL_MS =
|
|
(${manual_fast_rotation_seconds} * 1000.0) / ${steps_per_rotation};
|
|
|
|
const double SLOW_STEP_INTERVAL_MS =
|
|
(${manual_slow_rotation_seconds} * 1000.0) / ${steps_per_rotation};
|
|
|
|
const uint32_t COIL_RELEASE_MS = ${coil_release_ms};
|
|
const uint32_t STEP_LED_FLASH_MS = ${step_led_flash_ms};
|
|
|
|
auto all_coils_off = [&]() {
|
|
id(stepper_in1).turn_off();
|
|
id(stepper_in2).turn_off();
|
|
id(stepper_in3).turn_off();
|
|
id(stepper_in4).turn_off();
|
|
id(coils_are_on) = false;
|
|
};
|
|
|
|
auto flash_step_led = [&](uint32_t now_ms) {
|
|
if (!id(ip_flash_active)) {
|
|
id(onboard_step_led).turn_on();
|
|
id(step_led_started_ms) = now_ms;
|
|
id(step_led_is_on) = true;
|
|
}
|
|
};
|
|
|
|
auto set_coils_for_phase = [&](int phase) {
|
|
const bool sequence[8][4] = {
|
|
{true, false, false, false},
|
|
{true, true, false, false},
|
|
{false, true, false, false},
|
|
{false, true, true, false},
|
|
{false, false, true, false},
|
|
{false, false, true, true },
|
|
{false, false, false, true },
|
|
{true, false, false, true }
|
|
};
|
|
|
|
phase = ((phase % 8) + 8) % 8;
|
|
|
|
if (sequence[phase][0]) {
|
|
id(stepper_in1).turn_on();
|
|
} else {
|
|
id(stepper_in1).turn_off();
|
|
}
|
|
|
|
if (sequence[phase][1]) {
|
|
id(stepper_in2).turn_on();
|
|
} else {
|
|
id(stepper_in2).turn_off();
|
|
}
|
|
|
|
if (sequence[phase][2]) {
|
|
id(stepper_in3).turn_on();
|
|
} else {
|
|
id(stepper_in3).turn_off();
|
|
}
|
|
|
|
if (sequence[phase][3]) {
|
|
id(stepper_in4).turn_on();
|
|
} else {
|
|
id(stepper_in4).turn_off();
|
|
}
|
|
|
|
id(coils_are_on) = true;
|
|
};
|
|
|
|
auto do_one_step = [&](int direction, uint32_t now_ms) {
|
|
if (direction >= 0) {
|
|
id(current_phase_index) = (id(current_phase_index) + 1) % 8;
|
|
id(current_position_step) = (id(current_position_step) + 1) % STEPS_PER_ROTATION;
|
|
} else {
|
|
id(current_phase_index) = (id(current_phase_index) + 7) % 8;
|
|
id(current_position_step) = (id(current_position_step) + STEPS_PER_ROTATION - 1) % STEPS_PER_ROTATION;
|
|
}
|
|
|
|
set_coils_for_phase(id(current_phase_index));
|
|
id(last_step_ms) = now_ms;
|
|
flash_step_led(now_ms);
|
|
};
|
|
|
|
const bool fast_forward_active =
|
|
id(physical_fast_forward_active) || id(web_fast_forward_active);
|
|
|
|
const bool slow_forward_active =
|
|
id(physical_slow_forward_active) || id(web_slow_forward_active);
|
|
|
|
const bool fast_reverse_active =
|
|
id(physical_fast_reverse_active) || id(web_fast_reverse_active);
|
|
|
|
const bool slow_reverse_active =
|
|
id(physical_slow_reverse_active) || id(web_slow_reverse_active);
|
|
|
|
int selected_mode = 0;
|
|
int selected_direction = NORMAL_DIRECTION_MULTIPLIER;
|
|
double selected_step_interval_ms = NORMAL_STEP_INTERVAL_MS;
|
|
|
|
if (fast_forward_active) {
|
|
selected_mode = 1;
|
|
selected_direction = NORMAL_DIRECTION_MULTIPLIER;
|
|
selected_step_interval_ms = FAST_STEP_INTERVAL_MS;
|
|
} else if (slow_forward_active) {
|
|
selected_mode = 2;
|
|
selected_direction = NORMAL_DIRECTION_MULTIPLIER;
|
|
selected_step_interval_ms = SLOW_STEP_INTERVAL_MS;
|
|
} else if (fast_reverse_active) {
|
|
selected_mode = 3;
|
|
selected_direction = -NORMAL_DIRECTION_MULTIPLIER;
|
|
selected_step_interval_ms = FAST_STEP_INTERVAL_MS;
|
|
} else if (slow_reverse_active) {
|
|
selected_mode = 4;
|
|
selected_direction = -NORMAL_DIRECTION_MULTIPLIER;
|
|
selected_step_interval_ms = SLOW_STEP_INTERVAL_MS;
|
|
}
|
|
|
|
const uint32_t now_ms = millis();
|
|
|
|
if (id(last_loop_ms) == 0) {
|
|
id(last_loop_ms) = now_ms;
|
|
id(current_motion_mode) = selected_mode;
|
|
id(step_accumulator_ms) = 0.0;
|
|
return;
|
|
}
|
|
|
|
const uint32_t elapsed_ms = now_ms - id(last_loop_ms);
|
|
id(last_loop_ms) = now_ms;
|
|
|
|
if (selected_mode != id(current_motion_mode)) {
|
|
id(current_motion_mode) = selected_mode;
|
|
id(step_accumulator_ms) = 0.0;
|
|
}
|
|
|
|
id(step_accumulator_ms) += (double) elapsed_ms;
|
|
|
|
if (id(step_accumulator_ms) >= selected_step_interval_ms) {
|
|
do_one_step(selected_direction, now_ms);
|
|
id(step_accumulator_ms) -= selected_step_interval_ms;
|
|
|
|
// If the loop was badly delayed, avoid trying to catch up with a burst
|
|
// of very fast steps that the motor may not physically follow.
|
|
if (id(step_accumulator_ms) > selected_step_interval_ms) {
|
|
id(step_accumulator_ms) = 0.0;
|
|
}
|
|
}
|
|
|
|
if (
|
|
id(coils_are_on) &&
|
|
selected_mode == 0 &&
|
|
((uint32_t) (now_ms - id(last_step_ms)) >= COIL_RELEASE_MS)
|
|
) {
|
|
all_coils_off();
|
|
}
|
|
|
|
if (
|
|
!id(ip_flash_active) &&
|
|
id(step_led_is_on) &&
|
|
((uint32_t) (now_ms - id(step_led_started_ms)) >= STEP_LED_FLASH_MS)
|
|
) {
|
|
id(onboard_step_led).turn_off();
|
|
id(step_led_is_on) = false;
|
|
}
|
|
|
|
- interval: ${ip_flash_loop_interval}
|
|
then:
|
|
- lambda: |-
|
|
if (!id(ip_flash_active)) {
|
|
return;
|
|
}
|
|
|
|
const int IP_FLASH_REPEAT_COUNT = ${ip_flash_repeat_count};
|
|
const int IP_FLASH_ZERO_DIGIT_BLINKS = ${ip_flash_zero_digit_blinks};
|
|
|
|
const uint32_t IP_FLASH_ON_MS = ${ip_flash_on_ms};
|
|
const uint32_t IP_FLASH_BETWEEN_FLASH_GAP_MS = ${ip_flash_between_flash_gap_ms};
|
|
const uint32_t IP_FLASH_DIGIT_GAP_MS = ${ip_flash_digit_gap_ms};
|
|
const uint32_t IP_FLASH_OCTET_GAP_MS = ${ip_flash_octet_gap_ms};
|
|
const uint32_t IP_FLASH_REPEAT_GAP_MS = ${ip_flash_repeat_gap_ms};
|
|
|
|
const uint32_t now_ms = millis();
|
|
|
|
if ((int32_t) (now_ms - id(ip_flash_next_action_ms)) < 0) {
|
|
return;
|
|
}
|
|
|
|
auto get_octet = [&](int index) -> int {
|
|
switch (index) {
|
|
case 0:
|
|
return id(ip_flash_octet_0);
|
|
case 1:
|
|
return id(ip_flash_octet_1);
|
|
case 2:
|
|
return id(ip_flash_octet_2);
|
|
case 3:
|
|
default:
|
|
return id(ip_flash_octet_3);
|
|
}
|
|
};
|
|
|
|
auto get_digit_count = [&](int value) -> int {
|
|
if (value >= 100) {
|
|
return 3;
|
|
}
|
|
|
|
if (value >= 10) {
|
|
return 2;
|
|
}
|
|
|
|
return 1;
|
|
};
|
|
|
|
auto get_digit = [&](int value, int digit_index) -> int {
|
|
const int digit_count = get_digit_count(value);
|
|
|
|
if (digit_count == 3) {
|
|
if (digit_index == 0) {
|
|
return value / 100;
|
|
}
|
|
|
|
if (digit_index == 1) {
|
|
return (value / 10) % 10;
|
|
}
|
|
|
|
return value % 10;
|
|
}
|
|
|
|
if (digit_count == 2) {
|
|
if (digit_index == 0) {
|
|
return value / 10;
|
|
}
|
|
|
|
return value % 10;
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
const int current_octet = get_octet(id(ip_flash_octet_index));
|
|
const int current_digit_count = get_digit_count(current_octet);
|
|
const int current_digit = get_digit(current_octet, id(ip_flash_digit_index));
|
|
const int target_blinks =
|
|
current_digit == 0 ? IP_FLASH_ZERO_DIGIT_BLINKS : current_digit;
|
|
|
|
if (id(ip_flash_led_is_on)) {
|
|
id(onboard_step_led).turn_off();
|
|
id(ip_flash_led_is_on) = false;
|
|
id(ip_flash_blinks_completed)++;
|
|
|
|
if (id(ip_flash_blinks_completed) < target_blinks) {
|
|
id(ip_flash_next_action_ms) = now_ms + IP_FLASH_BETWEEN_FLASH_GAP_MS;
|
|
return;
|
|
}
|
|
|
|
id(ip_flash_blinks_completed) = 0;
|
|
id(ip_flash_digit_index)++;
|
|
|
|
if (id(ip_flash_digit_index) < current_digit_count) {
|
|
id(ip_flash_next_action_ms) = now_ms + IP_FLASH_DIGIT_GAP_MS;
|
|
return;
|
|
}
|
|
|
|
id(ip_flash_digit_index) = 0;
|
|
id(ip_flash_octet_index)++;
|
|
|
|
if (id(ip_flash_octet_index) < 4) {
|
|
id(ip_flash_next_action_ms) = now_ms + IP_FLASH_OCTET_GAP_MS;
|
|
return;
|
|
}
|
|
|
|
id(ip_flash_octet_index) = 0;
|
|
id(ip_flash_repeats_completed)++;
|
|
|
|
if (id(ip_flash_repeats_completed) >= IP_FLASH_REPEAT_COUNT) {
|
|
id(ip_flash_active) = false;
|
|
id(ip_flash_led_is_on) = false;
|
|
id(onboard_step_led).turn_off();
|
|
|
|
ESP_LOGI("ip_flash", "Finished flashing IPv4 address.");
|
|
return;
|
|
}
|
|
|
|
id(ip_flash_next_action_ms) = now_ms + IP_FLASH_REPEAT_GAP_MS;
|
|
return;
|
|
}
|
|
|
|
id(onboard_step_led).turn_on();
|
|
id(ip_flash_led_is_on) = true;
|
|
id(ip_flash_next_action_ms) = now_ms + IP_FLASH_ON_MS |