Week 12

Mechanical Design, Machine Design - Group Assignment

The first project, BIOCRUSHER, is a bio-machine designed for processing Amazonian fibers and producing bio-paper. The system integrates mechanical design, actuation, automation, safety mechanisms, and a Nextion HMI interface, demonstrating the application of biomaterials through digital fabrication technologies.

The second project, STRING ART, is a digital fabrication machine that automates the creation of string art compositions. Using a motion control system and continuous thread management, the machine generates complex visual patterns and gradients based on digital images or predefined designs. Both projects explored machine design, control systems, and creative fabrication processes.

Fab Lab Peru icon

The objective was to design, build, actuate, automate and document a collaborative machine with a mechanism, application and complete team workflow.

GROUP 01

Week summary

Our group developed BIOCRUSHER, a semi-automated bio-machine designed to process Amazonian fibers for bio-paper and biomaterial experimentation. The machine includes a controlled iris inlet, a safety door interlock, a counter-rotating blade mechanism, DC motor actuation, NEMA stepper motors for material collection movement, an ESP32 control system and a Nextion touchscreen interface.

The project began with collaborative brainstorming and workflow definition. The team identified local fibers such as cassava, coconut, banana and pineapple as potential materials for bio-paper production. The machine focuses on the first step of the process: cutting and reducing fibers into smaller pieces before cooking, blending, sheet formation, pressing and drying.

Group assignment requirement: design a machine that includes mechanism, actuation, automation and application; build the mechanical parts and operate them manually; actuate and automate the machine; document the group project and individual contributions.

Quick data

  • Machine: BIOCRUSHER / BIO SHREDDER
  • Topic: Mechanical Design + Machine Design
  • Application: Amazonian fibers for bio-paper production
  • Mechanism: Iris inlet + counter-rotating shredding blades, DC motor and NEMA stepper motors, ESP32, sensors, motor drivers and Nextion HMI
  • Student: Carmen Elena Gutierrez Apolinario, Esteban M. Valladares, Jianfranco Bazan J., Mario Chong, Rocio Maravi, Grace Schwan, Cindy Marilyn Crispin.

General Flyer

BIOCRUSHER general flyer

General Video

Assignment requirements and evidence

Fab Academy requirementHow BIOCRUSHER addresses itEvidence to include
Design a machine with mechanismCounter-rotating shredding blade matrix, iris inlet, rails and container displacement system.CAD screenshots, exploded view and blade assembly photos.
Include actuationDC motor drives the shredding blades; NEMA motors move the rails and receiving container.Motor tests, driver wiring and motion videos.
Include automationESP32 controls motors, sensors, timing, HMI commands and safety conditions.Code, serial monitor, HMI screen and system tests.
Include applicationProcessing Amazonian fibers for biomaterials and bio-paper production.Fiber tests, shredded material and bio-paper workflow.
Build mechanical parts and operate manuallyThe blade system, chassis, iris mechanism and container movement were assembled and first tested manually.Manual operation photos and videos.
Actuate and automate the machineThe machine was integrated with drivers, sensors, Nextion interface and control logic.Automated test videos and final demo.

Team planning and task allocation

The team divided responsibilities according to mechanical design, electronics, programming, material testing and documentation. Each member contributed to the group result and linked their individual page to the group page.

Team memberMain responsibilityDesign thinking focusIndividual page link
Esteban ValladaresMechanical design, modeling, machine operability and fabricationIdeate, Prototype, Testlink
Carmen GutierrezMechanical design, modeling and CAD supportIdeate, PrototypeLink
Jianfranco BazanElectronics, PCB, drivers and programmingIdeate, Prototype, TestLink
Rocio MaraviElectronics, stepper motor tests, Nextion HMI interface and documentation supportAll phases: record, log, prototype and testLink
Cindy CrispinResearch, local fiber selection and material testingEmpathize, Define, TestLink
Grace SchwanTechnical documentation, photography and project logAll phases: record and logAdd link
Mario ChongTechnical documentation, photography and VS Code documentationAll phases: record and logLink

Inspiration

The BIOCRUSHER project was inspired by the traditional knowledge and practices of local communities that work with natural fibers and biomaterials. These communities transform organic resources into useful products such as handmade paper, crafts and sustainable materials, demonstrating a strong connection between technology, nature and culture.

From a technical perspective, the design was also influenced by different mechanical systems. The shredding mechanism was inspired by paper shredders, which use interlocking blades to reduce material size efficiently. Additionally, the idea of continuous material flow and controlled feeding was influenced by industrial food machines, such as pizza preparation systems, where materials move in a guided and organized process.

Another important aspect of this project was the collaborative nature of the team. For this assignment, team members came together from different cities and regions of Peru, including Satipo, Madre de Dios, Lima and Huaral. This diversity enriched the design process by bringing different perspectives, experiences and ideas into the development of the machine.

The combination of traditional inspiration, industrial references and collaborative work allowed the team to develop a machine that connects local knowledge with digital fabrication and modern engineering.

Sources of inspiration

Figure 1. Local communities working with natural fibers and biomaterials.

Figure 2. Traditional fiber processing techniques used for handmade materials.

Figure 3. Paper shredder mechanism inspiring the cutting system design.

Figure 4. Industrial pizza machine inspiring continuous material flow.

Figure 5. Mechanical systems with guided motion used as reference.

Figure 6. Team collaboration across regions: Satipo, Madre de Dios, Lima and Huaral.

Five-phase design thinking process

1. Empathize

We researched local fibers and sustainable fabrication needs for accessible biomaterial production.

2. Define

We defined the machine as a fiber shredder for the bio-paper workflow and safety-driven operation.

3. Ideate

We sketched mechanisms, discussed blade geometry, and planned electronics and HMI interaction.

4. Prototype

We fabricated the chassis, blade supports, gears, interface, PCB and rail motion system.

5. Test

We validated motion, safety logic, sensor response, material shredding and improvement opportunities.

Goal

The goal was to build a semi-automated bio-machine capable of cutting and reducing Amazonian fibers into smaller particles for bio-paper and biomaterial experimentation. The system had to be safe, modular, replicable and suitable for digital fabrication.

Application

BIOCRUSHER supports the first stage of bio-paper production by cutting fibers into small pieces. After shredding, the fibers can be cooked with ash or alkaline solution, blended with water, formed into sheets, pressed and dried.

System design

The machine integrates a controlled material inlet, safety door, shredding chamber, motor actuation, axis movement, receiving container and HMI interface.

Fiber input
   ↓
Iris inlet mechanism
   ↓
Safety door verification
   ↓
Counter-rotating shredding blades
   ↓
Receiving container
   ↓
Rail movement using NEMA motors
   ↓
Material delivery
   ↓
Bio-paper process
Nextion HMI
   ↓ Serial UART
ESP32 controller
   ↓
Motor drivers
   ↓
DC motor + NEMA motors
   ↓
Sensors feedback
   ↓
Safe automated operation

Machine operation

The BIOCRUSHER works as a semi-automated fiber processing system. The user places the biomaterial into the input area. The machine has an iris mechanism that opens and closes to regulate the amount of material entering the shredding chamber. This iris system is connected to the safety logic of the machine, because the machine is allowed to operate only when the protective door is closed.

When the door sensor confirms that the door is closed, the ESP32 enables the operating sequence. The DC motor activates the blade shafts, which cut and shred the fiber. The speed can be adjusted according to the selected fiber type. Softer fibers can use a lower voltage or lower PWM value, while harder fibers can use a higher value to increase shaft speed and torque response.

After the fiber is shredded, the material falls into a receiving container. This container is moved by a rail system controlled by NEMA stepper motors. The system moves the container downward to receive the shredded material and then forward to deliver the processed output. Sensors can indicate whether the container is full or empty, and the Nextion screen displays the current process time, machine status, safety status and container condition.

Safety rule: if the protective door is open, the machine must stop and remain locked. If the door is closed, the user may start the shredding and collection sequence.

Nextion HMI Interface

BIOCRUSHER Nextion HMI interface

Crusher Control Screen

The Nextion HMI interface was designed as the main control panel of the BIOCRUSHER machine. It allows the user to monitor the machine status and control the main operating elements in a simple and visual way.

The screen includes indicators for the motor, safety door, iris mechanism and receiving cup. The motor section displays the current motor value and percentage, while the door section shows if the safety door is closed or open. The machine is programmed to operate only when the door is closed.

The iris section shows the opening state of the feeding mechanism, which controls how much biomaterial enters the shredding chamber. The cup indicator helps the operator know when the container must be removed or replaced after receiving shredded material.

  • Motor: shows motor activity and speed percentage.
  • Door: displays the safety door condition.
  • Iris: monitors the biomaterial inlet mechanism.
  • Cup: indicates when the receiving container must be removed.

Electronic components and estimated cost

The electronics subsystem includes the controller, HMI display, motor drivers, sensors, power supply and communication modules used to actuate and monitor the machine. The cost table is shown in US dollars as an estimated reference.

IDCategoryComponentQtyEstimated unit cost (USD)Description
ITM-001Electronics / HMINextion 3.5" touchscreen display1$86.50Serial HMI touchscreen used as the main interface of the machine.
ITM-002Electronics / HMIFT232BL Mini USB converter 3.3V / 5V1$3.65USB-to-serial adapter used to program or communicate with the Nextion display.
ITM-003Electronics / CommunicationBidirectional logic level converter 5V to 3.3V2$1.10TTL level converter used to adapt serial communication levels.
ITM-004Power / DC MotorIBT-2 / BTS7960 driver1$13.00High-current H-bridge driver for DC motor speed and direction control.
ITM-005Power12V 15A power supply1$39.20Main power supply for actuators and peripherals.
ITM-006ControllerESP32 development board1$8.00Main controller for sensors, motors, HMI communication and automation logic.
ITM-007MotionNEMA stepper motor2$12.00Stepper motors used for the rail and receiving container displacement system.
ITM-008Motion driverDRV8825 stepper driver2$3.00Driver modules for stepper motor speed, direction and microstepping control.
ITM-009ActuationDC motor1$10.00Motor used to drive the shredding shafts and blade system.
ITM-010SafetyDoor safety sensor1$2.00Sensor used to allow operation only when the protective door is closed.
ITM-011Motion / SafetyLimit switches4$1.00Detect end positions of axes, rails and moving mechanisms.
ITM-012DetectionContainer full / empty sensor1$2.00Detects whether the receiving container is empty or full.
ITM-013WiringWires, terminals and connectorsVarious$5.00Electrical interconnection between the controller, drivers, sensors and actuators.

Mechanical design and fabrication

The mechanical system is based on two counter-rotating blade shafts. Each shaft contains repeated blade units arranged as a modular matrix to cut fibers efficiently. The team modeled the parts in CAD, validated the assembly and fabricated structural supports using digital fabrication processes such as 3D printing, laser cutting and CNC machining.

Shredding blade matrix

The cutting mechanism uses intermeshed blade stacks assembled on shafts. This system reduces fiber size by pulling and cutting the material as the shafts rotate.

Chassis and supports

The main structure includes MDF or acrylic panels, aluminum profiles, 3D printed supports, bearing blocks and motor mounting parts.

Mechanical fabrication gallery

Mechanical photo 1

Defining the product and determining the critical steps in the production of biomaterials.

Mechanical photo 2

We chose the critical step, which is the crushing of the fiber for the production of biomaterials.

Mechanical photo 3

A modular design is chosen with the idea that each floor fulfills a function for a critical step in the production of biomaterials.

Mechanical photo 4

Dual-shaft shredding system composed of intermeshing blade discs mounted on parallel shafts. This mechanism enables efficient cutting and size reduction of fibrous biomaterials through continuous rotation and shear action.

Mechanical photo 5

Mecanismo de engranajes impreso en 3D utilizado para transmitir movimiento rotacional dentro de la máquina..

Mechanical photo 6

Linear motion system using two stepper motors: one controls vertical movement along the Z-axis, while the second moves the collection tray along the X-axis.

Mechanical GIF 1

Demonstration of the use of gears for rotating the shredding blades.

