From 404f75cef583b5df792728e4890f5f0b6de27b4e Mon Sep 17 00:00:00 2001 From: Cedarium Zhang Date: Fri, 30 Jan 2026 12:10:24 +0800 Subject: [PATCH] Initial commit --- ...irection_controller.c.5A06F5FCE234F4F4.idx | Bin 0 -> 10998 bytes ...irection_controller.h.272F22AA470B01BD.idx | Bin 0 -> 6928 bytes .../clangd/index/main.c.56E3DAFB108A7419.idx | Bin 0 -> 5782 bytes .../stepper_motor.c.70AA6D4EB61FBF00.idx | Bin 0 -> 14504 bytes .../stepper_motor.h.7981A7FE686E5526.idx | Bin 0 -> 5824 bytes .clangd | 2 + .devcontainer/Dockerfile | 13 + .devcontainer/devcontainer.json | 21 + .gitignore | 78 ++ .vscode/c_cpp_properties.json | 23 + .vscode/launch.json | 15 + .vscode/settings.json | 20 + CMakeLists.txt | 6 + components/stepper_motor/CMakeLists.txt | 3 + .../stepper_motor/include/stepper_motor.h | 129 ++++ components/stepper_motor/stepper_motor.c | 693 ++++++++++++++++++ main/CMakeLists.txt | 2 + main/main.c | 274 +++++++ 18 files changed, 1279 insertions(+) create mode 100644 .cache/clangd/index/direction_controller.c.5A06F5FCE234F4F4.idx create mode 100644 .cache/clangd/index/direction_controller.h.272F22AA470B01BD.idx create mode 100644 .cache/clangd/index/main.c.56E3DAFB108A7419.idx create mode 100644 .cache/clangd/index/stepper_motor.c.70AA6D4EB61FBF00.idx create mode 100644 .cache/clangd/index/stepper_motor.h.7981A7FE686E5526.idx create mode 100644 .clangd create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 components/stepper_motor/CMakeLists.txt create mode 100644 components/stepper_motor/include/stepper_motor.h create mode 100644 components/stepper_motor/stepper_motor.c create mode 100644 main/CMakeLists.txt create mode 100644 main/main.c diff --git a/.cache/clangd/index/direction_controller.c.5A06F5FCE234F4F4.idx b/.cache/clangd/index/direction_controller.c.5A06F5FCE234F4F4.idx new file mode 100644 index 0000000000000000000000000000000000000000..5d07c4b71f4b938108cff5f118d43356dbf7e3bb GIT binary patch literal 10998 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 X(}7EUYi_R|<-bF?-Np$u{OA7wq1W;W literal 0 HcmV?d00001 diff --git a/.cache/clangd/index/direction_controller.h.272F22AA470B01BD.idx b/.cache/clangd/index/direction_controller.h.272F22AA470B01BD.idx new file mode 100644 index 0000000000000000000000000000000000000000..cdd16c4d8e67523e645c1ca86b1a7c0f945a2a72 GIT binary patch literal 6928 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 literal 0 HcmV?d00001 diff --git a/.cache/clangd/index/stepper_motor.c.70AA6D4EB61FBF00.idx b/.cache/clangd/index/stepper_motor.c.70AA6D4EB61FBF00.idx new file mode 100644 index 0000000000000000000000000000000000000000..1573250e17cfe290cbc74916cba6419fa7b7bd3d GIT binary patch literal 14504 zcmeHOeRx#WnZFbU_ri&W!Zwd=+oU*U`cl?D?WZgA-Hy3s4f_jToXryZzRTOrpu~zhc;;TqZk=sMf@#;9=A=TzC#1wGjYq>t$F;)_ z_#tA)LX9xwB*1vwhLW_4z39fAdKeGABNm-&ThnKZtE?P1EjYb$+_64~DM4Xdw zU_{?>VTAY=%Z)ousumU@Y{$TlxZ6O)=Dh!)lSkPi(^~WwjIm0_=?`| z#BghoWIAqDk!7jJqhgDC-Lgz4X1YYUS>ii#ETxwcy1_6qbUlRw*WHc@htzoPVvTr~p=S zPd$(fMbe}RaC4}UtX*vg)~nVIMcQr#!Eez7a5fYRhJaMj6j00>PfIM9+|J8OorGQP zMk6*fhuQBD{BGvLOj~#wsA3@DN1dI- z8#DyP84nI0GO)K5ss;(5q#-l7aZ7_? zTy$5%aFTY&wXLc~Yp^$8LOiS;*YjpN@y%bb2eWrvLqmfV58<6FE`MgxqVsLbP1$we zE-VAE10JXxKh>&>CbDWza-6wV2sR+>^XqgH7+4GY7n7BH$qk1R2~4U1cyS_593-mR zwNs)pVsZxQ-kThXCNfI-l{E66!PjXd6t&ezvIlP512J1S+UVI`(7J!74?NLK7t?R# z13E_t5*hjJ7fQQ!Fz#BF_zMX{B0=NMJR5{Mg>^g@i&7nT zVN<%%csd4x0*h7RRJnF?9+BVc;zBHxtg(Y$beM!8kS5b=JN$&vM98L$<5i|n4DLCUVgNSq(+7J^|;*24q8`qji$Dl?Xx5gs{$H zBo|N;9my_+k%gnxQBPD#$&R>TC=<5;Wri1M2`tOF2oeJvk}@m}$DHuodRR7Cv4mX% zOO&w_PL+mXZ-N%_TAo)^c%4!~60mVppFwaYLX*<0$c`CN4+LowXS~_KppvkgY0$=* zf)}Ca0^w8$x`g=0T}2h_CV_=(2d&QW%>xM$U+d=KwlLbyVR?LC8nY3!1ScLCOoaSQ1sj}%7g(ZeiWj6r560j41lVONbQfaTp z1`Vx_Ik4c~QaxT~^?K~Cp;1s7>Qhp1Hz{DQdc0rOtvECI>u>vNjknnf*(13wvB`{z zPLU@bH=Pm>YgsJ_!6?%NZJ7rUQhX0_7SnQ4G?)}+%P%q9OC6gfn|M<2r_I8Y!GVCS z3H{DI#_Qsh_{-|G*G3y(d)-{;<}$0Thi@=I?pJn;;8Iq^y-8bPj-)8thnaq|+}BT< zG5P^1JanE^9gW#T1A)Mpp#eCiB;CO1NIaSdOikEhlc2qU%A_*~GVVY;l(6SU%i-b# zGJCYV3i7)EK6inlBvLLb50VrskU5)WmsZsN4SURti}qg;zEMxMTNju&xMFa{T>*}0j%4-?-NVCVkreO$I*)2Np zpxfwv-6pHy7@kZyZs30SGHu4x>E#na810zX$8{C{_C2LS?%c22OU$Y9hK?N$`0F;6 zu*baun^=(UN0UTn2`0O4eM{`mK7lomOJms;yq8OZKg7iG&m65$km<{8OrrMO( z;l~YoHqj-`wsfU!)#ix=njP1X&+rK@$DrHEeQ02I)9aoEsAJHBumU6mw(nysr-@J93 z`B2MqhE#@Ik&W$gzsx_ZBl$nazg#ls=PwviPpI-O<6F0p3o{qJx@O>`vii?HsW+I5 z@{8^y7iM1e_rIC+{rqbm8d85Suh+EoTKD35t-iaI)48)w#?boQgGYYx>v6yHNxh&- zpZq>03(MUy-#da#8VUZ8SJLFXtB0f_!b|+=~NdeP&4gpvrTa z_8fGwKwCgML8QuzhyS?kzRzF3YDfd1%GI}YWJ706s;>J zPCnkaTGtJ8I2vBy&EFh>MhIvm8Y!SrXq15VXlRdC;0?>%tD(Ij-lw5`BHpi|{Twev zbGkpe?`p1Wm^-wtJG4Hmq1mth;@sJ%Uh*l1`6~teO3|5*4D%Se^%&~R@bBx7{_F?c z{rrHi-a&Am$gUJ*AdzGfwl+h0+9uZZ}#()GAf%3Uwoi`A}+RiV8Vs&9oV^uJEcU#AM=-K6?9iS`yXzeVlh>63X%^_^1l zI6jE-4A{n!F~eepFmwa88$DX{EMiIr`+Zxlz&OYSCH=t%H#HHDF2#hU#a<4 zYJ$C4^EGREjJq#>aD3#lKNNivNPP^I977Nn=;!qKcUMpS)39%rAYc$$EE|kL2pEJ8 z$OdB&0tV^t%Xy4J2pFWF5pk=mw~Cl;dFIJ7k zoFT|up`sP4(EmDBU&k4P%uOoVB-*J=F~%TZ5IUtA93MpbLCzRt9zy6466{pg7=sWn z2+JB{2m%ISS>yN$LRXN%@inAh6YVQCv{Do7%^GUf48|aqa-iI-RKqY2%lcutn9;;A zUz7FMMEs_#zsWJ{Ue3+rC5(t{9$=u^2s3P7_V8nmH=mdR)ZM1#Y}5Q~L?s(O=-=;| zyjdW$!~MfOAvMgc+MQc9$QS6xF!yNwJzQwtLI3#Q-(LQ~_d6JGb}+s@#M)Kkt9>r3!#Q4tdMrctaQrmt`!pKC@fOr)3-am*^JcyO z*@cBR5PPJ5WEQcmoiX>qj~MuV9Zc+jgF)=@7aOp=&p^vtv4hc6>(X{A`l-Cb@|7bG59m7V$<| z-^ek`ydZ+pvpj2v=^Pm737#1~t3nsXRDP7|U}y(J+hypN^0A|{&kt+pAW8>O+9gU! z{kXCLPt0zB)M=D|R95OVd#e^hH@Kl|?yWtf-mi3S2-t2+n`UTH!xV3f2PH3;8u)%vE4zB~`!kzp9IeXUu$t-@+ z!&{q(CKj+v*Dzax{69>twj-2N;9}9acE^d73 z<+(pS2)?FIeYqRyQ$dFRZ%^)o3Eyiv1TE}VyYE)}u!kn>otl=L^)AgYC;BJ$qX@_JD8ey4if~MiA{^7B z2#Z1pq7pueFr53sh2z6EetC|>6tr74SWNldeJ3lI6y|;mhFu!krFCXYz-9_dK!04p ze9bC=f&Qd|8Or~zwr_C5*e{@|?OM)utt+dEI`5d4?pORrFl^OwwsOO`Dbshop)R}( zhAkS}qUEzYSbowaH}jhe)`wv}sh}s7o@{i6xl++rDt$R#qv&fy{EUL05$nIIpjVZi zY+VfVw~GE-r7y={DEb#7-k_ols!;!|s-IQ+GQS(<=c@6!I)LMa$XJL3yb9^7kWl|w zWIQX{cOhe!hz}tBfLQ-6q`!swvNw5#`8Lwu7V(cX{YP3~w$9)mO@ESODGC3D4Li?G zhTXaN%kB6=KRwtNN%C?!Nh;>0-eXHix`UT`=XFUok3NwnJvGjpsarj6zwM6h-HIiP GSNwm_+qO6W literal 0 HcmV?d00001 diff --git a/.cache/clangd/index/stepper_motor.h.7981A7FE686E5526.idx b/.cache/clangd/index/stepper_motor.h.7981A7FE686E5526.idx new file mode 100644 index 0000000000000000000000000000000000000000..1ca034b945114ad16078d98a96b7aa817dadf185 GIT binary patch literal 5824 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 /dev/null 2>&1" >> ~/.bashrc + +ENTRYPOINT [ "/opt/esp/entrypoint.sh" ] + +CMD ["/bin/bash", "-c"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b801786 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "name": "ESP-IDF QEMU", + "build": { + "dockerfile": "Dockerfile" + }, + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "idf.espIdfPath": "/opt/esp/idf", + "idf.toolsPath": "/opt/esp", + "idf.gitPath": "/usr/bin/git" + }, + "extensions": [ + "espressif.esp-idf-extension", + "espressif.esp-idf-web" + ] + } + }, + "runArgs": ["--privileged"] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7805557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Directory metadata +.directory + +# Temporary files +*~ +*.swp +*.swo +*.bak +*.tmp + +# Log files +*.log + +# Build artifacts and directories +**/build/ +build/ +*.o +*.a +*.out +*.exe # For any host-side utilities compiled on Windows + +# ESP-IDF specific build outputs +*.bin +*.elf +*.map +flasher_args.json # Generated in build directory +sdkconfig.old +sdkconfig + +# ESP-IDF dependencies +# For older versions or manual component management +/components/.idf/ +**/components/.idf/ +# For modern ESP-IDF component manager +managed_components/ +# If ESP-IDF tools are installed/referenced locally to the project +.espressif/ + +# CMake generated files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +install_manifest.txt +CTestTestfile.cmake + +# Python environment files +*.pyc +*.pyo +*.pyd +__pycache__/ +*.egg-info/ +dist/ + +# Virtual environment folders +venv/ +.venv/ +env/ + +# Language Servers +.clangd/ +.ccls-cache/ +compile_commands.json + +# Windows specific +Thumbs.db +ehthumbs.db +Desktop.ini + +# User-specific configuration files +*.user +*.workspace # General workspace files, can be from various tools +*.suo # Visual Studio Solution User Options +*.sln.docstates # Visual Studio diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..48fa5bb --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,23 @@ +{ + "configurations": [ + { + "name": "ESP-IDF", + "compilerPath": "${config:idf.toolsPathWin}\\tools\\xtensa-esp-elf\\esp-14.2.0_20251107\\xtensa-esp-elf\\bin\\xtensa-esp32-elf-gcc.exe", + "compileCommands": "${config:idf.buildPath}/compile_commands.json", + "includePath": [ + "${config:idf.espIdfPath}/components/**", + "${config:idf.espIdfPathWin}/components/**", + "${workspaceFolder}/**" + ], + "browse": { + "path": [ + "${config:idf.espIdfPath}/components", + "${config:idf.espIdfPathWin}/components", + "${workspaceFolder}" + ], + "limitSymbolsToIncludedHeaders": true + } + } + ], + "version": 4 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2511a38 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + }, + { + "type": "espidf", + "name": "Launch", + "request": "launch" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0f56e22 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "C_Cpp.intelliSenseEngine": "default", + "idf.espIdfPathWin": "C:\\Users\\Admin\\esp\\v5.5.2\\esp-idf", + "idf.pythonInstallPath": "C:\\Users\\Admin\\.espressif\\tools\\idf-python\\3.11.2\\python.exe", + "idf.openOcdConfigs": [ + "board/esp32s3-builtin.cfg" + ], + "idf.portWin": "COM3", + "idf.toolsPathWin": "C:\\Users\\Admin\\.espressif", + "idf.customExtraVars": { + "IDF_TARGET": "esp32s3" + }, + "clangd.path": "C:\\Users\\Admin\\.espressif\\tools\\esp-clang\\esp-19.1.2_20250312\\esp-clang\\bin\\clangd.exe", + "clangd.arguments": [ + "--background-index", + "--query-driver=**", + "--compile-commands-dir=c:\\Users\\Admin\\OneDrive\\Project\\招财猫\\Stepper base\\build" + ], + "idf.flashType": "UART" +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b967d7a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(Stepper base) diff --git a/components/stepper_motor/CMakeLists.txt b/components/stepper_motor/CMakeLists.txt new file mode 100644 index 0000000..37a4dd0 --- /dev/null +++ b/components/stepper_motor/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "stepper_motor.c" + INCLUDE_DIRS "include" + PRIV_REQUIRES "driver") diff --git a/components/stepper_motor/include/stepper_motor.h b/components/stepper_motor/include/stepper_motor.h new file mode 100644 index 0000000..69522b0 --- /dev/null +++ b/components/stepper_motor/include/stepper_motor.h @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "driver/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========== GPIO Pin Definitions ========== */ +#define IN1_PIN (35) /**< Stepper motor IN1 control pin */ +#define IN2_PIN (36) /**< Stepper motor IN2 control pin */ +#define IN3_PIN (37) /**< Stepper motor IN3 control pin */ +#define IN4_PIN (38) /**< Stepper motor IN4 control pin */ + +/* ========== Speed Configuration (delay per step, unit: microseconds μs) ========== */ +/** + * @note Uses half-step driving mode for doubled precision and smoother motion + * @note Smaller delay means faster speed + */ +#define STEPPER_SPEED_ULTRA_FAST 600 /**< Ultra fast mode: 600μs/step (0.6ms) */ +#define STEPPER_SPEED_FAST 800 /**< Fast mode: 1000μs/step (1ms) */ +#define STEPPER_SPEED_NORMAL 1500 /**< Normal mode: 1500μs/step (1.5ms) */ +#define STEPPER_SPEED_SLOW 2000 /**< Slow mode: 2000μs/step (2ms) */ + +/* ========== Acceleration/Deceleration Configuration ========== */ +#define STEPPER_START_DELAY_US 1500 /**< Start delay (slower, ensures smooth start) */ +#define STEPPER_ACCEL_STEPS 30 /**< Acceleration steps */ +#define STEPPER_DECEL_STEPS 30 /**< Deceleration steps */ + +/* ========== Stepper Motor Action Type Enumeration ========== */ +/** + * @brief Stepper motor predefined action types + */ +typedef enum { + STEPPER_ACTION_SHAKE_HEAD, /**< Shake head action (fixed amplitude and cycles) */ + STEPPER_ACTION_SHAKE_HEAD_DECAY, /**< Gradually decaying shake head action */ + STEPPER_ACTION_LOOK_AROUND, /**< Look around observation action */ + STEPPER_ACTION_BEAT_SWING, /**< Follow drum beat swing motion */ + STEPPER_ACTION_CAT_NUZZLE, /**< Cat nuzzling action */ + STEPPER_ACTION_MAX /**< Number of action types (for boundary check) */ +} stepper_action_type_t; + +/* ========== Function Declarations ========== */ + +/** + * @brief Rotate by specified angle (with acceleration/deceleration) + * + * @param angle Rotation angle, positive for right (clockwise), negative for left (counterclockwise) + * @param target_delay_us Target speed delay (microseconds), will automatically accelerate from slow speed to this speed at start + */ +void stepper_rotate_angle_with_accel(float angle, int target_delay_us); + +/** + * @brief Shake head action function + * + * @param amplitude Shake amplitude (one-sided angle), e.g., 30 means shake 30 degrees left and right + * @param cycles Number of shake cycles, one complete left-right shake counts as 1 + * @param speed_us Shake speed (microsecond delay), recommend using STEPPER_SPEED_xxx macros + */ +void stepper_shake_head(float amplitude, int cycles, int speed_us); + +/** + * @brief Gradually decaying shake head action function + * + * @param initial_amplitude Initial shake amplitude (one-sided angle), e.g., 30 means initially shake 30 degrees left and right + * @param decay_rate Decay rate, range 0.0~1.0 for percentage decay, >1.0 for fixed angle decay + * e.g., 0.8 means amplitude becomes 80% after each shake + * e.g., 5.0 means decrease by 5 degrees each time + * @param speed_us Shake speed (microsecond delay), recommend using STEPPER_SPEED_xxx macros + */ +void stepper_shake_head_decay(float initial_amplitude, float decay_rate, int speed_us); + +/** + * @brief Look around action function (with small amplitude scanning + random offset) + * + * @param left_angle Main angle to turn left (positive value), e.g., 45 means turn left 45 degrees + * @param right_angle Main angle to turn right (positive value), e.g., 45 means turn right 45 degrees + * @param scan_angle Small amplitude scanning angle at left and right sides (positive value), e.g., 10 means scan 10 degrees left and right + * @param pause_ms Pause time after each movement (milliseconds), simulating "observation" motion + * @param large_speed_us Large movement speed (microsecond delay), used for main position rotation + * @param small_speed_us Small scanning speed (microsecond delay), used for small range observation + * + * @note Random offset is added to each rotation for more natural motion + */ +void stepper_look_around(float left_angle, float right_angle, float scan_angle, + int pause_ms, int large_speed_us, int small_speed_us); + +/** + * @brief Follow drum beat swing function + * + * @param angle Swing angle for each beat (positive value), e.g., 10 means swing 10 degrees left and right + * @param speed_us Rotation speed (microsecond delay) + * + * @note Each call to this function automatically switches direction (left-right-left-right...) + */ +void stepper_beat_swing(float angle, int speed_us); + +/** + * @brief Cat nuzzling action function (gently turn left and return to center, repeat several times) + * + * @param angle Angle to turn left (positive value), e.g., 20 means turn left 20 degrees + * @param cycles Number of nuzzles, each includes a complete "turn-return to center" motion + * @param speed_us Rotation speed (microsecond delay), recommend using slower speed like STEPPER_SPEED_SLOW + * + * @note Uses slow smooth acceleration/deceleration throughout for gentle feel + */ +void stepper_cat_nuzzle(float angle, int cycles, int speed_us); + +/** + * @brief Turn off all stepper motor coils (power off) + * + * @note After calling this function, motor will no longer hold position and can be rotated by external force + */ +void stepper_motor_power_off(void); + +/** + * @brief Initialize stepper motor GPIO pins + */ +void stepper_motor_gpio_init(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/stepper_motor/stepper_motor.c b/components/stepper_motor/stepper_motor.c new file mode 100644 index 0000000..63717de --- /dev/null +++ b/components/stepper_motor/stepper_motor.c @@ -0,0 +1,693 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_rom_sys.h" +#include "esp_random.h" +#include "stepper_motor.h" + +static const char *TAG = "Stepper Motor"; + +/* ========== Step Sequence Definitions ========== */ + +/** + * @brief 8-beat half-step sequence - Clockwise (forward rotation) + * @note Alternates single-phase and dual-phase excitation for smoother motion and higher precision + */ +static const int step_sequence_cw[8][4] = { + {1, 0, 0, 0}, // Step 1: IN1 (single phase) + {1, 1, 0, 0}, // Step 2: IN1+IN2 (dual phase) + {0, 1, 0, 0}, // Step 3: IN2 (single phase) + {0, 1, 1, 0}, // Step 4: IN2+IN3 (dual phase) + {0, 0, 1, 0}, // Step 5: IN3 (single phase) + {0, 0, 1, 1}, // Step 6: IN3+IN4 (dual phase) + {0, 0, 0, 1}, // Step 7: IN4 (single phase) + {1, 0, 0, 1} // Step 8: IN4+IN1 (dual phase) +}; + +/** + * @brief 8-beat half-step sequence - Counterclockwise (reverse rotation) + * @note Alternates single-phase and dual-phase excitation for smoother motion and higher precision + */ +static const int step_sequence_ccw[8][4] = { + {1, 0, 0, 1}, // Step 1: IN4+IN1 (dual phase) + {0, 0, 0, 1}, // Step 2: IN4 (single phase) + {0, 0, 1, 1}, // Step 3: IN3+IN4 (dual phase) + {0, 0, 1, 0}, // Step 4: IN3 (single phase) + {0, 1, 1, 0}, // Step 5: IN2+IN3 (dual phase) + {0, 1, 0, 0}, // Step 6: IN2 (single phase) + {1, 1, 0, 0}, // Step 7: IN1+IN2 (dual phase) + {1, 0, 0, 0} // Step 8: IN1 (single phase) +}; + +/* ========== Private Functions ========== */ + +/** + * @brief Set stepper motor pin states + * + * @param in1 IN1 pin state (0 or 1) + * @param in2 IN2 pin state (0 or 1) + * @param in3 IN3 pin state (0 or 1) + * @param in4 IN4 pin state (0 or 1) + */ +static void set_motor_pins(int in1, int in2, int in3, int in4) +{ + gpio_set_level(IN1_PIN, in1); + gpio_set_level(IN2_PIN, in2); + gpio_set_level(IN3_PIN, in3); + gpio_set_level(IN4_PIN, in4); +} + +/** + * @brief Stepper motor clockwise one step + * + * @param step Current step number (will be auto modulo 8) + */ +static void stepper_step_cw(int step) +{ + step = step % 8; // Ensure step is in range 0-7 (8 steps in half-step mode) + set_motor_pins(step_sequence_cw[step][0], + step_sequence_cw[step][1], + step_sequence_cw[step][2], + step_sequence_cw[step][3]); +} + +/** + * @brief Stepper motor counterclockwise one step + * + * @param step Current step number (will be auto modulo 8) + */ +static void stepper_step_ccw(int step) +{ + step = step % 8; // Ensure step is in range 0-7 (8 steps in half-step mode) + set_motor_pins(step_sequence_ccw[step][0], + step_sequence_ccw[step][1], + step_sequence_ccw[step][2], + step_sequence_ccw[step][3]); +} + +/** + * @brief Microsecond precision delay function + * + * @param delay_us Delay time (microseconds) + */ +static inline void precise_delay_us(int delay_us) +{ + if (delay_us > 0) { + esp_rom_delay_us(delay_us); + } +} + +/* ========== Public Function Implementations ========== */ + +/** + * @brief Clockwise rotation with acceleration/deceleration + * + * @param steps Number of rotation steps + * @param target_delay_us Target speed delay (microseconds) + * + * @note Uses linear acceleration/deceleration algorithm, divided into acceleration, constant speed, and deceleration phases + */ +static void stepper_rotate_cw_with_accel(int steps, int target_delay_us) +{ + int start_delay_us = STEPPER_START_DELAY_US; + int accel_steps = STEPPER_ACCEL_STEPS; + int decel_steps = STEPPER_DECEL_STEPS; + + // If total steps too few, adjust acceleration/deceleration steps + if (steps < (accel_steps + decel_steps)) { + accel_steps = steps / 3; + decel_steps = steps / 3; + } + + int constant_steps = steps - accel_steps - decel_steps; + int current_delay_us; + + ESP_LOGD(TAG, "Accel-Decel profile: accel=%d, constant=%d, decel=%d steps", + accel_steps, constant_steps, decel_steps); + + // Acceleration phase + for (int i = 0; i < accel_steps; i++) { + // Linear interpolation: gradually decrease from start_delay to target_delay + current_delay_us = start_delay_us - (start_delay_us - target_delay_us) * i / accel_steps; + stepper_step_cw(i); + precise_delay_us(current_delay_us); + } + + // Constant speed phase + for (int i = accel_steps; i < accel_steps + constant_steps; i++) { + stepper_step_cw(i); + precise_delay_us(target_delay_us); + } + + // Deceleration phase + for (int i = accel_steps + constant_steps; i < steps; i++) { + // Linear interpolation: gradually increase from target_delay to start_delay + int decel_progress = i - (accel_steps + constant_steps); + current_delay_us = target_delay_us + (start_delay_us - target_delay_us) * decel_progress / decel_steps; + stepper_step_cw(i); + precise_delay_us(current_delay_us); + } +} + +/** + * @brief Counterclockwise rotation with acceleration/deceleration + * + * @param steps Number of rotation steps + * @param target_delay_us Target speed delay (microseconds) + * + * @note Uses linear acceleration/deceleration algorithm, divided into acceleration, constant speed, and deceleration phases + */ +static void stepper_rotate_ccw_with_accel(int steps, int target_delay_us) +{ + int start_delay_us = STEPPER_START_DELAY_US; + int accel_steps = STEPPER_ACCEL_STEPS; + int decel_steps = STEPPER_DECEL_STEPS; + + // If total steps too few, adjust acceleration/deceleration steps + if (steps < (accel_steps + decel_steps)) { + accel_steps = steps / 3; + decel_steps = steps / 3; + } + + int constant_steps = steps - accel_steps - decel_steps; + int current_delay_us; + + ESP_LOGD(TAG, "Accel-Decel profile: accel=%d, constant=%d, decel=%d steps", + accel_steps, constant_steps, decel_steps); + + // Acceleration phase + for (int i = 0; i < accel_steps; i++) { + current_delay_us = start_delay_us - (start_delay_us - target_delay_us) * i / accel_steps; + stepper_step_ccw(i); + precise_delay_us(current_delay_us); + } + + // Constant speed phase + for (int i = accel_steps; i < accel_steps + constant_steps; i++) { + stepper_step_ccw(i); + precise_delay_us(target_delay_us); + } + + // Deceleration phase + for (int i = accel_steps + constant_steps; i < steps; i++) { + int decel_progress = i - (accel_steps + constant_steps); + current_delay_us = target_delay_us + (start_delay_us - target_delay_us) * decel_progress / decel_steps; + stepper_step_ccw(i); + precise_delay_us(current_delay_us); + } +} + +/** + * @brief Rotate by specified angle (with acceleration/deceleration) + * + * @param angle Rotation angle, positive for right (clockwise), negative for left (counterclockwise) + * @param target_delay_us Target speed delay (microseconds), will automatically accelerate from slow speed to this speed at start + * + * @note Half-step mode: 4128 steps = 360 degrees + * @note Uses linear acceleration/deceleration algorithm to ensure smooth start and stop + */ +void stepper_rotate_angle_with_accel(float angle, int target_delay_us) +{ + // Half-step mode: 4128 steps = 360 degrees + int steps = (int)(angle * 4128.0 / 360.0 + 0.5); + + if (steps == 0) { + ESP_LOGI(TAG, "Angle too small, no rotation needed"); + return; + } + + if (steps > 0) { + ESP_LOGD(TAG, "Rotating %.1f° CW with acceleration (%d steps, target %dus/step)", + angle, steps, target_delay_us); + stepper_rotate_cw_with_accel(steps, target_delay_us); + } else { + ESP_LOGD(TAG, "Rotating %.1f° CCW with acceleration (%d steps, target %dus/step)", + angle, -steps, target_delay_us); + stepper_rotate_ccw_with_accel(-steps, target_delay_us); + } +} + +/** + * @brief Shake head action function + * + * @param amplitude Shake amplitude (one-sided angle), e.g., 30 means shake 30 degrees left and right + * @param cycles Number of shake cycles, one complete left-right shake counts as 1 + * @param speed_us Shake speed (microsecond delay), recommend using STEPPER_SPEED_xxx macros + * + * Motion sequence (example with amplitude=30, cycles=2): + * 1. Turn left to -30° → pause + * 2. Turn right to +30° → pause → turn left to -30° → pause (cycle 1) + * 3. Turn right to +30° → pause → turn left to -30° → pause (cycle 2) + * 4. Return to center 0° + */ +void stepper_shake_head(float amplitude, int cycles, int speed_us) +{ + if (amplitude <= 0 || cycles <= 0) { + ESP_LOGW(TAG, "Invalid shake head parameters: amplitude=%.1f, cycles=%d", amplitude, cycles); + return; + } + + ESP_LOGD(TAG, "Shake head started: amplitude=%.1f°, cycles=%d, speed=%dus/step", + amplitude, cycles, speed_us); + + // Step 1: Turn left from center to start position + ESP_LOGD(TAG, "Moving to left start position (%.1f°)", -amplitude); + stepper_rotate_angle_with_accel(-amplitude, speed_us); + vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause + + // Shake cycles + for (int i = 0; i < cycles; i++) { + ESP_LOGD(TAG, "Shake cycle %d/%d", i + 1, cycles); + + // Turn right (from left to right, rotate 2*amplitude degrees) + stepper_rotate_angle_with_accel(2 * amplitude, speed_us); + vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause + + // Turn left (from right to left, rotate 2*amplitude degrees) + stepper_rotate_angle_with_accel(-2 * amplitude, speed_us); + vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause + } + + // Last step: Return from left to center position + ESP_LOGD(TAG, "Returning to center position"); + stepper_rotate_angle_with_accel(amplitude, speed_us); +} + +/** + * @brief Gradually decaying shake head action function + * + * @param initial_amplitude Initial shake amplitude (one-sided angle), e.g., 30 means initially shake 30 degrees left and right + * @param decay_rate Decay rate + * - Range 0.0~1.0: Percentage decay, e.g., 0.8 means amplitude becomes 80% each time + * - >1.0: Fixed angle decay, e.g., 5.0 means decrease by 5 degrees each time + * @param speed_us Shake speed (microsecond delay), recommend using STEPPER_SPEED_xxx macros + * + * @note Shake amplitude gradually decreases until below minimum threshold (5 degrees) then stops + * @note Speed automatically decreases when amplitude is small to avoid jitter + */ +void stepper_shake_head_decay(float initial_amplitude, float decay_rate, int speed_us) +{ + if (initial_amplitude <= 0) { + ESP_LOGW(TAG, "Invalid amplitude: %.1f", initial_amplitude); + return; + } + + if (decay_rate <= 0) { + ESP_LOGW(TAG, "Invalid decay rate: %.3f", decay_rate); + return; + } + + ESP_LOGD(TAG, "Decay shake head started: initial_amplitude=%.1f°, decay_rate=%.3f, speed=%dus/step", + initial_amplitude, decay_rate, speed_us); + + float current_amplitude = initial_amplitude; + float min_amplitude = 5.0; // Minimum amplitude threshold (degrees), stop shaking below this to avoid small angle jitter + float smooth_threshold = 8.0; // Smooth threshold (degrees), reduce speed below this + int cycle_count = 0; + bool use_percentage_decay = (decay_rate > 0.0 && decay_rate < 1.0); // Determine if percentage decay or fixed value decay + float current_position = 0.0; // Track current position relative to center + + // Step 1: Turn left from center to start position + ESP_LOGD(TAG, "Moving to left start position (%.1f°)", -current_amplitude); + stepper_rotate_angle_with_accel(-current_amplitude, speed_us); + current_position = -current_amplitude; + vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause + + // Shake loop, until amplitude below threshold + while (current_amplitude >= min_amplitude) { + cycle_count++; + + // Dynamically adjust speed based on current amplitude, slower speed for smaller amplitude to avoid jitter + int current_speed_us = speed_us; + if (current_amplitude < smooth_threshold) { + // Linear interpolation: speed factor increases from 1.0 to 1.3 (max 30% slowdown) as amplitude decreases from smooth_threshold to min_amplitude + float speed_factor = 1.0 + 0.3 * (smooth_threshold - current_amplitude) / (smooth_threshold - min_amplitude); + current_speed_us = (int)(speed_us * speed_factor); + ESP_LOGD(TAG, "Decay shake cycle %d: amplitude=%.1f°, speed adjusted to %dus/step (factor=%.2f)", + cycle_count, current_amplitude, current_speed_us, speed_factor); + } else { + ESP_LOGD(TAG, "Decay shake cycle %d: amplitude=%.1f°", cycle_count, current_amplitude); + } + + // Turn right (from left to right, rotate 2*current_amplitude degrees) + stepper_rotate_angle_with_accel(2 * current_amplitude, current_speed_us); + current_position += 2 * current_amplitude; + vTaskDelay(pdMS_TO_TICKS(15)); // Brief pause, longer when amplitude is small + + // Calculate next amplitude + float next_amplitude; + if (use_percentage_decay) { + // Percentage decay mode: amplitude = current amplitude × decay rate + next_amplitude = current_amplitude * decay_rate; + ESP_LOGD(TAG, "Percentage decay: next amplitude=%.1f° (%.1f%%)", + next_amplitude, decay_rate * 100); + } else { + // Fixed value decay mode: amplitude = current amplitude - decay value + next_amplitude = current_amplitude - decay_rate; + ESP_LOGD(TAG, "Fixed decay: next amplitude=%.1f° (-%.1f°)", + next_amplitude, decay_rate); + } + + // Check if should continue shaking + if (next_amplitude < min_amplitude) { + ESP_LOGD(TAG, "Next amplitude too small (%.1f° < %.1f°), stopping at position %.1f°", + next_amplitude, min_amplitude, current_position); + break; + } + + current_amplitude = next_amplitude; + + // Dynamically adjust speed based on current amplitude + current_speed_us = speed_us; + if (current_amplitude < smooth_threshold) { + float speed_factor = 1.0 + 0.3 * (smooth_threshold - current_amplitude) / (smooth_threshold - min_amplitude); + current_speed_us = (int)(speed_us * speed_factor); + } + + // Turn left (from right to left, rotate 2*current_amplitude degrees) + stepper_rotate_angle_with_accel(-2 * current_amplitude, current_speed_us); + current_position -= 2 * current_amplitude; + vTaskDelay(pdMS_TO_TICKS(15)); // Brief pause + + // Calculate next amplitude again (symmetric decay) + if (use_percentage_decay) { + next_amplitude = current_amplitude * decay_rate; + } else { + next_amplitude = current_amplitude - decay_rate; + } + + // Check next amplitude + if (next_amplitude < min_amplitude) { + ESP_LOGD(TAG, "Next amplitude too small (%.1f° < %.1f°), stopping at position %.1f°", + next_amplitude, min_amplitude, current_position); + break; + } + + current_amplitude = next_amplitude; + } + + // Last step: Return to center position + ESP_LOGD(TAG, "Returning to center from position %.1f°", current_position); + + // Use slightly slower speed when returning to center to ensure smoothness + float abs_position = (current_position > 0) ? current_position : -current_position; + int return_speed_us = speed_us; + if (abs_position < smooth_threshold) { + // If remaining angle is small, slightly reduce return speed (max 20% slowdown) + float speed_factor = 1.2; + return_speed_us = (int)(speed_us * speed_factor); + ESP_LOGD(TAG, "Using slower speed for return: %dus/step", return_speed_us); + } + + stepper_rotate_angle_with_accel(-current_position, return_speed_us); + + ESP_LOGD(TAG, "Decay shake head completed: total cycles=%d", cycle_count); +} + +/** + * @brief Generate random angle offset (within ±max_offset range) + * + * @param max_offset Maximum offset (degrees) + * @return float Random offset angle, range [-max_offset, +max_offset] + * + * @note Used to add randomness to motions for more natural movement + */ +static float get_random_angle_offset(float max_offset) +{ + if (max_offset <= 0) { + return 0; + } + + // Generate random number between 0 and 1 + uint32_t random_value = esp_random(); + float normalized = (float)(random_value % 10000) / 10000.0; // 0.0 ~ 1.0 + + // Convert to range -max_offset to +max_offset + float offset = (normalized * 2.0 - 1.0) * max_offset; + + return offset; +} + +/** + * @brief Look around action function (with small amplitude scanning + random offset) + * + * @param left_angle Main angle to turn left (positive value), e.g., 45 means turn left 45 degrees + * @param right_angle Main angle to turn right (positive value), e.g., 45 means turn right 45 degrees + * @param scan_angle Small amplitude scanning angle at left and right sides (positive value), e.g., 10 means scan 10 degrees left and right + * @param pause_ms Pause time after each movement (milliseconds), simulating "observation" motion + * @param large_speed_us Large movement speed (microsecond delay), used for main position rotation + * @param small_speed_us Small scanning speed (microsecond delay), used for small range observation, recommend slower than large_speed_us + * + * @note Random offset is added to each rotation for more natural motion + * @note Large movement: random offset ±10° + * @note Small scanning: random offset ±10° + * + * Motion sequence (example with left_angle=45, scan_angle=10, actual values will have random offset): + * 1. Turn left to -45°±10° (large speed) → pause + * 2. Scan left to -55°±10° (small speed) → pause + * 3. Scan right to -35°±10° (small speed) → pause + * 4. Turn right to +45°±10° (large speed) → pause + * 5. Scan right to +55°±10° (small speed) → pause + * 6. Scan left to +35°±10° (small speed) → pause + * 7. Return to center 0° (large speed) + */ +void stepper_look_around(float left_angle, float right_angle, float scan_angle, int pause_ms, int large_speed_us, int small_speed_us) +{ + if (left_angle < 0 || right_angle < 0 || scan_angle < 0) { + ESP_LOGW(TAG, "Invalid angles: left=%.1f, right=%.1f, scan=%.1f (should be positive)", + left_angle, right_angle, scan_angle); + return; + } + + if (pause_ms < 0) { + pause_ms = 0; + } + + ESP_LOGD(TAG, "Look around started: left=%.1f°, right=%.1f°, scan=%.1f°, pause=%dms, large_speed=%dus, small_speed=%dus", + left_angle, right_angle, scan_angle, pause_ms, large_speed_us, small_speed_us); + + // Accumulated position tracking (relative to starting center position) + float accumulated_position = 0.0; + + // ========== Phase 1: Look Left ========== + if (left_angle > 0) { + + // 1. Turn left to main position (large amplitude, fast) + random offset + float left_offset = get_random_angle_offset(10.0); // ±10 degrees random + float actual_left_angle = left_angle + left_offset; + stepper_rotate_angle_with_accel(-actual_left_angle, large_speed_us); + accumulated_position -= actual_left_angle; // Track accumulated position + vTaskDelay(pdMS_TO_TICKS(pause_ms)); + + // 2. Scan left with small amplitude (slow speed) + random offset + if (scan_angle > 0) { + float scan_left_offset = get_random_angle_offset(10.0); // ±10 degrees random + float actual_scan_left = scan_angle + scan_left_offset; + + // Ensure small amplitude rotation is not less than 5 degrees + if (actual_scan_left < 5.0) { + actual_scan_left = 5.0; + scan_left_offset = actual_scan_left - scan_angle; + } + + stepper_rotate_angle_with_accel(-actual_scan_left, small_speed_us); + accumulated_position -= actual_scan_left; // Track accumulated position + vTaskDelay(pdMS_TO_TICKS(pause_ms)); + + // 3. Scan right directly with small amplitude (slow speed, don't return to main position) + random offset + float scan_right_offset = get_random_angle_offset(10.0); // ±10 degrees random + float actual_scan_range = 2 * scan_angle + scan_left_offset - scan_right_offset; + + // Ensure small amplitude rotation is not less than 5 degrees + if (actual_scan_range < 5.0) { + actual_scan_range = 5.0; + } + + stepper_rotate_angle_with_accel(actual_scan_range, small_speed_us); + accumulated_position += actual_scan_range; // Track accumulated position + vTaskDelay(pdMS_TO_TICKS(pause_ms)); + } + } + + // ========== Phase 2: Look Right ========== + if (right_angle > 0) { + // 4. Turn from current position to right main position (fast) + random offset + float right_offset = get_random_angle_offset(10.0); // ±10 degrees random + float actual_right_angle = right_angle + right_offset; + + // Calculate rotation angle: turn from current accumulated position to right side + float turn_angle = actual_right_angle - accumulated_position; + stepper_rotate_angle_with_accel(turn_angle, large_speed_us); + accumulated_position += turn_angle; // Add actual rotated angle to avoid precision errors + vTaskDelay(pdMS_TO_TICKS(pause_ms)); + + // 5. Scan right with small amplitude (slow speed) + random offset + if (scan_angle > 0) { + float scan_right2_offset = get_random_angle_offset(10.0); // ±10 degrees random + float actual_scan_right = scan_angle + scan_right2_offset; + + // Ensure small amplitude rotation is not less than 5 degrees + if (actual_scan_right < 5.0) { + actual_scan_right = 5.0; + scan_right2_offset = actual_scan_right - scan_angle; + } + + stepper_rotate_angle_with_accel(actual_scan_right, small_speed_us); + accumulated_position += actual_scan_right; // Track accumulated position + vTaskDelay(pdMS_TO_TICKS(pause_ms)); + + // 6. Scan left directly with small amplitude (slow speed, don't return to main position) + random offset + float scan_left2_offset = get_random_angle_offset(10.0); // ±10 degrees random + float actual_scan_range2 = 2 * scan_angle + scan_right2_offset - scan_left2_offset; + + // Ensure small amplitude rotation is not less than 5 degrees + if (actual_scan_range2 < 5.0) { + actual_scan_range2 = 5.0; + } + + stepper_rotate_angle_with_accel(-actual_scan_range2, small_speed_us); + accumulated_position -= actual_scan_range2; // Track accumulated position + vTaskDelay(pdMS_TO_TICKS(pause_ms)); + } + } + + // ========== Phase 3: Return to Center ========== + + // Use accumulated position to return directly to center (tolerance: 0.5 degrees) + float abs_position = (accumulated_position > 0) ? accumulated_position : -accumulated_position; + if (abs_position > 0.5) { + stepper_rotate_angle_with_accel(-accumulated_position, large_speed_us); + } else { + ESP_LOGD(TAG, "Already near center (offset=%.2f°)", accumulated_position); + } + + ESP_LOGD(TAG, "Look around completed"); +} + +/** + * @brief Follow drum beat swing function + * + * @param angle Swing angle for each beat (positive value), e.g., 10 means swing 10 degrees left and right + * @param speed_us Rotation speed (microsecond delay) + * + * @note Each call to this function automatically switches direction (using static variable to track state) + * @note Call sequence: left → right → left → right ... + * + * Typical usage: Call this function when music beat is detected, motor will swing left and right following the beat + */ +void stepper_beat_swing(float angle, int speed_us) +{ + // Static variable to track current swing direction, false=left, true=right + static bool swing_direction = false; + + if (angle <= 0) { + ESP_LOGW(TAG, "Invalid angle: %.1f (should be positive)", angle); + return; + } + + if (swing_direction) { + // Turn right + stepper_rotate_angle_with_accel(angle, speed_us); + } else { + // Turn left + stepper_rotate_angle_with_accel(-angle, speed_us); + } + + // Switch direction + swing_direction = !swing_direction; +} + +/** + * @brief Cat nuzzling action function (gently turn left and return to center, repeat several times) + * + * @param angle Angle to turn left (positive value), e.g., 20 means turn left 20 degrees + * @param cycles Number of nuzzles, each includes a complete "turn-return to center" motion + * @param speed_us Rotation speed (microsecond delay), recommend using slower speed like STEPPER_SPEED_SLOW for gentle feel + * + * Motion sequence (example with angle=20, cycles=3): + * 1. Turn left to -20° (slow) → pause 100ms → return to center 0° (slow) → pause 50ms + * 2. Turn left to -20° (slow) → pause 100ms → return to center 0° (slow) → pause 50ms + * 3. Turn left to -20° (slow) → pause 100ms → return to center 0° (slow) + * + * @note Uses slow smooth acceleration/deceleration throughout for gentle feel + * @note Brief pause after each turn to simulate cat nuzzling contact feel + */ +void stepper_cat_nuzzle(float angle, int cycles, int speed_us) +{ + if (angle <= 0) { + ESP_LOGW(TAG, "Invalid angle: %.1f (should be positive)", angle); + return; + } + + if (cycles <= 0) { + ESP_LOGW(TAG, "Invalid cycles: %d (should be positive)", cycles); + return; + } + + ESP_LOGD(TAG, "Cat nuzzle started: angle=%.1f°, cycles=%d, speed=%dus/step", + angle, cycles, speed_us); + + // Execute nuzzle cycles + for (int i = 0; i < cycles; i++) { + ESP_LOGD(TAG, "Nuzzle cycle %d/%d", i + 1, cycles); + + // Slowly turn left + stepper_rotate_angle_with_accel(-angle, speed_us); + vTaskDelay(pdMS_TO_TICKS(100)); // Pause 100ms to simulate nuzzling contact feel + + // Slowly return to center + stepper_rotate_angle_with_accel(angle, speed_us); + + // Add brief pause between cycles (not after last cycle) + if (i < cycles - 1) { + vTaskDelay(pdMS_TO_TICKS(50)); // Brief pause after each nuzzle, prepare for next one + } + } + + ESP_LOGD(TAG, "Cat nuzzle completed"); +} + +/** + * @brief Turn off all stepper motor coils (power off) + * + * @note After calling this function, motor will no longer hold position and can be rotated by external force + * @note Saves power consumption and avoids motor heating from prolonged energization + */ +void stepper_motor_power_off(void) +{ + set_motor_pins(0, 0, 0, 0); +} + +/** + * @brief Initialize stepper motor GPIO pins + * + * @note Configures IN1-IN4 pins as output mode, initial state is low level + */ +void stepper_motor_gpio_init(void) +{ + gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << IN1_PIN) | (1ULL << IN2_PIN) | (1ULL << IN3_PIN) | (1ULL << IN4_PIN), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE + }; + + gpio_config(&io_conf); + + // Set all pins to low level + gpio_set_level(IN1_PIN, 0); + gpio_set_level(IN2_PIN, 0); + gpio_set_level(IN3_PIN, 0); + gpio_set_level(IN4_PIN, 0); + + ESP_LOGI(TAG, "GPIO initialization completed, IN1-IN4 pins set as output mode with default low level"); +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..cf2c455 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "main.c" + INCLUDE_DIRS ".") diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..33a787e --- /dev/null +++ b/main/main.c @@ -0,0 +1,274 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "driver/uart.h" +#include "esp_log.h" +#include "stepper_motor.h" + +static const char *TAG = "Direction Controller"; + +#define DIRECTION_TASK_STACK_SIZE (1024 * 4) + +/* UART Configuration */ +#define UART_PORT_NUM UART_NUM_0 +#define UART_BUF_SIZE 256 +#define INPUT_BUF_SIZE 64 + +/* Current angle tracking (relative to South = 0°) */ +static float s_current_angle = 0.0f; + +/* Direction definitions (angles relative to South = 0°) */ +typedef struct { + const char *name_cn; /* Chinese name */ + const char *name_en; /* English abbreviation */ + float angle; /* Angle relative to South */ +} direction_t; + +/* + * Direction angle mapping (South = 0°, clockwise positive) + * + * 北 (180°) + * | + * 西北(135°) | 东北(-135°/225°) + * | + * 西(90°) ------+------ 东(-90°/270°) + * | + * 西南(45°) | 东南(-45°/315°) + * | + * 南 (0°) [HOME] + */ +static const direction_t directions[] = { + {"南", "S", 0.0f}, /* South - Home position */ + {"西南", "SW", 45.0f}, /* Southwest */ + {"西", "W", 90.0f}, /* West */ + {"西北", "NW", 135.0f}, /* Northwest */ + {"北", "N", 180.0f}, /* North */ + {"东北", "NE", -135.0f}, /* Northeast */ + {"东", "E", -90.0f}, /* East */ + {"东南", "SE", -45.0f}, /* Southeast */ +}; +#define NUM_DIRECTIONS (sizeof(directions) / sizeof(directions[0])) + +/** + * @brief Calculate shortest rotation angle to target + * + * @param current Current angle + * @param target Target angle + * @return Shortest rotation angle (positive = clockwise, negative = counter-clockwise) + */ +static float calculate_shortest_rotation(float current, float target) +{ + float diff = target - current; + + /* Normalize to -180 ~ 180 range */ + while (diff > 180.0f) { + diff -= 360.0f; + } + while (diff < -180.0f) { + diff += 360.0f; + } + + return diff; +} + +/** + * @brief Rotate to specified direction + * + * @param target_angle Target angle (relative to South) + */ +static void rotate_to_angle(float target_angle) +{ + float rotation = calculate_shortest_rotation(s_current_angle, target_angle); + + if (rotation == 0.0f) { + ESP_LOGI(TAG, "Already at target position"); + return; + } + + ESP_LOGI(TAG, "Rotating from %.1f° to %.1f° (rotation: %.1f°)", + s_current_angle, target_angle, rotation); + + stepper_rotate_angle_with_accel(rotation, STEPPER_SPEED_FAST); + s_current_angle = target_angle; + stepper_motor_power_off(); + + ESP_LOGI(TAG, "Rotation complete, current position: %.1f°", s_current_angle); +} + +/** + * @brief Find direction by name + * + * @param input User input string + * @return Pointer to direction_t if found, NULL otherwise + */ +static const direction_t* find_direction(const char *input) +{ + for (int i = 0; i < NUM_DIRECTIONS; i++) { + if (strcmp(input, directions[i].name_cn) == 0 || + strcasecmp(input, directions[i].name_en) == 0) { + return &directions[i]; + } + } + return NULL; +} + +/** + * @brief Print help message with available directions + */ +static void print_help(void) +{ + printf("\n========================================\n"); + printf(" 方向控制系统 (Direction Control)\n"); + printf("========================================\n"); + printf("可用方向 (Available directions):\n"); + printf(" 南/S - South (0°) [默认位置]\n"); + printf(" 西南/SW - Southwest (45°)\n"); + printf(" 西/W - West (90°)\n"); + printf(" 西北/NW - Northwest (135°)\n"); + printf(" 北/N - North (180°)\n"); + printf(" 东北/NE - Northeast (-135°)\n"); + printf(" 东/E - East (-90°)\n"); + printf(" 东南/SE - Southeast (-45°)\n"); + printf("----------------------------------------\n"); + printf("命令 (Commands):\n"); + printf(" help - 显示帮助信息\n"); + printf(" pos - 显示当前位置\n"); + printf("========================================\n\n"); +} + +/** + * @brief Print current position + */ +static void print_current_position(void) +{ + const char *dir_name = "未知"; + for (int i = 0; i < NUM_DIRECTIONS; i++) { + if (directions[i].angle == s_current_angle) { + dir_name = directions[i].name_cn; + break; + } + } + printf("当前位置: %s (%.1f°)\n", dir_name, s_current_angle); +} + +/** + * @brief Direction control task - handles user input + */ +static void direction_control_task(void *arg) +{ + ESP_LOGI(TAG, "Direction control task started"); + + print_help(); + + char input_buf[INPUT_BUF_SIZE]; + int input_idx = 0; + uint8_t data; + + printf("请输入方向 > "); + fflush(stdout); + + while (1) { + /* Read one byte at a time */ + int len = uart_read_bytes(UART_PORT_NUM, &data, 1, pdMS_TO_TICKS(100)); + + if (len > 0) { + /* Echo character */ + uart_write_bytes(UART_PORT_NUM, (const char*)&data, 1); + + /* Handle Enter key (CR or LF) */ + if (data == '\r' || data == '\n') { + uart_write_bytes(UART_PORT_NUM, "\r\n", 2); + + if (input_idx > 0) { + input_buf[input_idx] = '\0'; + + /* Trim leading/trailing whitespace */ + char *cmd = input_buf; + while (*cmd == ' ') cmd++; + char *end = cmd + strlen(cmd) - 1; + while (end > cmd && *end == ' ') *end-- = '\0'; + + /* Process command */ + if (strcmp(cmd, "help") == 0 || strcmp(cmd, "?") == 0) { + print_help(); + } else if (strcmp(cmd, "pos") == 0) { + print_current_position(); + } else { + const direction_t *dir = find_direction(cmd); + if (dir != NULL) { + printf("目标方向: %s (%s, %.1f°)\n", + dir->name_cn, dir->name_en, dir->angle); + rotate_to_angle(dir->angle); + } else { + printf("未知方向: '%s'\n", cmd); + printf("输入 'help' 查看可用方向\n"); + } + } + } + + input_idx = 0; + printf("\n请输入方向 > "); + fflush(stdout); + } + /* Handle Backspace */ + else if (data == '\b' || data == 0x7F) { + if (input_idx > 0) { + input_idx--; + uart_write_bytes(UART_PORT_NUM, " \b", 2); + } + } + /* Regular character */ + else if (input_idx < INPUT_BUF_SIZE - 1) { + input_buf[input_idx++] = data; + } + } + } +} + +/** + * @brief Initialize UART for user input + */ +static void uart_init(void) +{ + uart_config_t uart_config = { + .baud_rate = 115200, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + + ESP_ERROR_CHECK(uart_driver_install(UART_PORT_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0)); + ESP_ERROR_CHECK(uart_param_config(UART_PORT_NUM, &uart_config)); +} + +void app_main(void) +{ + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " Direction Control System"); + ESP_LOGI(TAG, " Default position: South (南)"); + ESP_LOGI(TAG, "========================================"); + + /* Initialize peripherals */ + stepper_motor_gpio_init(); + uart_init(); + + /* Set initial position to South (0°) */ + s_current_angle = 0.0f; + stepper_motor_power_off(); + + ESP_LOGI(TAG, "Initial position: South (0°)"); + + /* Start direction control task */ + xTaskCreate(direction_control_task, "direction_control_task", + DIRECTION_TASK_STACK_SIZE, NULL, 5, NULL); +}