PlatformIO project that drives a stepper-motor-based linear guide from a NodeMCU v3 (ESP-12E / ESP8266). Exposes a small WiFi web UI plus plain HTTP endpoints for jogging, absolute moves, homing, motion-profile control and live status, with a latching limit-switch end-stop.
- MCU board: NodeMCU v3 (ESP8266, ESP-12E)
- Stepper driver: A4988 / DRV8825 / TMC22xx (STEP / DIR / EN interface)
- Motor: NEMA 17 (or similar) coupled to a lead screw or belt linear guide
- End-stop: Mechanical / opto switch at the min end of travel (position 0),
wired to GND, read via
INPUT_PULLUP(active LOW)
| NodeMCU pin | GPIO | Connects to |
|---|---|---|
| D1 | GPIO5 | Driver STEP |
| D2 | GPIO4 | Driver DIR |
| D3 | GPIO0 | Driver ENABLE (active LOW) |
| D5 | GPIO14 | LIMIT switch to GND (INPUT_PULLUP) |
| GND | - | Driver GND, switch GND, common ground |
| 3V3 | - | Driver logic VCC if required |
Power: drive the stepper's motor supply (VMOT, typically 12-24 V) from its own bench/PSU supply sized for the motor. Share grounds between the NodeMCU and driver. Do not power the motor from the NodeMCU 3V3/5V rail.
GPIO0 caveat: D3 is a boot-strap pin. The NodeMCU samples it at reset
to choose flash vs. UART-bootloader mode. Most stepper-driver carriers
leave ENABLE high-Z at power-up, which is fine. If your driver actively
pulls ENABLE low at power-up, the ESP8266 will enter the bootloader and
your firmware will not start — in that case move ENABLE to D6/GPIO12 or
D7/GPIO13 in src/main.cpp.
src/main.cpp— entire firmware (single translation unit).- WiFi connect with a 20 s timeout (boot continues even without WiFi).
ESP8266WebServeron port 80.- Motion via the
AccelStepperlibrary inDRIVER(STEP/DIR) mode. - The driver ENABLE line is owned exclusively by
enableDriver()via directdigitalWrite.AccelStepper::setEnablePin()is intentionally not called so the library never toggles the pin behind our back.
platformio.ini—nodemcuv2board, Arduino framework, lib deps and WiFi credentials passed in as-D WIFI_SSID="…" -D WIFI_PASSWORD="…".
The limit switch is treated as a hard end-stop:
- The switch is read every loop iteration with a short debounce (two consecutive LOW reads ≈ 0.5 ms apart).
- On trip,
hardHalt()cancels any pending motion immediately by setting the target to the current position and zeroing the speed.AccelStepper::stop()is not used here because it only schedules a decel, which would keep driving into the end-stop. - The
limitTrippedflag is latched. While latched:/moverejects any target that does not strictly retreat from the switch (HTTP 409)./jogrejects non-positive deltas and requires the switch to physically release before a positive jog clears the latch (HTTP 409 otherwise)./homeclears the latch as part of zeroing the axis.
Credentials live in secrets.ini (untracked, excluded by .gitignore):
[*]
build_flags =
-D WIFI_SSID=\"your_ssid\"
-D WIFI_PASSWORD=\"your_password\"platformio.ini references it via extra_configs = secrets.ini. Create this
file from the template above if you are setting up a fresh checkout.
In src/main.cpp:
| Constant | Default | Meaning |
|---|---|---|
STEPS_PER_MM |
40 | Steps per mm of travel (motor × microstep / lead) |
MAX_TRAVEL_MM |
1000 | Soft limit at the far end of the guide |
DEFAULT_SPEED_MM_S |
20 | Default max speed in mm/s |
DEFAULT_ACCEL_MM_S2 |
50 | Default acceleration in mm/s² |
ENABLE_ACTIVE |
HIGH |
Driver ENABLE polarity (most carriers: LOW) |
Example computation for STEPS_PER_MM: a 0.9° motor (400 steps/rev) on a
10 mm/rev lead screw → 400 / 10 = 40 steps/mm.
Pins are declared as constexpr uint8_t at the top of src/main.cpp:
PIN_STEP, PIN_DIR, PIN_ENABLE, PIN_LIMIT. Edit those if your
wiring differs.
Install PlatformIO (VS Code extension or standalone CLI), then from the project root.
Create secrets.ini (see WiFi credentials), then
upload over the serial/USB connection:
pio run -t upload -e nodemcuv2_usb
pio device monitor # serial monitor @ 115200 baudOn boot the firmware prints the assigned IP on the serial monitor (or
reports WiFi not connected, continuing without network. after the 20 s
timeout).
Once the device is on the network, upload over the air. The
platformio.ini already sets upload_protocol = espota and
upload_port = fsk40-linear-drive.local, so a plain:
pio run -t upload…will find the device via mDNS. You can also override the target explicitly:
pio run -t upload --upload-port 192.168.1.42All endpoints respond with 200 ok on success, 400 missing <arg> for
malformed requests, and 409 … when the limit switch is latched against
the requested motion.
| Method | Path | Action |
|---|---|---|
| GET | / |
Web UI (HTML, auto-refresh status every 500 ms) |
| GET | /status |
Plain-text status (see below) |
| GET | /enable |
Power driver outputs (manual digitalWrite on ENABLE) |
| GET | /disable |
Decelerate, then power down driver outputs |
| GET | /stop |
Decelerate to a stop (driver remains enabled) |
| GET | /home |
Zero current position and clear limit-switch latch |
| GET | /move?pos=<mm> |
Absolute move; clamped to [0, MAX_TRAVEL_MM]; auto-enables driver |
| GET | /jog?d=<mm> |
Relative jog; clamped against soft limits; auto-enables driver |
| GET | /profile?speed=<mm/s>&accel=<mm/s²> |
Update max speed and acceleration |
enabled : yes|no
position: <float> mm
target : <float> mm
running : yes|no
speed : <float> mm/s
accel : <float> mm/s^2
limit : TRIGGERED|clear
latched : yes (move + to clear)|no
curl http://<ip>/enable
curl "http://<ip>/profile?speed=30&accel=200"
curl "http://<ip>/move?pos=100" # absolute move to 100 mm
curl "http://<ip>/jog?d=-5" # back off 5 mm
curl http://<ip>/status
curl http://<ip>/disableRead these before powering the motor for the first time.
- Verify
STEPS_PER_MMandMAX_TRAVEL_MMbefore applying motor power. A wrong steps/mm value can drive the carriage into a hard stop at full configured speed. - The HTTP API is unauthenticated. Anyone on the same network can move
the axis. Run it on a trusted/isolated network, or add HTTP Basic auth
(
server.authenticate(...)) and disable the auto-enable behavior inhandleMove/handleJogif exposure is a concern. /homedoes not perform a homing sequence. It only zeroes the current position and clears the latch. If you need repeatable absolute homing, replacehandleHomewith a routine that drives towardPIN_LIMITat a slow speed until triggered, then zeros there.- One-sided end-stop. The latch logic assumes the limit switch is at
the min end (position 0). If you fit a switch at the max end as well,
the latch direction logic in
handleMove/handleJogneeds to track which end tripped. - The driver is auto-enabled by
/moveand/jogif it is not already enabled. Use/disableexplicitly when leaving the machine unattended so the motor coils de-energize.
.
├── platformio.ini # board, framework, lib deps, WiFi build flags
├── src/main.cpp # firmware (single file)
├── include/ # project headers (currently empty)
├── lib/ # private libraries (currently empty)
├── .gitignore # excludes .pio/ build output
└── README.md
Declared in platformio.ini:
waspinator/AccelStepper@^1.64— step-pulse generation with accelerationbblanchon/ArduinoJson@^7.0.4— currently declared but not used; safe to remove if you do not plan to add JSON endpoints
No license file is included. Add one (MIT, Apache-2.0, …) if you intend to share or publish the firmware.