Mechanical GIF 2

Operation of the DC motor that will rotate the shredding blades.

Mechanical GIF 3

Functional test of the door security magnetic sensor.

Programming and control logic

The control logic was programmed using Arduino IDE for the ESP32. The code manages the safety door, material selection, motor speed, iris mechanism, shredding time, container movement and status messages sent to the Nextion HMI.



#include 
#include 

// =====================================================
// PROTOCOLO NEXTION / ESP32
// =====================================================
// Nextion -> ESP32
//   \n
//      motorPct: 0..100
//      dir: 0 = CRUSH, 1 = REVERSE
//      irisPct: 0..100
//
//   \n   -> REMOVE CUP (pulso/evento)
//   \n   -> RETURN CUP (pulso/evento)
//
// ESP32 -> Nextion
//   SOLO comandos válidos de Nextion ASCII + 0xFF 0xFF 0xFF
//   Ejemplo mínimo:
//      vaDoor.val=0   -> puerta cerrada
//      vaDoor.val=1   -> puerta abierta

// =====================================================
// PINES
// =====================================================

// --- Motor DC con IBT-2 ---
const uint8_t PIN_RPWM = 13;
const uint8_t PIN_LPWM = 14;

// --- Sensor de puerta ---
const uint8_t PIN_PUERTA = 27;
const bool PUERTA_CERRADA_NIVEL = LOW;

// --- Servo compuerta / iris ---
const uint8_t PIN_SERVO = 26;

// --- Stepper 1 (DRV8825) ---
// Eje Z (recorrido corto, final de carrera 1)
const uint8_t M1_STEP_PIN  = 18;
const uint8_t M1_DIR_PIN   = 19;
const uint8_t M1_LIMIT_PIN = 32;

// --- Stepper 2 (DRV8825) ---
// Eje Y (recorrido largo, final de carrera 2)
const uint8_t M2_STEP_PIN  = 21;
const uint8_t M2_DIR_PIN   = 22;
const uint8_t M2_LIMIT_PIN = 33;

// --- Nextion UART ---
const uint8_t NEXTION_RX_PIN = 23;   // ESP32 RX <- TX Nextion
const uint8_t NEXTION_TX_PIN = 25;   // ESP32 TX -> RX Nextion
const uint32_t NEXTION_BAUD  = 9600;

// UART2 para Nextion
HardwareSerial NextionSerial(2);

// =====================================================
// CALIBRACION FINAL
// =====================================================
bool SERVO_AUMENTAR_ANGULO_ABRE = false;
int  SERVO_ANGULO_CERRADO = 180;
int  SERVO_ANGULO_ABIERTO = 0;

const bool LIMIT1_ACTIVE_LEVEL = LOW;
const bool LIMIT2_ACTIVE_LEVEL = LOW;

int8_t M1_HOME_DIR = 1;
int8_t M2_HOME_DIR = 1;

// =====================================================
// MICROSTEPPING 1/16
// =====================================================
const long MICROSTEP_FACTOR = 16;

// Valores calibrados originalmente en paso completo
const long M1_RANGE_STEPS_FULL_BASE         = 700;
const long M2_RANGE_STEPS_FULL_BASE         = 3600;
const long M2_RETURN_CUP_Y_STEPS_FULL_BASE  = 300;

// Escalados
long M1_RANGE_STEPS = M1_RANGE_STEPS_FULL_BASE * MICROSTEP_FACTOR; // 11200
long M2_RANGE_STEPS = M2_RANGE_STEPS_FULL_BASE * MICROSTEP_FACTOR; // 57600
long M2_RETURN_CUP_Y_STEPS = M2_RETURN_CUP_Y_STEPS_FULL_BASE * MICROSTEP_FACTOR; // 5760

// =====================================================
// CONFIG PWM MOTOR DC
// =====================================================
const uint32_t PWM_FREQ_MOTOR = 20000;
const uint8_t  PWM_RES_MOTOR  = 8;
const uint16_t PWM_MAX_MOTOR  = (1 << PWM_RES_MOTOR) - 1;

// =====================================================
// CONFIG PWM SERVO
// =====================================================
const uint32_t PWM_FREQ_SERVO = 50;
const uint8_t  PWM_RES_SERVO  = 16;
const uint32_t PWM_MAX_SERVO  = (1UL << PWM_RES_SERVO) - 1;
const uint32_t SERVO_PERIODO_US = 20000;

int SERVO_MIN_US = 500;
int SERVO_MAX_US = 2500;

// Servo un poco más rápido que antes, pero aún suave
unsigned long INTERVALO_SERVO_MS = 25;
int PASO_SERVO = 1;

// =====================================================
// MOTOR DC / PUERTA
// =====================================================
unsigned long TIEMPO_CAMBIO_SENTIDO_MS = 1000;
unsigned long INTERVALO_RAMPA_MS       = 20;
uint8_t       PASO_RAMPA               = 2;

unsigned long DEBOUNCE_PUERTA_MS       = 60;
unsigned long INTERVALO_REPORTE_MS     = 1000;

// =====================================================
// CONFIG STEPPERS CON ACCELSTEPPER
// MÁS LENTO PARA EVITAR PÉRDIDA DE PASOS
// =====================================================
const float M1_MAX_SPEED = 400.0f * MICROSTEP_FACTOR;   // 6400
const float M2_MAX_SPEED = 400.0f * MICROSTEP_FACTOR;   // 6400

const float M1_ACCEL = 700.0f * MICROSTEP_FACTOR;       // 11200
const float M2_ACCEL = 700.0f * MICROSTEP_FACTOR;       // 11200

// Homing normal
const float M1_HOME_FAST_SPEED = 120.0f * MICROSTEP_FACTOR;  // 1920
const float M2_HOME_FAST_SPEED = 120.0f * MICROSTEP_FACTOR;  // 1920
const float M1_HOME_SLOW_SPEED = 40.0f  * MICROSTEP_FACTOR;  // 640
const float M2_HOME_SLOW_SPEED = 40.0f  * MICROSTEP_FACTOR;  // 640
const float M1_HOME_BACKOFF_SPEED = 80.0f * MICROSTEP_FACTOR; // 1280
const float M2_HOME_BACKOFF_SPEED = 80.0f * MICROSTEP_FACTOR; // 1280

// Homing de arranque aún más silencioso
const float STARTUP_HOME_FAST_SPEED   = 90.0f  * MICROSTEP_FACTOR; // 1440
const float STARTUP_HOME_SLOW_SPEED   = 30.0f  * MICROSTEP_FACTOR; // 480
const float STARTUP_HOME_BACKOFF_SPEED = 60.0f * MICROSTEP_FACTOR; // 960

const long HOME_SEARCH_STEPS = 200000L;
const long HOME_RELEASE_MAX_STEPS = 5000L;

// Si inviertes esto, recalibra homeDir
bool M1_INVERT_DIR = false;
bool M2_INVERT_DIR = false;

// =====================================================
// NEXTION RX BUFFER
// =====================================================
char nextionRxBuf[80];
uint8_t nextionRxIdx = 0;

// =====================================================
// SERIAL USB BUFFER
// =====================================================
char bufferRx[40];
uint8_t idxRx = 0;

// =====================================================
// SOLICITUDES Y ESTADOS APLICADOS (HMI / CONTROL)
// =====================================================
uint8_t requestedMotorPct = 0;
uint8_t requestedDir      = 0;   // 0=CRUSH, 1=REVERSE
uint8_t requestedIrisPct  = 0;

uint8_t appliedMotorPct   = 0;
uint8_t appliedDir        = 0;
uint8_t appliedIrisPct    = 0;

bool cupRemovePulse = false;
bool cupReturnPulse = false;

uint8_t lastIssuedMotorPct = 255;
uint8_t lastIssuedDir      = 255;
uint8_t lastIssuedIrisPct  = 255;

// feedback Nextion
int lastDoorFeedbackSent = -1;
unsigned long tNextionFeedback = 0;
unsigned long INTERVALO_NEXTION_FEEDBACK_MS = 250;

// =====================================================
// ESTADOS MOTOR DC
// =====================================================
enum Direccion {
  DIR_STOP = 0,
  DIR_D,
  DIR_I
};

Direccion dirActual = DIR_STOP;
uint8_t velocidadActual = 0;
uint8_t velocidadObjetivo = 0;

Direccion dirPendiente = DIR_STOP;
uint8_t velocidadPendiente = 0;
bool inversionEnProceso = false;
bool esperandoPausaInversion = false;

Direccion dirUsuario = DIR_STOP;
uint8_t velocidadUsuario = 0;

unsigned long tRampa = 0;
unsigned long tEsperaInversion = 0;

// =====================================================
// ESTADO PUERTA
// =====================================================
bool puertaCerradaRaw = false;
bool puertaCerradaRawAnterior = false;
bool puertaCerradaEstable = false;

unsigned long tCambioPuerta = 0;
unsigned long tReporte = 0;

// =====================================================
// ESTADO SERVO
// =====================================================
int servoAnguloActual = 0;
int servoAnguloObjetivo = 0;
unsigned long tServo = 0;

// =====================================================
// STEPPERS
// =====================================================
AccelStepper stepper1(AccelStepper::DRIVER, M1_STEP_PIN, M1_DIR_PIN);
AccelStepper stepper2(AccelStepper::DRIVER, M2_STEP_PIN, M2_DIR_PIN);

enum HomePhase {
  HOME_IDLE = 0,
  HOME_RELEASE,
  HOME_SEEK_FAST,
  HOME_BACKOFF,
  HOME_SEEK_SLOW
};

struct StepperAxis {
  AccelStepper* stepper;

  uint8_t stepPin;
  uint8_t dirPin;
  uint8_t limitPin;
  bool limitActiveLevel;

  bool invertDir;
  int8_t homeDir;     // +1 significa que DIR lógico positivo va hacia home
  long rangeSteps;    // 0..range en coordenada máquina

  bool jogMode = false;
  bool homed = false;

  long targetMachinePos = 0;

  float maxSpeed = 1000.0f;
  float accel = 1000.0f;

  // homing silencioso
  HomePhase homePhase = HOME_IDLE;
  float homeFastSpeed = 1000.0f;
  float homeSlowSpeed = 300.0f;
  float homeBackoffSpeed = 600.0f;
  long homePhaseStartPos = 0;
};

StepperAxis axis1;
StepperAxis axis2;

// =====================================================
// SECUENCIAS AUTOMATICAS
// =====================================================
enum AutoSequenceState {
  AUTOSEQ_NONE = 0,
  AUTOSEQ_STARTUP_HOME_Z,
  AUTOSEQ_STARTUP_HOME_Y,
  AUTOSEQ_REMOVE_HOME_Z,
  AUTOSEQ_REMOVE_HOME_Y,
  AUTOSEQ_REMOVE_MOVE_Y_MAX,
  AUTOSEQ_RETURN_HOME_Z,
  AUTOSEQ_RETURN_MOVE_Y_NEAR_HOME,
  AUTOSEQ_RETURN_MOVE_Z_MAX
};

AutoSequenceState autoSeqState = AUTOSEQ_STARTUP_HOME_Z;
bool autoSeqStepStarted = false;
bool startupHomingDone = false;

// =====================================================
// UTILS
// =====================================================
uint8_t minU8(uint8_t a, uint8_t b) {
  return (a < b) ? a : b;
}

int clampInt(int x, int lo, int hi) {
  if (x < lo) return lo;
  if (x > hi) return hi;
  return x;
}

long clampLong(long x, long lo, long hi) {
  if (x < lo) return lo;
  if (x > hi) return hi;
  return x;
}

const char* textoDireccion(Direccion d) {
  switch (d) {
    case DIR_D:    return "D";
    case DIR_I:    return "I";
    case DIR_STOP: return "STOP";
    default:       return "?";
  }
}

bool esSentidoValido(Direccion d) {
  return (d == DIR_D || d == DIR_I);
}

void toUpperInPlace(char* s) {
  for (size_t i = 0; s[i] != '\0'; i++) {
    s[i] = (char)toupper((unsigned char)s[i]);
  }
}

