Skip to content

4.0.5 pre2 with unipolar motor support, improved SD speed & Wifi disconnect resilience

Pre-release
Pre-release

Choose a tag to compare

@f1adang f1adang released this 26 Aug 19:50

Highlights

WebUI uploads to SD are much faster

uploadWrite() slept 1 ms on every chunk. Chunks arrive at roughly the TCP
segment size, so the sleep cost about a millisecond per kilobyte — seconds of pure
sleeping on a large file, entirely independent of how fast the card is. It now
yields every 32 chunks, which keeps the original intent of letting other tasks run.

Two further costs in the same path:

  • FileStream left its stdio stream on the default buffer, so the small writes
    arriving from the network were handed to FATFS in equally small pieces. Buffering
    at 4096 bytes lets whole runs of sectors reach the card at once.
  • uploadEnd() closed the file before taking its FluidPath, dropping the mount
    count to zero and forcing a full card re-initialisation for the rehash and size
    check. Under memory pressure that failed outright with ESP_ERR_NO_MEM, reporting
    a file that had been written correctly as a failed upload.

A separate mount race that made uploads report "No SD card" is also fixed.

A job no longer dies when WiFi does

Losing WiFi mid-job used to end the cut, and usually took the controller with it.
There were five independent causes, each fixed here:

What happened Cause
Wrote through a freed pointer every GCode line The job held a raw pointer to the channel that launched it
Heap exhausted, then abort() Channel input queued unboundedly while a job ran; a FreeRTOS mutex leaked per channel
Task watchdog reboot WebClient::write() waited forever for an HTTP reader that had vanished
std::terminate() An exception escaping a support task panicked the board
Cache disabled but cached memory region accessed The step ISR read vtables out of flash

That last one was a build-configuration bug that had switched off the project's own
protection — see below. A job now runs to completion through a WiFi drop, reporting
once that its output channel went away.

Running a job no longer depends on the network

A job started from WebUI kept a raw pointer to the channel that launched it. When
WiFi dropped, that channel was freed and the controller wrote through the dangling
pointer on every subsequent GCode line. A job now survives losing its WebUI or
WebSocket connection and runs to completion, reporting once that its output channel
went away.

Three other ways a network failure could stop a cut:

  • Heap exhaustion. Input arriving on a channel was queued unboundedly while a
    job held the line-processing path, and nothing drained it until the job ended.
    Every Channel also leaked a FreeRTOS mutex on destruction — one per HTTP
    command, one per WebSocket connection.
  • Task watchdog reboot. WebClient::write() waited without limit for an HTTP
    reader that had vanished with the WiFi, blocking the watchdog-subscribed protocol
    task mid-job.
  • std::terminate(). An exception escaping any of the polling, output or
    web-command tasks panicked the board. They now contain it and take another pass.

Crash fixes: flash access from the step ISR

Jobs could die with Guru Meditation Error: Cache disabled but cached memory region accessed. Any SPI flash access disables the cache on both cores; an ISR that
touches flash in that window panics.

  • platformio_override.ini set extra_scripts for the wifi, noradio and bt
    environments. That replaces the value inherited through extends rather than
    adding to it, silently dropping vtable_in_dram.py — so the project's own
    sections.ld was never found and every vtable stayed in flash (0 in DRAM, 240
    in flash). The PinDetail and Spindle vtables that vtable_in_dram.ld
    explicitly names are read by the step ISR. Now 30 vtables are in DRAM.
  • The unipolar driver was reached from the step ISR by virtual dispatch, and its
    vtable is not covered by that linker script. MotorDriver now supplies plain
    IRAM function pointers, cached at registration; phase pins are written with
    gpio_write() instead of through Pin, whose write() dispatches through a
    PinDetail vtable.
  • state_is() was called into flash from the ISR, and spindle->setSpeedfromISR()
    read the Spindle vtable. Both are now ISR-safe. These affect any board, not only
    unipolar builds.

Verified against the linked image: nothing reachable from the step ISR references
flash.

SD and WebUI uploads

  • Fixed a mount race that made uploads report "No SD card".
  • Reduced per-write overhead in the file write path.
  • An SD read error is no longer reported as end-of-file. Previously a transient read
    failure ended a job partway through and reported it as complete; it is now a
    visible error, and the partial line is discarded rather than executed.

Diagnostics

A job that stops now says why, at a level that reaches the console:

[MSG:INFO: Job started: /sd/part.gcode]
[MSG:INFO: /sd/part.gcode job sent]
[MSG:ERR: Job aborted in state Cycle by alarm 1 (Hard Limit)]
  • Restart reason: is logged at startup — power-on, panic, task watchdog or
    brownout were previously indistinguishable.
  • The low-memory warning is re-enabled and no longer allocates to report itself.
  • New $Console/ReportInterval (ms, default 0) gives grbl status reports on the
    serial console, including the SD: progress field — the one connection that
    survives a WiFi failure.

Assets

FluidNC-v4.0.5-pre2-wifi-esp32-firmware.bin — classic ESP32, wifi
environment. The firmware reports itself as v4.0.5-pre2-wifi.

This is the application image, which is what the Web Installer's
firmware-update path uploads and what WebUI's OTA firmware update accepts. It
preserves the filesystem, so your config and WebUI files on SPIFFS are left alone.

esptool.py --chip esp32 write_flash 0x10000 \
    FluidNC-v4.0.5-pre2-wifi-esp32-firmware.bin

For a fresh install on a blank board this file is not enough on its own — the
Web Installer's fresh-install path also needs the bootloader (0x1000), partition
table (0x8000), boot_app0 (0xe000) and a filesystem image, none of which are
published here. Use a full release bundle from build-release.py for that.