-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpilot.ino
More file actions
203 lines (175 loc) · 8.47 KB
/
Copy pathpilot.ino
File metadata and controls
203 lines (175 loc) · 8.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// ═════════════════════════════════════════════════════════════
// PILOT — auto-pilot heading hold + manual rudder override
// (included as tab in simplepilot_v2.12)
//
// Manual mode logic preserved exactly from original SimplePilot
// code — do not modify the manual override section.
// Only the auto-pilot section has been updated for:
// - ICM-20948 DMP heading source (was LSM303)
// - Shortest-path heading error (was wrap-around broken)
// - Non-blocking motor pulses (was delay()-based)
// - PI controller for more aggressive + steady-state correction
//
// v2.12 FIX: StopActuator() only called on the TRANSITION from
// auto→manual (when wasPilotActive was true), not every loop
// iteration. v2.11 called it unconditionally, which killed
// manual tiller control every loop when pilot was OFF.
// v2.11 FIX: All 3 joystick deadband checks now use
// JOYPILOT_CENTER (was incorrectly JOYCRUISE_CENTER).
// ═════════════════════════════════════════════════════════════
// Track whether pilot was active last cycle (for heading lock on entry)
static bool wasPilotActive = false;
// ─── Shortest-path heading error (handles 0°/360° wrap-around) ──
// Returns error in range (-180, +180].
// Positive = target is to starboard (right).
// Negative = target is to port (left).
float headingError(float target, float current) {
float err = target - current;
if (err > 180.0f) err -= 360.0f;
if (err < -180.0f) err += 360.0f;
return err;
}
// ─── Pilot control ────────────────────────────────────────────
void PilotControl() {
// Read Inputs from pot on Joystick and switch
int rawJoyPilot = get_averaged_reading(PILOT_JOY_PIN, NUM_SAMPLES);
pilotActive = false;
if (warmup) { // check switch after warmup and IMU has had a chance to settle
pilotActive = (digitalRead(PILOT_SW_PIN) == LOW); // Low = ON
}
// Block auto-pilot during compass calibration (heading unreliable)
if (autoCalActive) {
pilotActive = false;
}
// Apply Safety Constraint to Target
int JoyPilotPos = constrain(rawJoyPilot, LIMIT_LEFT, LIMIT_RIGHT);
// Get heading from DMP compass (cached at 25Hz in main loop)
// (Original code had: compass.read(); Heading = compass.heading(); here)
if (!warmup && headingValid) {
targetHeading = currentHeading; // at power up set both heading and setpoint to same
}
// Detect manual joystick override (joystick outside deadband)
// v2.11 FIX: Use JOYPILOT_CENTER, not JOYCRUISE_CENTER
bool joyOverride = (JoyPilotPos < (JOYPILOT_CENTER - PILOT_DEADBAND)) ||
(JoyPilotPos > (JOYPILOT_CENTER + PILOT_DEADBAND));
// ── MANUAL OVERRIDE (always active — preserved from original) ──
// Joystick deflection moves rudder and updates target heading.
// Direction mapping preserved from original:
// low pot value → moveRight()
// high pot value → moveLeft()
// v2.11 FIX: Use JOYPILOT_CENTER, not JOYCRUISE_CENTER
if (JoyPilotPos < (JOYPILOT_CENTER - PILOT_DEADBAND)) { // check pot value and move actuator based on pot value
motorSpeed = 255;
moveRight();
if (headingValid) targetHeading = currentHeading;
}
else if (JoyPilotPos > (JOYPILOT_CENTER + PILOT_DEADBAND)) {
motorSpeed = 255;
moveLeft();
if (headingValid) targetHeading = currentHeading;
}
else {
// Safety: Stop if no stick input AND no auto-pilot pulse running
// (Without the motorMoving guard, StopActuator() kills the auto-pilot's
// timed pulse on the very next loop iteration, making auto mode impossible)
if (!motorMoving) StopActuator();
}
delay(1);
// ── AUTO-PILOT MODE — PI controller (auto mode only) ────────
if (pilotActive && headingValid) {
// On first entry to auto mode, lock current heading as target
// and reset PI controller state
if (!wasPilotActive) {
targetHeading = currentHeading;
integralSum = 0.0f;
lastSteerTimeMs = millis();
}
wasPilotActive = true;
// Reset integral on manual joystick override — user is steering,
// so stale integral from the previous heading is invalid
if (joyOverride) {
integralSum = 0.0f;
lastSteerTimeMs = millis();
}
// Only compute steering if joystick centered and no motor pulse running
if (!joyOverride && !motorMoving) {
float err = headingError(targetHeading, currentHeading);
int absErr = (int)fabsf(err);
// Cap error at MAX_ERROR_DEG (safety — prevents huge PI output)
if (absErr > MAX_ERROR_DEG) {
absErr = MAX_ERROR_DEG;
err = (err > 0) ? (float)MAX_ERROR_DEG : -(float)MAX_ERROR_DEG;
}
if (absErr > HEADING_HYSTERESIS) {
// ── PI computation ──────────────────────────────────
unsigned long now = millis();
float dt = (now - lastSteerTimeMs) / 1000.0f;
// Clamp dt to reasonable range (0.01s – 2.0s) to prevent
// integral spike on first calculation or after long pause
if (dt < 0.01f) dt = 0.01f;
if (dt > 2.0f) dt = 2.0f;
lastSteerTimeMs = now;
// Accumulate integral (only when outside hysteresis)
integralSum += (err * KI * dt);
// Anti-windup: clamp integral to ±INTEGRAL_LIMIT
integralSum = constrain(integralSum, -INTEGRAL_LIMIT, INTEGRAL_LIMIT);
// PI output: P-term + I-term
float piOutput = (KP * err) + integralSum;
int absOutput = (int)fabsf(piOutput);
// ── Map PI output to motor speed and pulse duration ──
// Motor speed: MIN_AUTO_SPEED + output, capped at MAX_MOTOR_SPEED
motorSpeed = constrain(MIN_AUTO_SPEED + absOutput, MIN_AUTO_SPEED, MAX_MOTOR_SPEED);
// Pulse duration: output × PULSE_GAIN, minimum 20ms so actuator actually moves
unsigned long pulseDuration = (unsigned long)(absOutput * PULSE_GAIN);
if (pulseDuration < 20) pulseDuration = 20;
// Steer in direction of error
if (piOutput > 0) {
moveRight();
} else {
moveLeft();
}
// Non-blocking: start timed motor pulse
motorStopMs = millis() + pulseDuration;
motorMoving = true;
} else {
// Within hysteresis — no steering pulse, but keep timer current
// so next pulse's dt calculation isn't inflated
lastSteerTimeMs = millis();
}
}
digitalWrite(PilotLEDpin, HIGH); // Turn ON LED
} else {
// Manual mode — pilot switch OFF or heading not valid
// v2.12 FIX: Only clean up on the TRANSITION from auto→manual.
// If we always call StopActuator(), manual joystick control is
// killed every loop (motor starts from moveRight/moveLeft above,
// then immediately stopped here). Only stop on transition.
if (wasPilotActive) {
StopActuator(); // Kill any auto-pilot pulse in progress
motorMoving = false; // Cancel stale auto-pilot pulse timer
integralSum = 0.0f; // Reset integral when leaving auto mode
}
wasPilotActive = false;
warmup = true; // after first manual cycle, allow autopilot
digitalWrite(PilotLEDpin, LOW); // Turn off LED
}
// Debug Output (Every 1sec)
if (millis() - lastPrintMs > 1000) {
Serial.print("PilotActive:"); Serial.print(pilotActive ? " ON " : " OFF");
Serial.print(" | JOY-PILOT:"); Serial.print(rawJoyPilot);
Serial.print(" | TGT:"); Serial.print(JoyPilotPos);
Serial.print(" | CUR HDNG:"); Serial.print((int)currentHeading);
Serial.print(" | TARGET HDNG:"); Serial.print((int)targetHeading);
Serial.print(" | DAC-OUT:"); Serial.print(currentDacOutput); // sig from throttle pot
if (pilotActive) {
float err = headingError(targetHeading, currentHeading);
float pTerm = KP * err;
Serial.printf(" | ERR:%+0.0f P:%+0.1f I:%+0.1f", err, pTerm, integralSum);
Serial.println(motorMoving ? " [STEERING]" : " [HOLDING]");
} else {
Serial.println(" | [STANDBY]");
}
lastPrintMs = millis();
}
delay(1);
}