long mapLongClamped(long x, long in_min, long in_max, long out_min, long out_max) {
  if (in_max == in_min) return out_min;

  if (in_min < in_max) {
    if (x < in_min) x = in_min;
    if (x > in_max) x = in_max;
  } else {
    if (x > in_min) x = in_min;
    if (x < in_max) x = in_max;
  }

  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

bool autoSequenceActive() {
  return autoSeqState != AUTOSEQ_NONE;
}

// =====================================================
// NEXTION HELPERS
// =====================================================
void sendNextionCommand(const String &cmd) {
  NextionSerial.print(cmd);
  NextionSerial.write(0xFF);
  NextionSerial.write(0xFF);
  NextionSerial.write(0xFF);
}

void updateHmiFeedback() {
  unsigned long ahora = millis();
  if (ahora - tNextionFeedback < INTERVALO_NEXTION_FEEDBACK_MS) return;
  tNextionFeedback = ahora;

  int doorVal = puertaCerradaEstable ? 0 : 1;

  if (doorVal != lastDoorFeedbackSent) {
    sendNextionCommand(String("vaDoor.val=") + String(doorVal));
    lastDoorFeedbackSent = doorVal;
  }
}

// =====================================================
// PARSER NEXTION
// =====================================================
void processHmiFrame(const String &msg) {
  if (msg.length() < 3) return;

  if (msg.startsWith("", &motorPct, &dir, &irisPct) == 3) {
      motorPct = clampInt(motorPct, 0, 100);
      dir      = clampInt(dir, 0, 1);
      irisPct  = clampInt(irisPct, 0, 100);

      requestedMotorPct = (uint8_t)motorPct;
      requestedDir      = (uint8_t)dir;
      requestedIrisPct  = (uint8_t)irisPct;

      Serial.printf("[HMI] H motor=%u dir=%u iris=%u\n",
                    requestedMotorPct, requestedDir, requestedIrisPct);
    } else {
      Serial.printf("[HMI] Frame H invalido: %s\n", msg.c_str());
    }
    return;
  }

  if (msg.startsWith("", &evt) == 1) {
      if (evt == 1) {
        cupRemovePulse = true;
        Serial.println("[HMI] Evento REMOVE CUP");
      } else if (evt == 2) {
        cupReturnPulse = true;
        Serial.println("[HMI] Evento RETURN CUP");
      } else {
        Serial.printf("[HMI] Evento C desconocido: %d\n", evt);
      }
    } else {
      Serial.printf("[HMI] Frame C invalido: %s\n", msg.c_str());
    }
    return;
  }

  Serial.printf("[HMI] Frame ignorado: %s\n", msg.c_str());
}

void readNextionFrames() {
  while (NextionSerial.available() > 0) {
    char ch = (char)NextionSerial.read();

    if (ch == '\r') continue;

    if (ch == '\n') {
      if (nextionRxIdx > 0) {
        nextionRxBuf[nextionRxIdx] = '\0';
        processHmiFrame(String(nextionRxBuf));
        nextionRxIdx = 0;
      }
    } else {
      if (nextionRxIdx < sizeof(nextionRxBuf) - 1) {
        nextionRxBuf[nextionRxIdx++] = ch;
      } else {
        nextionRxIdx = 0;
      }
    }
  }
}

// =====================================================
// MOTOR DC
// =====================================================
uint16_t porcentajeADuty(uint8_t porcentaje) {
  if (porcentaje > 100) porcentaje = 100;
  return (uint16_t)((porcentaje * PWM_MAX_MOTOR) / 100);
}

void escribirPuente(Direccion dir, uint8_t velPercent) {
  uint16_t duty = porcentajeADuty(velPercent);

  switch (dir) {
    case DIR_D:
      ledcWrite(PIN_RPWM, duty);
      ledcWrite(PIN_LPWM, 0);
      break;

    case DIR_I:
      ledcWrite(PIN_RPWM, 0);
      ledcWrite(PIN_LPWM, duty);
      break;

    case DIR_STOP:
    default:
      ledcWrite(PIN_RPWM, 0);
      ledcWrite(PIN_LPWM, 0);
      break;
  }
}

void apagarMotorPorSeguridad() {
  inversionEnProceso = false;
  esperandoPausaInversion = false;

  dirPendiente = DIR_STOP;
  velocidadPendiente = 0;

  dirActual = DIR_STOP;
  velocidadActual = 0;
  velocidadObjetivo = 0;

  escribirPuente(DIR_STOP, 0);
}

void detenerMotorNormal() {
  inversionEnProceso = false;
  esperandoPausaInversion = false;

  dirPendiente = DIR_STOP;
  velocidadPendiente = 0;

  velocidadObjetivo = 0;
}

void iniciarInversion(Direccion nuevaDir, uint8_t nuevaVel) {
  dirPendiente = nuevaDir;
  velocidadPendiente = nuevaVel;

  inversionEnProceso = true;
  esperandoPausaInversion = false;
  velocidadObjetivo = 0;

  Serial.printf("Inversion solicitada. Frenando a 0 y esperando %lu ms.\n",
                TIEMPO_CAMBIO_SENTIDO_MS);
}

void procesarSolicitudMotor(Direccion nuevaDir, uint8_t nuevaVel) {
  if (nuevaVel > 100) {
    Serial.println("Error: velocidad fuera de rango (0-100)");
    return;
  }

  if (nuevaVel == 0) {
    dirUsuario = DIR_STOP;
    velocidadUsuario = 0;
  } else {
    dirUsuario = nuevaDir;
    velocidadUsuario = nuevaVel;
  }

  if (!puertaCerradaEstable) {
    apagarMotorPorSeguridad();

    if (nuevaVel == 0) {
      Serial.println("Puerta abierta. Motor detenido y configuracion en STOP.");
    } else {
      Serial.printf("Puerta abierta. Configuracion guardada para reanudar luego: %s%u\n",
                    textoDireccion(dirUsuario), velocidadUsuario);
    }
    return;
  }

  if (nuevaVel == 0) {
    Serial.println("Parada solicitada.");
    detenerMotorNormal();
    return;
  }

  if (dirActual == DIR_STOP && velocidadActual == 0 && !inversionEnProceso) {
    dirActual = nuevaDir;
    velocidadObjetivo = nuevaVel;
    Serial.printf("Arranque: %s%u\n", textoDireccion(nuevaDir), nuevaVel);
    return;
  }

  if (!inversionEnProceso && dirActual == nuevaDir) {
    velocidadObjetivo = nuevaVel;
    Serial.printf("Nueva velocidad: %s%u\n", textoDireccion(nuevaDir), nuevaVel);
    return;
  }

  if (!inversionEnProceso && esSentidoValido(dirActual) && nuevaDir != dirActual) {
    iniciarInversion(nuevaDir, nuevaVel);
    return;
  }

  if (inversionEnProceso) {
    dirPendiente = nuevaDir;
    velocidadPendiente = nuevaVel;
    Serial.printf("Destino pendiente actualizado: %s%u\n",
                  textoDireccion(nuevaDir), nuevaVel);
  }
}

void actualizarRampaMotor() {
  if (!puertaCerradaEstable) return;

  unsigned long ahora = millis();
  if (ahora - tRampa < INTERVALO_RAMPA_MS) return;
  tRampa = ahora;

  if (inversionEnProceso && !esperandoPausaInversion) {
    if (velocidadActual > velocidadObjetivo) {
      uint8_t diferencia = (uint8_t)(velocidadActual - velocidadObjetivo);
      uint8_t delta = minU8(PASO_RAMPA, diferencia);
      velocidadActual -= delta;
      escribirPuente(dirActual, velocidadActual);
    }

    if (velocidadActual == 0) {
      escribirPuente(DIR_STOP, 0);
      dirActual = DIR_STOP;
      esperandoPausaInversion = true;
      tEsperaInversion = millis();
      Serial.println("Motor en 0. Iniciando pausa de inversion...");
    }
    return;
  }

  if (inversionEnProceso && esperandoPausaInversion) {
    if (millis() - tEsperaInversion >= TIEMPO_CAMBIO_SENTIDO_MS) {
      dirActual = dirPendiente;
      velocidadObjetivo = velocidadPendiente;

      inversionEnProceso = false;
      esperandoPausaInversion = false;

      Serial.printf("Inversion completada. Nuevo objetivo: %s%u\n",
                    textoDireccion(dirActual), velocidadObjetivo);
    }
    return;
  }

  if (velocidadActual < velocidadObjetivo) {
    uint8_t faltante = (uint8_t)(velocidadObjetivo - velocidadActual);
    uint8_t delta = minU8(PASO_RAMPA, faltante);
    velocidadActual += delta;
    escribirPuente(dirActual, velocidadActual);
  }
  else if (velocidadActual > velocidadObjetivo) {
    uint8_t diferencia = (uint8_t)(velocidadActual - velocidadObjetivo);
    uint8_t delta = minU8(PASO_RAMPA, diferencia);
    velocidadActual -= delta;

    if (velocidadActual == 0 && velocidadObjetivo == 0) {
      escribirPuente(DIR_STOP, 0);
      dirActual = DIR_STOP;
    } else {
      escribirPuente(dirActual, velocidadActual);
    }
  }

  appliedMotorPct = velocidadActual;
  appliedDir = (dirActual == DIR_I) ? 1 : 0;
}

// =====================================================
// SERVO / IRIS
// =====================================================
uint32_t servoPulseUsToDuty(uint32_t pulseUs) {
  return (pulseUs * PWM_MAX_SERVO) / SERVO_PERIODO_US;
}

void escribirServoAngulo(int angulo) {
  angulo = clampInt(angulo, 0, 180);
  long pulseUs = mapLongClamped(angulo, 0, 180, SERVO_MIN_US, SERVO_MAX_US);
  uint32_t duty = servoPulseUsToDuty((uint32_t)pulseUs);
  ledcWrite(PIN_SERVO, duty);
}

void moverServoA(int angulo) {
  servoAnguloObjetivo = clampInt(angulo, 0, 180);
  Serial.printf("Servo objetivo -> %d grados\n", servoAnguloObjetivo);
}

int irisPctToServoAngle(uint8_t pct) {
  return (int)mapLongClamped((long)pct, 0, 100, SERVO_ANGULO_CERRADO, SERVO_ANGULO_ABIERTO);
}

uint8_t servoAngleToIrisPct(int angle) {
  return (uint8_t)clampLong(
    mapLongClamped((long)angle, SERVO_ANGULO_CERRADO, SERVO_ANGULO_ABIERTO, 0, 100),
    0, 100
  );
}

void abrirCompuerta() {
  requestedIrisPct = 100;
  moverServoA(SERVO_ANGULO_ABIERTO);
  Serial.printf("Compuerta ABRIR -> %d grados\n", SERVO_ANGULO_ABIERTO);
}

void cerrarCompuerta() {
  requestedIrisPct = 0;
  moverServoA(SERVO_ANGULO_CERRADO);
  Serial.printf("Compuerta CERRAR -> %d grados\n", SERVO_ANGULO_CERRADO);
}

void actualizarServo() {
  unsigned long ahora = millis();
  if (ahora - tServo < INTERVALO_SERVO_MS) return;
  tServo = ahora;

  if (servoAnguloActual < servoAnguloObjetivo) {
    servoAnguloActual += PASO_SERVO;
    if (servoAnguloActual > servoAnguloObjetivo) servoAnguloActual = servoAnguloObjetivo;
    escribirServoAngulo(servoAnguloActual);
  }
  else if (servoAnguloActual > servoAnguloObjetivo) {
    servoAnguloActual -= PASO_SERVO;
    if (servoAnguloActual < servoAnguloObjetivo) servoAnguloActual = servoAnguloObjetivo;
    escribirServoAngulo(servoAnguloActual);
  }

  appliedIrisPct = servoAngleToIrisPct(servoAnguloActual);
}

// =====================================================
// PUERTA
// =====================================================
bool leerPuertaCrudaCerrada() {
  return (digitalRead(PIN_PUERTA) == PUERTA_CERRADA_NIVEL);
}

