#include <Servo.h>
#include <EEPROM.h>
#include <math.h>
// ----------------- Pinos -----------------
const uint8_t PIN_SERVO_BASE = 9;
const uint8_t PIN_SERVO_OMBRO = 10;
const uint8_t PIN_SERVO_BRACO = 11;
const uint8_t PIN_SERVO_GARRA = 6;
const uint8_t PIN_J1_X = A0;
const uint8_t PIN_J1_Y = A1;
const uint8_t PIN_J1_SW = 2;
const uint8_t PIN_J2_X = A2;
const uint8_t PIN_J2_Y = A3;
const uint8_t PIN_J2_SW = 3;
// ----------------- Servos -----------------
Servo servoBase, servoOmbro, servoBraco, servoGarra;
// ----------------- Limites mecânicos -----------------
float baseMin = 0.0f, baseMax = 180.0f, baseInit = 90.0f;
float ombroMin = 10.0f, ombroMax = 170.0f, ombroInit = 90.0f;
float bracoMin = 15.0f, bracoMax = 165.0f, bracoInit = 90.0f;
float garraMin = 10.0f, garraMax = 120.0f, garraInit = 60.0f;
// Soft-limits (graus antes do limite)
const float SOFT_ZONE_BASE = 12.0f;
const float SOFT_ZONE_OMBRO = 12.0f;
const float SOFT_ZONE_BRACO = 12.0f;
const float SOFT_ZONE_GARRA = 10.0f;
// Sentido (+1 normal, -1 invertido)
int8_t INVERT_BASE = +1;
int8_t INVERT_OMBRO = -1;
int8_t INVERT_BRACO = -1;
int8_t INVERT_GARRA = +1;
// ----------------- Controle base -----------------
const uint16_t CONTROL_PERIOD_MS = 10; // 100 Hz
const int DEADZONE = 50;
// Velocidade máx no MANUAL (referência de v)
float MAX_RATE_BASE = 180.0f;
float MAX_RATE_OMBRO = 150.0f;
float MAX_RATE_BRACO = 150.0f;
float MAX_RATE_GARRA = 200.0f;
// Expo do joystick
float EXPO_BASE = 1.5f;
float EXPO_OMBRO = 1.5f;
float EXPO_BRACO = 1.5f;
float EXPO_GARRA = 1.2f;
// Soft-start
const float START_RAMP_DEG_PER_S = 90.0f;
// ----------------- Estados gerais -----------------
float angBase, angOmbro, angBraco, angGarra;
bool preciseMode = false;
bool invertGarra = false;
bool estop = false;
bool lastJ2 = HIGH;
uint32_t lastToggleMs = 0;
const uint16_t TOGGLE_DEBOUNCE_MS = 200;
uint32_t estopPressStart = 0;
bool j1Prev = HIGH, j2Prev = HIGH;
// EEPROM centros
struct CalData { uint32_t magic; int cx1, cy1, cx2, cy2; } cal = {0,512,512,512,512};
const uint32_t CAL_MAGIC = 0xA55AF11A;
// ----------------- Macro -----------------
struct Pose { float base, ombro, braco, garra; };
const uint8_t MAX_POSES = 16;
Pose poses[MAX_POSES];
uint8_t poseCount = 0;
bool recordMode = false;
bool playbackMode = false;
uint8_t playIndex = 0;
const uint16_t LONG_PRESS_MS = 2000;
uint32_t j1PressStart = 0, j2PressStart = 0;
// ----------------- Playback: AxisJL (posição jerk-limited) -----------------
struct AxisJL {
float p, v, a;
float Vmax, Amax, Jmax;
float target;
float posEps=0.4f, velEps=0.5f, accEps=2.0f;
void setLimits(float vMax, float aMax, float jMax){ Vmax=vMax; Amax=aMax; Jmax=jMax; }
void setStateFromAngle(float angle){ p=angle; v=0; a=0; }
void setTarget(float tgt){ target=tgt; }
bool reached() const { return (fabs(p-target)<=posEps)&& (fabs(v)<=velEps)&& (fabs(a)<=accEps); }
float stoppingDistance() const {
float dv=fabs(v), da=fabs(a);
return (dv*dv)/(2.0f*Amax) + (da*da)/(2.0f*Jmax) + 0.1f;
}
void step(float dt){
float d = target - p;
float dir = (d>=0)?1.0f:-1.0f;
bool needBrake = (fabs(d) <= stoppingDistance());
float j = 0.0f;
if (needBrake){
if (v*dir > velEps || a*dir > accEps){
if (a*dir > 0.0f) j = -Jmax*dir;
else j = (-Jmax)*((v*dir>0.0f)?dir:-dir);
} else {
j = (a>0)?-Jmax:((a<0)?+Jmax:0);
}
} else {
if (v*dir < Vmax){
if (a*dir < Amax) j = +Jmax*dir;
else j = 0.0f;
} else {
if (fabs(a) > accEps) j = (a>0)?-Jmax:+Jmax;
else j = 0.0f;
}
}
a += j*dt; if (a> Amax) a= Amax; if (a<-Amax) a=-Amax;
v += a*dt; if (v> Vmax) v= Vmax; if (v<-Vmax) v=-Vmax;
p += v*dt;
if ( ((target-p)*dir<0.0f) && (fabs(v)<velEps) && (fabs(a)<accEps) ){ p=target; v=0; a=0; }
}
};
// ----------------- Manual: VelJL (velocidade jerk-limited) -----------------
struct VelJL {
float v=0.0f, a=0.0f;
float Vmax=180.0f;
float Amax=300.0f;
float Jmax=2500.0f;
float velEps=0.2f, accEps=2.0f;
void setLimits(float vMax, float aMax, float jMax){
Vmax=vMax; Amax=aMax; Jmax=jMax;
}
float step(float v_cmd, float dt){
if (v_cmd > Vmax) v_cmd = Vmax;
if (v_cmd < -Vmax) v_cmd = -Vmax;
float dv = v_cmd - v;
float dir = (dv>=0)?1.0f:-1.0f;
float j = 0.0f;
if (fabs(dv) > velEps){
if ( (a*dir) < Amax ) j = +Jmax*dir;
else j = 0.0f;
if ( (v_cmd==0.0f && fabs(v)<velEps) || (dv*dir<0.0f && fabs(dv)<2*velEps) ){
j = (a>0)?-Jmax:((a<0)?+Jmax:0.0f);
}
} else {
if (fabs(a) > accEps) j = (a>0)?-Jmax:+Jmax;
else j = 0.0f;
}
a += j*dt; if (a> Amax) a= Amax; if (a<-Amax) a=-Amax;
v += a*dt; if (v> Vmax) v= Vmax; if (v<-Vmax) v=-Vmax;
return v;
}
};
AxisJL jlBase, jlOmbro, jlBraco, jlGarra;
VelJL vmBase, vmOmbro, vmBraco, vmGarra;
// ----------------- Utils -----------------
inline float constrainf(float x,float a,float b){ if(x<a)return a; if(x>b)return b; return x; }
inline float expoMap(int deflRaw,float expo){
float x = deflRaw/511.0f; if(x>1)x=1; if(x<-1)x=-1; float s=(x>=0)?1.0f:-1.0f; return s*powf(fabs(x),expo);
}
int readAxisCentered(uint8_t pin,int center){
int v=analogRead(pin); int d=v-center; if(abs(d)<DEADZONE) return 0; if(d>0)d-=DEADZONE; else d+=DEADZONE; return d;
}
void attachServos(){
servoBase.attach(PIN_SERVO_BASE);
servoOmbro.attach(PIN_SERVO_OMBRO);
servoBraco.attach(PIN_SERVO_BRACO);
servoGarra.attach(PIN_SERVO_GARRA);
}
void writeServos(){
servoBase.write((int)angBase);
servoOmbro.write((int)angOmbro);
servoBraco.write((int)angBraco);
servoGarra.write((int)angGarra);
}
void loadCalFromEEPROM(){
EEPROM.get(0, cal);
if (cal.magic != CAL_MAGIC){ cal.magic=CAL_MAGIC; cal.cx1=512; cal.cy1=512; cal.cx2=512; cal.cy2=512; EEPROM.put(0,cal); }
}
void saveCalToEEPROM(){ cal.magic=CAL_MAGIC; EEPROM.put(0,cal); }
float softLimitFactorSigned(float ang,float amin,float amax,float zone,float rate){
if(rate>0){ float d=amax-ang; return (d<zone)?constrainf(d/zone,0,1):1.0f; }
if(rate<0){ float d=ang-amin; return (d<zone)?constrainf(d/zone,0,1):1.0f; }
return 1.0f;
}
float rampToward(float current,float target,float maxRate,float dt){
float delta=target-current, step=maxRate*dt;
if(delta> step) return current+step;
if(delta<-step) return current-step;
return target;
}
// ----------------- Setup -----------------
void setup(){
Serial.begin(115200);
pinMode(PIN_J1_SW, INPUT_PULLUP);
pinMode(PIN_J2_SW, INPUT_PULLUP);
loadCalFromEEPROM();
if (digitalRead(PIN_J1_SW)==LOW){
delay(300);
cal.cx1=analogRead(PIN_J1_X); cal.cy1=analogRead(PIN_J1_Y);
cal.cx2=analogRead(PIN_J2_X); cal.cy2=analogRead(PIN_J2_Y);
saveCalToEEPROM();
Serial.println(F("[CAL] Centros salvos."));
}
attachServos();
angBase = constrainf((float)servoBase.read(), baseMin, baseMax);
angOmbro = constrainf((float)servoOmbro.read(), ombroMin, ombroMax);
angBraco = constrainf((float)servoBraco.read(), bracoMin, bracoMax);
angGarra = constrainf((float)servoGarra.read(), garraMin, garraMax);
jlBase .setStateFromAngle(angBase ); jlBase .setLimits(120,400,2500);
jlOmbro.setStateFromAngle(angOmbro); jlOmbro.setLimits(100,350,2200);
jlBraco.setStateFromAngle(angBraco); jlBraco.setLimits(100,350,2200);
jlGarra.setStateFromAngle(angGarra); jlGarra.setLimits(140,500,3000);
vmBase .setLimits(MAX_RATE_BASE , 300, 2500);
vmOmbro.setLimits(MAX_RATE_OMBRO, 280, 2200);
vmBraco.setLimits(MAX_RATE_BRACO, 280, 2200);
vmGarra.setLimits(MAX_RATE_GARRA, 350, 3000);
}
// ----------------- Loop -----------------
void loop(){
static uint32_t lastMs=0;
uint32_t now=millis();
if (now-lastMs < CONTROL_PERIOD_MS) return;
float dt=(now-lastMs)/1000.0f; lastMs=now;
bool j1 = (digitalRead(PIN_J1_SW)==LOW);
bool j2 = (digitalRead(PIN_J2_SW)==LOW);
preciseMode = j1 && !playbackMode;
static uint32_t bothPressStart=0;
if (j1 && j2){
if(bothPressStart==0) bothPressStart=now;
else if ((now-bothPressStart)>=1000 && (j1Prev||j2Prev)){
estop=!estop;
Serial.print(F("[E-STOP] ")); Serial.println(estop?F("ATIVADO"):F("DESATIVADO"));
bothPressStart=now+10000;
}
} else bothPressStart=0;
j1Prev=j1; j2Prev=j2;
if (!estop){
static uint32_t j1PressStart=0;
if (j1){
if (j1PressStart==0) j1PressStart=now;
else if (!recordMode && !playbackMode && (now-j1PressStart)>=LONG_PRESS_MS){
recordMode=true; Serial.println(F("[MACRO] Gravacao INICIADA (J2 clique salva pose)."));
j1PressStart=now+10000;
}
} else {
if (recordMode && j1PressStart && (now-j1PressStart)>=LONG_PRESS_MS){
recordMode=false; Serial.print(F("[MACRO] Gravacao ENCERRADA. Poses=")); Serial.println(poseCount);
}
j1PressStart=0;
}
static uint32_t j2PressStart=0;
if (j2){
if (j2PressStart==0) j2PressStart=now;
else if (!playbackMode && (now-j2PressStart)>=LONG_PRESS_MS && poseCount>0){
playbackMode=true; recordMode=false; playIndex=0;
jlBase .setTarget(constrainf(poses[0].base , baseMin , baseMax ));
jlOmbro.setTarget(constrainf(poses[0].ombro, ombroMin, ombroMax));
jlBraco.setTarget(constrainf(poses[0].braco, bracoMin, bracoMax));
jlGarra.setTarget(constrainf(poses[0].garra, garraMin, garraMax));
Serial.println(F("[MACRO] Reproducao INICIADA (jerk-limited)."));
j2PressStart=now+10000;
} else if (playbackMode && (now-j2PressStart)>=LONG_PRESS_MS){
playbackMode=false; Serial.println(F("[MACRO] Reproducao PARADA."));
j2PressStart=now+10000;
}
} else j2PressStart=0;
}
bool j2State=(digitalRead(PIN_J2_SW)==LOW);
if (j2State && !lastJ2 && (now-lastToggleMs)>TOGGLE_DEBOUNCE_MS){
if (recordMode){
if (poseCount<MAX_POSES){
poses[poseCount++] = { angBase, angOmbro, angBraco, angGarra };
Serial.print(F("[MACRO] Pose salva (#")); Serial.print(poseCount); Serial.println(F(")."));
} else Serial.println(F("[MACRO] Limite de poses atingido."));
} else if (!playbackMode){
invertGarra=!invertGarra;
Serial.print(F("[GARRA] invertida=")); Serial.println(invertGarra?F("SIM"):F("NAO"));
}
lastToggleMs=now;
}
lastJ2=j2State;
static bool rampDone=false;
if (!rampDone){
angBase = rampToward(angBase, baseInit, START_RAMP_DEG_PER_S, dt);
angOmbro = rampToward(angOmbro, ombroInit, START_RAMP_DEG_PER_S, dt);
angBraco = rampToward(angBraco, bracoInit, START_RAMP_DEG_PER_S, dt);
angGarra = rampToward(angGarra, garraInit, START_RAMP_DEG_PER_S, dt);
if (fabs(angBase-baseInit)<0.5f && fabs(angOmbro-ombroInit)<0.5f &&
fabs(angBraco-bracoInit)<0.5f && fabs(angGarra-garraInit)<0.5f){
rampDone=true;
jlBase .setStateFromAngle(angBase );
jlOmbro.setStateFromAngle(angOmbro);
jlBraco.setStateFromAngle(angBraco);
jlGarra.setStateFromAngle(angGarra);
}
writeServos(); return;
}
if (estop){ writeServos(); return; }
if (playbackMode && poseCount>0){
if (jlBase.reached() && jlOmbro.reached() && jlBraco.reached() && jlGarra.reached()){
playIndex = (playIndex+1) % poseCount;
const Pose &t = poses[playIndex];
jlBase .setTarget(constrainf(t.base , baseMin , baseMax ));
jlOmbro.setTarget(constrainf(t.ombro, ombroMin, ombroMax));
jlBraco.setTarget(constrainf(t.braco, bracoMin, bracoMax));
jlGarra.setTarget(constrainf(t.garra, garraMin, garraMax));
}
jlBase.step(dt); jlOmbro.step(dt); jlBraco.step(dt); jlGarra.step(dt);
angBase = constrainf(jlBase.p , baseMin , baseMax );
angOmbro = constrainf(jlOmbro.p, ombroMin, ombroMax);
angBraco = constrainf(jlBraco.p, bracoMin, bracoMax);
angGarra = constrainf(jlGarra.p, garraMin, garraMax);
writeServos(); return;
}
int x1 = readAxisCentered(PIN_J1_X, cal.cx1);
int y1 = readAxisCentered(PIN_J1_Y, cal.cy1);
int x2 = readAxisCentered(PIN_J2_X, cal.cx2);
int y2 = readAxisCentered(PIN_J2_Y, cal.cy2);
float ux1 = expoMap(x1, EXPO_BASE );
float uy1 = expoMap(y1, EXPO_OMBRO);
float ux2 = expoMap(x2, EXPO_GARRA);
float uy2 = expoMap(y2, EXPO_BRACO);
float precisionK = preciseMode ? 0.5f : 1.0f;
float vrefBase = INVERT_BASE * ux1 * MAX_RATE_BASE * precisionK;
float vrefOmbro = INVERT_OMBRO * uy1 * MAX_RATE_OMBRO * precisionK;
float vrefBraco = INVERT_BRACO * uy2 * MAX_RATE_BRACO * precisionK;
int garraDir = (invertGarra ? -1 : +1);
float vrefGarra = INVERT_GARRA * garraDir * ux2 * MAX_RATE_GARRA * precisionK;
float fBase = softLimitFactorSigned(angBase , baseMin , baseMax , SOFT_ZONE_BASE , vrefBase );
float fOmbro = softLimitFactorSigned(angOmbro, ombroMin, ombroMax, SOFT_ZONE_OMBRO, vrefOmbro);
float fBraco = softLimitFactorSigned(angBraco, bracoMin, bracoMax, SOFT_ZONE_BRACO, vrefBraco);
float fGarra = softLimitFactorSigned(angGarra, garraMin, garraMax, SOFT_ZONE_GARRA, vrefGarra);
vrefBase *= fBase;
vrefOmbro *= fOmbro;
vrefBraco *= fBraco;
vrefGarra *= fGarra;
float vBase = vmBase .step(vrefBase , dt);
float vOmbro = vmOmbro.step(vrefOmbro, dt);
float vBraco = vmBraco.step(vrefBraco, dt);
float vGarra = vmGarra.step(vrefGarra, dt);
angBase = constrainf(angBase + vBase *dt, baseMin , baseMax );
angOmbro = constrainf(angOmbro + vOmbro *dt, ombroMin, ombroMax);
angBraco = constrainf(angBraco + vBraco *dt, bracoMin, bracoMax);
angGarra = constrainf(angGarra + vGarra *dt, garraMin, garraMax);
writeServos();
static uint32_t lastPrint=0;
if (now-lastPrint>350){
lastPrint=now;
Serial.print(F("ManualJL vB=")); Serial.print(vBase,1);
Serial.print(F(" vO=")); Serial.print(vOmbro,1);
Serial.print(F(" vR=")); Serial.print(vBraco,1);
Serial.print(F(" vG=")); Serial.print(vGarra,1);
Serial.print(F(" | A: ")); Serial.print((int)angBase); Serial.print(' ');
Serial.print((int)angOmbro); Serial.print(' ');
Serial.print((int)angBraco); Serial.print(' ');
Serial.print((int)angGarra); Serial.println();
}
}