diff --git a/.vite/deps/_metadata.json b/.vite/deps/_metadata.json new file mode 100644 index 0000000..cf72b45 --- /dev/null +++ b/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "6f652a7d", + "configHash": "aba780af", + "lockfileHash": "e3b0c442", + "browserHash": "e137aec8", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/.vite/deps/package.json b/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/files/ssd1306.py b/files/ssd1306.py new file mode 100644 index 0000000..6264290 --- /dev/null +++ b/files/ssd1306.py @@ -0,0 +1,164 @@ +# MicroPython SSD1306 OLED driver, I2C and SPI interfaces + +from micropython import const +import framebuf + + +# register definitions +SET_CONTRAST = const(0x81) +SET_ENTIRE_ON = const(0xA4) +SET_NORM_INV = const(0xA6) +SET_DISP = const(0xAE) +SET_MEM_ADDR = const(0x20) +SET_COL_ADDR = const(0x21) +SET_PAGE_ADDR = const(0x22) +SET_DISP_START_LINE = const(0x40) +SET_SEG_REMAP = const(0xA0) +SET_MUX_RATIO = const(0xA8) +SET_IREF_SELECT = const(0xAD) +SET_COM_OUT_DIR = const(0xC0) +SET_DISP_OFFSET = const(0xD3) +SET_COM_PIN_CFG = const(0xDA) +SET_DISP_CLK_DIV = const(0xD5) +SET_PRECHARGE = const(0xD9) +SET_VCOM_DESEL = const(0xDB) +SET_CHARGE_PUMP = const(0x8D) + + +# Subclassing FrameBuffer provides support for graphics primitives +# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html +class SSD1306(framebuf.FrameBuffer): + def __init__(self, width, height, external_vcc): + self.width = width + self.height = height + self.external_vcc = external_vcc + self.pages = self.height // 8 + self.buffer = bytearray(self.pages * self.width) + super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) + self.init_display() + + def init_display(self): + for cmd in ( + SET_DISP, # display off + # address setting + SET_MEM_ADDR, + 0x00, # horizontal + # resolution and layout + SET_DISP_START_LINE, # start at line 0 + SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 + SET_MUX_RATIO, + self.height - 1, + SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 + SET_DISP_OFFSET, + 0x00, + SET_COM_PIN_CFG, + 0x02 if self.width > 2 * self.height else 0x12, + # timing and driving scheme + SET_DISP_CLK_DIV, + 0x80, + SET_PRECHARGE, + 0x22 if self.external_vcc else 0xF1, + SET_VCOM_DESEL, + 0x30, # 0.83*Vcc + # display + SET_CONTRAST, + 0xFF, # maximum + SET_ENTIRE_ON, # output follows RAM contents + SET_NORM_INV, # not inverted + SET_IREF_SELECT, + 0x30, # enable internal IREF during display on + # charge pump + SET_CHARGE_PUMP, + 0x10 if self.external_vcc else 0x14, + SET_DISP | 0x01, # display on + ): # on + self.write_cmd(cmd) + self.fill(0) + self.show() + + def poweroff(self): + self.write_cmd(SET_DISP) + + def poweron(self): + self.write_cmd(SET_DISP | 0x01) + + def contrast(self, contrast): + self.write_cmd(SET_CONTRAST) + self.write_cmd(contrast) + + def invert(self, invert): + self.write_cmd(SET_NORM_INV | (invert & 1)) + + def rotate(self, rotate): + self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3)) + self.write_cmd(SET_SEG_REMAP | (rotate & 1)) + + def show(self): + x0 = 0 + x1 = self.width - 1 + if self.width != 128: + # narrow displays use centred columns + col_offset = (128 - self.width) // 2 + x0 += col_offset + x1 += col_offset + self.write_cmd(SET_COL_ADDR) + self.write_cmd(x0) + self.write_cmd(x1) + self.write_cmd(SET_PAGE_ADDR) + self.write_cmd(0) + self.write_cmd(self.pages - 1) + self.write_data(self.buffer) + + +class SSD1306_I2C(SSD1306): + def __init__(self, i2c, addr=0x3C, external_vcc=False): + self.i2c = i2c + self.addr = addr + self.temp = bytearray(2) + self.write_list = [b"\x40", None] # Co=0, D/C#=1 + super().__init__(128, 64, external_vcc) + + def write_cmd(self, cmd): + self.temp[0] = 0x80 # Co=1, D/C#=0 + self.temp[1] = cmd + self.i2c.writeto(self.addr, self.temp) + + def write_data(self, buf): + self.write_list[1] = buf + self.i2c.writevto(self.addr, self.write_list) + + +class SSD1306_SPI(SSD1306): + def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): + self.rate = 10 * 1024 * 1024 + dc.init(dc.OUT, value=0) + res.init(res.OUT, value=0) + cs.init(cs.OUT, value=1) + self.spi = spi + self.dc = dc + self.res = res + self.cs = cs + import time + + self.res(1) + time.sleep_ms(1) + self.res(0) + time.sleep_ms(10) + self.res(1) + super().__init__(width, height, external_vcc) + + def write_cmd(self, cmd): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs(1) + self.dc(0) + self.cs(0) + self.spi.write(bytearray([cmd])) + self.cs(1) + + def write_data(self, buf): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs(1) + self.dc(1) + self.cs(0) + self.spi.write(buf) + self.cs(1) diff --git a/index.html b/index.html index 27059e2..318d46d 100644 --- a/index.html +++ b/index.html @@ -24,7 +24,7 @@ - + @@ -226,6 +226,7 @@ motorIN2.duty_u16(0)
from machine import Pin, PWM
+import time
# Initialize motor PWM pins
motorIN1 = PWM(Pin(8))
@@ -235,7 +236,7 @@ def motor(power):
# Make sure power is never greater than 100 or less than -100
if power > 100:
power = 100
- elif power < -100:
+ elif power < -100:
power = -100
# Convert 0-100 value, to 0 to 65535
@@ -245,7 +246,7 @@ def motor(power):
if power > 0:
motorIN1.duty_u16(duty)
motorIN2.duty_u16(0)
- elif power < 0:
+ elif power < 0:
motorIN1.duty_u16(0)
motorIN2.duty_u16(duty)
else:
@@ -273,7 +274,7 @@ for i in range(-100, 100):
motor code into a new module.
We make a second file called motor.py which we'll import and call from our main
- code.py file.
+ main.py file.
Now all we need to do from the main code is:
@@ -293,7 +294,7 @@ for i in range(-100, 100):
-# code.py
+# main.py
import time
import motor
@@ -312,6 +313,8 @@ for i in range(-100, 100):
print(i)
time.sleep(0.1)
+
+
@@ -334,7 +337,7 @@ class Motor:
# Constrain power to -100 to 100
if power > 100:
power = 100
- elif power < -100:
+ elif power < -100:
power = -100
# Scale to duty cycle (0–65535 for RP2040)
@@ -343,7 +346,7 @@ class Motor:
if power > 0:
self.in1.duty_u16(duty)
self.in2.duty_u16(0)
- elif power < 0:
+ elif power < 0:
self.in1.duty_u16(0)
self.in2.duty_u16(duty)
else:
@@ -410,51 +413,41 @@ class Motor:
# color.py
+import struct
-import time
+class Color:
+ def __init__(self, bus, address=0x29):
+ self._bus = bus
+ self._i2c_address = address
+ self._bus.writeto(self._i2c_address, b'\x80\x03')
+ self._bus.writeto(self._i2c_address, b'\x81\x2b')
+ self._bus.writeto(self._i2c_address, b'\x81\xFF') # Integration time 0xFF=2.4ms 0xF0=38.4ms 0x00=614.4ms
+ self._bus.writeto(self._i2c_address, b'\x8F\x02') # Gain 0x00=1x, 0x01=4x 0.02=16x, 0x03=60x
-TCS_ADDR = 0x29
-COMMAND_BIT = 0x80
+ def scaled(self):
+ crgb = self.raw()
+ if crgb[0] > 0:
+ return tuple(float(x) / crgb[0] for x in crgb[1:])
-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()
+ return (0,0,0)
-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 rgb(self):
+ return tuple(int(x * 255) for x in self.scaled())
-def read_register(i2c, addr, reg, length):
- """Reads multiple bytes from a register."""
- result = bytearray(length)
+ def light(self):
+ return self.raw()[0]
+
+ def brightness(self, level=65.535):
+ return int((self.light() / level))
- def transfer():
- i2c.writeto(addr, bytes([COMMAND_BIT | reg]))
- i2c.readfrom_into(addr, result)
- return result
+ def valid(self):
+ self._bus.writeto(self._i2c_address, b'\x93')
+ return self._bus.readfrom(self._i2c_address, 1)[0] & 1
- return i2c_locked(i2c, transfer)
+ def raw(self):
+ self._bus.writeto(self._i2c_address, b'\xb4')
+ return struct.unpack("<HHHH", self._bus.readfrom(self._i2c_address, 8))
-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
@@ -462,73 +455,111 @@ def read_rgbc(i2c):
- Step 3: code.py
+ Step 3: main.py
Finally, this is how we call our color.py code from our main code, initialise and read the
sensor.
- We use the busio library to handle our i2c. We initialise the connection by telling
+
We use I2C and Pin from the machine>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.
We then pass on that connection to initialize the sensor using
- color.init_sensor(i2c)
+ color.Color(i2c)
- The whenever we read color.read_rgbc(i2c) it will give us an array with the
- [red, green, blue, color], with the color being the total light reflected, rather
- than just a single color.
+
The whenever we read color.rgb() it will give us an array with the
+ [red, green, blue].
+ If we want to read the transparent sensor and get the total brightness, we call color.light()
-# code.py
+# main.py
-import busio
import time
-import board
+from machine import Pin, I2C
import color
-i2c = busio.I2C(scl=board.GP1, sda=board.GP0, frequency=1_000_000)
-color.init_sensor(i2c)
+i2c = I2C(0, scl=Pin(1), sda=Pin(0))
+rc = color.Color(i2c)
while True:
- value = color.read_rgbc(i2c)
- print(value)
- time.sleep(0.1)
+ rgb = rc.rgb()
+ brightness = rc.light()
+ print(rgb, brightness)
+ time.sleep(0.1)
+
-
+
Step 4: How to use it?
- To take these numbers and use them effectively, we need to break out the different colour
- channels.
-
- To the right is an example of how we might take the total colour from the result, and then use it
+
To take these numbers and use them effectively, it depends on what we want to do.
+ TO follow a black line on a white background we only need the total brightness so we use the color.light() function.
+ ke the total colour from the result, and then use it
to decide whether to turn or not.
-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
+value = rc.light() # Read the sensor
-if c > 30: # If the c value is greater than 30
+
+if value > 700: # If the value is greater than 700
# Turn Right
left_motor.move(50)
- left_motor.move(-50)
+ right_motor.move(-50)
else:
# Go Straight
left_motor.move(50)
- left_motor.move(50)
+ right_motor.move(50)
+
+
+ Step 5: Adding a second sensor
+ One sensor can follow an edge, but for a proper line follower we want a left and a
+ right sensor.
+
+
+ There's a catch: every TCS3472 colour sensor uses the same I2C address (0x29). If you
+ wire both straight to the same SDA and SCL pins, they'll both answer every message and the
+ readings will be garbage.
+
+ The fix is to have a second I2C bus, luckily the RP2040 supports this.
+
+ We need to go back to our i2c declaration and add a channel number to it, i2c = I2C(0, scl=Pin(1), sda=Pin(0))
+
+ That extra "0" specifically tells the RP2040 to use the first I2C bus. Then we can add a second bus by running i2c2 = I2C(1, scl=Pin(3), sda=Pin(2))
+ Once we've initialized our right colour sensor with rc = color.Color(i2c2) then we're good to go!
+
+
+# main.py
+from machine import Pin, I2C
+import time
+import color
+
+i2c = I2C(0, scl=Pin(1), sda=Pin(0)) # Initialize the first I2C bus
+i2c2 = I2C(1, scl=Pin(3), sda=Pin(2)) # Initialize the second I2C bus
+
+lc = color.Color(i2c2) # Initialize the left colour sensor
+rc = color.Color(i2c) # Initialize the right colour sensor
+
+while True:
+ left_color = lc.light()
+ right_color = rc.light()
+ print(left_color, right_color)
+ time.sleep(0.1)
+
+
+
+
+
+
+
@@ -595,9 +626,9 @@ def i2c_locked(i2c, func, *args, **kwargs):
def select_channel(i2c, channel):
"""Selects the TCA9548A multiplexer channel."""
- if not 0 <= channel <= 7:
+ if not 0 <= channel <= 7:
raise ValueError("Channel must be 0-7")
- i2c_locked(i2c, i2c.writeto, TCA_ADDR, bytes([1 << channel]))
+ i2c_locked(i2c, i2c.writeto, TCA_ADDR, bytes([1 << channel]))
@@ -605,7 +636,7 @@ def select_channel(i2c, channel):
- Step 3: code.py
+ Step 3: main.py
So in our main code we just import the muxer module, and make sure we switch to the correct
channel before sending any messages.
@@ -614,7 +645,7 @@ def select_channel(i2c, channel):
-# code.py
+# main.py
import busio
import time
@@ -683,51 +714,47 @@ while True:
Step 2: Coding
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.
+ we'll upload this file to our device, or copy->paste the contents into a new file called ssd1306.py.
-
-
+
- We initialize the display with display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c).
+
We initialize the display with disp = ssd1306.SSD1306_I2C(i2c).
We colour the entire screen either black (off) or blue/white (on) with
- display.fill(0) for off, or 1 for on.
+ disp.fill(0) for off, or 1 for on.
- We write text with display.text(text, x, y, color).
+ We write text with disp.text(text, x, y, color).
Finally, before anythin will actually appear on the screen, we send it using
- display.show().
+ disp.show().
-import board
+from machine import Pin, I2C
import time
-import busio
-import adafruit_ssd1306
+import ssd1306
-i2c = busio.I2C(scl=board.GP1, sda=board.GP0, frequency=1_000_000)
-display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)
+i2c = I2C(0, scl=Pin(1), sda=Pin(0))
+disp = ssd1306.SSD1306_I2C(i2c)
-
-display.fill(0) # Fill the screen with BLACK
+disp.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
+disp.text("Hello World", 0, 0, 1)
+disp.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
+disp.fill(1)# Fille the screen with COLOR
+disp.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
+disp.text("Hello World", 10, 20, 0)
+disp.show() # Send the update to the screen
@@ -748,35 +775,29 @@ display.show() # Send the update to the screen
-import board
+from machine import Pin, I2C
import time
-import busio
-import adafruit_ssd1306
+import 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)
+i2c = I2C(0, scl=Pin(1), sda=Pin(0)) # Initialize the first I2C bus
+i2c2 = I2C(1, scl=Pin(3), sda=Pin(2)) # Initialize the second I2C bus
+
+disp = ssd1306.SSD1306_I2C(i2c)
# initialize two colour sensors
-muxer.select_channel(i2c, 0)
-color.init_sensor(i2c)
-muxer.select_channel(i2c, 1)
-color.init_sensor(i2c)
-
+lc = color.Color(i2c2)
+rc = color.Color(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]
+ left_color = lc.light()
+ right_color = rc.light()
# 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()
+ disp.fill(0)
+ disp.text("L: " + str(left_color), 0, 0, 1)
+ disp.text("R: " + str(right_color), 0, 10, 1)
+ disp.show()
time.sleep(0.1)
@@ -816,16 +837,13 @@ while True:
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.
-
-
We only need to initialize the pixel with
- pixel = neopixel.NeoPixel(board.GP29, 1, brightness=0.2).
+ pixel = neopixel.NeoPixel(Pin(16), 1).
- The "1" is the number of pixels we have, and we CAN set the brightness up to 1.0 but it's quite
- bright.
+ The Pin is the RP2040 pin that the LED is connected to.The "1" is the number of pixels we have.
Then we just need to tell the pixel what colour to be, we talk to
- pixel[0] = (red,green,blue) because
+ pixel[0] = (green,red,blue) because
it's the only one we have. The numbers for each colour go from minimum 0, to maximum 255.
@@ -833,57 +851,30 @@ while True:
-import board
+from machine import Pin
import time
import neopixel
-pixel = neopixel.NeoPixel(board.GP29, 1, brightness=0.2)
+pixel = neopixel.NeoPixel(Pin(16), 1)
+pixel[0] = (20, 0, 0)
+pixel.write()
while True:
- pixel[0] = (255, 0, 0) # Red
+ pixel[0] = (255, 0, 0) # Green
+ pixel.write()
time.sleep(1)
- pixel[0] = (0, 255, 0) # Green
+ pixel[0] = (0, 255, 0) # Red
+ pixel.write()
time.sleep(1)
pixel[0] = (0, 0, 255) # Blue
+ pixel.write()
time.sleep(1)
-
-
- Step 3: Make some pretty effects
- This example has three loops, each one fades from one primary colour to the next.
-
- We increase one LEDs brightness by assigning it i, which counts from 0-255
-
- We decrease another LEDs brightness by making it 255-i so it begins at max, and
- counts
- down to zero as i becomes higher.
-
-
-
-
-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)
-
+
@@ -959,9 +950,12 @@ class Sonar:
# 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)
+ # Convert to distance in millimeters
+ # Speed of sound ~343 m/s = 0.343 mm/µs
+ distance_mm = (duration / 2) * 0.343
+ return int(distance_mm)
+
+
@@ -983,8 +977,8 @@ sonar = sonar.Sonar(trigger_pin=26, echo_pin=28)
while True:
dist = sonar.distance()
- print("Distance:", dist, "cm")
- time.sleep(1)
+ print("Distance:", dist, "mm")
+ time.sleep(0.1)
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. +
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.
def median_filter(values):
@@ -1213,11 +1208,12 @@ print(filtered)
Step 5: Fine tuning
- Before moving on, play with the buffer size to see how it effects the results AND the responsiveness to changes.
+ Before moving on, play with the buffer size to see how it effects the results AND the
+ responsiveness to changes.
Try and find a good balance with the time.sleep() value as well.
-
-
+
-# code.py
+# main.py
import pid