void reanudarUltimaConfiguracion() {
  if (!puertaCerradaEstable) return;

  if (!esSentidoValido(dirUsuario) || velocidadUsuario == 0) {
    Serial.println("Puerta cerrada. No hay configuracion previa para reanudar.");
    return;
  }

  dirActual = dirUsuario;
  velocidadObjetivo = velocidadUsuario;

  Serial.printf("Puerta cerrada. Reanudando ultima configuracion: %s%u\n",
                textoDireccion(dirUsuario), velocidadUsuario);
}

void manejarEventoPuertaAbierta() {
  Serial.println("EVENTO: PUERTA ABIERTA -> APAGADO FORZADO");
  apagarMotorPorSeguridad();
}

void manejarEventoPuertaCerrada() {
  Serial.println("EVENTO: PUERTA CERRADA");
  reanudarUltimaConfiguracion();
}

void actualizarPuerta() {
  bool lecturaActual = leerPuertaCrudaCerrada();

  if (lecturaActual != puertaCerradaRawAnterior) {
    puertaCerradaRawAnterior = lecturaActual;
    tCambioPuerta = millis();
  }

  if (millis() - tCambioPuerta >= DEBOUNCE_PUERTA_MS) {
    puertaCerradaRaw = lecturaActual;

    if (puertaCerradaEstable != puertaCerradaRaw) {
      puertaCerradaEstable = puertaCerradaRaw;

      if (puertaCerradaEstable) {
        manejarEventoPuertaCerrada();
      } else {
        manejarEventoPuertaAbierta();
      }
    }
  }
}

// =====================================================
// APLICACION DE SOLICITUDES HMI
// =====================================================
void applyCrusherMotor() {
  Direccion reqDir = (requestedDir == 0) ? DIR_D : DIR_I;
  procesarSolicitudMotor(reqDir, requestedMotorPct);
}

void applyIris() {
  int targetAngle = irisPctToServoAngle(requestedIrisPct);
  moverServoA(targetAngle);
}

void applyRequestedStateIfChanged() {
  if (requestedMotorPct != lastIssuedMotorPct || requestedDir != lastIssuedDir) {
    applyCrusherMotor();
    lastIssuedMotorPct = requestedMotorPct;
    lastIssuedDir = requestedDir;
  }

  if (requestedIrisPct != lastIssuedIrisPct) {
    applyIris();
    lastIssuedIrisPct = requestedIrisPct;
  }
}

// =====================================================
// STEPPERS CON ACCELSTEPPER
// =====================================================
bool axisLimitActive(StepperAxis &ax) {
  return digitalRead(ax.limitPin) == ax.limitActiveLevel;
}

// Convierte posición máquina (0=home, positivo alejándose)
// a posición lógica de AccelStepper.
long axisLogicalFromMachine(const StepperAxis &ax, long machinePos) {
  return (ax.homeDir == 1) ? -machinePos : machinePos;
}

// Convierte posición lógica de AccelStepper a posición máquina.
long axisMachineFromLogical(const StepperAxis &ax, long logicalPos) {
  return (ax.homeDir == 1) ? -logicalPos : logicalPos;
}

long axisMachinePosition(const StepperAxis &ax) {
  return axisMachineFromLogical(ax, ax.stepper->currentPosition());
}

bool axisIsBusy(const StepperAxis &ax) {
  return (ax.homePhase != HOME_IDLE) || ax.jogMode || (ax.stepper->distanceToGo() != 0);
}

float axisLogicalSpeedTowardHome(const StepperAxis &ax, float speedAbs) {
  return (ax.homeDir == 1) ? +fabs(speedAbs) : -fabs(speedAbs);
}

float axisLogicalSpeedAwayFromHome(const StepperAxis &ax, float speedAbs) {
  return -axisLogicalSpeedTowardHome(ax, speedAbs);
}

void axisConfigureMove(StepperAxis &ax, float maxSpeed, float accel) {
  ax.stepper->setMaxSpeed(maxSpeed);
  ax.stepper->setAcceleration(accel);
}

void axisStopImmediate(StepperAxis &ax) {
  long pos = ax.stepper->currentPosition();
  ax.stepper->moveTo(pos);
  ax.homePhase = HOME_IDLE;
  ax.jogMode = false;
  ax.targetMachinePos = axisMachinePosition(ax);
}

void axisSetAtHome(StepperAxis &ax) {
  ax.stepper->setCurrentPosition(0);
  ax.stepper->moveTo(0);
  ax.homePhase = HOME_IDLE;
  ax.jogMode = false;
  ax.homed = true;
  ax.targetMachinePos = 0;
}

void axisStartQuietHome(StepperAxis &ax, const char* nombre,
                        float fastSpeed, float slowSpeed, float backoffSpeed) {
  ax.homeFastSpeed = fastSpeed;
  ax.homeSlowSpeed = slowSpeed;
  ax.homeBackoffSpeed = backoffSpeed;
  ax.jogMode = false;
  ax.homed = false;

  // durante homing usamos runSpeed(), no movimiento acelerado
  ax.stepper->moveTo(ax.stepper->currentPosition());

  if (axisLimitActive(ax)) {
    ax.homePhase = HOME_RELEASE;
    ax.stepper->setSpeed(axisLogicalSpeedAwayFromHome(ax, ax.homeBackoffSpeed));
    ax.homePhaseStartPos = ax.stepper->currentPosition();
    Serial.printf("%s -> HOME silencioso: liberando switch...\n", nombre);
  } else {
    ax.homePhase = HOME_SEEK_FAST;
    ax.stepper->setSpeed(axisLogicalSpeedTowardHome(ax, ax.homeFastSpeed));
    ax.homePhaseStartPos = ax.stepper->currentPosition();
    Serial.printf("%s -> HOME silencioso: búsqueda rápida...\n", nombre);
  }
}

void axisStartHome(StepperAxis &ax, const char* nombre) {
  axisStartQuietHome(ax, nombre, ax.homeFastSpeed, ax.homeSlowSpeed, ax.homeBackoffSpeed);
}

void axisStartAbsolute(StepperAxis &ax, const char* nombre, long machineTarget) {
  if (!ax.homed) {
    Serial.printf("%s -> primero haz HOME\n", nombre);
    return;
  }

  machineTarget = clampLong(machineTarget, 0, ax.rangeSteps);
  long logicalTarget = axisLogicalFromMachine(ax, machineTarget);

  axisConfigureMove(ax, ax.maxSpeed, ax.accel);
  ax.stepper->moveTo(logicalTarget);
  ax.targetMachinePos = machineTarget;
  ax.homePhase = HOME_IDLE;
  ax.jogMode = false;

  Serial.printf("%s -> movimiento absoluto a %ld\n", nombre, machineTarget);
}

void axisStartRelative(StepperAxis &ax, const char* nombre, long delta) {
  if (!ax.homed) {
    Serial.printf("%s -> primero haz HOME\n", nombre);
    return;
  }

  long currentMachine = axisMachinePosition(ax);
  long targetMachine = clampLong(currentMachine + delta, 0, ax.rangeSteps);

  if (targetMachine == currentMachine) {
    Serial.printf("%s -> ya esta en el limite permitido\n", nombre);
    return;
  }

  axisStartAbsolute(ax, nombre, targetMachine);
}

void axisStartJog(StepperAxis &ax, const char* nombre, int8_t coordDir) {
  if (!ax.homed) {
    Serial.printf("%s -> primero haz HOME\n", nombre);
    return;
  }

  axisConfigureMove(ax, ax.maxSpeed, ax.accel);

  long farTarget = (coordDir > 0) ? ax.rangeSteps : 0;
  long logicalTarget = axisLogicalFromMachine(ax, farTarget);

  ax.stepper->moveTo(logicalTarget);
  ax.targetMachinePos = farTarget;
  ax.homePhase = HOME_IDLE;
  ax.jogMode = true;

  Serial.printf("%s -> JOG %s\n", nombre, (coordDir > 0 ? "+" : "-"));
}

void axisUpdateHomeQuiet(StepperAxis &ax, const char* nombre) {
  switch (ax.homePhase) {
    case HOME_IDLE:
      return;

    case HOME_RELEASE:
      ax.stepper->runSpeed();
      if (!axisLimitActive(ax)) {
        ax.homePhase = HOME_SEEK_FAST;
        ax.stepper->setSpeed(axisLogicalSpeedTowardHome(ax, ax.homeFastSpeed));
        ax.homePhaseStartPos = ax.stepper->currentPosition();
        Serial.printf("%s -> switch liberado, búsqueda rápida.\n", nombre);
      } else {
        long delta = labs(ax.stepper->currentPosition() - ax.homePhaseStartPos);
        if (delta > HOME_RELEASE_MAX_STEPS) {
          ax.homePhase = HOME_IDLE;
          Serial.printf("%s -> ERROR liberando switch.\n", nombre);
        }
      }
      return;

    case HOME_SEEK_FAST:
      ax.stepper->runSpeed();
      if (axisLimitActive(ax)) {
        ax.homePhase = HOME_BACKOFF;
        ax.stepper->setSpeed(axisLogicalSpeedAwayFromHome(ax, ax.homeBackoffSpeed));
        ax.homePhaseStartPos = ax.stepper->currentPosition();
        Serial.printf("%s -> switch tocado, retroceso.\n", nombre);
      } else {
        long delta = labs(ax.stepper->currentPosition() - ax.homePhaseStartPos);
        if (delta > HOME_SEARCH_STEPS) {
          ax.homePhase = HOME_IDLE;
          Serial.printf("%s -> ERROR no encontró home.\n", nombre);
        }
      }
      return;

    case HOME_BACKOFF:
      ax.stepper->runSpeed();
      if (!axisLimitActive(ax)) {
        ax.homePhase = HOME_SEEK_SLOW;
        ax.stepper->setSpeed(axisLogicalSpeedTowardHome(ax, ax.homeSlowSpeed));
        ax.homePhaseStartPos = ax.stepper->currentPosition();
        Serial.printf("%s -> búsqueda lenta final.\n", nombre);
      } else {
        long delta = labs(ax.stepper->currentPosition() - ax.homePhaseStartPos);
        if (delta > HOME_RELEASE_MAX_STEPS) {
          ax.homePhase = HOME_IDLE;
          Serial.printf("%s -> ERROR en backoff.\n", nombre);
        }
      }
      return;

    case HOME_SEEK_SLOW:
      ax.stepper->runSpeed();
      if (axisLimitActive(ax)) {
        axisSetAtHome(ax);
        Serial.printf("%s -> HOME completo, posición = 0\n", nombre);
      } else {
        long delta = labs(ax.stepper->currentPosition() - ax.homePhaseStartPos);
        if (delta > HOME_RELEASE_MAX_STEPS) {
          ax.homePhase = HOME_IDLE;
          Serial.printf("%s -> ERROR en búsqueda lenta.\n", nombre);
        }
      }
      return;
  }
}

void axisUpdate(StepperAxis &ax, const char* nombre) {
  if (ax.homePhase != HOME_IDLE) {
    axisUpdateHomeQuiet(ax, nombre);
    return;
  }

  long currentMachine = axisMachinePosition(ax);

  if (currentMachine < 0) {
    ax.stepper->setCurrentPosition(axisLogicalFromMachine(ax, 0));
    ax.stepper->moveTo(axisLogicalFromMachine(ax, 0));
    currentMachine = 0;
  }

  if (currentMachine > ax.rangeSteps) {
    ax.stepper->setCurrentPosition(axisLogicalFromMachine(ax, ax.rangeSteps));
    ax.stepper->moveTo(axisLogicalFromMachine(ax, ax.rangeSteps));
    currentMachine = ax.rangeSteps;
  }

  ax.stepper->run();

  if (ax.jogMode) {
    long p = axisMachinePosition(ax);

    if (p <= 0 && ax.targetMachinePos == 0) {
      ax.stepper->setCurrentPosition(axisLogicalFromMachine(ax, 0));
      ax.stepper->moveTo(axisLogicalFromMachine(ax, 0));
      ax.jogMode = false;
      Serial.printf("%s -> jog detenido en home\n", nombre);
      return;
    }

    if (p >= ax.rangeSteps && ax.targetMachinePos == ax.rangeSteps) {
      ax.stepper->setCurrentPosition(axisLogicalFromMachine(ax, ax.rangeSteps));
      ax.stepper->moveTo(axisLogicalFromMachine(ax, ax.rangeSteps));
      ax.jogMode = false;
      Serial.printf("%s -> jog detenido en maximo\n", nombre);
      return;
    }
  }
}

