added pid lesson
parent
68dde0951a
commit
7e8a2b5666
|
|
@ -0,0 +1,89 @@
|
|||
export class LineFollowerSim {
|
||||
constructor(canvas, updateBoxFn) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext("2d");
|
||||
|
||||
this.box = {
|
||||
x: 250,
|
||||
y: 150,
|
||||
width: 40,
|
||||
height: 20,
|
||||
speed: 0.4,
|
||||
dir: 0
|
||||
};
|
||||
|
||||
this.lineX = 250;
|
||||
this.lineTargetX = 250;
|
||||
this.lineWidth = 12;
|
||||
|
||||
this.updateBox = updateBoxFn || this.defaultUpdateBox.bind(this);
|
||||
this.loop = this.loop.bind(this);
|
||||
}
|
||||
|
||||
updateLine() {
|
||||
if (Math.random() < 0.02) {
|
||||
this.lineTargetX = 250 + (Math.random() - 0.5) * 200;
|
||||
}
|
||||
|
||||
this.lineX += (this.lineTargetX - this.lineX) * 0.005;
|
||||
}
|
||||
|
||||
defaultUpdateBox() {
|
||||
const leftSensor = this.box.x - this.box.width / 2;
|
||||
const rightSensor = this.box.x + this.box.width / 2;
|
||||
|
||||
const onLine = x => x >= this.lineX - this.lineWidth / 2 && x <= this.lineX + this.lineWidth / 2;
|
||||
|
||||
const leftOnLine = onLine(leftSensor);
|
||||
const rightOnLine = onLine(rightSensor);
|
||||
|
||||
if (leftOnLine && !rightOnLine) {
|
||||
this.box.dir = -1;
|
||||
} else if (rightOnLine && !leftOnLine) {
|
||||
this.box.dir = 1;
|
||||
} else {
|
||||
//this.box.dir = 0;
|
||||
}
|
||||
|
||||
this.box.x += this.box.dir * this.box.speed;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const ctx = this.ctx;
|
||||
const { x, y, width, height } = this.box;
|
||||
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.lineX, 0);
|
||||
ctx.lineTo(this.lineX, this.canvas.height);
|
||||
ctx.strokeStyle = "blue";
|
||||
ctx.lineWidth = this.lineWidth;
|
||||
ctx.stroke();
|
||||
|
||||
// Box
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillRect(x - width / 2, y - height / 2, width, height);
|
||||
|
||||
// Sensors
|
||||
ctx.fillStyle = "black";
|
||||
ctx.beginPath();
|
||||
ctx.arc(x - width / 2, y - height / 2, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + width / 2, y - height / 2, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
loop() {
|
||||
this.updateLine();
|
||||
this.updateBox();
|
||||
this.draw();
|
||||
requestAnimationFrame(this.loop);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.loop();
|
||||
}
|
||||
}
|
||||
235
index.html
235
index.html
|
|
@ -31,6 +31,7 @@
|
|||
<button class="tab-btn text-gray-600 hover:text-blue-600 hidden" data-target="lesson9">Motor
|
||||
Encoders</button>
|
||||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson10">Reciever</button>
|
||||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson11">PID</button>
|
||||
<!-- Add more tabs here -->
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -925,12 +926,17 @@ while True:
|
|||
</br>
|
||||
<p><a href="files/adafruit_hcsr04.py">adafruit_hcsr04.py</a></p>
|
||||
</br>
|
||||
<p>Note the <code>timeout=0.005</code>, this is important as it tells the code how long to wait for an ECHO. If we don't have that, our code could be blocked for quite a lot of time if the only obstacles are a long way away.</p>
|
||||
<p>Note the <code>timeout=0.005</code>, this is important as it tells the code how long to wait for
|
||||
an ECHO. If we don't have that, our code could be blocked for quite a lot of time if the only
|
||||
obstacles are a long way away.</p>
|
||||
|
||||
</br>
|
||||
<p>Even though all we really need is to get the distance with <code>sonar.distance</code>, when no echo is received in time an error will occur that will stop your code from running.</p>
|
||||
<p>Even though all we really need is to get the distance with <code>sonar.distance</code>, when no
|
||||
echo is received in time an error will occur that will stop your code from running.</p>
|
||||
</br>
|
||||
<p>With our <code>try-except</code> block, any errors will just run whatever is in the <code>except</code> part. In this case, nothing.</p>
|
||||
<p>With our <code>try-except</code> block, any errors will just run whatever is in the
|
||||
<code>except</code> part. In this case, nothing.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||||
|
|
@ -957,9 +963,12 @@ while True:
|
|||
<!-- Step 3 -->
|
||||
<div class="prose">
|
||||
<h2>Step 3: Clean it up a bit</h2>
|
||||
<p>To tidy up our main loop, we can move all that to a function so the main loop just needs <code>get_distance()</code>.</p>
|
||||
<p>To tidy up our main loop, we can move all that to a function so the main loop just needs
|
||||
<code>get_distance()</code>.
|
||||
</p>
|
||||
</br>
|
||||
<p>If your code is getting quite long, you might even take functions like this and put them in your own module to keep your main code easy to read.</p>
|
||||
<p>If your code is getting quite long, you might even take functions like this and put them in your
|
||||
own module to keep your main code easy to read.</p>
|
||||
</div>
|
||||
<div>
|
||||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||||
|
|
@ -1064,7 +1073,8 @@ class ThumbInput:
|
|||
</br>
|
||||
<p>Initialize the receiver with <code>receiver = ThumbInput()</code></p>
|
||||
</br>
|
||||
<p>Create a new loop as this won't work nicely with any i2c, this loop should be placed after the motors are created, and before the i2c is initialized.</p>
|
||||
<p>Create a new loop as this won't work nicely with any i2c, this loop should be placed after the
|
||||
motors are created, and before the i2c is initialized.</p>
|
||||
</div>
|
||||
<div>
|
||||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||||
|
|
@ -1085,6 +1095,123 @@ while True:
|
|||
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Lesson 6 (hidden initially) -->
|
||||
<section id="lesson11" class="lesson-content hidden">
|
||||
<h1 class="text-3xl font-bold mb-6">PID</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="prose">
|
||||
<h2>Step 1: Coding</h2>
|
||||
<p>Up until now we've been using an algorithm known as "bang-bang" to track the line.</p>
|
||||
</br>
|
||||
<p>If the left sensor sees something, we take a single action, <code>turn left</code>.</p>
|
||||
<p>Else if the right sensor sees something, we take a single action, <code>gurn right</code>.</p>
|
||||
<p>Else <code>go straight</code>.</p>
|
||||
</br>
|
||||
<p>This works well, but it can be a bit jerky.</p>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h2>Bang-Bang Algorithm</h2>
|
||||
<canvas id="canvasBangBang" width="500" height="300"></canvas>
|
||||
</br>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="prose">
|
||||
<h2>Step 2: Use a PID algorithm</h2>
|
||||
<p>A PID algorithm on the other hand tries to always keep the robot exactly on the line.</p>
|
||||
</br>
|
||||
<p>First we have to change our two colour values, <code>left_color</code> and
|
||||
<code>right_color</code> into a single <code>error</code> value.</p>
|
||||
<p><code>right_color - left_color = error</code></p>
|
||||
</br>
|
||||
<p>Now if we are on the line our error should be 0, with ot becoming positive if we move one way,
|
||||
and negative if we move the other.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>PID Algorithm</h2>
|
||||
<canvas id="canvasPID" width="500" height="300"></canvas>
|
||||
<div style="max-width: 500px; margin: 0 auto; font-family: sans-serif;">
|
||||
<h3>PID Controller Parameters</h3>
|
||||
<label>Kp: <input type="range" id="kpSlider" min="0" max="1" step="0.001" value="0.01"> <span
|
||||
id="kpVal">0.01</span></label><br>
|
||||
<label>Ki: <input type="range" id="kiSlider" min="0" max="0.5" step="0.0001" value="0.0000">
|
||||
<span id="kiVal">0.0</span></label><br>
|
||||
<label>Kd: <input type="range" id="kdSlider" min="0" max="0.5" step="0.001" value="0.000">
|
||||
<span id="kdVal">0.0</span></label><br><br>
|
||||
<strong>Error (right_color-left_color):</strong> <span id="errorLabel">0</span>
|
||||
</br> <button
|
||||
id="resetBtn"
|
||||
class="px-2 py-1 bg-red-600 text-white text-sm font-semibold rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400"
|
||||
>
|
||||
Reset Robot Position
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="prose">
|
||||
<h2>Step 3: The Code</h2>
|
||||
<p>The pid.py library is simple, we initialize it with a <code>P</code>, <code>I</code>, and
|
||||
<code>D</code>.</p>
|
||||
</br>
|
||||
<p>These stand for <code>Proportional</code>, <code>Integral</code>, and <code>Derivative</code></p>
|
||||
</br>
|
||||
<p>The <code>P</code> value controls how aggressively proportional the final result will be, if we
|
||||
set it too high, the output will be too high and the robot will adjust too hard. If we set it
|
||||
too low, the robot will be too lazy about tracking the line and may not turn fast enough to stay
|
||||
on it.</p>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||||
# pid.py
|
||||
|
||||
class PIDController:
|
||||
def __init__(self, kp, ki, kd):
|
||||
self.kp = kp
|
||||
self.ki = ki
|
||||
self.kd = kd
|
||||
|
||||
self.integral = 0
|
||||
self.last_error = 0
|
||||
|
||||
def compute(self, error):
|
||||
self.integral += error
|
||||
derivative = error - self.last_error
|
||||
self.last_error = error
|
||||
|
||||
output = (self.kp * error) + (self.ki * self.integral) + (self.kd * derivative)
|
||||
return output
|
||||
|
||||
</code></pre>
|
||||
</br>
|
||||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||||
# code.py
|
||||
|
||||
import pid
|
||||
|
||||
linePID = pid.PIDController(kp=1.0, ki=0.0, kd=0.0)
|
||||
|
||||
while True:
|
||||
error = right_color - left_color # Calculate the error
|
||||
output = linePID.compute(error)
|
||||
# Use the output to adjust the motors
|
||||
print(error, output)
|
||||
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
// Tab button logic
|
||||
|
|
@ -1112,6 +1239,102 @@ while True:
|
|||
});
|
||||
});
|
||||
</script>
|
||||
<script type="module">
|
||||
|
||||
import { LineFollowerSim } from './LineFollowerSim.js';
|
||||
|
||||
// Bang-Bang controller (default)
|
||||
const canvas1 = document.getElementById('canvasBangBang');
|
||||
const sim1 = new LineFollowerSim(canvas1);
|
||||
sim1.start();
|
||||
|
||||
// PID controller example
|
||||
const canvas2 = document.getElementById('canvasPID');
|
||||
|
||||
|
||||
// Get sliders and output spans
|
||||
const kpSlider = document.getElementById("kpSlider");
|
||||
const kiSlider = document.getElementById("kiSlider");
|
||||
const kdSlider = document.getElementById("kdSlider");
|
||||
const kpVal = document.getElementById("kpVal");
|
||||
const kiVal = document.getElementById("kiVal");
|
||||
const kdVal = document.getElementById("kdVal");
|
||||
const errorLabel = document.getElementById("errorLabel");
|
||||
|
||||
let error = 0, integral = 0, lastError = 0;
|
||||
let lastUpdateTime = 0;
|
||||
const pidUpdateInterval = 50; // ms
|
||||
|
||||
// Setup velocity on the box (just once, after constructing the sim)
|
||||
|
||||
|
||||
const pidControl = function () {
|
||||
// Always move the box every frame based on velocity
|
||||
this.box.x += this.box.vel;
|
||||
|
||||
// Only update velocity (PID) every `pidUpdateInterval` ms
|
||||
const now = performance.now();
|
||||
if (now - lastUpdateTime < pidUpdateInterval) return;
|
||||
lastUpdateTime = now;
|
||||
|
||||
const sensorMid = this.box.x;
|
||||
const target = this.lineX;
|
||||
|
||||
const proximityThreshold = this.box.width;
|
||||
const distance = Math.abs(target - sensorMid);
|
||||
|
||||
if (distance < proximityThreshold) {
|
||||
error = target - sensorMid;
|
||||
integral += error;
|
||||
const derivative = error - lastError;
|
||||
lastError = error;
|
||||
|
||||
const Kp = parseFloat(kpSlider.value);
|
||||
const Ki = parseFloat(kiSlider.value);
|
||||
const Kd = parseFloat(kdSlider.value);
|
||||
|
||||
kpVal.textContent = Kp.toFixed(3);
|
||||
kiVal.textContent = Ki.toFixed(4);
|
||||
kdVal.textContent = Kd.toFixed(3);
|
||||
errorLabel.textContent = error.toFixed(2);
|
||||
|
||||
// Apply PID to velocity
|
||||
this.box.vel = Kp * error + Ki * integral + Kd * derivative;
|
||||
} else {
|
||||
errorLabel.textContent = "—";
|
||||
lastError = 0;
|
||||
integral *= 0.9;
|
||||
|
||||
// Optionally slow the robot when it's off track
|
||||
this.box.vel *= 0.9;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const sim2 = new LineFollowerSim(canvas2, pidControl);
|
||||
sim2.box.vel = 0;
|
||||
sim2.start();
|
||||
|
||||
const resetBtn = document.getElementById("resetBtn");
|
||||
|
||||
resetBtn.addEventListener("click", () => {
|
||||
// Put the box instantly on the line
|
||||
sim2.box.x = sim2.lineX;
|
||||
|
||||
// Reset PID controller internal state
|
||||
error = 0;
|
||||
integral = 0;
|
||||
lastError = 0;
|
||||
|
||||
// Reset velocity (if you use velocity)
|
||||
sim2.box.vel = 0;
|
||||
|
||||
// Also update error label immediately
|
||||
errorLabel.textContent = "0";
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue