Project 3.12: "N1+N2+N3 Integration"
What you’ll learn
- Integration: Combine IR, Bluetooth, Serial, LCD, sensors, and motors across multiple boards.
- Coordination: Assign clear roles (Controller, Robot, Station, Game) and keep messages concise.
- Reliability: Use helper functions and pacing to avoid overload and keep feedback immediate.
- Usability: Format displays for clarity, add alerts, and confirm actions with ACKs.
- Creativity: Leave hooks for students to expand modes, views, and mini‑games safely.
Blocks glossary
- IR send/receive: Quick one‑way command path (Controller → Robot).
- Bluetooth central/peripheral: Two‑way text for modes, ACKs, telemetry, and game events.
- Serial print: Minimal logs to track actions and keep debugging simple.
- LCD (I2C): Render formatted sensor values, status, and messages.
- Digital input/output: Buttons, LEDs for control and feedback.
- PWM output: Motor speed control on ENA/ENB.
- ADC input: Sensor readings (e.g., light sensor on Pin 2).
- def function: Small reusable helpers (send_cmd, apply_mode, fit16, ack).
- Loop: Steady cadence across boards; don’t spam.
What you need
| Role | Board | Connections |
|---|---|---|
| Controller | D1 R32 | Buttons A–F, IR TX → 26, Bluetooth central |
| Robot | D1 R32 | IR RX → 26; L298N: ENA → 5 (PWM), ENB → 18 (PWM), IN1 → 23, IN2 → 19, IN3 → 13, IN4 → 21; Bluetooth peripheral |
| Station | D1 R32 | LCD 1602 I2C (SCL → 26, SDA → 5), ADC2 sensor, Bluetooth peripheral |
| Game | D1 R32 | Buttons, LEDs, Bluetooth peripheral |
- Share GND across boards and drivers.
- Keep Bluetooth devices within 1–3 m; align IR 20–50 cm.
Before you start
- Open serial monitors for Controller, Robot, Station, and Game.
- Quick test:
print("Ready!") # Confirm serial is working
🎮 Microprojects (5 mini missions)
🎮 Microproject 3.12.1 – Complete remote control system
Goal: Controller sends dual‑medium commands (IR + Bluetooth), Robot decodes both.
# Microproject 3.12.1 – Controller: IR + Bluetooth commands
import irremote # Load IR library
import ble_central # Load Bluetooth central helper
import time # Load time library
ir_tx = irremote.NEC_TX(26, False, 100) # IR transmitter on Pin 26
print("[Controller] IR TX ready on 26") # Serial: confirm IR init
central = ble_central.BLESimpleCentral() # Bluetooth central object
print("[Controller] BT central ready") # Serial: confirm BT init
def connect_robot(): # Helper: connect to Robot-R32
central.scan() # Scan peripherals nearby
time.sleep_ms(600) # Short scanning delay
central.connect('Robot-R32') # Connect by name
print("[Controller] BT connected to Robot-R32") # Serial: connection OK
connect_robot() # Establish BT link
CODE_FORWARD = 0x18 # IR forward command code
def send_ir(code): # Helper: send IR command
ir_tx.transmit(0x00, code, 0x00) # Transmit IR packet (addr/control 0x00)
print("[Controller] IR TX:", hex(code)) # Serial: log IR send
def send_bt(text): # Helper: send Bluetooth text
central.send(text) # Transmit text to robot
print("[Controller] BT TX:", text) # Serial: log BT send
while True: # Demo loop
send_ir(CODE_FORWARD) # Send IR forward command
send_bt("CMD:FORWARD") # Send BT forward command
time.sleep_ms(1000) # Pace loop at ~1 second
Reflection: Redundant commands make control resilient.
Challenge: Add STOP on button F and send both “CMD:STOP” and the IR STOP code.
🎮 Microproject 3.12.2 – Advanced remote‑controlled robot
Goal: Robot decodes IR + BT, applies assisted modes, and is ready for pulse precision.
# Microproject 3.12.2 – Robot: decode IR + BT and apply modes
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import irremote # Load IR library
import machine # Load hardware pin/PWM library
import time # Load time library
ble_p = ble_peripheral.BLESimplePeripheral('Robot-R32') # Peripheral named 'Robot-R32'
print("[Robot] BLE 'Robot-R32' ready") # Serial: confirm BT init
handle = ble_handle.Handle() # Callback handle for RX
print("[Robot] BT handle ready") # Serial: confirm handle
ir_rx = irremote.NEC_RX(26, 8) # IR receiver on Pin 26 with buffer 8
print("[Robot] IR RX ready on 26") # Serial: confirm IR init
pwmA = machine.PWM(machine.Pin(5)) # ENA PWM (left)
pwmB = machine.PWM(machine.Pin(18)) # ENB PWM (right)
pwmA.freq(2000) # Set PWM frequency 2kHz
pwmB.freq(2000) # Set PWM frequency 2kHz
speed = 650 # Default duty (NORMAL)
pwmA.duty(speed) # Apply left duty
pwmB.duty(speed) # Apply right duty
print("[Robot] Start duty =", speed) # Serial: initial speed
def apply_mode(text): # Helper: set speed by mode text
global speed # Use global speed variable
if text == "MODE:TURBO": # Turbo mode
speed = 1023 # Max duty
elif text == "MODE:QUIET": # Quiet mode
speed = 400 # Low duty
elif text == "MODE:NORMAL": # Normal mode
speed = 650 # Medium duty
elif text == "MODE:STOP": # Stop mode
speed = 0 # Duty zero
pwmA.duty(speed) # Apply left duty
pwmB.duty(speed) # Apply right duty
ble_p.send("ACK:" + text) # ACK mode change
print("[Robot] Mode applied:", text) # Serial: log applied mode
def handle_method(msg): # Callback: process BT messages
s = str(msg) # Ensure string type
print("[Robot] BT RX:", s) # Serial: show incoming text
if s.startswith("MODE:"): # If mode command
apply_mode(s) # Apply the mode
elif s == "CMD:FORWARD": # Forward command (BT)
print("[Robot] BT CMD FORWARD") # Serial: placeholder forward action
ble_p.send("ACK:FORWARD") # ACK forward command
elif s == "CMD:STOP": # Stop command (BT)
apply_mode("MODE:STOP") # Apply stop via mode helper
else: # Unknown BT command
ble_p.send("ERR:UNKNOWN") # Error feedback
handle.recv(handle_method) # Register BT callback
print("[Robot] Callback registered") # Serial: callback active
while True: # IR decode loop
if ir_rx.any(): # If any IR code available
code = ir_rx.code[0] # Read first code from buffer
print("[Robot] IR RX:", hex(code)) # Serial: log IR code (hook for actions)
time.sleep_ms(150) # Loop pacing for responsiveness
Reflection: Dual‑path control with modes keeps actions clear.
Challenge: Add pulse stepping for “STEP:FWD/STEP:BACK” with timed motor pulses.
🎮 Microproject 3.12.3 – Total monitoring station
Goal: Station shows SENSOR, SYSTEM, and BT messages neatly on LCD.
# Microproject 3.12.3 – Station: LCD monitoring (SENSOR + SYSTEM + BT)
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import machine # Load hardware (I2C/ADC) library
import i2clcd # Load LCD library (1602 I2C)
import time # Load time library
ble_p = ble_peripheral.BLESimplePeripheral('Station-R32') # Peripheral named 'Station-R32'
print("[Station] BLE 'Station-R32' ready") # Serial: confirm BT init
handle = ble_handle.Handle() # Callback handle for RX
print("[Station] Handle ready") # Serial: confirm handle
i2c = machine.SoftI2C( # Create software I2C bus
scl = machine.Pin(26), # SCL on Pin 26
sda = machine.Pin(5), # SDA on Pin 5
freq = 100000 # I2C at 100 kHz
) # End I2C creation
lcd = i2clcd.LCD(i2c, 16, 0x27) # LCD controller (16 cols, addr 0x27)
print("[Station] LCD ready") # Serial: confirm LCD
adc2 = machine.ADC(machine.Pin(2)) # ADC sensor on Pin 2
adc2.atten(machine.ADC.ATTN_11DB) # Full voltage range 0–3.3V
adc2.width(machine.ADC.WIDTH_12BIT) # 12-bit resolution
print("[Station] ADC2 ready") # Serial: confirm ADC
page = "SENSOR" # Active page state
last_rx = "" # Last BT message shown
print("[State] page=SENSOR") # Serial: initial page
def fit16(text): # Helper: fit/pad to 16 chars
s = str(text) # Cast to string
return s[:16] if len(s) > 16 else s.ljust(16) # Truncate or pad
def show(row, text): # Helper: render one row
lcd.shows(fit16(text), # Fit the text to 16 chars
column = 0, # Start at column 0
line = row, # Row 0 or 1
center = False) # Left align for data lines
print("[LCD]", row, ":", fit16(text)) # Serial: mirror display
def handle_method(msg): # Callback: receive BT
global last_rx, page # Use global state
s = str(msg) # Ensure string
print("[Station] BT RX:", s) # Serial: log incoming
last_rx = s # Save last message
if s.startswith("SRC:"): # If source selection
page = s.split(":")[1] # Set page (SENSOR/SYSTEM/BT)
ble_p.send("ACK:SRC=" + page) # Acknowledge change
handle.recv(handle_method) # Register BT callback
print("[Station] Callback registered") # Serial: callback active
while True: # UI loop
if page == "SENSOR": # SENSOR view
raw = adc2.read() # Read ADC raw
volts = (raw * 3.3) / 4095 # Convert to volts
show(0, "Sensor ADC2") # Header
show(1, "Raw " + str(raw)) # Raw value line
elif page == "SYSTEM": # SYSTEM view
ms = time.ticks_ms() # Uptime ms
secs = ms // 1000 # Seconds
show(0, "System Status") # Header
show(1, "Up {:02d}:{:02d}".format(secs // 60, secs % 60)) # mm:ss uptime
else: # BT view
show(0, "Last BT Msg") # Header
show(1, last_rx if last_rx else "(none)") # Last message or placeholder
time.sleep_ms(850) # Smooth update cadence
Reflection: Clean display makes monitoring calm and informative.
Challenge: Add “SRC:CYCLE” to rotate pages automatically.
🎮 Microproject 3.12.4 – Multi‑device gaming
Goal: A simple “serve/hit/miss” mini‑game using Bluetooth across two boards.
# Microproject 3.12.4 – Game: serve/hit/miss (Peripheral side)
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import machine # Load hardware pin library
import time # Load time library
ble_p = ble_peripheral.BLESimplePeripheral('Game-R32') # Peripheral named 'Game-R32'
print("[Game-B] BLE 'Game-R32' ready") # Serial: confirm BT init
handle = ble_handle.Handle() # Callback handle
print("[Game-B] Handle ready") # Serial: confirm handle
led = machine.Pin(13, machine.Pin.OUT) # LED for feedback
print("[Game-B] LED=13 ready") # Serial: confirm LED
turn = "A" # Track turn (starts at A)
round_active = False # Rally state flag
def set_led(on): # Helper: LED on/off
led.value(1 if on else 0) # Set LED state
print("[Game-B] LED:", "ON" if on else "OFF") # Serial: LED state
def send(text): # Helper: send message to central
ble_p.send(text) # Transmit text
print("[Game-B] TX:", text) # Serial: log send
def handle_method(msg): # Callback: process game messages
global turn, round_active # Use global rally state
s = str(msg) # Ensure string type
print("[Game-B] RX:", s) # Serial: log incoming
if s == "SERVE?": # Start rally request
round_active = True # Mark rally active
turn = "A" # Serve to A
set_led(True) # LED ON to mark rally
send("SERVE:A") # Notify serve side
elif s == "HIT:A" and round_active and turn == "A": # A hits correctly
turn = "B" # Ball goes to B
send("BALL:B") # Notify next turn
elif s == "HIT:B" and round_active and turn == "B": # B hits correctly
turn = "A" # Ball goes to A
send("BALL:A") # Notify next turn
elif s == "MISS:A" and round_active and turn == "A": # A misses
round_active = False # End rally
set_led(False) # LED OFF
send("MISS:A") # Announce miss
elif s == "MISS:B" and round_active and turn == "B": # B misses
round_active = False # End rally
set_led(False) # LED OFF
send("MISS:B") # Announce miss
else: # Unknown or out-of-turn
send("ERR:STATE") # Error feedback
handle.recv(handle_method) # Register callback
print("[Game-B] Callback registered") # Serial: callback active
while True: # Idle; actions via callback
time.sleep_ms(150) # Keep CPU cool
# Microproject 3.12.4 – Game: serve/hit/miss (Central side)
import ble_central # Load Bluetooth central helper
import ble_handle # Load Bluetooth handle helper
import machine # Load hardware pin library
import time # Load time library
btn_hit = machine.Pin(26, machine.Pin.IN, machine.Pin.PULL_UP) # HIT button
btn_miss = machine.Pin(25, machine.Pin.IN, machine.Pin.PULL_UP) # MISS button
print("[Game-A] Buttons HIT=26 MISS=25 ready") # Serial: confirm inputs
central = ble_central.BLESimpleCentral() # Central object
print("[Game-A] Central ready") # Serial: confirm BT init
handle = ble_handle.Handle() # Callback handle
print("[Game-A] Handle ready") # Serial: confirm handle
def connect_peer(): # Helper: connect to Game-R32
central.scan() # Scan for peripherals
time.sleep_ms(600) # Short scan wait
central.connect('Game-R32') # Connect by name
print("[Game-A] Connected to Game-R32") # Serial: connection OK
def send(text): # Helper: transmit text
central.send(text) # Send message
print("[Game-A] TX:", text) # Serial: log send
def handle_method(msg): # Callback: print feedback
print("[Game-A] RX:", msg) # Serial: log incoming
handle.recv(handle_method) # Register callback
print("[Game-A] Callback registered") # Serial: callback active
connect_peer() # Connect to peripheral
send("SERVE?") # Ask to start rally
turn = "A" # Local expected turn state
while True: # Input loop for hits/misses
if btn_hit.value() == 0: # If HIT pressed
send("HIT:" + turn) # Send HIT with current turn
turn = "B" if turn == "A" else "A" # Flip expected turn
time.sleep_ms(250) # Debounce
if btn_miss.value() == 0: # If MISS pressed
send("MISS:" + turn) # Send MISS with current turn
turn = "B" if turn == "A" else "A" # Flip state
time.sleep_ms(250) # Debounce
time.sleep_ms(40) # Poll delay
Reflection: Quick game messages keep play smooth without overwhelming Bluetooth.
Challenge: Add score tracking and “Best of 7” win logic.
🎮 Microproject 3.12.5 – Creative integrative project
Goal: A flexible template students can expand (control, monitor, play in one).
# Microproject 3.12.5 – Integrative template (Peripheral role)
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import machine # Load hardware pin/ADC library
import i2clcd # Load LCD library (optional)
import time # Load time library
ble_p = ble_peripheral.BLESimplePeripheral('Integrate-R32') # Peripheral named 'Integrate-R32'
print("[Integrate] BLE 'Integrate-R32' ready") # Serial: confirm BT init
handle = ble_handle.Handle() # Callback handle
print("[Integrate] Handle ready") # Serial: confirm handle
# Optional LCD setup (comment out if not used)
i2c = machine.SoftI2C( # Create I2C bus for LCD
scl = machine.Pin(26), # SCL on Pin 26
sda = machine.Pin(5), # SDA on Pin 5
freq = 100000 # 100 kHz bus
) # End I2C creation
lcd = i2clcd.LCD(i2c, 16, 0x27) # LCD controller
print("[Integrate] LCD ready") # Serial: confirm LCD
adc2 = machine.ADC(machine.Pin(2)) # Sensor on ADC2 (demo)
adc2.atten(machine.ADC.ATTN_11DB) # Full voltage range
adc2.width(machine.ADC.WIDTH_12BIT) # 12-bit resolution
print("[Integrate] ADC2 ready") # Serial: confirm ADC
led = machine.Pin(13, machine.Pin.OUT) # LED feedback on Pin 13
print("[Integrate] LED=13 ready") # Serial: confirm LED
state = {"mode": "IDLE", "view": "SENSOR"} # Minimal state dict
print("[Integrate] State:", state) # Serial: show initial state
def fit16(text): # Helper: fit text to 16 chars
s = str(text) # Cast to string
return s[:16] if len(s) > 16 else s.ljust(16) # Truncate or pad
def show(row, text): # Helper: LCD row render
lcd.shows(fit16(text), # Fit to 16 characters
column = 0, # Start at column 0
line = row, # Target row 0/1
center = False) # Left align for readability
print("[LCD]", row, ":", fit16(text)) # Serial: mirror
def send(text): # Helper: Bluetooth send
ble_p.send(text) # Transmit text message
print("[Integrate] TX:", text) # Serial: log send
def set_mode(m): # Helper: update mode
state["mode"] = m # Change mode
send("ACK:MODE=" + m) # Confirm mode change
def set_view(v): # Helper: update view
state["view"] = v # Change view
send("ACK:VIEW=" + v) # Confirm view change
def handle_method(msg): # Callback: main router
s = str(msg) # Ensure string
print("[Integrate] RX:", s) # Serial: log incoming
if s.startswith("MODE:"): # Mode change request
set_mode(s.split(":")[1]) # Apply mode
elif s.startswith("VIEW:"): # View change request
set_view(s.split(":")[1]) # Apply view
elif s == "LED:ON": # LED ON command
led.value(1) # Turn LED ON
send("ACK:LED=ON") # Confirm
elif s == "LED:OFF": # LED OFF command
led.value(0) # Turn LED OFF
send("ACK:LED=OFF") # Confirm
else: # Unknown command
send("ERR:UNKNOWN") # Error feedback
handle.recv(handle_method) # Register callback
print("[Integrate] Callback registered") # Serial: callback active
while True: # Main loop: render chosen view
if state["view"] == "SENSOR": # If SENSOR view selected
raw = adc2.read() # Read ADC raw value
show(0, "ADC2 Sensor") # Header
show(1, "Raw " + str(raw)) # Raw value
elif state["view"] == "SYSTEM": # If SYSTEM view selected
ms = time.ticks_ms() # Read uptime ms
show(0, "System Status") # Header
show(1, "Up {:02d}:{:02d}".format((ms // 1000) // 60, (ms // 1000) % 60)) # mm:ss uptime
else: # Else CUSTOM/BT view
show(0, "Integrate-R32") # Header
show(1, "Mode " + state["mode"]) # Mode line
time.sleep_ms(850) # Smooth update cadence
Reflection: A safe template invites creativity while keeping structure intact.
Challenge: Add a “COMPOSE” page that shows two short values (e.g., ADC and mode) on one line.
✨ Main project – N1+N2+N3 integration (multi‑board ecosystem)
System outline
- Controller: Sends IR and BT commands; keeps concise logs.
- Robot: Decodes IR and BT; applies modes; ready for precise pulses.
- Station: Displays SENSOR/SYSTEM/BT views; handles source commands.
- Game: Runs a mini rally; confirms turns and misses.
- Integration hooks: Shared message patterns (CMD, MODE, SRC, ACK/ERR) keep everything consistent.
# Project 3.12 – Integrated ecosystem (Controller + Robot + Station + Game)
# This outline shows how each board cooperates using small, readable helpers.
# Controller: already implemented in Microproject 3.12.1
# Robot: already implemented in Microproject 3.12.2
# Station: already implemented in Microproject 3.12.3
# Game: already implemented in Microproject 3.12.4
# Tip: Keep all messages short (CMD:..., MODE:..., SRC:...) and confirm with ACK:...
# Avoid spamming: use ~800–1000 ms cadence and minimal serial logs per event.
External explanation
- What it teaches: How to stitch together multiple Level‑3 skills into a coherent, fault‑tolerant, student‑friendly system.
- Why it works: Each device has a clear role; helpers prevent repetition; consistent message names avoid confusion; displays keep users informed.
- Key concept: Integration succeeds when each part is simple and the interfaces between parts are crystal‑clear.
Story time
Four tiny teammates: the Controller calls the plays, the Robot moves with style, the Station narrates the world, and the Game keeps spirits high. Together, they feel like a little campus of robots.
Debugging (2)
Debugging 3.12.A – Memory saturation
- Symptom: Random slowdowns or resets when many features run.
- Fix: Reduce print frequency, reuse helpers, and avoid long strings.
# Keep one concise log per event and avoid giant strings
print("[Robot] IR RX:", hex(code)) # Short, informative
# Reuse shared helpers (fit16, apply_mode) to prevent duplication
Debugging 3.12.B – Interference with multiple communications
- Symptom: Bluetooth feels laggy when IR floods, or LCD flickers during fast updates.
- Fix: Add small delays, avoid simultaneous sends, and batch UI updates.
# Pace each loop separately (~850–1000 ms for displays, ~800–1000 ms for commands)
time.sleep_ms(850) # Smooth LCD refresh
# Pick one route per cycle and avoid sending IR+BT at the exact same instant
Final checklist
- Controller sends IR and BT commands with clean spacing.
- Robot decodes commands, applies modes, and acknowledges actions.
- Station displays SENSOR/SYSTEM/BT clearly with 16‑char formatting.
- Game runs a rally with serve/hit/miss and immediate feedback.
- Serial logs remain concise; loops use steady cadences.
Extras
- Student tip: Rename devices (“Robot‑R32”, “Station‑R32”, “Game‑R32”) for easy pairing.
- Instructor tip: Let teams decide roles; rotate responsibilities to explore each device.
- Glossary:
- ACK: A short confirmation message sent back to the sender.
- SRC: A source selection keyword for display stations.
- Cadence: The rate of updates — steady is better than fast-and-noisy.
- Mini tips:
- Fit LCD text to 16 characters and avoid overlap.
- Keep message labels consistent across all boards.
- Celebrate small wins — a clean ACK and a smooth move mean your system is healthy.