// =====================================================
// SECUENCIAS AUTOMATICAS
// =====================================================
void startRemoveCupSequence() {
  if (autoSequenceActive()) return;
  autoSeqState = AUTOSEQ_REMOVE_HOME_Z;
  autoSeqStepStarted = false;
  Serial.println("[AUTO] Iniciando REMOVE CUP");
}

void startReturnCupSequence() {
  if (autoSequenceActive()) return;
  autoSeqState = AUTOSEQ_RETURN_HOME_Z;
  autoSeqStepStarted = false;
  Serial.println("[AUTO] Iniciando RETURN CUP");
}

void updateAutoSequence() {
  switch (autoSeqState) {
    case AUTOSEQ_NONE:
      break;

    case AUTOSEQ_STARTUP_HOME_Z:
      if (!autoSeqStepStarted) {
        Serial.println("[AUTO] Startup: seteo suave eje Z");
        axisStartQuietHome(axis1, "Z/M1",
                           STARTUP_HOME_FAST_SPEED,
                           STARTUP_HOME_SLOW_SPEED,
                           STARTUP_HOME_BACKOFF_SPEED);
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis1)) {
        if (!axis1.homed) {
          Serial.println("[AUTO] ERROR: home startup Z falló.");
          autoSeqState = AUTOSEQ_NONE;
        } else {
          autoSeqState = AUTOSEQ_STARTUP_HOME_Y;
          autoSeqStepStarted = false;
        }
      }
      break;

    case AUTOSEQ_STARTUP_HOME_Y:
      if (!autoSeqStepStarted) {
        Serial.println("[AUTO] Startup: seteo suave eje Y");
        axisStartQuietHome(axis2, "Y/M2",
                           STARTUP_HOME_FAST_SPEED,
                           STARTUP_HOME_SLOW_SPEED,
                           STARTUP_HOME_BACKOFF_SPEED);
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis2)) {
        if (!axis2.homed) {
          Serial.println("[AUTO] ERROR: home startup Y falló.");
          autoSeqState = AUTOSEQ_NONE;
        } else {
          startupHomingDone = true;
          autoSeqState = AUTOSEQ_NONE;
          autoSeqStepStarted = false;
          Serial.println("[AUTO] Startup completo. Ambos ejes en origen.");
        }
      }
      break;

    case AUTOSEQ_REMOVE_HOME_Z:
      if (!autoSeqStepStarted) {
        axisStartHome(axis1, "Z/M1");
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis1)) {
        if (!axis1.homed) {
          Serial.println("[AUTO] ERROR: home Z en REMOVE CUP falló.");
          autoSeqState = AUTOSEQ_NONE;
        } else {
          autoSeqState = AUTOSEQ_REMOVE_HOME_Y;
          autoSeqStepStarted = false;
        }
      }
      break;

    case AUTOSEQ_REMOVE_HOME_Y:
      if (!autoSeqStepStarted) {
        axisStartHome(axis2, "Y/M2");
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis2)) {
        if (!axis2.homed) {
          Serial.println("[AUTO] ERROR: home Y en REMOVE CUP falló.");
          autoSeqState = AUTOSEQ_NONE;
        } else {
          autoSeqState = AUTOSEQ_REMOVE_MOVE_Y_MAX;
          autoSeqStepStarted = false;
        }
      }
      break;

    case AUTOSEQ_REMOVE_MOVE_Y_MAX:
      if (!autoSeqStepStarted) {
        axisStartAbsolute(axis2, "Y/M2", axis2.rangeSteps);
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis2)) {
        autoSeqState = AUTOSEQ_NONE;
        autoSeqStepStarted = false;
        Serial.println("[AUTO] REMOVE CUP completado.");
      }
      break;

    case AUTOSEQ_RETURN_HOME_Z:
      if (!autoSeqStepStarted) {
        axisStartHome(axis1, "Z/M1");
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis1)) {
        if (!axis1.homed) {
          Serial.println("[AUTO] ERROR: home Z en RETURN CUP falló.");
          autoSeqState = AUTOSEQ_NONE;
        } else {
          autoSeqState = AUTOSEQ_RETURN_MOVE_Y_NEAR_HOME;
          autoSeqStepStarted = false;
        }
      }
      break;

    case AUTOSEQ_RETURN_MOVE_Y_NEAR_HOME:
      if (!autoSeqStepStarted) {
        axisStartAbsolute(axis2, "Y/M2", M2_RETURN_CUP_Y_STEPS);
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis2)) {
        autoSeqState = AUTOSEQ_RETURN_MOVE_Z_MAX;
        autoSeqStepStarted = false;
      }
      break;

    case AUTOSEQ_RETURN_MOVE_Z_MAX:
      if (!autoSeqStepStarted) {
        // aquí Z se aleja del final y sube al máximo
        axisStartAbsolute(axis1, "Z/M1", axis1.rangeSteps);
        autoSeqStepStarted = true;
      }
      if (!axisIsBusy(axis1)) {
        autoSeqState = AUTOSEQ_NONE;
        autoSeqStepStarted = false;
        Serial.println("[AUTO] RETURN CUP completado.");
      }
      break;
  }
}

// =====================================================
// CUP EVENTS
// =====================================================
void handleCupRemove() {
  if (!cupRemovePulse) return;
  if (!startupHomingDone) return;
  if (autoSequenceActive()) return;

  cupRemovePulse = false;
  startRemoveCupSequence();
}

void handleCupReturn() {
  if (!cupReturnPulse) return;
  if (!startupHomingDone) return;
  if (autoSequenceActive()) return;

  cupReturnPulse = false;
  startReturnCupSequence();
}

// =====================================================
// REPORTE GENERAL
// =====================================================
void reportarEstadoSistema() {
  if (millis() - tReporte < INTERVALO_REPORTE_MS) return;
  tReporte = millis();

  long pos1 = axisMachinePosition(axis1);
  long pos2 = axisMachinePosition(axis2);

  Serial.printf(
    "PUERTA:%s | REQ_MOTOR:%u/%u | APP_MOTOR:%u/%u | REQ_IRIS:%u | APP_IRIS:%u | SERVO:%d/%d | "
    "FC1:%s POS1:%ld/%ld HOMED1:%d BUSY1:%d | "
    "FC2:%s POS2:%ld/%ld HOMED2:%d BUSY2:%d | AUTOSEQ:%d\n",
    puertaCerradaEstable ? "CERRADA" : "ABIERTA",
    requestedMotorPct, requestedDir,
    appliedMotorPct, appliedDir,
    requestedIrisPct, appliedIrisPct,
    servoAnguloActual, servoAnguloObjetivo,
    axisLimitActive(axis1) ? "ACT" : "LIB",
    pos1, axis1.rangeSteps, axis1.homed ? 1 : 0, axisIsBusy(axis1) ? 1 : 0,
    axisLimitActive(axis2) ? "ACT" : "LIB",
    pos2, axis2.rangeSteps, axis2.homed ? 1 : 0, axisIsBusy(axis2) ? 1 : 0,
    (int)autoSeqState
  );
}

// =====================================================
// COMANDOS SERIAL DEBUG
// =====================================================
void imprimirAyuda() {
  Serial.println("===== COMANDOS =====");
  Serial.println("Motor DC:");
  Serial.println("  D100");
  Serial.println("  I50");
  Serial.println("  D0");
  Serial.println("");
  Serial.println("Servo:");
  Serial.println("  ABRIR");
  Serial.println("  CERRAR");
  Serial.println("  ANG90");
  Serial.println("  IRIS60");
  Serial.println("");
  Serial.println("Steppers:");
  Serial.println("  HOME1");
  Serial.println("  HOME2");
  Serial.println("  P1+1000");
  Serial.println("  P1-500");
  Serial.println("  P2+1000");
  Serial.println("  P2-500");
  Serial.println("  J1+ / J1-");
  Serial.println("  J2+ / J2-");
  Serial.println("  STOP1");
  Serial.println("  STOP2");
  Serial.println("");
  Serial.println("Secuencias:");
  Serial.println("  REMOVECUP");
  Serial.println("  RETURNCUP");
  Serial.println("");
  Serial.println("General:");
  Serial.println("  ESTADO");
  Serial.println("  AYUDA");
}

void imprimirEstadoUnaVez() {
  long pos1 = axisMachinePosition(axis1);
  long pos2 = axisMachinePosition(axis2);

  Serial.printf(
    "PUERTA:%s | REQ_MOTOR:%u/%u | APP_MOTOR:%u/%u | REQ_IRIS:%u | APP_IRIS:%u | SERVO:%d/%d | "
    "FC1:%s POS1:%ld/%ld HOMED1:%d BUSY1:%d | "
    "FC2:%s POS2:%ld/%ld HOMED2:%d BUSY2:%d | AUTOSEQ:%d\n",
    puertaCerradaEstable ? "CERRADA" : "ABIERTA",
    requestedMotorPct, requestedDir,
    appliedMotorPct, appliedDir,
    requestedIrisPct, appliedIrisPct,
    servoAnguloActual, servoAnguloObjetivo,
    axisLimitActive(axis1) ? "ACT" : "LIB",
    pos1, axis1.rangeSteps, axis1.homed ? 1 : 0, axisIsBusy(axis1) ? 1 : 0,
    axisLimitActive(axis2) ? "ACT" : "LIB",
    pos2, axis2.rangeSteps, axis2.homed ? 1 : 0, axisIsBusy(axis2) ? 1 : 0,
    (int)autoSeqState
  );
}

