1590 lines
61 KiB
HTML
1590 lines
61 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Responsive Robot Lessons</title>
|
||
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||
<script>hljs.highlightAll();</script>
|
||
<link rel="stylesheet" href="styles.css" />
|
||
<style>
|
||
</style>
|
||
</head>
|
||
|
||
<body class="bg-gray-50 text-gray-900">
|
||
|
||
<!-- Tabs -->
|
||
<div class="bg-white shadow sticky top-0 z-10">
|
||
<div class="max-w-8xl mx-auto px-4 py-4 flex space-x-6 select-none">
|
||
<button class="tab-btn active text-blue-600" data-target="lesson1">Environment Setup</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson2">Uploading Code</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson3">Motors</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson4">Color Sensors</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson5">I2C Multiplexing</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson6">OLED Display</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson7">RGB LED(Neopixel)</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson8">Sonar</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600" data-target="lesson8b">Sonar Filtering</button>
|
||
<button class="tab-btn text-gray-600 hover:text-blue-600 hidden" data-target="lesson9">Motor
|
||
Encoders</button>
|
||
<button hidden 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>
|
||
|
||
<!-- Lessons container -->
|
||
<div class="max-w-7xl mx-auto px-6 py-8 space-y-8">
|
||
|
||
<!-- Lesson 1 -->
|
||
<section id="lesson1" class="lesson-content">
|
||
<h1 class="text-3xl font-bold mt=0 mb-6">Setting up our MicroPython toolchain</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Setting up the IDE</h2>
|
||
<p>There are many IDEs that we can use to program MicroPython robot, we'll be using one called
|
||
"Thonny". So let's begin by downloading it.</p>
|
||
<h3>Download</h3>
|
||
<ul class="ml-6 list-disc">
|
||
<li><a href="https://github.com/thonny/thonny/releases/download/v4.1.7/thonny-4.1.7.exe"
|
||
class="text-blue-600 hover:underline">Windows</a>
|
||
</li>
|
||
|
||
<li><a href="https://github.com/thonny/thonny/releases/download/v4.1.7/thonny-4.1.7.pkg"
|
||
class="text-blue-600 hover:underline">Mac</a></li>
|
||
</ul>
|
||
<p>You can also get it from the Thonny website, <a
|
||
href="https://thonny.org/">https://thonny.org/</a></p>
|
||
</div>
|
||
<div>
|
||
|
||
</div>
|
||
|
||
<!-- Step 2: Image -->
|
||
<div class="prose">
|
||
<h2>Step 2: Install MicroPython firmware on board</h2>
|
||
<ol class="list-decimal ml-6">
|
||
<li>In Thonny, click on the Run->Configure Interpreter buttons at the top of the window.</li>
|
||
<li>Choose "MicroPython (Raspberry Pi Pico) from the first dropdown menu.</li>
|
||
<li>Click the "<u>Install or update MicroPython</u>" button down the bottom-right of the window.
|
||
</li>
|
||
<li>You need to put your device in FLASH mode, hold down the BOOT button, press and release the
|
||
RESET button.</li>
|
||
<li>You should now find a drive called "RPI-RP2". in the Target volume dropdown, shoose it.</li>
|
||
<li>Choose the variant "Raspberry Pi Pico - Pico / Pico H"</li>
|
||
<li>Press Install button to install the firmware.</li>
|
||
<li>Reset the device again with the RESET button.</li>
|
||
<li>Press the cancel button to close the firmware update panel, then press OK to officially
|
||
begin coding.</li>
|
||
</ol>
|
||
</div>
|
||
<div>
|
||
<img src="images/thonny_firmware.png" alt="Robot moving further"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
<div>
|
||
<img src="images/rp2040zero.png" alt="Robot moving further"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Lesson 2 (hidden initially) -->
|
||
<section id="lesson2" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">The Thonny IDE</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Open/Create the main.py file</h2>
|
||
<p>When your device is plugged into your computer, you should be able to press the File->Load
|
||
button, and choose "Raspberry Pi Pico" to load from.</p>
|
||
<p>If no such file exists we need to make it, press File->New, and then save it to the Raspberry Pi
|
||
Pico as "main.py"</p>
|
||
</div>
|
||
<div>
|
||
|
||
</div>
|
||
|
||
<!-- Step 2: Image -->
|
||
<div class="prose">
|
||
<h2>Step 2: Open the Serial Monitor</h2>
|
||
<p>To get messages back from your device, and to read errors, press the view->shellbutton and make
|
||
sure
|
||
you have the shell window open at the bottom.</p>
|
||
|
||
</br>
|
||
<h2>Step 3: Hello World</h2>
|
||
<p>Your code is only updated on your device when you press "Run". If you write the code
|
||
<code>print("Hello World")</code> and then save, yuou should see the message appear in the
|
||
Serial monitor.
|
||
</p>
|
||
<p>Note that the code won't persist on the robot after a reset unless you also SAVE it to the
|
||
device, RUN just runs it once</p>
|
||
|
||
</br>
|
||
<h2>Step 4: Code all the things!</h2>
|
||
<p>From here you can start to write code, if you need a refresher on basic python code, go to the
|
||
online lessons here.</p>
|
||
<p><a href="https://realrobots.net/robocode/">https://realrobots.net/robocode/</a></p>
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<img src="images/thonny_code.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Lesson 3 (hidden initially) -->
|
||
<section id="lesson3" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Driving a Motor</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Wiring</h2>
|
||
<p>!!DO <strong>NOT</strong> CONNECT POSITIVE BATTERY DIRECTLY TO THE MICROCONTROLLER!!</p>
|
||
</br>
|
||
<p>The connections on the RP2040 all use 3.3v (3v3), which is far too little to drive the motors. It
|
||
can drive a motor DRIVER though. The motor driver lets us control the larger voltage from the
|
||
battery to the motor.</p>
|
||
</br>
|
||
<p><code>MOTOR+ (red) ------------- DRIVER OUT1</code></p>
|
||
<p><code>MOTOR- (black) ----------- DRIVER OUT2</code></p>
|
||
|
||
<p><code>RP2040 GP9 --------------- DRIVER IN2</code></p>
|
||
<p><code>RP2040 GP8 --------------- DRIVER IN1</code></p>
|
||
|
||
<p><code>BATTERY+ (red) ----------- DRIVER VCC</code></p>
|
||
<p><code>BATTERY- (black) --------- DRIVER GND --------- RP2040 GND</code></p>
|
||
</br>
|
||
<p>The driver is powered by the battery, but the microcontroller with be powered by the USB. It's
|
||
very important that the positive battery voltage is never connected directly to the RP2040 as
|
||
that will immediately release the magic smoke.</p>
|
||
<p>The GND on the RP2040, driver, and battery must all be connected though. The GND is 0 volts, and
|
||
is used as a reference to make sure 0 is the same on every device.</p>
|
||
</div>
|
||
<div>
|
||
<img src="images/motor_instructions_chalk.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Coding</h2>
|
||
<p>We control the motor driver by switching which of the OUT1 and OUT2 connections is the positive
|
||
and negative. When one is "high" and the other "low", the motor spins one way. If we switch
|
||
which is high and low the motor will spin the other way.</p>
|
||
</br>
|
||
<p>To set OUT1 to max voltage, we set IN1 to 3.3v, the same for OUT2 and IN2. We can get finer
|
||
control by using PWM controls, which turn the pins high and low quickly enough that it simulates
|
||
voltages in between 0v and 3.3v</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
from machine import Pin, PWM
|
||
import time
|
||
|
||
motorIN1 = PWM(Pin(8))
|
||
motorIN2 = PWM(Pin(9))
|
||
|
||
# DRIVE FORWARD
|
||
motorIN1.duty_u16(65535) # This is the maximum value
|
||
motorIN2.duty_u16(0) # This is the minimum value
|
||
time.sleep(1)
|
||
|
||
# DRIVE BACKWARD
|
||
motorIN1.duty_u16(0)
|
||
motorIN2.duty_u16(65535)
|
||
time.sleep(1)
|
||
|
||
# STOP
|
||
motorIN1.duty_u16(0)
|
||
motorIN2.duty_u16(0) </code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 3: Making it easier</h2>
|
||
<p>This works, but takes a lot of typing each time, and is a little hard to read. On the right we
|
||
can see a function which lets us drive the motor with a single command, and set the speed with a
|
||
number from -100 to 100.</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
from machine import Pin, PWM
|
||
|
||
# Initialize motor PWM pins
|
||
motorIN1 = PWM(Pin(8))
|
||
motorIN2 = PWM(Pin(9))
|
||
|
||
def motor(power):
|
||
# Make sure power is never greater than 100 or less than -100
|
||
if power > 100:
|
||
power = 100
|
||
elif power < -100:
|
||
power = -100
|
||
|
||
# Convert 0-100 value, to 0 to 65535
|
||
duty = abs(power) * 65535 // 100
|
||
|
||
# Apply power to the correct motor pin
|
||
if power > 0:
|
||
motorIN1.duty_u16(duty)
|
||
motorIN2.duty_u16(0)
|
||
elif power < 0:
|
||
motorIN1.duty_u16(0)
|
||
motorIN2.duty_u16(duty)
|
||
else:
|
||
motorIN1.duty_u16(0)
|
||
motorIN2.duty_u16(0)
|
||
|
||
# TESTS
|
||
motor(100) # FULL FORWARD
|
||
time.sleep(3)
|
||
motor(-100) # FULL REVERSE
|
||
time.sleep(3)
|
||
|
||
# Start at FULL REVERSE and slowly change from -100 to 100
|
||
for i in range(-100, 100):
|
||
motor(i)
|
||
print(i)
|
||
time.sleep(0.1) </code></pre>
|
||
</div>
|
||
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 4: Making it professional</h2>
|
||
<p>Finally, to make our final code modular, cleaner, and easier to understand we can offload all out
|
||
motor code into a new module.</p>
|
||
</br>
|
||
<p>We make a second file called <code>motor.py</code> which we'll import and call from our main
|
||
<code>code.py</code> file.
|
||
</p>
|
||
<p>Now all we need to do from the main code is:</p>
|
||
</br>
|
||
<p>Import the module</p>
|
||
<p><code>from motor import Motor</code></p>
|
||
</br>
|
||
<p>Initialize a motor object</p>
|
||
<p><code>left_motor = Motor(8, 9)</code></p>
|
||
</br>
|
||
<p>Set the power</p>
|
||
<p><code>left_motor.move(100)</code></p>
|
||
</br>
|
||
<p>You can also add a second, third, or 20th motor at any time just by initializing another
|
||
Motor object.</p>
|
||
<p><code>right_motor = Motor(10, 11)</code></p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm">
|
||
<code class="language-python">
|
||
# code.py
|
||
import time
|
||
import motor
|
||
|
||
left_motor = motor.Motor(9,8)
|
||
|
||
# TESTS
|
||
left_motor.move(100) # FULL FORWARD
|
||
time.sleep(2)
|
||
left_motor.move(-100) # FULL REVERSE
|
||
time.sleep(2)
|
||
left_motor.move(0)
|
||
|
||
# Start at FULL REVERSE and slowly change from -100 to 100
|
||
for i in range(-100, 100):
|
||
left_motor.move(i)
|
||
print(i)
|
||
time.sleep(0.1)
|
||
|
||
</code></pre>
|
||
</br>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm">
|
||
<code class="language-python">
|
||
# motor.py
|
||
from machine import Pin, PWM
|
||
|
||
class Motor:
|
||
def __init__(self, in1, in2, frequency=1000):
|
||
# Set up PWM outputs on the specified pins
|
||
self.in1 = PWM(Pin(in1))
|
||
self.in2 = PWM(Pin(in2))
|
||
self.in1.freq(frequency)
|
||
self.in2.freq(frequency)
|
||
# Initialize duty to 0
|
||
self.in1.duty_u16(0)
|
||
self.in2.duty_u16(0)
|
||
|
||
def move(self, power):
|
||
# Constrain power to -100 to 100
|
||
if power > 100:
|
||
power = 100
|
||
elif power < -100:
|
||
power = -100
|
||
|
||
# Scale to duty cycle (0–65535 for RP2040)
|
||
duty = abs(power) * 65535 // 100
|
||
|
||
if power > 0:
|
||
self.in1.duty_u16(duty)
|
||
self.in2.duty_u16(0)
|
||
elif power < 0:
|
||
self.in1.duty_u16(0)
|
||
self.in2.duty_u16(duty)
|
||
else:
|
||
self.in1.duty_u16(0)
|
||
self.in2.duty_u16(0)
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
<!-- Lesson 4 (hidden initially) -->
|
||
<section id="lesson4" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Reading the Colour Sensor</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Wiring</h2>
|
||
<p>The TCS3472 Colour sensor is a smart device that uses I2C communication. This is a method of
|
||
communication from one microcontroller to another, using only 2 wires (SCL and SDA).</p>
|
||
</br>
|
||
<p><code>RP2040 3v3 --------------- SENSOR 3v3</code></p>
|
||
<p><code>RP2040 GND --------------- SENSOR GND</code></p>
|
||
<p><code>RP2040 GP0 --------------- SENSOR SDA</code></p>
|
||
<p><code>RP2040 GP1 --------------- SENSOR SCL</code></p>
|
||
</br>
|
||
<p>So we need to connect power (3v3 and GND), and the two GPIO pins for communication.</p>
|
||
<p>We don't use VIN, this is for if we are using more than 3v3, the sensor has its own voltage
|
||
regulator on board.</p>
|
||
<p>The INT pin on the sensor is an INTERRUPT. It flips on and off when theres new data to be read.
|
||
</p>
|
||
<p>The LED pin on the sensor can be connected to a GPIO and switched high and low to turn it off and
|
||
on. It's on by default.</p>
|
||
</div>
|
||
<div>
|
||
<img src="images/color_sensor_instructions_chalk.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Coding</h2>
|
||
<p>The minimal code to communicate with the colour sensor is a bit more in depth than the motor.
|
||
We need to write and read registers, send bytes back and forth to turn it on, tell it what speed
|
||
to work at, and how sensitive to be. </p>
|
||
</br>
|
||
<p>Since it's quite a lot we'll begin by putting it in a separate file. The code here includes the
|
||
minimum needed to set up and read the colour sensor.</p>
|
||
</br>
|
||
<p><code>init_sensor()</code> sends 4 messages to the sensor. The first tells it to turn on, the
|
||
second tells it to turn on its RGB colour sensing.</p>
|
||
</br>
|
||
<p>The "Integration Time" controls how long the sensor will collect data before collating it into a
|
||
message for you. Since we want to read hundreds of times a second we set it low.</p>
|
||
</br>
|
||
<p>Finally we set the "gain" quite high, this amplifies the signal, which is necessary since our
|
||
fast integration time leaves the sensor with low sentivity.</p>
|
||
</br>
|
||
<p>If we only had to check the colour every half a second or so, we could get a much more sensitive
|
||
result by setting a high integration time, and low gain.</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# color.py
|
||
|
||
import time
|
||
|
||
TCS_ADDR = 0x29
|
||
COMMAND_BIT = 0x80
|
||
|
||
def i2c_locked(i2c, func, *args, **kwargs):
|
||
"""Wraps I2C operations with lock acquisition and release."""
|
||
while not i2c.try_lock():
|
||
pass
|
||
try:
|
||
return func(*args, **kwargs)
|
||
finally:
|
||
i2c.unlock()
|
||
|
||
def write_register(i2c, addr, reg, value):
|
||
"""Writes a byte to a register."""
|
||
i2c_locked(i2c, i2c.writeto, addr, bytes([COMMAND_BIT | reg, value]))
|
||
|
||
def read_register(i2c, addr, reg, length):
|
||
"""Reads multiple bytes from a register."""
|
||
result = bytearray(length)
|
||
|
||
def transfer():
|
||
i2c.writeto(addr, bytes([COMMAND_BIT | reg]))
|
||
i2c.readfrom_into(addr, result)
|
||
return result
|
||
|
||
return i2c_locked(i2c, transfer)
|
||
|
||
def init_sensor(i2c):
|
||
time.sleep(0.01)
|
||
write_register(i2c, TCS_ADDR, 0x00, 0x01) # Enable (POWER ON)
|
||
time.sleep(0.01)
|
||
write_register(i2c, TCS_ADDR, 0x00, 0x03) # Enable (COLOR SENSING ON)
|
||
write_register(i2c, TCS_ADDR, 0x01, 0xFF) # Integration time 0xFF=2.4ms 0xF0=38.4ms 0x00=614.4ms
|
||
write_register(i2c, TCS_ADDR, 0x0F, 0x03) # Gain 0x00=1x, 0x01=4x 0.02=16x, 0x03=60x
|
||
|
||
def read_rgbc(i2c):
|
||
data = read_register(i2c, TCS_ADDR, 0x14, 8)
|
||
c = data[1] << 8 | data[0]
|
||
r = data[3] << 8 | data[2]
|
||
g = data[5] << 8 | data[4]
|
||
b = data[7] << 8 | data[6]
|
||
return r, g, b, c
|
||
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 3: code.py</h2>
|
||
<p>Finally, this is how we call our color.py code from our main code, initialise and read the
|
||
sensor.</p>
|
||
</br>
|
||
<p>We use the <code>busio</code> library to handle our i2c. We initialise the connection by telling
|
||
it which pins we're using for SCL and SDA, and how fast to communicate.</p>
|
||
</br>
|
||
<p>We then pass on that connection to initialize the sensor using
|
||
<code>color.init_sensor(i2c)</code>
|
||
</p>
|
||
</br>
|
||
<p>The whenever we read <code>color.read_rgbc(i2c)</code> it will give us an array with the
|
||
<code>[red, green, blue, color]</code>, with the color being the total light reflected, rather
|
||
than just a single color.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# code.py
|
||
|
||
import busio
|
||
import time
|
||
import board
|
||
import color
|
||
|
||
i2c = busio.I2C(scl=board.GP1, sda=board.GP0, frequency=1_000_000)
|
||
color.init_sensor(i2c)
|
||
|
||
while True:
|
||
value = color.read_rgbc(i2c)
|
||
print(value)
|
||
time.sleep(0.1) </code></pre>
|
||
</div>
|
||
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 4: How to use it?</h2>
|
||
<p>To take these numbers and use them effectively, we need to break out the different colour
|
||
channels.</p>
|
||
</br>
|
||
<p>To the right is an example of how we might take the total colour from the result, and then use it
|
||
to decide whether to turn or not.</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm">
|
||
<code class="language-python">
|
||
value = color.read_rgbc(i2c) # Read the sensor [r,g,b,c]
|
||
# Extract each of the color values from the array
|
||
r = value[0] # red
|
||
g = value[1] # green
|
||
b = value[2] # blue
|
||
c = value[3] # all colors
|
||
|
||
if c > 30: # If the c value is greater than 30
|
||
# Turn Right
|
||
left_motor.move(50)
|
||
left_motor.move(-50)
|
||
else:
|
||
# Go Straight
|
||
left_motor.move(50)
|
||
left_motor.move(50)
|
||
</code></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Lesson 5 (hidden initially) -->
|
||
<section id="lesson5" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Reading MORE Colour Sensors</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Wiring</h2>
|
||
<p>Although you can connect many devices to the SCL SDA pins, each device must have a different
|
||
address. This is a problem because every colour sensor has the same address.</p>
|
||
|
||
</br>
|
||
<p>If we just connected all our devices to the same SCA and SDA lines, they would both try to
|
||
respond at the same time to every message addressed to them</p>
|
||
</br>
|
||
<p>So we use a MULTIPLEXER. This lets us have 8 different I2C channels, with only one active at
|
||
once. So with the connections in the picture, we'd switch to channel 5 to talk to the left
|
||
sensor, and channel 7 to talk to the right.</p>
|
||
</br>
|
||
<p><code>RP2040 3v3 -- MUXER 3v3 -- SENSOR 1 3v3 -- SENSOR 2 3v3</code></p>
|
||
<p><code>RP2040 GND -- MUXER GND -- SENSOR 1 GND -- SENSOR 2 GND</code></p>
|
||
<p><code>RP2040 GP0 -- MUXER SDA</code></p>
|
||
<p><code>RP2040 GP1 -- MUXER SCL</code></p>
|
||
<p><code>MUXER SD5 --- SENSOR 1 SDA</code></p>
|
||
<p><code>MUXER SC5 --- SENSOR 1 SCL</code></p>
|
||
<p><code>MUXER SD7 --- SENSOR 2 SDA</code></p>
|
||
<p><code>MUXER SC7 --- SENSOR 2 SCL</code></p>
|
||
</br>
|
||
<p>Using this method, we can connect up to 8 colour sensors.</p>
|
||
</div>
|
||
<div>
|
||
<img src="images/muxer_instructions_chalk.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Coding</h2>
|
||
<p>Create a new module and paste the code on the right in there.</p>
|
||
</br>
|
||
<p>It's similar to the colour sensor code, we pass the <code>select_channel()</code> function the
|
||
i2c, and a number for which channel (0-7) we want to talk through.</p>
|
||
</br>
|
||
<p>It sends a single message over the I2C bus, to the multiplexers address, telling it which channel
|
||
to open.</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# muxer.py
|
||
|
||
TCA_ADDR = 0x70
|
||
|
||
def i2c_locked(i2c, func, *args, **kwargs):
|
||
"""Wraps I2C operations with lock acquisition and release."""
|
||
while not i2c.try_lock():
|
||
pass
|
||
try:
|
||
return func(*args, **kwargs)
|
||
finally:
|
||
i2c.unlock()
|
||
|
||
def select_channel(i2c, channel):
|
||
"""Selects the TCA9548A multiplexer channel."""
|
||
if not 0 <= channel <= 7:
|
||
raise ValueError("Channel must be 0-7")
|
||
i2c_locked(i2c, i2c.writeto, TCA_ADDR, bytes([1 << channel]))
|
||
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 3: code.py</h2>
|
||
<p>So in our main code we just import the muxer module, and make sure we switch to the correct
|
||
channel before sending any messages.</p>
|
||
</br>
|
||
<p>We only need to keep one colour sensor object, but we do need to make sure we send the
|
||
initialization message on each channel.</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# code.py
|
||
|
||
import busio
|
||
import time
|
||
import board
|
||
import color
|
||
import muxer
|
||
|
||
i2c = busio.I2C(scl=board.GP1, sda=board.GP0, frequency=1_000_000)
|
||
|
||
# select channel 5
|
||
muxer.select_channel(i2c, 5)
|
||
# init sensor on channel 5
|
||
color.init_sensor(i2c)
|
||
|
||
# select channel 7
|
||
muxer.select_channel(i2c, 7)
|
||
#init sensor on channel 7
|
||
color.init_sensor(i2c)
|
||
|
||
while True:
|
||
# select channel 5
|
||
muxer.select_channel(i2c, 5)
|
||
# read the values on channel 5
|
||
value5 = color.read_rgbc(i2c)
|
||
|
||
# select channel 7
|
||
muxer.select_channel(i2c, 7)
|
||
# read the values on channel 7
|
||
value7 = color.read_rgbc(i2c)
|
||
|
||
# print both sets of readings
|
||
print(value5, value7)
|
||
|
||
time.sleep(0.1)
|
||
</code></pre>
|
||
</div>
|
||
|
||
|
||
|
||
</section>
|
||
|
||
<!-- Lesson 6 (hidden initially) -->
|
||
<section id="lesson6" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Using the OLED Display</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Wiring</h2>
|
||
<p>The OLED display also communicates using I2C, so all we need to do is connect the power (3v3 and
|
||
GND), and the I2C lines (SDA and SCL).</p>
|
||
</br>
|
||
<p><code>RP2040 3v3 ------- OLED 3v3</code></p>
|
||
<p><code>RP2040 GND ------- OLED GND</code></p>
|
||
<p><code>RP2040 GP0 ------- OLED SDA</code></p>
|
||
<p><code>RP2040 GP1 ------- OLED SCL</code></p>
|
||
</br>
|
||
</div>
|
||
<div>
|
||
<img src="images/oled_instructions_chalk.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Coding</h2>
|
||
<p>The code for the OLED is a little more complicated that what we want to code for ourselves, so
|
||
we'll download a library and place it in out CIRCUITPY/lib folder.</p>
|
||
</br>
|
||
<p><a href="files/adafruit_ssd1306.mpy">adafruit_ssd1306.mpy</a></p>
|
||
<p><a href="files/adafruit_framebuf.py">adafruit_framebuf.py</a></p>
|
||
</br>
|
||
|
||
|
||
<p>We initialize the display with <code>display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)</code>.
|
||
</p>
|
||
</br>
|
||
<p>We colour the entire screen either black (off) or blue/white (on) with
|
||
<code>display.fill(0)</code> for off, or 1 for on.
|
||
</p>
|
||
</br>
|
||
<p>We write text with <code>display.text(text, x, y, color)</code>.</p>
|
||
</br>
|
||
<p>Finally, before anythin will actually appear on the screen, we send it using
|
||
<code>display.show()</code>.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
import board
|
||
import time
|
||
import busio
|
||
import adafruit_ssd1306
|
||
|
||
i2c = busio.I2C(scl=board.GP1, sda=board.GP0, frequency=1_000_000)
|
||
display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)
|
||
|
||
|
||
display.fill(0) # Fill the screen with BLACK
|
||
# Print Hello world at the top left in COLOR
|
||
display.text("Hello World", 0, 0, 1)
|
||
display.show() # Send the update to the screen
|
||
time.sleep(2)
|
||
|
||
|
||
display.fill(1)# Fille the screen with COLOR
|
||
display.show() # Send the update to the screen
|
||
time.sleep(2)
|
||
|
||
# Print Hello world at x:10, y:20 in BLACK
|
||
display.text("Hello World", 10, 20, 0)
|
||
display.show() # Send the update to the screen
|
||
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 3: How to use it?</h2>
|
||
<p>One of the main things we'll use the screen for is getting feedback on what the sensors are
|
||
seeing, or what the robot is doing.</p>
|
||
</br>
|
||
<p>So when the robot is following a line, we might do something like the code on the right so that
|
||
we can see the sensor colours in real time.</p>
|
||
</br>
|
||
<div class="flex justify-center">
|
||
<img src="images/oled_example.jpg" alt="Robot turning right" class="rounded shadow w-64" />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
import board
|
||
import time
|
||
import busio
|
||
import adafruit_ssd1306
|
||
import color
|
||
import muxer
|
||
|
||
i2c = busio.I2C(scl=board.GP1, sda=board.GP0, frequency=1_000_000)
|
||
display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)
|
||
|
||
# initialize two colour sensors
|
||
muxer.select_channel(i2c, 0)
|
||
color.init_sensor(i2c)
|
||
muxer.select_channel(i2c, 1)
|
||
color.init_sensor(i2c)
|
||
|
||
|
||
while True:
|
||
# Read the colour channel from both colour sensors
|
||
muxer.select_channel(i2c, 0)
|
||
value0 = color.read_rgbc(i2c)[3]
|
||
muxer.select_channel(i2c, 1)
|
||
value1 = color.read_rgbc(i2c)[3]
|
||
|
||
# Print the results on the display
|
||
display.fill(0)
|
||
display.text("L: " + str(value0), 0, 0, 1)
|
||
display.text("R: " + str(value1), 0, 10, 1)
|
||
display.show()
|
||
time.sleep(0.1)
|
||
</code></pre>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<!-- Lesson 6 (hidden initially) -->
|
||
<section id="lesson7" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">RGB LED (Neopixel)</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Wiring</h2>
|
||
<p>The RGB LED has its own little microcontroller we can talk to and set each of the three LEDs
|
||
inside. We only need power and a single data connection.</p>
|
||
</br>
|
||
<p><code>RP2040 3v3 ------- RGB 5v</code></p>
|
||
<p><code>RP2040 GND ------- RGB GND</code></p>
|
||
<p><code>RP2040 GP29 ------ RGB DIN</code></p>
|
||
</br>
|
||
<p>Even though the RGB LED says it wants 5v, we give it 3v3. It's designed for 5v, but if we give it
|
||
5v power then the data sent to it using a 3v3 signal from GP29 won't be recognized.</p>
|
||
</br>
|
||
<p>Many of these leds can be connected in a string, with each one passing the messages along to the
|
||
next. We'll just be using one in the examples here.</p>
|
||
</div>
|
||
<div>
|
||
<img src="images/neopixel_instructions_chalk.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Coding</h2>
|
||
<p>Once again we'll be using a library to handle all the bits and bytes under the hood, so we only
|
||
have to worry about setting colours.</p>
|
||
</br>
|
||
<p><a href="files/neopixel.py">neopixel.py</a></p>
|
||
</br>
|
||
<p>We only need to initialize the pixel with
|
||
<code>pixel = neopixel.NeoPixel(board.GP29, 1, brightness=0.2)</code>.
|
||
</p>
|
||
<p>The "1" is the number of pixels we have, and we CAN set the brightness up to 1.0 but it's quite
|
||
bright.</p>
|
||
</br>
|
||
<p>Then we just need to tell the pixel what colour to be, we talk to
|
||
<code>pixel[0] = (red,green,blue)</code> because
|
||
it's the only one we have. The numbers for each colour go from minimum 0, to maximum 255.
|
||
</p>
|
||
|
||
</br>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
import board
|
||
import time
|
||
import neopixel
|
||
|
||
pixel = neopixel.NeoPixel(board.GP29, 1, brightness=0.2)
|
||
|
||
while True:
|
||
pixel[0] = (255, 0, 0) # Red
|
||
time.sleep(1)
|
||
pixel[0] = (0, 255, 0) # Green
|
||
time.sleep(1)
|
||
pixel[0] = (0, 0, 255) # Blue
|
||
time.sleep(1)
|
||
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 3: Make some pretty effects</h2>
|
||
<p>This example has three loops, each one fades from one primary colour to the next.</p>
|
||
</br>
|
||
<p>We increase one LEDs brightness by assigning it <code>i</code>, which counts from 0-255</p>
|
||
</br>
|
||
<p>We decrease another LEDs brightness by making it <code>255-i</code> so it begins at max, and
|
||
counts
|
||
down to zero as i becomes higher.</p>
|
||
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
import board
|
||
import time
|
||
import neopixel
|
||
|
||
pixel = neopixel.NeoPixel(board.GP29, 1, brightness=0.2)
|
||
|
||
while True:
|
||
# Fade Red->Green
|
||
for i in range(256):
|
||
pixel[0] = (255-i, i, 0)
|
||
|
||
# Fade Green->Blue
|
||
for i in range(256):
|
||
pixel[0] = (0, 255-i, i)
|
||
|
||
# Fade Blue->Red
|
||
for i in range(256):
|
||
pixel[0] = (i, 0, 255-i)
|
||
</code></pre>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
|
||
|
||
<!-- Lesson 6 (hidden initially) -->
|
||
<section id="lesson8" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Sonar HC-SR04</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Wiring</h2>
|
||
<p>The HC-SR04 sonar sends out a little pulse when we turn the TRIG pin on and off. When the sound
|
||
bounces back from something the ECHO pin will turn on and off. We use the time between the TRIG
|
||
and the ECHO to determine how far the sound got before bouncing back from an object.</p>
|
||
</br>
|
||
<p><code>RP2040 3v3 ------- SONAR 5v</code></p>
|
||
<p><code>RP2040 GND ------- SONAR GND</code></p>
|
||
<p><code>RP2040 GP26 ------ SONAR TRIG</code></p>
|
||
<p><code>RP2040 GP28 ------ SONAR ECHO</code></p>
|
||
</br>
|
||
|
||
|
||
</div>
|
||
<div>
|
||
<img src="images/sonar_instructions_chalk.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Coding</h2>
|
||
<p>We'll use a library to handle sending and listening for the pulses. Put this module on your
|
||
device.</p>
|
||
</br>
|
||
<p>When the "distance()" function is called, it sends a short pulse on the trigger pin, then waits
|
||
for a response on the echo pin. The time it takes for the echo to return is used to calculate
|
||
the distance.</p>
|
||
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# sonar.py
|
||
import machine
|
||
import time
|
||
|
||
class Sonar:
|
||
def __init__(self, trigger_pin, echo_pin):
|
||
self.trigger = machine.Pin(trigger_pin, machine.Pin.OUT)
|
||
self.echo = machine.Pin(echo_pin, machine.Pin.IN)
|
||
|
||
def distance(self):
|
||
# Send a 10µs pulse to trigger
|
||
self.trigger.off()
|
||
time.sleep_us(2)
|
||
self.trigger.on()
|
||
time.sleep_us(10)
|
||
self.trigger.off()
|
||
|
||
# Wait for echo start
|
||
while self.echo.value() == 0:
|
||
pass
|
||
start = time.ticks_us()
|
||
|
||
# Wait for echo end
|
||
while self.echo.value() == 1:
|
||
pass
|
||
end = time.ticks_us()
|
||
|
||
# Calculate duration
|
||
duration = time.ticks_diff(end, start)
|
||
|
||
# Convert to distance (speed of sound ~343 m/s)
|
||
distance = (duration / 2) * 0.0343
|
||
return int(distance)
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 3: Call it in the main.py</h2>
|
||
<p>Now all we need to do is create the object and call the distance function in the main.py file.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# main.py
|
||
import time
|
||
import sonar
|
||
|
||
# Create sonar object with trigger on GP3 and echo on GP2
|
||
sonar = sonar.Sonar(trigger_pin=26, echo_pin=28)
|
||
|
||
while True:
|
||
dist = sonar.distance()
|
||
print("Distance:", dist, "cm")
|
||
time.sleep(1)
|
||
</code></pre>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
|
||
<!-- Lesson 6 (hidden initially) -->
|
||
<section id="lesson10" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Receiver</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Coding</h2>
|
||
<p>Create a new file called <code>remote.py</code> and add the code to the right.</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# remote.py
|
||
|
||
import board
|
||
import busio
|
||
|
||
START_BYTE = 0xAA
|
||
END_BYTE = 0xBB
|
||
PACKET_LENGTH = 8
|
||
|
||
# Calibration values
|
||
LEFT_MIN = 0
|
||
LEFT_MID = 131
|
||
LEFT_MAX = 203
|
||
RIGHT_MIN = 51
|
||
RIGHT_MID = 120
|
||
RIGHT_MAX = 255
|
||
|
||
class ThumbInput:
|
||
def __init__(self, tx=board.GP0, rx=board.GP1, baudrate=115200):
|
||
self.uart = busio.UART(tx=tx, rx=rx, baudrate=baudrate, timeout=0.1)
|
||
self.buffer = bytearray()
|
||
|
||
def read(self):
|
||
"""Returns (left_percent, right_percent) in range [-100, 100], or None if packet incomplete."""
|
||
data = self.uart.read(1)
|
||
|
||
if data:
|
||
byte = data[0]
|
||
self.buffer.append(byte)
|
||
|
||
if len(self.buffer) > PACKET_LENGTH:
|
||
self.buffer = self.buffer[-PACKET_LENGTH:]
|
||
|
||
if len(self.buffer) == PACKET_LENGTH and self.buffer[0] == START_BYTE and self.buffer[-1] == END_BYTE:
|
||
payload = self.buffer[1:7]
|
||
self.buffer = bytearray()
|
||
left_raw = payload[1]
|
||
right_raw = payload[3]
|
||
|
||
left = self._map_thumbstick(left_raw, LEFT_MIN, LEFT_MID, LEFT_MAX)
|
||
right = self._map_thumbstick(right_raw, RIGHT_MIN, RIGHT_MID, RIGHT_MAX)
|
||
|
||
return (int(left), int(right))
|
||
|
||
return None
|
||
|
||
def _map_thumbstick(self, x, min_val, mid_val, max_val):
|
||
if x < mid_val:
|
||
return (x - mid_val) / (mid_val - min_val) * 100
|
||
else:
|
||
return (x - mid_val) / (max_val - mid_val) * 100
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Using the recieved signal</h2>
|
||
<p>Import just the <code>ThumbInput</code> part of the library.</p>
|
||
</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>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
|
||
from remote import ThumbInput
|
||
|
||
receiver = ThumbInput()
|
||
|
||
while True:
|
||
result = receiver.read()
|
||
if result:
|
||
left_motor.move(result[0])
|
||
right_motor.move(result[1])
|
||
#print("Left:", result[0], "Right:", result[1])
|
||
time.sleep(0.001)
|
||
</code></pre>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Lesson 6 (hidden initially) -->
|
||
<section id="lesson8b" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Sonar Filtering</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
<!-- Step 1 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Understand the problem</h2>
|
||
<p>Like most inputs, the sonar data is noisy, and there are different types of noise.</p>
|
||
<p>On the right you can see a fairly fixed output, but with sudden spikes of irregular data.</p>
|
||
</br>
|
||
<p>So we need code that will filter our spikes like that, the easiest is to just throw them away. To
|
||
do this we'll use a simple moving average filter.</p>
|
||
|
||
|
||
|
||
</div>
|
||
<div>
|
||
<img src="images/sonar_noise.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Coding a moving average filter</h2>
|
||
<p>Instead of just taking single data points and making decisions, we'll keep an array of the last 8
|
||
readings.</p>
|
||
</br>
|
||
<p>First we need to start keeping a rolling buffer of the last 8 readings.</p>
|
||
<p>We'll use a list to store the readings, and we'll use the <code>append()</code> method to add new
|
||
readings to the end of the list.</p>
|
||
<p>We'll use the <code>pop(0)</code> method to remove the oldest reading from the start of the list.
|
||
</p>
|
||
<p>We'll use the <code>len()</code> method to check the size of the list.</p>
|
||
<p>We'll use the <code>print()</code> method to print the list.</p>
|
||
</br>
|
||
<p><b>NOTE:</b> We can get smoother results by using a larger buffer size, but this can also start
|
||
to introduce lag.</p>
|
||
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
|
||
|
||
BUFFER_SIZE = 8 # The size of the buffer
|
||
buffer = [] # Create an empty array to hold
|
||
# the readings (this is the rolling buffer)
|
||
|
||
while True:
|
||
dist = sonar.distance() # Get a new reading
|
||
buffer.append(dist) # Add it to the buffer
|
||
|
||
# If the buffer is too big, remove the oldest reading
|
||
if len(buffer) > BUFFER_SIZE:
|
||
buffer.pop(0)
|
||
|
||
print(buffer) # Print the buffer
|
||
|
||
|
||
</code></pre>
|
||
</div>
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 3: Take the average</h2>
|
||
<p>Now all we need to do is create the object and call the distance function in the main.py file.
|
||
</p>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
def average_filter(values):
|
||
total = 0
|
||
for i in range(len(values)):
|
||
total = total + values[i]
|
||
|
||
return total / len(values)
|
||
|
||
|
||
# Call in the main loop
|
||
filtered = average_filter(values)
|
||
print(filtered)
|
||
</code>
|
||
</pre>
|
||
</div>
|
||
|
||
<div>
|
||
<img src="images/sonar_noise_averaged.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<div class="prose">
|
||
<h2>Step 4: Take the median</h2>
|
||
<p>That's better, but those large peaks are still throwing our average off quite a bit. We can improve this by taking the MEDIAN of the buffer rather than the average.
|
||
</p>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
def median_filter(values):
|
||
sorted_vals = sorted(values) # sort the values from low->high
|
||
mid = int(len(sorted_vals) / 2) # get the middle index
|
||
return sorted_vals[mid] # return the value of the median
|
||
|
||
|
||
# Call in the main loop
|
||
filtered = median_filter(values)
|
||
print(filtered)
|
||
</code>
|
||
</pre>
|
||
</div>
|
||
|
||
<div>
|
||
<img src="images/sonar_noise_median.png" alt="Robot turning right"
|
||
class="rounded shadow w-full max-w-xs md:max-w-full" />
|
||
</div>
|
||
|
||
<div class="prose">
|
||
<h2>Step 5: Fine tuning</h2>
|
||
<p>Before moving on, play with the buffer size to see how it effects the results AND the responsiveness to changes.</p>
|
||
<p>Try and find a good balance with the time.sleep() value as well.</p>
|
||
|
||
</div>
|
||
|
||
|
||
</section>
|
||
|
||
|
||
<!-- Lesson 6 (hidden initially) -->
|
||
<section id="lesson10" class="lesson-content hidden">
|
||
<h1 class="text-3xl font-bold mb-6">Receiver</h1>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
||
|
||
|
||
<!-- Step 2 -->
|
||
<div class="prose">
|
||
<h2>Step 1: Coding</h2>
|
||
<p>Create a new file called <code>remote.py</code> and add the code to the right.</p>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
# remote.py
|
||
|
||
import board
|
||
import busio
|
||
|
||
START_BYTE = 0xAA
|
||
END_BYTE = 0xBB
|
||
PACKET_LENGTH = 8
|
||
|
||
# Calibration values
|
||
LEFT_MIN = 0
|
||
LEFT_MID = 131
|
||
LEFT_MAX = 203
|
||
RIGHT_MIN = 51
|
||
RIGHT_MID = 120
|
||
RIGHT_MAX = 255
|
||
|
||
class ThumbInput:
|
||
def __init__(self, tx=board.GP0, rx=board.GP1, baudrate=115200):
|
||
self.uart = busio.UART(tx=tx, rx=rx, baudrate=baudrate, timeout=0.1)
|
||
self.buffer = bytearray()
|
||
|
||
def read(self):
|
||
"""Returns (left_percent, right_percent) in range [-100, 100], or None if packet incomplete."""
|
||
data = self.uart.read(1)
|
||
|
||
if data:
|
||
byte = data[0]
|
||
self.buffer.append(byte)
|
||
|
||
if len(self.buffer) > PACKET_LENGTH:
|
||
self.buffer = self.buffer[-PACKET_LENGTH:]
|
||
|
||
if len(self.buffer) == PACKET_LENGTH and self.buffer[0] == START_BYTE and self.buffer[-1] == END_BYTE:
|
||
payload = self.buffer[1:7]
|
||
self.buffer = bytearray()
|
||
left_raw = payload[1]
|
||
right_raw = payload[3]
|
||
|
||
left = self._map_thumbstick(left_raw, LEFT_MIN, LEFT_MID, LEFT_MAX)
|
||
right = self._map_thumbstick(right_raw, RIGHT_MIN, RIGHT_MID, RIGHT_MAX)
|
||
|
||
return (int(left), int(right))
|
||
|
||
return None
|
||
|
||
def _map_thumbstick(self, x, min_val, mid_val, max_val):
|
||
if x < mid_val:
|
||
return (x - mid_val) / (mid_val - min_val) * 100
|
||
else:
|
||
return (x - mid_val) / (max_val - mid_val) * 100
|
||
|
||
</code></pre>
|
||
</div>
|
||
|
||
<!-- Step 3 -->
|
||
<div class="prose">
|
||
<h2>Step 2: Using the recieved signal</h2>
|
||
<p>Import just the <code>ThumbInput</code> part of the library.</p>
|
||
</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>
|
||
</div>
|
||
<div>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm"><code class="language-python">
|
||
|
||
from remote import ThumbInput
|
||
|
||
receiver = ThumbInput()
|
||
|
||
while True:
|
||
result = receiver.read()
|
||
if result:
|
||
left_motor.move(result[0])
|
||
right_motor.move(result[1])
|
||
#print("Left:", result[0], "Right:", result[1])
|
||
time.sleep(0.001)
|
||
</code></pre>
|
||
</div>
|
||
|
||
</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>
|
||
<div style="max-width: 500px; margin: 0 auto; font-family: sans-serif;">
|
||
<h3>Bang-Bang Controller Parameters</h3>
|
||
<label>Threshold: <input type="range" id="bbThresholdSlider" min="0" max="1024" step="0.001"
|
||
value="500"> <span id="bbThresholdValue">500</span></label><br>
|
||
<label>Turn Speed: <input type="range" id="bbTurnSpeedSlider" min="0" max="100" step="1"
|
||
value="50">
|
||
<span id="bbTurnSpeedValue">50</span></label><br>
|
||
<p><strong>left_color:</strong> <span id="leftColor">0</span></p>
|
||
<strong>right_color:</strong> <span id="rightColor">0</span>
|
||
<pre class="bg-gray-100 p-4 rounded shadow text-sm">
|
||
<code id="code-block" class="language-python"></code>
|
||
</pre>
|
||
</br> <button id="resetBangBangBtn"
|
||
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>
|
||
</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>
|
||
</br>
|
||
<p>Try adjusting the <code>Kp</code> value to control how aggressively the robot will attempt to
|
||
drive towards the line.</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.074"> <span
|
||
id="kpVal">0.074</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
|
||
const tabs = document.querySelectorAll('.tab-btn');
|
||
const lessons = document.querySelectorAll('.lesson-content');
|
||
|
||
tabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
// Remove active class from all tabs
|
||
tabs.forEach(t => {
|
||
t.classList.remove('active', 'text-blue-600');
|
||
t.classList.add('text-gray-600');
|
||
});
|
||
|
||
// Hide all lessons
|
||
lessons.forEach(lesson => lesson.classList.add('hidden'));
|
||
|
||
// Show the selected lesson
|
||
const targetId = tab.getAttribute('data-target');
|
||
document.getElementById(targetId).classList.remove('hidden');
|
||
|
||
// Activate the clicked tab
|
||
tab.classList.add('active', 'text-blue-600');
|
||
tab.classList.remove('text-gray-600');
|
||
});
|
||
});
|
||
</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>
|
||
|
||
</html> |