added pid lesson

master
Jake 2025-07-31 01:04:30 +08:00
parent 68dde0951a
commit 7e8a2b5666
2 changed files with 326 additions and 14 deletions

89
LineFollowerSim.js Normal file
View File

@ -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();
}
}

View File

@ -31,6 +31,7 @@
<button class="tab-btn text-gray-600 hover:text-blue-600 hidden" data-target="lesson9">Motor <button class="tab-btn text-gray-600 hover:text-blue-600 hidden" data-target="lesson9">Motor
Encoders</button> 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="lesson10">Reciever</button>
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson11">PID</button>
<!-- Add more tabs here --> <!-- Add more tabs here -->
</div> </div>
</div> </div>
@ -925,12 +926,17 @@ while True:
</br> </br>
<p><a href="files/adafruit_hcsr04.py">adafruit_hcsr04.py</a></p> <p><a href="files/adafruit_hcsr04.py">adafruit_hcsr04.py</a></p>
</br> </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> </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> </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>
<div> <div>
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python"> <pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
@ -957,9 +963,12 @@ while True:
<!-- Step 3 --> <!-- Step 3 -->
<div class="prose"> <div class="prose">
<h2>Step 3: Clean it up a bit</h2> <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> </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>
<div> <div>
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python"> <pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
@ -1064,7 +1073,8 @@ class ThumbInput:
</br> </br>
<p>Initialize the receiver with <code>receiver = ThumbInput()</code></p> <p>Initialize the receiver with <code>receiver = ThumbInput()</code></p>
</br> </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>
<div> <div>
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python"> <pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
@ -1085,6 +1095,123 @@ while True:
</section> </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> </div>
<script> <script>
// Tab button logic // Tab button logic
@ -1112,6 +1239,102 @@ while True:
}); });
}); });
</script> </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> </body>