void procesarComando(const char* cmdOriginal) {
  if (cmdOriginal == nullptr || cmdOriginal[0] == '\0') return;

  char cmd[40];
  strncpy(cmd, cmdOriginal, sizeof(cmd) - 1);
  cmd[sizeof(cmd) - 1] = '\0';
  toUpperInPlace(cmd);

  if (strcmp(cmd, "ABRIR") == 0) {
    abrirCompuerta();
    return;
  }

  if (strcmp(cmd, "CERRAR") == 0) {
    cerrarCompuerta();
    return;
  }

  if (strcmp(cmd, "REMOVECUP") == 0) {
    cupRemovePulse = true;
    return;
  }

  if (strcmp(cmd, "RETURNCUP") == 0) {
    cupReturnPulse = true;
    return;
  }

  if (strncmp(cmd, "IRIS", 4) == 0) {
    char* endPtr = nullptr;
    long pct = strtol(cmd + 4, &endPtr, 10);

    if (endPtr == cmd + 4 || *endPtr != '\0') {
      Serial.println("Formato invalido. Usa IRIS60");
      return;
    }

    pct = clampLong(pct, 0, 100);
    requestedIrisPct = (uint8_t)pct;
    applyIris();
    lastIssuedIrisPct = requestedIrisPct;
    return;
  }

  if (strncmp(cmd, "ANG", 3) == 0) {
    char* endPtr = nullptr;
    long ang = strtol(cmd + 3, &endPtr, 10);

    if (endPtr == cmd + 3 || *endPtr != '\0') {
      Serial.println("Formato invalido. Usa ANG90");
      return;
    }

    if (ang < 0 || ang > 180) {
      Serial.println("Angulo invalido. 0 a 180.");
      return;
    }

    moverServoA((int)ang);
    return;
  }

  if (autoSequenceActive()) {
    if (strcmp(cmd, "HOME1") == 0 || strcmp(cmd, "HOME2") == 0 ||
        strcmp(cmd, "J1+") == 0 || strcmp(cmd, "J1-") == 0 ||
        strcmp(cmd, "J2+") == 0 || strcmp(cmd, "J2-") == 0 ||
        strcmp(cmd, "STOP1") == 0 || strcmp(cmd, "STOP2") == 0 ||
        strncmp(cmd, "P1", 2) == 0 || strncmp(cmd, "P2", 2) == 0) {
      Serial.println("Secuencia automática activa. Espera a que termine.");
      return;
    }
  }

  if (strcmp(cmd, "HOME1") == 0) {
    axisStartHome(axis1, "M1");
    return;
  }

  if (strcmp(cmd, "HOME2") == 0) {
    axisStartHome(axis2, "M2");
    return;
  }

  if (strcmp(cmd, "J1+") == 0) {
    axisStartJog(axis1, "M1", +1);
    return;
  }

  if (strcmp(cmd, "J1-") == 0) {
    axisStartJog(axis1, "M1", -1);
    return;
  }

  if (strcmp(cmd, "J2+") == 0) {
    axisStartJog(axis2, "M2", +1);
    return;
  }

  if (strcmp(cmd, "J2-") == 0) {
    axisStartJog(axis2, "M2", -1);
    return;
  }

  if (strcmp(cmd, "STOP1") == 0) {
    axisStopImmediate(axis1);
    Serial.println("M1 detenido");
    return;
  }

  if (strcmp(cmd, "STOP2") == 0) {
    axisStopImmediate(axis2);
    Serial.println("M2 detenido");
    return;
  }

  if (strncmp(cmd, "P1", 2) == 0) {
    char* endPtr = nullptr;
    long delta = strtol(cmd + 2, &endPtr, 10);

    if (endPtr == cmd + 2 || *endPtr != '\0') {
      Serial.println("Formato invalido. Usa P1+1000 o P1-500");
      return;
    }

    axisStartRelative(axis1, "M1", delta);
    return;
  }

  if (strncmp(cmd, "P2", 2) == 0) {
    char* endPtr = nullptr;
    long delta = strtol(cmd + 2, &endPtr, 10);

    if (endPtr == cmd + 2 || *endPtr != '\0') {
      Serial.println("Formato invalido. Usa P2+1000 o P2-500");
      return;
    }

    axisStartRelative(axis2, "M2", delta);
    return;
  }

  if (cmd[0] == 'D' || cmd[0] == 'I') {
    char* endPtr = nullptr;
    long valor = strtol(cmd + 1, &endPtr, 10);

    if (endPtr == cmd + 1 || *endPtr != '\0') {
      Serial.println("Formato invalido. Ejemplos: D50, I100, D0");
      return;
    }

    if (valor < 0 || valor > 100) {
      Serial.println("Valor invalido. Debe estar entre 0 y 100");
      return;
    }

    requestedMotorPct = (uint8_t)valor;
    requestedDir      = (cmd[0] == 'D') ? 0 : 1;
    applyCrusherMotor();
    lastIssuedMotorPct = requestedMotorPct;
    lastIssuedDir = requestedDir;
    return;
  }

  if (strcmp(cmd, "ESTADO") == 0) {
    imprimirEstadoUnaVez();
    return;
  }

  if (strcmp(cmd, "AYUDA") == 0) {
    imprimirAyuda();
    return;
  }

  Serial.println("Comando no reconocido. Escribe AYUDA.");
}

void leerSerialNoBloqueante() {
  while (Serial.available() > 0) {
    char ch = Serial.read();

    if (ch == '\n' || ch == '\r') {
      if (idxRx > 0) {
        bufferRx[idxRx] = '\0';
        procesarComando(bufferRx);
        idxRx = 0;
      }
    } else if (ch == ' ' || ch == '\t') {
      // ignorar
    } else {
      if (idxRx < sizeof(bufferRx) - 1) {
        bufferRx[idxRx++] = ch;
      } else {
        idxRx = 0;
        Serial.println("Comando demasiado largo");
      }
    }
  }
}

// =====================================================
// SETUP AXIS
// =====================================================
void setupAxis(
  StepperAxis &ax,
  AccelStepper* stepperObj,
  uint8_t stepPin,
  uint8_t dirPin,
  uint8_t limitPin,
  bool limitActiveLevel,
  bool invertDir,
  int8_t homeDir,
  float runSpeed,
  float runAccel,
  long rangeSteps,
  float homeFast,
  float homeSlow,
  float homeBackoff
) {
  ax.stepper = stepperObj;
  ax.stepPin = stepPin;
  ax.dirPin = dirPin;
  ax.limitPin = limitPin;
  ax.limitActiveLevel = limitActiveLevel;
  ax.invertDir = invertDir;
  ax.homeDir = homeDir;
  ax.maxSpeed = runSpeed;
  ax.accel = runAccel;
  ax.rangeSteps = rangeSteps;
  ax.homeFastSpeed = homeFast;
  ax.homeSlowSpeed = homeSlow;
  ax.homeBackoffSpeed = homeBackoff;

  pinMode(ax.limitPin, INPUT);

  ax.stepper->setPinsInverted(ax.invertDir, false, false);
  ax.stepper->setMaxSpeed(ax.maxSpeed);
  ax.stepper->setAcceleration(ax.accel);
  ax.stepper->setMinPulseWidth(4);
  ax.stepper->setCurrentPosition(0);
  ax.targetMachinePos = 0;
  ax.homePhase = HOME_IDLE;
}

// =====================================================
// SETUP / LOOP
// =====================================================
void setup() {
  Serial.begin(115200);

  NextionSerial.begin(NEXTION_BAUD, SERIAL_8N1, NEXTION_RX_PIN, NEXTION_TX_PIN);

  bool ok1 = ledcAttach(PIN_RPWM, PWM_FREQ_MOTOR, PWM_RES_MOTOR);
  bool ok2 = ledcAttach(PIN_LPWM, PWM_FREQ_MOTOR, PWM_RES_MOTOR);
  bool ok3 = ledcAttach(PIN_SERVO, PWM_FREQ_SERVO, PWM_RES_SERVO);

  if (!ok1 || !ok2 || !ok3) {
    Serial.println("Error configurando PWM LEDC");
    while (true) {}
  }

  pinMode(PIN_PUERTA, INPUT_PULLUP);

  bool estadoInicialPuerta = leerPuertaCrudaCerrada();
  puertaCerradaRaw = estadoInicialPuerta;
  puertaCerradaRawAnterior = estadoInicialPuerta;
  puertaCerradaEstable = estadoInicialPuerta;

  setupAxis(axis1, &stepper1, M1_STEP_PIN, M1_DIR_PIN, M1_LIMIT_PIN,
            LIMIT1_ACTIVE_LEVEL, M1_INVERT_DIR, M1_HOME_DIR,
            M1_MAX_SPEED, M1_ACCEL, M1_RANGE_STEPS,
            M1_HOME_FAST_SPEED, M1_HOME_SLOW_SPEED, M1_HOME_BACKOFF_SPEED);

  setupAxis(axis2, &stepper2, M2_STEP_PIN, M2_DIR_PIN, M2_LIMIT_PIN,
            LIMIT2_ACTIVE_LEVEL, M2_INVERT_DIR, M2_HOME_DIR,
            M2_MAX_SPEED, M2_ACCEL, M2_RANGE_STEPS,
            M2_HOME_FAST_SPEED, M2_HOME_SLOW_SPEED, M2_HOME_BACKOFF_SPEED);

  tCambioPuerta = millis();
  tReporte = 0;
  tRampa = millis();
  tServo = millis();
  tNextionFeedback = 0;

  servoAnguloActual = clampInt(SERVO_ANGULO_CERRADO, 0, 180);
  servoAnguloObjetivo = servoAnguloActual;
  escribirServoAngulo(servoAnguloActual);

  requestedIrisPct = 0;
  appliedIrisPct = 0;

  apagarMotorPorSeguridad();

  lastDoorFeedbackSent = -1;

  autoSeqState = AUTOSEQ_STARTUP_HOME_Z;
  autoSeqStepStarted = false;
  startupHomingDone = false;

  Serial.println("Sistema listo.");
  Serial.println("UART Nextion lista en GPIO23(RX) y GPIO25(TX)");
  Serial.println("Steppers con AccelStepper.");
  Serial.println("Home silencioso con runSpeed(): liberar -> buscar -> retroceder -> buscar lento.");
  Serial.println("Microstepping 1/16 aplicado a ambos ejes.");
  imprimirAyuda();
  Serial.printf("Estado inicial puerta: %s\n",
                puertaCerradaEstable ? "CERRADA" : "ABIERTA");
  Serial.printf("Servo inicial: %d grados\n", servoAnguloActual);
  Serial.printf("M1_RANGE_STEPS = %ld\n", M1_RANGE_STEPS);
  Serial.printf("M2_RANGE_STEPS = %ld\n", M2_RANGE_STEPS);
  Serial.printf("M2_RETURN_CUP_Y_STEPS = %ld\n", M2_RETURN_CUP_Y_STEPS);
}

