Code Page
Introduction
C0P1
the Protocol Droid
To say that artificial intelligence (AI) was not important in this project would be a gross understatement. Given the complexity of inter-hardware communication, the novelty of concepts such as Flask, and the sheer number of robots and props involved, AI was and had to be used.
We even renamed Microsoft Copilot™, C0P1 (a pun on Star Wars' C3PO) our very own protocol droid. It helped us in so many aspects of the project. We are eternally grateful :-)
All existing code has been ported to a class-based template and any new code, developed using that template. For the LEGO Mindstorms EV3 robots, there is an
EV3DEV and
Pybricks MicroPython version. For the LEGO SPIKE Prime robot and props, there is a
Pybricks version.
Versioning was implemented and all iterations can be found on OneDrive in the
Versions folder.
Robots
Bluey, the Velociraptor
The below are the main code for the ball / signature following and sending of its JSON data to the Flask server endpoint:
# Follow signature
def follow_signature(self):
pixy = self.hw.pixy
steer = self.hw.move_steering
# Read Pixy blocks
try:
status, blocks_raw = pixy.get_blocks(1, 1)
except OSError:
print("[PIXY] I2C error. Retrying ...")
time.sleep(0.05)
return
# Send JSON to Flask server
self.send_pixy_json(blocks_raw)
# Filter blocks
blocks_for_following = []
if blocks_raw:
for b in blocks_raw:
if b.sig == TARGET_SIG:
blocks_for_following.append(b)
# Pick largest block
block = self.get_largest_block(blocks_for_following)
if block:
# Compute steering error
error = block.x_center - FRAME_CENTER
turn = KP * error
turn = max(min(turn, 100), -100)
message = '[FOLLOW] x:' + str(block.x_center) + ', Turn:' + str(turn)
print(message)
# Drive forward while steering
steer.on(-turn, SpeedPercent(20))
else:
print("[FOLLOW] Object not found")
steer.off()
# Find largest block
def get_largest_block(self, blocks):
if not blocks:
return None
return max(blocks, key=lambda b: b.width * b.height)
# Send Pixy2 JSON to Flask server endpoint
def send_pixy_json(self, blocks_raw):
blocks_json = []
if blocks_raw:
for b in blocks_raw:
print(vars(b))
if b.sig == TARGET_SIG:
blocks_json.append({
"x": 316 - b.x_center,
"y": 208 - b.y_center,
"w": b.width,
"h": b.height,
"sig": b.sig,
"angle": getattr(b, "angle", None)
})
payload = {
"device": self.name,
"blocks": blocks_json,
"vectors": [],
"barcodes": []
}
try:
requests.post(self.server.server + "/receive_frame", json=payload, timeout=0.2)
except:
pass
The entire code can be downloaded here.
Brontie, the Brachiosaurus
The below are the main code for the leaves detection and eating, and dino lifting:
# Detect leaves
def detect_leaves(self):
ir = self.hw.sensor_ir
# heading: -25..25, distance: 0..100 (cm)
heading, distance = ir.heading_and_distance(channel=1)
message = 'Heading: ' + str(heading) + ' Distance: ' + str(distance)
print(message)
# Turn toward beacon (softened)
self.turn(heading)
# No distance reading, do nothing
if distance is None or distance == 0:
return False
# Soft approach zone (40–30 cm)
if 30 < distance <= 40:
print("[LEAVES] Soft approach ...")
self.hw.motor_move.on_for_seconds(SPEED_LOW, 0.3)
return False
# Eating zone (< 30 cm)
if distance <= 30:
print("[LEAVES] Leaves detected. Stopping ...")
self.hw.motor_move.stop()
self.lift_dino()
return True
return False
# Eat leaves
def eat_leaves(self):
jaw = self.hw.motor_eat
print("[EAT] Eating leaves ...")
# Open jaw
jaw.on_for_seconds(SPEED_LOW, DURATION_HIGH)
# Pause
time.sleep(3)
# Close jaw
jaw.on_for_seconds(-SPEED_LOW, DURATION_HIGH)
print("[EAT] Done eating leaves ...")
self.server.send_event("EVENT", "01")
self.leaves_not_detected = False
return self.leaves_not_detected
# Lift dinosaur
def lift_dino(self):
left = self.hw.motor_lift_left
right = self.hw.motor_lift_right
move = self.hw.motor_move
turn = self.hw.motor_turn
print("[LIFT] Lifting Brontie ...")
msg = "Standing on hind legs"
self.server.send_event("STATUS", msg)
# Lift up
left.run_forever(speed_sp=-SPEED_LIFT)
right.run_forever(speed_sp=-SPEED_LIFT)
time.sleep(WAIT_LIFT)
left.stop()
right.stop()
# Eat leaves while lifted
self.eat_leaves()
# Lower down
left.run_forever(speed_sp=SPEED_LIFT)
right.run_forever(speed_sp=SPEED_LIFT)
time.sleep(WAIT_LIFT)
left.stop()
right.stop()
move.stop()
turn.stop()
print("[LIFT] Done lifting Brontie ...")
The entire code can be downloaded here for the new version and here for the original.
Hamster Ball, the gyrosphere
The below are the main code for the ball following and rotation adjustment:
# Move with motor offset
def move(self, direction, speed, rotation):
# Convert motor angles to radians (relative to unit circle) i.e.: 0 degrees is East
angle_A = radians(45)
angle_B = radians(135)
angle_C = radians(225)
angle_D = radians(315)
# Convert direction to radians
direction_angle = radians(direction)
# Calculate speed for motors based on position of IR ball
speed_A = cos((direction_angle - angle_A)) * speed + rotation
speed_B = cos((direction_angle - angle_B)) * speed + rotation
speed_C = cos((direction_angle - angle_C)) * speed + rotation
speed_D = cos((direction_angle - angle_D)) * speed + rotation
# Run motors at the calculated speed
self.hw.motor_a.run(speed_A)
self.hw.motor_b.run(speed_B)
self.hw.motor_c.run(speed_C)
self.hw.motor_d.run(speed_D)
# Rotate smoothly
def smooth_rotation(self, desired_heading):
# Initialise function constants
# Proportional gain (smoothness control)
Kp = 0.5 # Lower value is smoother, higher is snappier
# Deadzone to prevent jitter near 0 degrees
deadzone = 4 # degrees
# Reset heading to 0 degrees i.e.: 'North'
self.hub.imu.reset_heading(0)
current_heading = self.hub.imu.heading()
# Calculate how far desired & current are apart
error = desired_heading - current_heading
# Set error between -180 and 180 degrees
if error > 180:
error -= 360
elif error < -180:
error += 360
if abs(error) < deadzone:
return 0
# Proportional correction
rotation_temp = Kp * error
# Clamp to avoid extreme rotation speeds
rotation = max(min(rotation_temp, 100), -100)
print("Current:", current_heading,
"Desired:", desired_heading,
"Error:", error,
"Rotation:", rotation)
return rotation
The entire code can be downloaded here for the new version and here for the original converted to Pybricks or here for the original from the SPIKE Prime app.
Indie, the Indominus Rex
The below is the main code for the action handler:
# Update action
def update_action(self):
action_current = FORWARD_SLOW
self.timer_action.reset()
yield action_current
while self.timer_action.time() < 800:
yield
action_current = STOP
yield action_current
while True:
distance = self.hw.sensor_us.distance()
action_new = STOP
# Far: gentle random turn & growl
if distance is not None and distance >= DIST_FAR:
print("[DIST] Far")
turn = urandom.choice([TURN_LEFT, TURN_RIGHT])
action_current = Action(speed_drive=FORWARD_SLOW.speed_drive,
steering=turn.steering)
yield action_current
self.timer_action.reset()
while self.timer_action.time() < 800:
yield
self.pending_behaviour["sound"] = "GROWL"
# Mid: forward fast & jaw movement
elif distance is not None and DIST_MID <= distance < DIST_FAR:
print("[DIST] Mid")
action_current = FORWARD_FAST
yield action_current
self.pending_behaviour["motion_jaw"] = "open_close"
self.timer_action.reset()
while self.timer_action.time() < 800:
yield
self.pending_behaviour["sound"] = "ATTACK"
# Near: slow forward & arms, then slow backward
elif distance is not None and DIST_NEAR <= distance < DIST_MID:
print("[DIST] Near")
action_current = FORWARD_SLOW
yield action_current
self.pending_behaviour["motion_arms"] = "up_down"
action_current = BACKWARD_SLOW
yield action_current
self.timer_action.reset()
while self.timer_action.time() < 800:
yield
self.pending_behaviour["sound"] = "WIN"
else:
# Very close or no reading: stop
print("[DIST] Very close or None")
action_current = STOP
yield action_current
self.timer_action.reset()
while self.timer_action.time() < 200:
yield
self.timer_action.reset()
while self.timer_action.time() < 100:
yield
The entire code can be downloaded here for the new version and here for the original. The code is based on Laurens Valk's Gyro Boy given the complexity of self-balancing.
Jeep, the Jeep
The below are the main code for the RFID tag reading, line following and obstacle avoidance, as well as Flask server communication:
# Find RFID tags
def check_rfid(self):
uid = self.hw.sensor_rfid.readUID()
uid_str = None
# Normal behaviour after startup
if uid and uid != self.hw.last_uid:
self.hw.last_uid = uid
# TCP event
uid_str = str(uid).strip()
mapped = RFID_MAP.get(uid_str, "00")
# Ignore stale UID
if self.first_uid and mapped != "01":
self.first_uid = False
return
self.server.send_event("RFID", mapped)
print("[RFID] UID:", uid, "Mapped:", mapped)
if uid is None:
self.last_uid = None
# Follow the line
def follow_line(self):
# MODIFY TO SUIT REQUIRED CODE
# Declare function variables
intensity_left = self.hw.sensor_colour_left.reflection()
intensity_right = self.hw.sensor_colour_right.reflection()
if intensity_left >= self.threshold and intensity_right >= self.threshold:
# Both see white, move forward
self.hw.drive_base.drive(SPEED_MEDIUM, 0)
elif intensity_left < self.threshold and intensity_right >= self.threshold:
# Left sensor sees black, turn left medium
self.hw.drive_base.drive(SPEED_MEDIUM, -RATE_MEDIUM)
elif intensity_left >= self.threshold and intensity_right < self.threshold:
# Right sensor sees black, turn right medium
self.hw.drive_base.drive(SPEED_MEDIUM, RATE_MEDIUM)
elif intensity_left < self.threshold and intensity_right < self.threshold:
# Both see 'black', move forward slowly
self.hw.drive_base.drive(SPEED_LOW, 0)
# Wait to avoid excessive loop execution
wait(TIME_WAIT)
# Detect & avoid obstacle
def handle_obstacle(self):
distance = self.hw.sensor_us.distance()
now = self.clock.time() / 1000 # s
if distance is None:
return False # Treat as no obstacle
if distance < THRESHOLD_OBSTACLE:
msg = "Obstacle:" + str(distance) + "mm"
self.server.send_event("STATUS", msg)
if self.obstacle_start_time is None:
self.obstacle_start_time = now
print("[OBS] Obstacle detected. Waiting ...")
self.hw.stop()
# If obstacle persists too long, try to drive around
if not self.avoiding and (now - self.obstacle_start_time) > OBSTACLE_TIMEOUT:
print("[OBS] Timeout. Attempting to drive around ...")
self.avoiding = True
# Simple avoidance: back up, turn, move forward a bit
self.hw.drive_base.straight(-100)
self.hw.drive_base.turn(urandom.choice([-60, 60]))
self.hw.drive_base.straight(150)
# Reset and let line-follow re-acquire
self.obstacle_start_time = None
self.avoiding = False
return True
else:
self.obstacle_start_time = None
return False
# Send event
def send_event(self, kind, detail):
# Populate JSON payload
payload = {
"device": self.device,
"type": kind,
"value": detail
}
# Try sending JSON event to /event endpoint on Flask server
try:
r = requests.post(self.server + "/event", json=payload, headers={"Connection": "close"}
)
r.close()
print("[EVENT]", payload)
except Exception as e:
print("[HTTP] Sending event failed with error '", e, "'")
wait(200)
# Receive command
def get_command(self):
# Try receiving JSON command from /sent_command/ endpoint on Flask server
try:
r = requests.get(self.server + "/sent_command/" + self.device, headers={"Connection": "close"}
)
data = r.json()
r.close()
return data.get("command sent")
except Exception as e:
print("[HTTP] Receiving command failed with error '", e, "'")
wait(200)
return None
The entire code can be downloaded here for the new version and here for the original.
Rexie, the T-Rex
The below are the main code for the temperature scanning and obstacle 'attacking':
# Scan left, centre, right
def heat_seek_step(self):
left = self.hw.motor_left
right = self.hw.motor_right
tank = self.hw.move_tank
jaw = self.hw.motor_jaw
centre_temp = self.scan_direction(0)
left_temp = self.scan_direction(-SCAN_ANGLE)
right_temp = self.scan_direction(SCAN_ANGLE)
# Find hottest direction
temps = {
"centre": centre_temp,
"left": left_temp,
"right": right_temp
}
direction = max(temps, key=temps.get)
hottest = temps[direction]
print("[SCAN] Temperatures found", temps, ". Hottest at", direction, hottest)
msg = "Temp.:" + str(hottest) + "degC, Dir.:" + direction
self.server.send_event("STATUS", msg)
# Jaw animation
# jaw.on_for_seconds(SpeedPercent(40), 0.2)
# jaw.on_for_seconds(SpeedPercent(-40), 0.2)
jaw.on_to_position(SpeedPercent(40), -90)
jaw.on_to_position(SpeedPercent(40), 0)
# Move based on direction
if direction == "right":
tank.on_for_seconds(SpeedPercent(-20), SpeedPercent(20), 0.3)
elif direction == "left":
tank.on_for_seconds(SpeedPercent(20), SpeedPercent(-20), 0.3)
else:
# Move forward toward heat
tank.on_for_seconds(SpeedPercent(-30), SpeedPercent(-30), 1)
# Scan
def scan_direction(self, angle):
scanner = self.hw.motor_sensor
scanner.on_to_position(SpeedPercent(20), angle, brake=True, block=True)
time.sleep(0.2)
temperature = self.read_target()
# Reset to centre
scanner.on_to_position(SpeedPercent(20), 0, brake=True, block=True)
return temperature
# Read ambient temperature
def read_ambient(self):
sensor = self.hw.sensor_ir
sensor.mode = 'AMBIENT-C'
time.sleep(0.05)
ambient = sensor.value(0) / 100.0
print("[SCAN] Ambient temperature", ambient, "degC")
return ambient
# Read target temperature
def read_target(self):
sensor = self.hw.sensor_ir
sensor.mode = 'TARGET-C'
time.sleep(0.05)
target = sensor.value(0) / 100.0
print("[SCAN] Target temperature", target, "degC")
return target
# Attack obstacle
def attack_obstacle(self):
distance_cm = self.hw.sensor_us.distance_centimeters
if distance_cm is None:
return False # Treat as no obstacle
distance = distance_cm * 10
jaw = self.hw.motor_jaw
tank = self.hw.move_tank
if distance < OBSTACLE_DISTANCE:
print("[OBS] Obstacle detected at", distance, "mm")
msg = "Prey:" + str(distance) + "mm"
self.server.send_event("STATUS", msg)
self.server.send_event("EVENT", "01")
# Charge forward
tank.on_for_seconds(SpeedPercent(-60), SpeedPercent(-60), 1)
# Jaw snap
# jaw.on_for_seconds(SpeedPercent(80), 0.2)
# jaw.on_for_seconds(SpeedPercent(-80), 0.2)
jaw.on_to_position(SpeedPercent(80), -90)
jaw.on_to_position(SpeedPercent(80), 0)
self.server.send_event("EVENT", "04")
return True
return False
The entire code can be downloaded here.
Thomas, the monorail
The below are the main code for the barcode reading, line following and sending of its JSON data to the Flask server endpoint:
# Find barcodes
def check_bcid(self, data):
# Send JSON for dashboard
if not data or not data.barcodes:
self.hw.last_uid = None
return
# Take first barcode
bc = data.barcodes[0]
code_str = str(bc.code)
if code_str == self.hw.last_uid:
return # Already processed
# Map to orchestration
self.hw.last_uid = code_str
mapped = BC_MAP.get(code_str)
if mapped:
msg = self.name + ":BC:" + mapped
print("[BARCODE] UID:", code_str, "Mapped:", mapped)
self.server.send_event("BARCODE", mapped)
else:
print("[BARCODE] Unknown barcode", code_str)
# Follow line
def follow_line(self, data):
# MODIFY TO SUIT REQUIRED CODE
if not data or not data.vectors:
print("[LINE] Line not found")
self.hw.move_tank.stop()
return
# If vector found, calculate horizontal distance from middle
if data.number_of_vectors > 0:
dx = data.vectors[0].x1 - X_REF
self.move(dx)
# Send Pixy2 JSON to Flask server endpoint
def send_pixy_json(self, data):
vectors_json = []
barcodes_json = []
if data is not None:
# If vectors found
if data.vectors:
for v in data.vectors:
# Compute angle manually for Pixy2
dx = v.x1 - v.x0
dy = v.y1 - v.y0
angle = math.degrees(math.atan2(dy, dx))
# Compute vector length
#length = math.sqrt(dx*dx + dy*dy)
#dx /= length
#dy /= length
# Extend vector length
#x0_ext = v.x0 * SCALE + OFFSET_X
#y0_ext = v.y0 * SCALE + OFFSET_Y
#x1_ext = v.x1 * SCALE + OFFSET_X
#y1_ext = v.y1 * SCALE + OFFSET_Y
vectors_json.append({
#"x0": x0_ext,
#"y0": y0_ext,
#"x1": x1_ext,
#"y1": y1_ext,
"x0": v.x0,
"y0": v.y0,
"x1": v.x1,
"y1": v.y1,
"angle": round(angle, 2)
})
# If barcodes found
if data.barcodes:
for bc in data.barcodes:
barcodes_json.append({
"x": bc.x,
"y": bc.y,
"code": bc.code
})
payload = {
"device": self.name,
"blocks": [],
"vectors": vectors_json,
"barcodes": barcodes_json
}
try:
requests.post(self.server.server + "/receive_frame", json=payload, timeout=0.2)
except:
pass
The entire code can be downloaded here.
Props
Aaron and Pedro cave
The below are the main code for Aaron biting and Pedro flapping and lifting:
# Make Aaron jump out of cave, bite Indie & go back in cave
def bite(self):
print("[AARON] Bite triggered!")
m = self.hw.motor_aaron
# Jump out
m.run_angle(SPEED_MOTOR, ANGLE_OPEN)
wait(WAIT_TIME * 5)
# Bite motion
m.run_angle(SPEED_MOTOR, -ANGLE_ROTATE)
m.run_angle(SPEED_MOTOR, ANGLE_ROTATE)
wait(WAIT_TIME * 5)
# Return to cave
m.run_angle(SPEED_MOTOR, -ANGLE_OPEN)
# Broadcast event
self.ble.broadcast("EVENT", "AAP_bite_done")
# Make Pedro flap wings, lift up & down
def flap_lift(self, mode):
"""
mode = "flap", "lift", or "flap_lift"
"""
speed = SPEED_MOTOR
angle_flap = ANGLE_FLAP
angle_lift = ANGLE_LIFT
pedro_top = self.hw.motor_pedro_top
pedro_bottom = self.hw.motor_pedro_bottom
print("[PEDRO] Performing", mode)
# Number of repetitions
if mode == "flap":
reps = 6
elif mode == "lift":
reps = 2
elif mode == "flap_lift":
reps = 2
else:
return
# Initial movement
if mode == "flap":
pedro_top.run_target(speed, angle_flap)
elif mode == "lift":
pedro_bottom.run_target(speed, angle_lift)
elif mode == "flap_lift":
pedro_top.run_target(speed, angle_flap, wait=False)
pedro_bottom.run_target(speed, angle_lift)
# Repeated movement
for _ in range(reps):
if mode == "flap":
pedro_top.run_angle(speed, angle_flap)
elif mode == "lift":
pedro_bottom.run_angle(speed, angle_lift)
elif mode == "flap_lift":
pedro_top.run_angle(speed, angle_flap, wait=False)
pedro_bottom.run_angle(speed, angle_lift)
# Toggle direction
angle_flap = -angle_flap
angle_lift = -angle_lift
# Reset to neutral
if mode == "flap":
pedro_top.run_target(speed, 0)
elif mode == "lift":
pedro_bottom.run_target(speed, 0)
elif mode == "flap_lift":
pedro_top.run_target(speed, 0, wait=False)
pedro_bottom.run_target(speed, 0)
The entire code can be downloaded here for the new version and here for the original.
Jurassic Kingdom gate
The below are the main code for the lights, gate doors and motor reset (which is common to all SPIKE Prime props):
# Start lights at frequency of change
def light_up(self, frequency):
print('Turning lights on at', frequency, 'frequency ...')
# If frequency low, choose high wait time in ms
if frequency == 'low':
wait_lights = randint(200,500)
# If frequency high, choose low wait time in ms
else:
wait_lights = randint(100,200)
# Run gate lights
self.hw.motor_lights.run_angle(SPEED_MOTOR, ANGLE_LIGHTS) # Gate lights
wait(wait_lights)
self.hw.motor_lights.hold() # Gate lights
wait(WAIT_TIME)
# Shake trees at frequency of change
def shake_trees(self, frequency):
print('Shaking trees at', frequency, 'frequency ...')
# If frequency low, choose high wait time in ms
if frequency == 'low':
wait_trees = randint(200,500)
# If frequency high, choose low wait time in ms
else:
wait_trees = randint(100,200)
# Rotate tree motors
self.hw.motor_tree_left.run_angle(SPEED_MOTOR, ANGLE_ROTATE, wait=False) # Left tree motor
self.hw.motor_tree_left.run_angle(SPEED_MOTOR, ANGLE_ROTATE) # Right tree motor
wait(wait_trees)
ANGLE_ROTATE *= -1
# Open or close gate
def open_close_gate(self, action):
speed = SPEED_MOTOR / 5
if action == 'open':
print('Opening gate ...')
self.ble.broadcast("EVENT", "JKG_open")
self.hw.motor_gate_left.run_angle(speed, self.angle_gate, wait=False)
self.hw.motor_gate_right.run_angle(speed, self.angle_gate)
#self.angle_gate = self.angle_gate * -1
self.angle_gate *= -1
elif action == 'close':
print('Closing gate ...')
self.ble.broadcast("EVENT", "JKG_close")
self.hw.motor_gate_left.run_angle(speed, self.angle_gate, wait=False)
self.hw.motor_gate_right.run_angle(speed, self.angle_gate)
#self.angle_gate = self.angle_gate * -1
self.angle_gate *= -1
# Reset gate & light motors
def reset(self, motors):
# Initialize counter
counter_motor = 0
# Loop through motors array
for angle in motors:
# If a value is found in array
if angle != '':
print('Resetting motor', self.hw.list_motors[counter_motor], 'at', angle, 'degrees ...')
# Look up motor, reset position to angle
self.hw.list_motors[counter_motor].run_target(SPEED_MOTOR, angle)
# Increment counter
counter_motor += 1
The entire code can be downloaded here for the new version and here for the original.
Moses and feeding set
The below are the main code for Moses lunging and the shark rotating:
# Make Moses lunge
def moses_lunge(self):
print("[MOSES] Lunge!")
# Small turn to release elastic
self.hw.motor_moses_lunge.run_angle(SPEED_MOTOR, ANGLE_LUNGE_RELEASE)
self.lunge_done = True
self.ble.broadcast("EVENT", "lunged")
# Make Moses wind back
def moses_wind_back(self):
print("[MOSES] Winding back ...")
#self.hw.motor_moses_wind.run_angle(SPEED_MOTOR, ANGLE_WIND_BACK)
#self.hw.motor_moses_wind.run_angle(SPEED_MOTOR, -ANGLE_WIND_BACK)
self.hw.motor_moses_lunge.run_angle(SPEED_MOTOR, -ANGLE_LUNGE_RELEASE)
self.ble.broadcast("EVENT", "reset")
# Start shark rotating
def shark_start(self):
print("[SHARK] Start swinging")
self.shark_running = True
self.ble.broadcast("EVENT", "start")
# Stop shark rotating
def shark_stop(self):
print("[SHARK] Stop swinging")
self.shark_running = False
self.hw.motor_shark_turn.stop()
self.ble.broadcast("EVENT", "stop")
The entire code can be downloaded here.
Toilet from the iconic movie scene
The below are the main code for the toilet 'explosion' and the periodic method (which is similar for all SPIKE Prime props):
# Make toilet 'explode'
def explode(self):
self.ble.broadcast("CMD", "JKG_shhi")
print("[TOILET] EXPLOSION triggered!")
self.hw.motor_front.run_angle(SPEED_MOTOR, ANGLE_EXPLODE_FRONT)
self.hw.motor_back.run_angle(SPEED_MOTOR, ANGLE_EXPLODE_BACK)
self.exploded = True
self.ble.broadcast("CMD", "TOI_stop")
wait(3000)
self.ble.broadcast("CMD", "JKG_wait")
# Define functions run periodically
def periodic(self): # <--- Add required code
# If already exploded, do nothing
if self.exploded:
return
dist = self.hw.sensor_us.distance()
if dist is not None:
print("[US] Distance:", dist)
self.ble.broadcast("CMD", "TOI_open")
self.ble.broadcast("CMD", "MOF_start")
self.ble.broadcast("CMD", "JKG_shlo")
# First detection
if not self.waiting_for_confirm and dist < DIST_TRIGGER:
print("[TOILET] First detection - Waiting for confirm ...")
self.waiting_for_confirm = True
self.confirm_start_time = self.clock.time()
# Confirm detection
if self.waiting_for_confirm:
now = self.clock.time()
if dist < DIST_CONFIRM:
print("[TOILET] Confirmed — EXPLODING!")
self.explode()
self.waiting_for_confirm = False
#self.ble.broadcast("EVENT", "TOI_open")
#self.ble.broadcast("CMD", "TOI_open")
elif now - self.confirm_start_time > 1000:
print("[TOILET] Confirm timeout — Cancelling ...")
self.waiting_for_confirm = False
# Backup-confirm US detection
dist_back = self.hw.sensor_us_back.distance()
if dist_back is not None:
print("[US BACK] Distance:", dist_back)
self.ble.broadcast("CMD", "TOI_open")
self.ble.broadcast("CMD", "MOF_start")
self.ble.broadcast("CMD", "JKG_shlo")
# First detection
if not self.waiting_for_confirm and dist_back < DIST_TRIGGER:
print("[TOILET] First back detection - Waiting for confirm ...")
self.waiting_for_confirm = True
self.confirm_start_time = self.clock.time()
# Confirm detection
if self.waiting_for_confirm:
now_back = self.clock.time()
if dist_back < DIST_CONFIRM:
print("[TOILET] Confirmed — EXPLODING!")
self.explode()
self.waiting_for_confirm = False
#self.ble.broadcast("EVENT", "TOI_open")
#self.ble.broadcast("STATUS", "exploding")
elif now_back - self.confirm_start_time > 1000:
print("[TOILET] Confirm timeout — Cancelling ...")
self.waiting_for_confirm = False
The entire code can be downloaded here.
Volcano
The below are the main code for the BLE broadcast and observe methods (which are common to all SPIKE Prime props) and the rumbling and erupting:
# Send event
def broadcast(self, kind, value):
# Populate message
msg = self.device_name + ":" + kind + ":" + value
payload = msg[:21] # BLE advertisement limit of 21 char
print("[BLE] Sending payload '", payload, "'")
# Send 3 times for reliability
for i in range(3):
self.hub.ble.broadcast(payload)
wait(POLL_INTERVAL)
# Receiving event
def observe(self):
for ch in self.observe_channels:
msg = self.hub.ble.observe(ch)
if msg:
print("[BLE] Receiving message '", msg, "'")
return msg
return None
# Make volcano 'erupt'
def erupt(self):
print("[VOLCANO] ERUPTION triggered!")
self.erupted = True
self.rumbling = False
self.ble.broadcast("EVENT", "erupted")
self.ble.broadcast("CMD", "JKG_shhi")
# Make the IR remote motor move back & forth
def remote_pulse(self, speed):
self.hw.motor_remote.run_angle(speed, ANGLE_REMOTE_PULSE)
self.hw.motor_remote.run_angle(speed, -ANGLE_REMOTE_PULSE)
# Make volcano rumble
def rumble(self):
print("[VOLCANO] Rumble mode")
self.rumbling = True
self.erupted = False
The entire code can be downloaded here.