void loop() {
  leerSerialNoBloqueante();
  readNextionFrames();
  actualizarPuerta();
  updateAutoSequence();
  applyRequestedStateIfChanged();
  actualizarRampaMotor();
  actualizarServo();
  handleCupRemove();
  handleCupReturn();
  axisUpdate(axis1, "M1");
  axisUpdate(axis2, "M2");
  updateHmiFeedback();
  reportarEstadoSistema();

Programming and electronics gallery

Electronics photo 1

Sketch of the electrical connections that our machine should have

Electronics photo 2

biomaterial crusher connections.

Electronics photo 3

developing the iris-shaped entrance.

Electronics photo 4

Developing the Nextion screen interface

Electronics photo 5

PCB completed with all the necessary outputs and inputs for the operation of the crusher

Electronics photo 6

diagram of the parts of the crusher.

Iris test for opening and closing at the fiber entrance.

Blade shredding test to check DC motor power

Electronics GIF 3

Shredding test: we see that it shreds dry fibers well.

Modular design assembly and support structures

The BIOCRUSHER machine was designed using a modular approach, allowing each component to be fabricated, assembled and replaced independently. The structural system combines 3D printed parts, laser-cut panels and mechanical supports to ensure stability and adaptability during operation.

The support structure includes a rigid chassis made from laser-cut MDF/acrylic panels, while key mechanical components such as mounts, gears and guides were produced using 3D printing. This combination of digital fabrication techniques enables rapid prototyping, customization and easy maintenance of the machine.

Assembly and fabrication gallery

Designing the structure and casing of the crusher, after ensuring that the mechanical and electrical parts work.

Design with Nema stepper motors and the Z, X, Y axes

3D printed outer casing test, to make it modular.

Unlike unshredded and shredded fiber, our machine works.

Finishing the final adjustments and practicing the pitch.

And our machine is finished and it has very cheerful colors.

One final touch, now it will be easy to find our shredder in the dark

Final machine demonstration

Video. Final operation of the BIOCRUSHER machine showing the complete modular assembly, shredding mechanism and material collection system.

Problems and solutions

ProblemCauseSolution / action
Mechanical friction in the blade shaftsAlignment and tolerances between shaft, bearings and blade stack.Checked bearing blocks, adjusted spacing and verified smooth manual rotation.
Possible clogging with long fibersFiber length and irregular feeding.Added controlled iris inlet and tested different feed rates.
Safety risk when the chamber is openMoving blades are exposed during loading or maintenance.Added door sensor logic to block operation when open.
Motor speed not suitable for all fibersDifferent fibers have different hardness and moisture levels.Added material selection profiles for soft, medium and hard fibers.
Container alignment with outletRail motion needed controlled positioning.Used NEMA motors, limit switches and guided movement sequence.

Future improvements

Automatic torque control

Add current sensing to detect overload and automatically reduce feed or stop the motor.

Improved material profiles

Store specific speed and time settings for cassava, coconut, banana and pineapple fibers.

Better container detection

Add more reliable full/empty sensing and automatic container exchange sequence.

Modular blade system

Design replaceable blades for different fiber types and different particle sizes.

IoT monitoring

Send machine status, processing time and material data to a remote dashboard.

Enclosed electronics

Improve cable management and add a protected electronics box for long-term use.

Conclusion

The BIOCRUSHER project successfully demonstrated the integration of mechanical design, digital fabrication, electronics and programming into a functional machine capable of processing fibrous biomaterials. The development process allowed the team to apply a multidisciplinary approach, combining CAD design, 3D printing, laser cutting and embedded systems to create a modular and scalable solution.

One of the main achievements of the project was the implementation of a dual-shaft shredding mechanism, capable of reducing the size of different types of fibers. This system, combined with a controlled feeding mechanism (iris) and a safety door, ensured both efficiency and safe operation during the shredding process.

The integration of the ESP32 microcontroller and the Nextion HMI interface enabled real-time monitoring and control of the machine. Through this interface, users can manage key parameters such as motor speed, operation time, and system status, improving usability and interaction with the machine.

Additionally, the implementation of a dual-axis linear motion system using stepper motors allowed precise positioning of the material collection container. This automation step significantly improved the workflow by enabling controlled material handling after the shredding process.

From a learning perspective, this project strengthened teamwork, problem-solving skills and the ability to integrate multiple technologies into a single system. The iterative design process allowed continuous improvement, especially in areas such as mechanical alignment, system stability and electronic integration.

Overall, BIOCRUSHER represents a functional prototype that demonstrates how digital fabrication and automation can be applied to develop sustainable solutions for biomaterial processing, particularly in the context of Amazonian resources.

Future Improvements and Recommendations

Although the BIOCRUSHER prototype achieved its main objectives, several improvements can be implemented to enhance performance, reliability and scalability of the system.

One of the key areas for improvement is the mechanical system. Future iterations could include the use of more resistant materials for the shredding blades, such as hardened steel, to increase durability when processing harder fibers. Additionally, improving shaft alignment and reducing mechanical tolerances would minimize friction and energy loss.

The motor control system could also be enhanced by implementing closed-loop control. By integrating current sensors or encoders, the system would be able to adjust speed and torque dynamically, preventing overload and improving efficiency during operation.

In terms of automation, the integration of additional sensors would improve system feedback and decision-making. For example, optical or weight sensors could be used to detect the amount of processed material, enabling automatic stopping or container replacement.

The Nextion interface can be further developed to include advanced features such as data logging, error reporting and customizable operation profiles for different types of biomaterials. This would allow users to optimize machine performance based on specific material properties.

Another important improvement would be the implementation of IoT capabilities. By connecting the ESP32 to a network, the machine could transmit real-time data, allow remote monitoring and enable predictive maintenance strategies.

From a structural perspective, the enclosure of the machine should be improved to increase safety and protect internal components from external factors such as dust and moisture. A fully enclosed system with transparent panels would also enhance user safety and visibility during operation.

Finally, future work could focus on scaling the machine and integrating it into a complete biomaterial production system, including processes such as fiber cooking, pulping and sheet formation. This would transform the BIOCRUSHER from a standalone prototype into a complete production solution.

.

Team Experience and Weekly Anecdotes

Beyond the technical development, this week was also a valuable team-building experience. The BIOCRUSHER project required long working hours, constant collaboration and problem-solving under pressure. Throughout the process, the team strengthened communication, trust and coordination skills.

We spent extended sessions working together in the lab, often staying late to complete mechanical assemblies, test electronics and refine the system. These moments allowed us to share ideas, support each other and celebrate small achievements during the development process.

In between intensive work sessions, we also shared informal moments such as eating together, having snacks and enjoying time as a team. These experiences helped maintain motivation and created a positive and collaborative environment throughout the week.

This balance between technical work and team bonding was essential to complete the project successfully, making the experience not only productive but also memorable.

Team moments gallery

Team working late in the lab during assembly stage.

Group discussion and planning next steps.

Late-night debugging and testing session.

Sharing ideas during machine integration.

Team break with food after long working hours.

Celebrating progress after completing a milestone.

Collaborative work during electronics setup.

Leaving the lab late again, very late.

A photo from when we had to print in the UNI lab

Design files and downloads

The group page must include all design files used for the project: CAD, STL, DXF, electronics files, HMI files, source code, final slide and final video.

3D printing files

STL files for supports, gears, HMI mount and mechanisms.

Download STL ZIP

Laser / CNC files

DXF files for MDF, acrylic or structural panels.

Download DXF ZIP

Electronics files

PCB design, schematic, board files and wiring diagram.

Download electronics ZIP

Programming files

ESP32 Arduino code and Nextion HMI files.

Download code + HMI ZIP

Presentation files

Final slide PNG and demo video MP4.

Download presentation ZIP
GROUP 02

StringArt

Automated String Art Machine

A digital fabrication machine that automates the creation of string art compositions through motion control and thread management.


Video


Slide

Description of the image


Project overview

General Description

StringArt is a digital fabrication machine that automates the creation of string art compositions through the control of a motion system and the management of a continuous thread. Based on a digital image or pattern, the machine positions the thread between defined points on a surface, generating complex forms and visual gradients.

The project explores the intersection between art, design, and technology by integrating mechanical motion, electronic control, and computational logic in a single fabrication system.

Quick data

  • Project: StringArt
  • Type: Automated digital fabrication machine
  • Team: David Avila, Jennifer Wong
  • Main systems: Mechanics, electronics, programming

What is the project?

StringArt is designed to transform digital images into physical string art pieces by automatically positioning thread between predefined anchor points on a circular base.

What is it for?

The system significantly reduces the time and precision required in the manual string art process. It can be used in artistic, educational, and digital fabrication contexts, connecting computational design, numerical control, and mechanical systems.

What makes it interesting?

The project is particularly interesting due to the integration of multiple systems within a single machine: CNC-like controlled motion, computational logic for trajectory generation, and a physical thread control mechanism that introduces an additional layer of complexity.

Unlike other digital fabrication systems, this project combines technical precision with a highly expressive visual result, exploring the intersection between art, design, and technology.

StringArt machine

String Art Machine

Team Members

  • David Avila
  • Jennifer Wong

Project Concept

The project was conceived as an automated machine capable of translating a digital design into a physical string art composition through motion control and thread management.

Problem, User, and Main Idea

Problem

The traditional string art creation process is highly manual, repetitive, and dependent on user precision. It requires time, coordination, and consistency in thread tension, which limits the complexity of designs and makes it difficult to reproduce precise or scalable results.

User or Context

The project is aimed at designers, artists, and students interested in exploring new forms of digital fabrication and visual expression. It is also conceived as an educational tool within digital fabrication laboratories where programming, electronics, and mechanics are connected through creative applications.

Main Idea

To develop an automated machine capable of translating a digital design into a physical string art composition through motion control and thread management. The system executes predefined trajectories between points, enabling complex patterns in a precise, repeatable, and efficient manner.

Development Process

The development of StringArt was carried out iteratively, progressing from a conceptual exploration to the construction of an integrated mechanical system. Each stage allowed the identification of specific problems and guided design decisions, particularly in relation to motion stability and thread control, which proved to be the main challenge of the project.


Stage 1: System Definition and Initial Exploration

In the first stage, the general approach of the project was defined. Based on the analysis of existing machines, a configuration based on a rotating base was selected instead of a Cartesian CNC-type system, as this significantly reduced mechanical complexity and facilitated fabrication.

In parallel, research on electronic components was conducted in order to build the bill of materials. Initial tests were carried out on the stepper motor and other electronic components to validate their behavior and compatibility within the system.

At the electronic level, the use of a microcontroller as the central control unit was defined conceptually, establishing the base architecture of the system, although without implementation at this stage.

Audiovisual Record


Stage 2: Rotational System Design

In this stage, the first motion system was developed. A circular MDF base was built with an initial distribution of nails, which allowed the working geometry to be established and the concept of rotation as the main mechanism to be validated.

A stepper motor was integrated to evaluate movement, initially without an optimized transmission system. From this, the use of a gear system was explored to improve motion control, analyzing transmission ratios and reduction.

A configuration was implemented where the motor shaft used a 20-tooth gear, while the main plate incorporated a 120-tooth gear, achieving a reduction that improves angular resolution and positioning control.

In parallel, the first servo holder was designed and 3D printed, allowing the exploration of the position, orientation, and dimensions of the thread guiding system.

// Pines
#define STEP_PIN 2   // D2
#define DIR_PIN  4   // D4

void setup() {
  pinMode(STEP_PIN, OUTPUT);
  pinMode(DIR_PIN, OUTPUT);

  digitalWrite(DIR_PIN, HIGH); // Dirección inicial
}

void loop() {

  // Girar en un sentido
  digitalWrite(DIR_PIN, HIGH);

  for (int i = 0; i < 2000; i++) { // pasos
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(500); // velocidad
    digitalWrite(STEP_PIN, LOW);
    delayMicroseconds(500);
  }

  delay(1000);

  // Cambiar dirección
  digitalWrite(DIR_PIN, LOW);

  for (int i = 0; i < 2000; i++) {
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(500);
    digitalWrite(STEP_PIN, LOW);
    delayMicroseconds(500);
  }

  delay(2000);
}

Audiovisual Record

Photographic Record

Stage 1

Stage 1

Stage 1

Stage 1

Stage 1


Stage 3: Structural and Component Design

In this stage, the structural system was developed. The enclosure was designed using 12 mm MDF and fabricated through laser cutting, allowing a precise and modular construction.

The distribution of electronic components within the structure was defined, considering accessibility, organization, and future integration.

Components of the thread guiding system were redesigned, optimizing their geometry to improve rigidity and alignment. Additionally, lateral supports were incorporated to hold the base plate where the piece is constructed, increasing the structural stability of the system.

Furthermore, the support system for the rotating base was designed using ballcasters, reducing friction and improving movement stability.

Photographic Record

Stage 1

Stage 1

Stage 1

Stage 1

Stage 1

Stage 1

Stage 1


Stage 4: Testing and Validation

Functional tests of the system were carried out by preparing the canvas with different nail configurations and evaluating the behavior of both motion and thread.

  • Motion calibration proved to be complex, especially in relation to the number of nails.
  • Configurations with around 250 nails significantly increased control difficulty, while configurations closer to 100 nails provided better results.
  • Mechanical issues appeared in the guiding system, such as deformation of the needle or contact element.
  • The thread tension system was unstable, leading to loss of trajectory control.
  • The thread feeding system experienced tangling due to variations in tension.
  • The electronic integration was not yet fully implemented, which limited system control.

This stage was critical in identifying that the main problems were not only related to motion, but to the interaction between the mechanical system and thread control.


Stage 5: Improvements and System Integration

Based on the identified issues, a complete thread guiding and tension system was developed. This system integrates a servo, a guide tube, a mechanical tensioner, a spool holder, and an elevated hook that redirects the thread and prevents tangling.

This set of components enabled effective control of both thread trajectory and tension, solving one of the main challenges of the project.

In parallel, a physical switch was integrated for system power control, and the electronic architecture based on a microcontroller was consolidated, preparing the machine for the execution of digitally generated trajectories.

This stage marks the transition from a functional prototype to an integrated system, where coordination between mechanics, electronics, and programming begins to consolidate.

Photographic Record

Stage 1

Stage 1

Stage 1

Final Proposal

The StringArt system is designed as a digital fabrication machine that integrates a rotating mechanism, a thread guiding system, and an electronic control architecture. Its operation is based on the precise coordination between mechanical motion and thread tension control.


5.1 Motion System

The main motion of the system is based on a circular rotating base made of 12 mm MDF, on which between 100 and 200 nails are distributed along the perimeter, defining the working geometry.

Rotation is driven by a NEMA 17 stepper motor, which transmits motion through a system of 3D-printed gears. This configuration allows adjustment of rotational speed, improves angular positioning control, and increases precision in pattern execution.

To ensure stability during rotation, the base incorporates multiple ballcasters, which reduce friction and distribute load evenly, preventing deformation and vibrations. This combination allows continuous and controlled motion, which is essential for the correct positioning of the thread anchor points.

Stage 1


5.2 Thread Guiding and Tension System

The thread system is the most critical component of the project, as it directly defines the quality of the final result. For this purpose, an integrated module was developed that allows control of both the trajectory and tension of the thread during operation.

Servo structural support

A CAD-designed and 3D-printed part was developed to fix the servo in a lateral position. This design decision responds to the need to align the movement of the thread with the geometry of the machine, avoiding interference and ensuring proper integration with the rest of the system.

Stage 1

Guide tube coupling

A 3D-printed coupling was incorporated to hold a tube through which the nylon thread moves. This element controls the trajectory of the thread, reduces variability in positioning, and prevents deviations during movement, contributing to more precise pattern execution.

Stage 1

Tension system

Within the same module, a mechanical tensioner was integrated to maintain constant thread tension. This component is fundamental, since insufficient tension results in imprecise patterns, while excessive tension can deform the structure or break the thread.

Stage 1

Spool holder

A feeding system was designed using a spool holder, allowing continuous thread supply without interruptions during the execution process.

Upper guiding system

An elevated hook was incorporated to redirect the thread from the spool to the guiding system. This improves thread alignment, prevents tangling, and maintains a clean trajectory during operation.

Together, these elements transform thread handling into a controlled and repeatable system, solving one of the main challenges of the project.

Stage 1


5.3 Structural System

The overall structure of the system is built in MDF, prioritizing rigidity, ease of fabrication, and low cost as main design criteria. The components were manufactured using laser cutting, allowing precise and consistent cuts while optimizing production time.

The design includes press-fit joints that facilitate assembly without the need for additional fasteners, ensuring proper alignment and structural stability. This approach enables rapid assembly and easy iteration for adjustments.

Additionally, 3D-printed components allow the integration of customized parts that would not be feasible with conventional manufacturing, especially in critical elements such as the thread guiding system and the servo support. This combination of technologies facilitates design adaptation and system evolution according to project requirements.

Stage 1


5.4 Electronic System

The electronic system enables control of the machine’s movement and connects digital trajectories with their physical execution. It is composed of an ESP32 microcontroller, a support board, a power driver, a power supply, a stepper motor, and a wiring system.

The ESP32 microcontroller acts as the central unit, processing instructions and generating control signals. These signals are sent to the DM556 driver, which manages the power required to drive the stepper motor, controlling parameters such as direction and rotational speed.

The stepper motor converts these signals into physical motion, allowing controlled rotation of the base. The power supply provides the necessary energy for system operation, while cables and connectors enable interconnection between components.

In this way, the system translates digitally generated trajectories into a sequence of electrical signals that become motion, achieving precise and repeatable pattern execution. A physical switch is also included to safely turn the machine on and off during operation.


5.5 Programming

The system programming allows control of the machine’s movement and execution of digitally generated trajectories. This logic is implemented in the ESP32 microcontroller, which translates instructions into control signals for the motor.

The system receives as input a sequence of points or trajectories generated by digital tools such as String Art Creator. These trajectories are interpreted by the microcontroller, which generates control signals (pulses) sent to the stepper motor driver.

Based on these signals, the system controls the rotation of the base, managing direction and speed, allowing correct positioning of thread anchor points. This establishes the connection between computational design and physical execution.

The implemented logic enables continuous execution of movement sequences, ensuring process repeatability. The system is also prepared for future improvements, such as trajectory optimization or integration of feedback sensors.

#include 
#include 
#include 

// ---------------- LCD ----------------
LiquidCrystal_I2C lcd(0x27, 16, 2);

// ---------------- PINES ----------------
const int stepPin = 14;
const int dirPin  = 27;
const int servoPin = 13;

// ---------------- SERVO ----------------
Servo aguja;

const int ANGULO_ARRIBA = 40;
const int ANGULO_ABAJO  = 5;

// ---------------- CONFIG ----------------
const float pasosPorVuelta = 9600.0;
const int totalClavos = 100;

const int offsetAngular = 3;
const int pasosOffset = 85;

// 🔥 SECUENCIA OPTIMIZADA
int secuencia[] = {
  0,17,34,51,68,85,2,19,36,53,70,87,4,21,38,55,72,89,6,23,
  40,57,74,91,8,25,42,59,76,93,10,27,44,61,78,95,12,29,46,63,
  80,97,14,31,48,65,82,99,16,33,50,67,84,1,18,35,52,69,86,3,
  20,37,54,71,88,5,22,39,56,73,90,7,24,41,58,75,92,9,26,43,
  60,77,94,11,28,45,62,79,96,13,30,47,64,81,98,15,32,49,66,83
};

const int totalPasos = sizeof(secuencia) / sizeof(secuencia[0]);

// ---------------- VARIABLES ----------------
float posicionActual = 0.0;

unsigned long tiempoInicio = 0;
unsigned long ultimoLCD = 0;
unsigned long ultimoSerial = 0;

int progresoGlobal = 0;

// ---------------- LCD ----------------

void actualizarLCD() {

  if (millis() - ultimoLCD < 200) return;
  ultimoLCD = millis();

  unsigned long tiempoActual = millis() - tiempoInicio;

  int segundos = tiempoActual / 1000;
  int minutos = segundos / 60;
  segundos = segundos % 60;

  lcd.setCursor(0, 0);
  lcd.print("String Art 1.0");

  lcd.setCursor(0, 1);
  lcd.print("Prog: ");

  if (minutos < 10) lcd.print("0");
  lcd.print(minutos);
  lcd.print(":");
  if (segundos < 10) lcd.print("0");
  lcd.print(segundos);

  lcd.setCursor(12, 1);

  if (progresoGlobal < 100) lcd.print(" ");
  if (progresoGlobal < 10) lcd.print(" ");

  lcd.print(progresoGlobal);
  lcd.print("%");
}

// ---------------- SERIAL ----------------

void actualizarSerial(int i, int origen, int destino) {

  if (millis() - ultimoSerial < 300) return;
  ultimoSerial = millis();

  unsigned long tiempoActual = millis() - tiempoInicio;

  int segundos = tiempoActual / 1000;
  int minutos = segundos / 60;
  segundos = segundos % 60;

  Serial.print("[");
  if (minutos < 10) Serial.print("0");
  Serial.print(minutos);
  Serial.print(":");
  if (segundos < 10) Serial.print("0");
  Serial.print(segundos);
  Serial.print("] ");

  Serial.print("Paso ");
  Serial.print(i);
  Serial.print(" | ");

  Serial.print(origen);
  Serial.print(" -> ");
  Serial.print(destino);

  Serial.print(" | ");
  Serial.print(progresoGlobal);
  Serial.println("%");
}

// ---------------- MOTOR ----------------

void stepMotor(long pasos, bool dir) {
  digitalWrite(dirPin, dir);

  for (long i = 0; i < pasos; i++) {
    digitalWrite(stepPin, HIGH);
    delayMicroseconds(500);
    digitalWrite(stepPin, LOW);
    delayMicroseconds(500);
  }
}

void irAClavo(int clavo) {

  float pasosObjetivo = (pasosPorVuelta * clavo) / totalClavos;

  float delta = pasosObjetivo - posicionActual;
  bool sentido = (delta >= 0);

  if (sentido) pasosObjetivo += offsetAngular;
  else pasosObjetivo -= offsetAngular;

  delta = pasosObjetivo - posicionActual;

  stepMotor(abs((long)delta), sentido);

  posicionActual += (long)delta;
}

// ---------------- SERVO ----------------

void bajarAguja() {
  aguja.write(ANGULO_ABAJO);
  delay(200);
}

void subirAguja() {
  aguja.write(ANGULO_ARRIBA);
  delay(200);
}

void engancharHilo() {

  bajarAguja();
  stepMotor(pasosOffset, HIGH);
  subirAguja();
}

// ---------------- SETUP ----------------

void setup() {
  pinMode(stepPin, OUTPUT);
  pinMode(dirPin, OUTPUT);

  aguja.attach(servoPin);
  subirAguja();

  lcd.init();
  lcd.backlight();

  Serial.begin(115200);
  Serial.println("=== INICIO STRING ART ===");

  lcd.setCursor(0, 0);
  lcd.print("String Art 1.0");
  lcd.setCursor(0, 1);
  lcd.print("Iniciando...");
  delay(2000);

  lcd.clear();

  tiempoInicio = millis();
}

// ---------------- LOOP ----------------

void loop() {

  for (int i = 0; i < totalPasos - 1; i++) {

    progresoGlobal = (i * 100) / totalPasos;

    int origen = secuencia[i];
    int destino = secuencia[i + 1];

    actualizarLCD();                      
    actualizarSerial(i, origen, destino);  // 🔥 serial fluido

    irAClavo(origen);
    engancharHilo();
  }

  // -------- FIN --------

  unsigned long tiempoTotal = millis() - tiempoInicio;

  int segundos = tiempoTotal / 1000;
  int minutos = segundos / 60;
  segundos = segundos % 60;

  lcd.clear();

  lcd.setCursor(0, 0);
  lcd.print("String Art 1.0");

  lcd.setCursor(0, 1);
  lcd.print("Finish! ");

  if (minutos < 10) lcd.print("0");
  lcd.print(minutos);
  lcd.print(":");
  if (segundos < 10) lcd.print("0");
  lcd.print(segundos);

  Serial.println("=== TERMINADO ===");

  while (true) {
  }
}

5.6 System Integration

System integration consisted of connecting and synchronizing mechanical, electronic, and programming components, allowing the machine to function as a unified system.

The operation flow begins with the generation of digital trajectories from an image using tools such as String Art Creator. These trajectories are processed by the microcontroller, which interprets the required movement sequence.

The microcontroller then sends signals to the stepper motor driver, which drives the rotation of the base through the gear system. This movement, combined with the thread guiding and tension system, positions the thread between defined points, generating the physical pattern.

Proper integration ensures precise movement, controlled thread trajectory, and consistent results. During this stage, adjustments were made in mechanical alignment, system stability, and motion response to ensure continuous and reliable operation.

Audiovisual Record

Photographic Record

Stage 1

Stage 1

Stage 1

Challenges and Learnings

The development of StringArt presented several challenges, mainly related to motion precision and the integration between the mechanical system and control logic.

One of the first challenges was achieving stable rotation of the base, as initial versions showed issues with friction, imbalance, and vibrations. These factors directly affected system repeatability and generated cumulative errors in pattern execution.

However, the most critical challenge was found in programming and in the synchronization between digital trajectories and the physical movement of the machine.

At a learning level, the project demonstrated that precision in digital fabrication does not depend solely on mechanical design, but on the correct integration of software, electronics, and physical structure.

Conclusions

StringArt evolved from a conceptual mechanical exploration into an integrated digital fabrication system. The process made clear that the main difficulty was not only movement control, but the synchronization between mechanics, electronics, and thread behavior. The project shows how iterative development is essential when dealing with fabrication systems that combine technical precision and expressive results.

Bill of Materials

System Component Quantity Model / Specification Supplier (Peru)
Structure M5/M6 Screws Set Various Sodimac
MDF Base 1 12 mm Sodimac
Base Board 1 MDF 12 mm Sodimac
Motion Stepper Motor 1–2 JK57HS76-2804 (NEMA 23, 2.8A, 1.89 Nm) SAISAC
Gears 1 set 3D printed DIY
Steel Shaft 1 Stainless steel Malvinas
Thread System Spool Holder 1 3D printed / metal DIY
Tension System 1 Mechanical tensioner + springs Sodimac
Guide Tube 1 Plastic DIY
Nails 100–300 According to design Sodimac
Electronics Microcontroller 1 ESP32 + shield SAISAC
LCD Display 1 LCD 1602 with I2C module Electromanía
Driver 1 DM556 SAISAC
Power Supply 1 24V 5A (120W) SAISAC
Logic Level Converter 1 4CH 5V–3.3V SAISAC
Relay Module 1 5V Relay (1 channel) SAISAC
Cables and Connectors Set Dupont / 14AWG SAISAC
Extras Ballcasters 4–8 Ball caster wheels SAISAC
Power Switch 1 ON/OFF switch SAISAC
Solder Paste 1 YH-351 (35g) Mastertronics

Files and Links