Push
This commit is contained in:
commit
9dd690c5f4
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# ignore backup files
|
||||||
|
._*
|
||||||
|
# ignore the ESP32 MicroPython binary
|
||||||
|
esp32*.bin
|
||||||
|
# ignore the development config file
|
||||||
|
config-dev.json
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 RoboKoding LTD
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
35
Makefile
Normal file
35
Makefile
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SERIAL_PORT=/dev/ttyUSB0
|
||||||
|
#SERIAL_PORT=/dev/tty.SLAB_USBtoUART
|
||||||
|
#SERIAL_PORT=/dev/tty.wchusbserial1410
|
||||||
|
|
||||||
|
all: flash delay libs config update reset
|
||||||
|
upload: config update reset console
|
||||||
|
|
||||||
|
delay:
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
reset:
|
||||||
|
esptool.py -p $(SERIAL_PORT) --after hard_reset read_mac
|
||||||
|
picocom -b 115200 $(SERIAL_PORT)
|
||||||
|
|
||||||
|
libs:
|
||||||
|
ampy -p $(SERIAL_PORT) put uwebsockets.py
|
||||||
|
ampy -p $(SERIAL_PORT) put debounce.py
|
||||||
|
|
||||||
|
update:
|
||||||
|
ampy -p $(SERIAL_PORT) put hal.py
|
||||||
|
ampy -p $(SERIAL_PORT) put main.py
|
||||||
|
ampy -p $(SERIAL_PORT) put boot.py
|
||||||
|
|
||||||
|
config:
|
||||||
|
ampy -p $(SERIAL_PORT) put config.json
|
||||||
|
|
||||||
|
flash:
|
||||||
|
esptool.py -p $(SERIAL_PORT) --chip esp32 -b 115200 erase_flash
|
||||||
|
esptool.py -p $(SERIAL_PORT) --chip esp32 -b 115200 write_flash --flash_mode dio 0x1000 esp32-*.bin
|
||||||
|
|
||||||
|
console:
|
||||||
|
echo "Ctrl-A + Ctrl-Q to close Picocom"
|
||||||
|
picocom -b 115200 $(SERIAL_PORT)
|
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# sumorobot-firmware
|
||||||
|
|
||||||
|
The software that is running on the SumoRobots
|
||||||
|
|
||||||
|
<img alt="Code" src="https://www.robokoding.com/assets/img/sumorobot_firmware.png" width="50%">
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
* Change the SERIAL_PORT in the Makefile
|
||||||
|
* Add your WiFi networks to the config.json file
|
||||||
|
* Install [Python](https://www.python.org/downloads/)
|
||||||
|
* Install [esptool](https://github.com/espressif/esptool) (to flash MicroPython to the ESP32)
|
||||||
|
* Install [ampy](https://github.com/adafruit/ampy) (for uploading files)
|
||||||
|
* Download [the MicroPython binary](http://micropython.org/download#esp32) to this directory
|
||||||
|
* Upload the MicroPython binary and the SumoRobot firmware to your ESP32 (open a terminal and type: make all)
|
||||||
|
|
||||||
|
# Support
|
||||||
|
If you find our work useful, please consider donating : )
|
||||||
|
[![Donate using Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/robokoding/donate)
|
||||||
|
|
||||||
|
|
||||||
|
# TODOS
|
||||||
|
* variable motor speed control, then more interesting for kids
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
* [K-SPACE MTÜ](https://k-space.ee/)
|
61
boot.py
Normal file
61
boot.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import ujson
|
||||||
|
import network
|
||||||
|
from hal import *
|
||||||
|
from utime import sleep_ms
|
||||||
|
from machine import Timer, Pin
|
||||||
|
import ubinascii
|
||||||
|
|
||||||
|
print("Press Ctrl-C to stop boot script...")
|
||||||
|
sleep_ms(200)
|
||||||
|
|
||||||
|
#Pin(25, Pin.OUT).value(0)
|
||||||
|
|
||||||
|
|
||||||
|
# Open and parse the config file
|
||||||
|
with open("config.json", "r") as config_file:
|
||||||
|
config = ujson.load(config_file)
|
||||||
|
|
||||||
|
# if not config["sumo_id"]:
|
||||||
|
# config["sumo_id"] = ubinascii.hexlify(network.WLAN().config('mac'),':').decode().replace(":","")[6:]
|
||||||
|
# with open("config.part", "w") as config_file:
|
||||||
|
# config_file.write(ujson.dumps(config))
|
||||||
|
# os.rename("config.part", "config.json")
|
||||||
|
|
||||||
|
config["sumo_id"] = ubinascii.hexlify(network.WLAN().config('mac'),':').decode().replace(":","")[6:]
|
||||||
|
|
||||||
|
sleep_ms(500)
|
||||||
|
|
||||||
|
robotName = "Sumo-"+config["sumo_id"]
|
||||||
|
# Initialize the SumoRobot object
|
||||||
|
sumorobot = Sumorobot(config)
|
||||||
|
|
||||||
|
# Indiacte booting with blinking status LED
|
||||||
|
#timer = Timer(0)
|
||||||
|
#sumorobot.toggle_led()
|
||||||
|
#timer.init(period=2000, mode=Timer.PERIODIC, callback=sumorobot.toggle_led)
|
||||||
|
|
||||||
|
# Connect to WiFi
|
||||||
|
wlan = network.WLAN(network.STA_IF)
|
||||||
|
|
||||||
|
# Activate the WiFi interface
|
||||||
|
wlan.active(True)
|
||||||
|
|
||||||
|
wlan.config(dhcp_hostname=robotName)
|
||||||
|
|
||||||
|
# If not already connected
|
||||||
|
if not wlan.isconnected():
|
||||||
|
# Scan for WiFi networks
|
||||||
|
networks = wlan.scan()
|
||||||
|
# Go trough all scanned WiFi networks
|
||||||
|
for network in networks:
|
||||||
|
# Extract the networks SSID
|
||||||
|
ssid = network[0].decode("utf-8")
|
||||||
|
# Check if the SSID is in the config file
|
||||||
|
if ssid in config["wifis"].keys():
|
||||||
|
# Start to connect to the pre-configured network
|
||||||
|
wlan.connect(ssid, config["wifis"][ssid])
|
||||||
|
break
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
import gc
|
||||||
|
gc.collect()
|
25
config.json
Normal file
25
config.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"paly/stop_button_pin": 22,
|
||||||
|
"charging_pin":4,
|
||||||
|
"left_motor_pin":13,
|
||||||
|
"right_motor_pin":15,
|
||||||
|
"led_power_pin": 25,
|
||||||
|
"ultrasonic_echo_pin":14,
|
||||||
|
"ultrasonic_trigger_pin":12,
|
||||||
|
"line_left_pin":32,
|
||||||
|
"line_middle_pin":35,
|
||||||
|
"line_right_pin":34,
|
||||||
|
"sumo_id": "",
|
||||||
|
"firmware_version": 0.3,
|
||||||
|
"left_servo_tuning": 33,
|
||||||
|
"right_servo_tuning": 33,
|
||||||
|
"ultrasonic_distance": 40,
|
||||||
|
"left_line_threshold": 4500,
|
||||||
|
"middle_line_threshold": 4500,
|
||||||
|
"right_line_threshold": 4500,
|
||||||
|
"motors_reverse": 1,
|
||||||
|
"sumo_server": "sumo.koodur.com:80",
|
||||||
|
"wifis": {
|
||||||
|
"SSID": "PAROOL"
|
||||||
|
}
|
||||||
|
}
|
57
debounce.py
Normal file
57
debounce.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#
|
||||||
|
# inspired by: https://forum.micropython.org/viewtopic.php?t=1938#p10931
|
||||||
|
#
|
||||||
|
import micropython
|
||||||
|
|
||||||
|
try:
|
||||||
|
from machine import Timer
|
||||||
|
timer_init = lambda t, p, cb: t.init(period=p, callback=cb)
|
||||||
|
except ImportError:
|
||||||
|
from pyb import Timer
|
||||||
|
timer_init = lambda t, p, cb: t.init(freq=1000 // p, callback=cb)
|
||||||
|
|
||||||
|
# uncomment when debugging callback problems
|
||||||
|
#micropython.alloc_emergency_exception_buf(100)
|
||||||
|
|
||||||
|
|
||||||
|
class DebouncedSwitch:
|
||||||
|
def __init__(self, sw, cb, arg=None, delay=50, tid=4):
|
||||||
|
self.sw = sw
|
||||||
|
# Create references to bound methods beforehand
|
||||||
|
# http://docs.micropython.org/en/latest/pyboard/library/micropython.html#micropython.schedule
|
||||||
|
self._sw_cb = self.sw_cb
|
||||||
|
self._tim_cb = self.tim_cb
|
||||||
|
self._set_cb = getattr(self.sw, 'callback', None) or self.sw.irq
|
||||||
|
self.delay = delay
|
||||||
|
self.tim = Timer(tid)
|
||||||
|
self.callback(cb, arg)
|
||||||
|
|
||||||
|
def sw_cb(self, pin=None):
|
||||||
|
self._set_cb(None)
|
||||||
|
timer_init(self.tim, self.delay, self._tim_cb)
|
||||||
|
|
||||||
|
def tim_cb(self, tim):
|
||||||
|
tim.deinit()
|
||||||
|
if self.sw():
|
||||||
|
micropython.schedule(self.cb, self.arg)
|
||||||
|
self._set_cb(self._sw_cb if self.cb else None)
|
||||||
|
|
||||||
|
def callback(self, cb, arg=None):
|
||||||
|
self.tim.deinit()
|
||||||
|
self.cb = cb
|
||||||
|
self.arg = arg
|
||||||
|
self._set_cb(self._sw_cb if cb else None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pyb(ledno=1):
|
||||||
|
import pyb
|
||||||
|
sw = pyb.Switch()
|
||||||
|
led = pyb.LED(ledno)
|
||||||
|
return DebouncedSwitch(sw, lambda l: l.toggle(), led)
|
||||||
|
|
||||||
|
|
||||||
|
def test_machine(swpin=2, ledpin=16):
|
||||||
|
from machine import Pin
|
||||||
|
sw = Pin(swpin, Pin.IN)
|
||||||
|
led = Pin(ledpin, Pin.OUT)
|
||||||
|
return DebouncedSwitch(sw, lambda l: l.value(not l.value()), led)
|
257
hal.py
Normal file
257
hal.py
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import os
|
||||||
|
import ujson
|
||||||
|
from utime import sleep_us, sleep_ms
|
||||||
|
from machine import Pin, PWM, ADC, time_pulse_us, deepsleep
|
||||||
|
import random
|
||||||
|
|
||||||
|
# LEDs
|
||||||
|
STATUS = 0
|
||||||
|
#OPPONENT = 1
|
||||||
|
#LEFT_LINE = 2
|
||||||
|
#RIGHT_LINE = 3
|
||||||
|
|
||||||
|
# Directions
|
||||||
|
STOP = 0
|
||||||
|
LEFT = 1
|
||||||
|
MIDDLE = 6
|
||||||
|
RIGHT = 2
|
||||||
|
SEARCH = 3
|
||||||
|
FORWARD = 4
|
||||||
|
BACKWARD = 5
|
||||||
|
|
||||||
|
|
||||||
|
#states
|
||||||
|
MOVING = 0
|
||||||
|
STANDBY = 1
|
||||||
|
|
||||||
|
class Sumorobot(object):
|
||||||
|
# Constructor
|
||||||
|
def __init__(self, config = None):
|
||||||
|
# Config file
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.state = STANDBY
|
||||||
|
|
||||||
|
self.name = "Sumo-"+self.config["sumo_id"]
|
||||||
|
|
||||||
|
# Ultrasonic distance sensor
|
||||||
|
self.echo = Pin(self.config["ultrasonic_echo_pin"], Pin.IN)
|
||||||
|
self.trigger = Pin(self.config["ultrasonic_trigger_pin"], Pin.OUT)
|
||||||
|
|
||||||
|
# Servo PWM-s
|
||||||
|
self.pwm_left = PWM(Pin(self.config["left_motor_pin"]), freq=50, duty=0)
|
||||||
|
self.pwm_right = PWM(Pin(self.config["right_motor_pin"]), freq=50, duty=0)
|
||||||
|
|
||||||
|
# Bottom status LED
|
||||||
|
self.led_power = Pin(self.config["led_power_pin"], Pin.OUT)
|
||||||
|
self.charging = Pin(self.config["charging_pin"]);
|
||||||
|
|
||||||
|
self.playStop = Pin(self.config["paly/stop_button_pin"], Pin.IN, Pin.PULL_UP)
|
||||||
|
|
||||||
|
|
||||||
|
self.led_power.value(0)
|
||||||
|
|
||||||
|
self.adc_line_left = ADC(Pin(32))
|
||||||
|
self.adc_line_middle = ADC(Pin(35))
|
||||||
|
self.adc_line_right = ADC(Pin(34))
|
||||||
|
|
||||||
|
# Set reference voltage to 3.3V
|
||||||
|
self.adc_line_left.atten(ADC.ATTN_11DB)
|
||||||
|
self.adc_line_right.atten(ADC.ATTN_11DB)
|
||||||
|
self.adc_line_middle.atten(ADC.ATTN_11DB)
|
||||||
|
|
||||||
|
# To smooth out ultrasonic sensor value
|
||||||
|
self.opponent_score = 0
|
||||||
|
|
||||||
|
# For terminating sleep
|
||||||
|
self.terminate = False
|
||||||
|
|
||||||
|
# For search mode
|
||||||
|
self.search = 0
|
||||||
|
self.search_counter = 0
|
||||||
|
|
||||||
|
# Memorise previous servo speeds
|
||||||
|
self.prev_speed = {LEFT: 0, RIGHT: 0}
|
||||||
|
|
||||||
|
#saving line sensor valus, to read once in 50ms loop
|
||||||
|
self.line_left = 0
|
||||||
|
self.line_right = 0;
|
||||||
|
self.line_middle = 0;
|
||||||
|
|
||||||
|
self.speedForward = 100 if self.config["motors_reverse"] == 0 else -100
|
||||||
|
self.speedReverse = -100 if self.config["motors_reverse"] == 0 else 100
|
||||||
|
|
||||||
|
|
||||||
|
# Function to get distance (cm) from the object in front of the SumoRobot
|
||||||
|
def get_opponent_distance(self):
|
||||||
|
# Send a pulse
|
||||||
|
self.trigger.value(0)
|
||||||
|
sleep_us(5)
|
||||||
|
self.trigger.value(1)
|
||||||
|
sleep_us(10)
|
||||||
|
self.trigger.value(0)
|
||||||
|
# Wait for the pulse and calculate the distance
|
||||||
|
return (time_pulse_us(self.echo, 1, 30000) / 2) / 29.1
|
||||||
|
|
||||||
|
# Function to get boolean if there is something in front of the SumoRobot
|
||||||
|
def is_opponent(self):
|
||||||
|
# Get the opponent distance
|
||||||
|
self.opponent_distance = self.get_opponent_distance()
|
||||||
|
# When the opponent is close and the ping actually returned
|
||||||
|
if self.opponent_distance < self.config["ultrasonic_distance"] and self.opponent_distance > 0:
|
||||||
|
# When not maximum score
|
||||||
|
if self.opponent_score < 5:
|
||||||
|
# Increase the opponent score
|
||||||
|
self.opponent_score += 1
|
||||||
|
# When no opponent was detected
|
||||||
|
else:
|
||||||
|
# When not lowest score
|
||||||
|
if self.opponent_score > 0:
|
||||||
|
# Decrease the opponent score
|
||||||
|
self.opponent_score -= 1
|
||||||
|
|
||||||
|
# When the sensor saw something more than 2 times
|
||||||
|
opponent = True if self.opponent_score > 2 else False
|
||||||
|
|
||||||
|
# Trigger opponent LED
|
||||||
|
#self.set_led(OPPONENT, opponent)
|
||||||
|
|
||||||
|
return opponent
|
||||||
|
|
||||||
|
# Function to update line calibration and write it to the config file
|
||||||
|
def calibrate_line(self):
|
||||||
|
# Read the line sensor values
|
||||||
|
self.config["left_line_threshold"] = self.adc_line_left.read()
|
||||||
|
self.config["right_line_threshold"] = self.adc_line_right.read()
|
||||||
|
self.config["middle_line_threshold"] = self.adc_line_middle.read()
|
||||||
|
# Update the config file
|
||||||
|
with open("config.part", "w") as config_file:
|
||||||
|
config_file.write(ujson.dumps(config))
|
||||||
|
os.rename("config.part", "config.json")
|
||||||
|
|
||||||
|
# Function to get light inensity from the phototransistors
|
||||||
|
def get_line(self, dir):
|
||||||
|
# Check if the direction is valid
|
||||||
|
assert dir in (LEFT, RIGHT, MIDDLE)
|
||||||
|
|
||||||
|
# Return the given line sensor value
|
||||||
|
if dir == LEFT:
|
||||||
|
return self.adc_line_left.read()
|
||||||
|
elif dir == RIGHT:
|
||||||
|
return self.adc_line_right.read()
|
||||||
|
elif dir == MIDDLE:
|
||||||
|
return self.adc_line_middle.read()
|
||||||
|
|
||||||
|
def is_line(self, dir):
|
||||||
|
# Check if the direction is valid
|
||||||
|
assert dir in (LEFT, RIGHT, MIDDLE)
|
||||||
|
|
||||||
|
# Return the given line sensor value, storing it to variable, not to ask double in 50ms time loop
|
||||||
|
if dir == LEFT:
|
||||||
|
self.line_left = self.get_line(LEFT)
|
||||||
|
line = abs(self.line_left - self.config["left_line_threshold"]) > 1000
|
||||||
|
#self.set_led(LEFT_LINE, line)
|
||||||
|
return line
|
||||||
|
elif dir == RIGHT:
|
||||||
|
self.line_right = self.get_line(RIGHT)
|
||||||
|
line = abs(self.line_right - self.config["right_line_threshold"]) > 1000
|
||||||
|
#self.set_led(RIGHT_LINE, line)
|
||||||
|
return line
|
||||||
|
elif dir == MIDDLE:
|
||||||
|
self.line_middle = self.get_line(MIDDLE)
|
||||||
|
line = abs(self.line_middle - self.config["middle_line_threshold"]) > 1000
|
||||||
|
return line
|
||||||
|
|
||||||
|
def set_servo(self, dir, speed):
|
||||||
|
# Check if the direction is valid
|
||||||
|
assert dir in (LEFT, RIGHT)
|
||||||
|
# Check if the speed is valid
|
||||||
|
assert speed <= 100 and speed >= -100
|
||||||
|
|
||||||
|
# When the speed didn't change
|
||||||
|
if speed == self.prev_speed[dir]:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Record the new speed
|
||||||
|
self.prev_speed[dir] = speed
|
||||||
|
|
||||||
|
# Set the given servo speed
|
||||||
|
if dir == LEFT:
|
||||||
|
if speed == 0:
|
||||||
|
self.pwm_left.duty(0)
|
||||||
|
else:
|
||||||
|
# -100 ... 100 to 33 .. 102
|
||||||
|
self.pwm_left.duty(int(33 + self.config["left_servo_tuning"] + speed * 33 / 100))
|
||||||
|
elif dir == RIGHT:
|
||||||
|
if speed == 0:
|
||||||
|
self.pwm_right.duty(0)
|
||||||
|
else:
|
||||||
|
# -100 ... 100 to 33 .. 102
|
||||||
|
self.pwm_right.duty(int(33 + self.config["right_servo_tuning"] + speed * 33 / 100))
|
||||||
|
|
||||||
|
def move(self, dir):
|
||||||
|
# Check if the direction is valid
|
||||||
|
assert dir in (SEARCH, STOP, RIGHT, LEFT, BACKWARD, FORWARD)
|
||||||
|
# Go to the given direction
|
||||||
|
|
||||||
|
if dir == STOP:
|
||||||
|
self.set_state(STANDBY)
|
||||||
|
else:
|
||||||
|
self.set_state(MOVING)
|
||||||
|
|
||||||
|
if dir == STOP:
|
||||||
|
self.set_servo(LEFT, 0)
|
||||||
|
self.set_servo(RIGHT, 0)
|
||||||
|
elif dir == LEFT:
|
||||||
|
self.set_servo(LEFT, self.speedReverse)
|
||||||
|
self.set_servo(RIGHT, self.speedReverse)
|
||||||
|
elif dir == RIGHT:
|
||||||
|
self.set_servo(LEFT, self.speedForward)
|
||||||
|
self.set_servo(RIGHT, self.speedForward)
|
||||||
|
elif dir == SEARCH:
|
||||||
|
# Change search mode after X seconds
|
||||||
|
if self.search_counter == 50:
|
||||||
|
self.search = random.randrange(0,3)
|
||||||
|
self.search_counter = 0
|
||||||
|
#self.search = 0 if self.search > 2 else self.search
|
||||||
|
# When in search mode
|
||||||
|
if self.search == 0:
|
||||||
|
# Go forward
|
||||||
|
self.set_servo(LEFT, self.speedForward)
|
||||||
|
self.set_servo(RIGHT, self.speedReverse)
|
||||||
|
elif self.search == 1:
|
||||||
|
# Go left
|
||||||
|
self.set_servo(LEFT, self.speedReverse)
|
||||||
|
self.set_servo(RIGHT, self.speedReverse)
|
||||||
|
elif self.search == 2:
|
||||||
|
self.set_servo(LEFT, self.speedForward)
|
||||||
|
self.set_servo(RIGHT, self.speedForward)
|
||||||
|
# Increase search counter
|
||||||
|
self.search_counter += 1
|
||||||
|
elif dir == FORWARD:
|
||||||
|
self.set_servo(LEFT, self.speedForward)
|
||||||
|
self.set_servo(RIGHT, self.speedReverse)
|
||||||
|
elif dir == BACKWARD:
|
||||||
|
self.set_servo(LEFT, self.speedReverse)
|
||||||
|
self.set_servo(RIGHT, self.speedForward)
|
||||||
|
|
||||||
|
|
||||||
|
def sleep(self, delay):
|
||||||
|
# Check for valid delay
|
||||||
|
assert delay > 0
|
||||||
|
|
||||||
|
# Split the delay into 50ms chunks
|
||||||
|
for j in range(0, delay, 50):
|
||||||
|
# Check for forceful termination
|
||||||
|
if self.terminate:
|
||||||
|
# Terminate the delay
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
sleep_ms(50)
|
||||||
|
|
||||||
|
def set_state(self,value):
|
||||||
|
assert value in (MOVING,STANDBY)
|
||||||
|
self.state = value
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
return self.state
|
179
main.py
Normal file
179
main.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import _thread
|
||||||
|
import ubinascii
|
||||||
|
import ujson
|
||||||
|
import uwebsockets
|
||||||
|
import os
|
||||||
|
from debounce import DebouncedSwitch
|
||||||
|
|
||||||
|
# Code to execute
|
||||||
|
ast = ""
|
||||||
|
executeCode = False
|
||||||
|
|
||||||
|
# Scope, info to be sent to the client
|
||||||
|
scope = dict()
|
||||||
|
|
||||||
|
def buttoncallback(p=True):
|
||||||
|
global executeCode
|
||||||
|
|
||||||
|
sumorobot.terminate = executeCode
|
||||||
|
sumorobot.led_power.value(not executeCode)
|
||||||
|
sleep_ms(50)
|
||||||
|
executeCode = not executeCode
|
||||||
|
print(executeCode)
|
||||||
|
|
||||||
|
def writeCodeTofile(data):
|
||||||
|
with open("code.part", "w") as code_file:
|
||||||
|
code_file.write(ujson.dumps(data))
|
||||||
|
sleep_ms(50)
|
||||||
|
os.rename("code.part", "code")
|
||||||
|
sleep_ms(50)
|
||||||
|
|
||||||
|
|
||||||
|
def step():
|
||||||
|
global scope
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# Update scope
|
||||||
|
scope = dict(
|
||||||
|
line_left = sumorobot.line_left,
|
||||||
|
line_right = sumorobot.line_right,
|
||||||
|
line_middle = sumorobot.line_middle,
|
||||||
|
opponent = sumorobot.get_opponent_distance(),
|
||||||
|
battery_voltage = 0,
|
||||||
|
state = executeCode
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute code
|
||||||
|
if(executeCode):
|
||||||
|
exec(ast)
|
||||||
|
|
||||||
|
#sumorobot.playStop.irq(trigger=Pin.IRQ_RISING, handler=buttoncallback)
|
||||||
|
sw = DebouncedSwitch(sumorobot.playStop, buttoncallback, "dummy")
|
||||||
|
|
||||||
|
# When robot was stopped
|
||||||
|
if sumorobot.terminate:
|
||||||
|
# Disable forceful termination of delays in code
|
||||||
|
sumorobot.terminate = False
|
||||||
|
# Stop the robot
|
||||||
|
sumorobot.move(STOP)
|
||||||
|
# Leave time to process WebSocket commands
|
||||||
|
sleep_ms(50)
|
||||||
|
|
||||||
|
def ws_handler():
|
||||||
|
global executeCode
|
||||||
|
global ast
|
||||||
|
global has_wifi_connection
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# When WiFi has just been reconnected
|
||||||
|
if wlan.isconnected() and not has_wifi_connection:
|
||||||
|
#conn = uwebsockets.connect(url)
|
||||||
|
#sumorobot.set_led(STATUS, True)
|
||||||
|
has_wifi_connection = True
|
||||||
|
# When WiFi has just been disconnected
|
||||||
|
elif not wlan.isconnected() and has_wifi_connection:
|
||||||
|
#sumorobot.set_led(STATUS, False)
|
||||||
|
has_wifi_connection = False
|
||||||
|
elif not wlan.isconnected():
|
||||||
|
# Continue to wait for a WiFi connection
|
||||||
|
continue
|
||||||
|
|
||||||
|
try: # Try to read from the WebSocket
|
||||||
|
data = conn.recv()
|
||||||
|
except Exception as e: # Socket timeout, no data received
|
||||||
|
# Continue to try to read data
|
||||||
|
#print(e)
|
||||||
|
|
||||||
|
#if not conn.open:
|
||||||
|
conn = uwebsockets.connect(url)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# When an empty frame was received
|
||||||
|
if not data:
|
||||||
|
# Continue to receive data
|
||||||
|
continue
|
||||||
|
elif b'forward' in data:
|
||||||
|
ast = ""
|
||||||
|
sumorobot.move(FORWARD)
|
||||||
|
elif b'backward' in data:
|
||||||
|
ast = ""
|
||||||
|
sumorobot.move(BACKWARD)
|
||||||
|
elif b'right' in data:
|
||||||
|
ast = ""
|
||||||
|
sumorobot.move(RIGHT)
|
||||||
|
elif b'left' in data:
|
||||||
|
ast = ""
|
||||||
|
sumorobot.move(LEFT)
|
||||||
|
elif b'ping' in data:
|
||||||
|
#conn.send(ujson.dumps({"cmd":"sensord", "data":ujson.dumps(scope)}))
|
||||||
|
conn.send(ujson.dumps({"cmd":"sensors", "data":ujson.loads(repr(scope).replace("'", '"').replace("False","false").replace("True","true"))}))
|
||||||
|
elif b'code' in data:
|
||||||
|
executeCode = False
|
||||||
|
try:
|
||||||
|
data = ujson.loads(data)
|
||||||
|
writeCodeTofile(data['val'])
|
||||||
|
conn.send(ujson.dumps({"cmd":"code_upload", "status":True}))
|
||||||
|
except Exception as e:
|
||||||
|
conn.send(ujson.dumps({"cmd":"code_upload", "status":False}))
|
||||||
|
continue
|
||||||
|
data['val'] = data['val'].replace(";;", "\n")
|
||||||
|
print(data['val'])
|
||||||
|
ast = compile(data['val'], "snippet", "exec")
|
||||||
|
elif b'start' in data:
|
||||||
|
#buttoncallback()
|
||||||
|
executeCode = True
|
||||||
|
sumorobot.led_power.value(1)
|
||||||
|
sleep_ms(50)
|
||||||
|
data = ujson.loads(data)
|
||||||
|
writeCodeTofile(data['val'])
|
||||||
|
data['val'] = data['val'].replace(";;", "\n")
|
||||||
|
ast = compile(data['val'], "snippet", "exec")
|
||||||
|
elif b'stop' in data:
|
||||||
|
#ast = ""
|
||||||
|
sumorobot.led_power.value(0)
|
||||||
|
executeCode = False
|
||||||
|
sumorobot.move(STOP)
|
||||||
|
# for terminating delays in code
|
||||||
|
sumorobot.terminate = True
|
||||||
|
elif b'calibrate_line' in data:
|
||||||
|
sumorobot.led_power.value(1)
|
||||||
|
sleep_ms(50)
|
||||||
|
sumorobot.calibrate_line()
|
||||||
|
sumorobot.led_power.value(0)
|
||||||
|
elif b'Gone' in data:
|
||||||
|
print("server said 410 Gone, attempting to reconnect...")
|
||||||
|
#conn = uwebsockets.connect(url)
|
||||||
|
else:
|
||||||
|
print("unknown cmd:", data)
|
||||||
|
|
||||||
|
# Wait for WiFi to get connected
|
||||||
|
while not wlan.isconnected():
|
||||||
|
sleep_ms(100)
|
||||||
|
|
||||||
|
# Connect to the websocket
|
||||||
|
url = "ws://%s/p2p/sumo-%s/browser/" % (config['sumo_server'], config['sumo_id'])
|
||||||
|
conn = uwebsockets.connect(url)
|
||||||
|
|
||||||
|
# Set X seconds timeout for socket reads
|
||||||
|
conn.settimeout(3)
|
||||||
|
|
||||||
|
# Stop bootup blinking
|
||||||
|
#timer.deinit()
|
||||||
|
|
||||||
|
# WiFi is connected
|
||||||
|
has_wifi_connection = True
|
||||||
|
# Indicate that the WebSocket is connected
|
||||||
|
#sumorobot.set_led(STATUS, True)
|
||||||
|
|
||||||
|
if('code' in os.listdir()):
|
||||||
|
with open("code", "r") as code_file:
|
||||||
|
data = ujson.load(code_file).replace(";;", "\n")
|
||||||
|
ast = compile(data, "snippet", "exec")
|
||||||
|
#print(ast)
|
||||||
|
|
||||||
|
|
||||||
|
# Start the code processing thread
|
||||||
|
_thread.start_new_thread(step, ())
|
||||||
|
# Start the Websocket processing thread
|
||||||
|
_thread.start_new_thread(ws_handler, ())
|
241
uwebsockets.py
Normal file
241
uwebsockets.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
"""
|
||||||
|
Websockets client for micropython
|
||||||
|
|
||||||
|
Based very heavily on
|
||||||
|
https://github.com/aaugustin/websockets/blob/master/websockets/client.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
#import usocket as socket
|
||||||
|
import os
|
||||||
|
import ure as re
|
||||||
|
import urandom as random
|
||||||
|
import ustruct as struct
|
||||||
|
import usocket as socket
|
||||||
|
import ubinascii as binascii
|
||||||
|
from ucollections import namedtuple
|
||||||
|
|
||||||
|
# Opcodes
|
||||||
|
OP_CONT = const(0x0)
|
||||||
|
OP_TEXT = const(0x1)
|
||||||
|
OP_BYTES = const(0x2)
|
||||||
|
OP_CLOSE = const(0x8)
|
||||||
|
OP_PING = const(0x9)
|
||||||
|
OP_PONG = const(0xa)
|
||||||
|
|
||||||
|
# Close codes
|
||||||
|
CLOSE_OK = const(1000)
|
||||||
|
CLOSE_GOING_AWAY = const(1001)
|
||||||
|
CLOSE_PROTOCOL_ERROR = const(1002)
|
||||||
|
CLOSE_DATA_NOT_SUPPORTED = const(1003)
|
||||||
|
CLOSE_BAD_DATA = const(1007)
|
||||||
|
CLOSE_POLICY_VIOLATION = const(1008)
|
||||||
|
CLOSE_TOO_BIG = const(1009)
|
||||||
|
CLOSE_MISSING_EXTN = const(1010)
|
||||||
|
CLOSE_BAD_CONDITION = const(1011)
|
||||||
|
|
||||||
|
URL_RE = re.compile(r'ws://([A-Za-z0-9\-\.]+)(?:\:([0-9]+))?(/.+)?')
|
||||||
|
URI = namedtuple('URI', ('hostname', 'port', 'path'))
|
||||||
|
|
||||||
|
def urlparse(uri):
|
||||||
|
match = URL_RE.match(uri)
|
||||||
|
if match:
|
||||||
|
return URI(match.group(1), int(match.group(2)), match.group(3))
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid URL: %s" % uri)
|
||||||
|
|
||||||
|
class Websocket:
|
||||||
|
is_client = False
|
||||||
|
|
||||||
|
def __init__(self, sock):
|
||||||
|
self._sock = sock
|
||||||
|
self.open = True
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def settimeout(self, timeout):
|
||||||
|
self._sock.settimeout(timeout)
|
||||||
|
|
||||||
|
def read_frame(self, max_size=None):
|
||||||
|
# Frame header
|
||||||
|
byte1, byte2 = struct.unpack('!BB', self._sock.read(2))
|
||||||
|
|
||||||
|
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
|
||||||
|
fin = bool(byte1 & 0x80)
|
||||||
|
opcode = byte1 & 0x0f
|
||||||
|
|
||||||
|
# Byte 2: MASK(1) LENGTH(7)
|
||||||
|
mask = bool(byte2 & (1 << 7))
|
||||||
|
length = byte2 & 0x7f
|
||||||
|
|
||||||
|
if length == 126: # Magic number, length header is 2 bytes
|
||||||
|
length, = struct.unpack('!H', self._sock.read(2))
|
||||||
|
elif length == 127: # Magic number, length header is 8 bytes
|
||||||
|
length, = struct.unpack('!Q', self._sock.read(8))
|
||||||
|
|
||||||
|
if mask: # Mask is 4 bytes
|
||||||
|
mask_bits = self._sock.read(4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self._sock.read(length)
|
||||||
|
except MemoryError:
|
||||||
|
# We can't receive this many bytes, close the socket
|
||||||
|
self.close(code=CLOSE_TOO_BIG)
|
||||||
|
return True, OP_CLOSE, None
|
||||||
|
|
||||||
|
if mask:
|
||||||
|
data = bytes(b ^ mask_bits[i % 4]
|
||||||
|
for i, b in enumerate(data))
|
||||||
|
|
||||||
|
return fin, opcode, data
|
||||||
|
|
||||||
|
def write_frame(self, opcode, data=b''):
|
||||||
|
fin = True
|
||||||
|
mask = self.is_client # messages sent by client are masked
|
||||||
|
|
||||||
|
length = len(data)
|
||||||
|
|
||||||
|
# Frame header
|
||||||
|
# Byte 1: FIN(1) _(1) _(1) _(1) OPCODE(4)
|
||||||
|
byte1 = 0x80 if fin else 0
|
||||||
|
byte1 |= opcode
|
||||||
|
|
||||||
|
# Byte 2: MASK(1) LENGTH(7)
|
||||||
|
byte2 = 0x80 if mask else 0
|
||||||
|
|
||||||
|
if length < 126: # 126 is magic value to use 2-byte length header
|
||||||
|
byte2 |= length
|
||||||
|
self._sock.write(struct.pack('!BB', byte1, byte2))
|
||||||
|
|
||||||
|
elif length < (1 << 16): # Length fits in 2-bytes
|
||||||
|
byte2 |= 126 # Magic code
|
||||||
|
self._sock.write(struct.pack('!BBH', byte1, byte2, length))
|
||||||
|
|
||||||
|
elif length < (1 << 64):
|
||||||
|
byte2 |= 127 # Magic code
|
||||||
|
self._sock.write(struct.pack('!BBQ', byte1, byte2, length))
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
if mask: # Mask is 4 bytes
|
||||||
|
mask_bits = struct.pack('!I', random.getrandbits(32))
|
||||||
|
self._sock.write(mask_bits)
|
||||||
|
|
||||||
|
data = bytes(b ^ mask_bits[i % 4]
|
||||||
|
for i, b in enumerate(data))
|
||||||
|
|
||||||
|
self._sock.write(data)
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
assert self.open
|
||||||
|
|
||||||
|
while self.open:
|
||||||
|
try:
|
||||||
|
fin, opcode, data = self.read_frame()
|
||||||
|
except ValueError:
|
||||||
|
self._close()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not fin:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
if opcode == OP_TEXT:
|
||||||
|
return data
|
||||||
|
elif opcode == OP_BYTES:
|
||||||
|
return data
|
||||||
|
elif opcode == OP_CLOSE:
|
||||||
|
self._close()
|
||||||
|
return
|
||||||
|
elif opcode == OP_PONG:
|
||||||
|
# Ignore this frame, keep waiting for a data frame
|
||||||
|
continue
|
||||||
|
elif opcode == OP_PING:
|
||||||
|
# We need to send a pong frame
|
||||||
|
self.write_frame(OP_PONG, data)
|
||||||
|
# And then wait to receive
|
||||||
|
continue
|
||||||
|
elif opcode == OP_CONT:
|
||||||
|
# This is a continuation of a previous frame
|
||||||
|
raise NotImplementedError(opcode)
|
||||||
|
else:
|
||||||
|
raise ValueError(opcode)
|
||||||
|
|
||||||
|
def send(self, buf):
|
||||||
|
assert self.open
|
||||||
|
|
||||||
|
if isinstance(buf, str):
|
||||||
|
opcode = OP_TEXT
|
||||||
|
buf = buf.encode('utf-8')
|
||||||
|
elif isinstance(buf, bytes):
|
||||||
|
opcode = OP_BYTES
|
||||||
|
else:
|
||||||
|
raise TypeError()
|
||||||
|
|
||||||
|
self.write_frame(opcode, buf)
|
||||||
|
|
||||||
|
def close(self, code=CLOSE_OK, reason=''):
|
||||||
|
if not self.open:
|
||||||
|
return
|
||||||
|
|
||||||
|
buf = struct.pack('!H', code) + reason.encode('utf-8')
|
||||||
|
|
||||||
|
self.write_frame(OP_CLOSE, buf)
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
self.open = False
|
||||||
|
self._sock.close()
|
||||||
|
|
||||||
|
class WebsocketClient(Websocket):
|
||||||
|
is_client = True
|
||||||
|
|
||||||
|
def connect(uri):
|
||||||
|
"""
|
||||||
|
Connect a websocket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Parse the given WebSocket URI
|
||||||
|
uri = urlparse(uri)
|
||||||
|
assert uri
|
||||||
|
|
||||||
|
# Connect the socket
|
||||||
|
sock = socket.socket()
|
||||||
|
addr = socket.getaddrinfo(uri.hostname, uri.port)
|
||||||
|
sock.connect(addr[0][4])
|
||||||
|
|
||||||
|
# Sec-WebSocket-Key is 16 bytes of random base64 encoded
|
||||||
|
key = binascii.b2a_base64(os.urandom(16))[:-1]
|
||||||
|
|
||||||
|
# WebSocket initiation headers
|
||||||
|
headers = [
|
||||||
|
b'GET %s HTTP/1.1' % uri.path or '/',
|
||||||
|
b'Upgrade: websocket',
|
||||||
|
b'Connection: Upgrade',
|
||||||
|
b'Host: %s:%s' % (uri.hostname, uri.port),
|
||||||
|
b'Origin: http://%s:%s' % (uri.hostname, uri.port),
|
||||||
|
b'Sec-WebSocket-Key: ' + key,
|
||||||
|
b'Sec-WebSocket-Version: 13',
|
||||||
|
b'',
|
||||||
|
b''
|
||||||
|
]
|
||||||
|
|
||||||
|
# Concatenate the headers and add new lines
|
||||||
|
data = b'\r\n'.join(headers)
|
||||||
|
|
||||||
|
# Send the WebSocket initiation packet
|
||||||
|
sock.send(data)
|
||||||
|
|
||||||
|
# Check for the WebSocket response header
|
||||||
|
header = sock.readline()[:-2]
|
||||||
|
assert header == b'HTTP/1.1 101 Switching Protocols', header
|
||||||
|
|
||||||
|
# We don't (currently) need these headers
|
||||||
|
# FIXME: should we check the return key?
|
||||||
|
while header:
|
||||||
|
header = sock.readline()[:-2]
|
||||||
|
|
||||||
|
return WebsocketClient(sock)
|
Loading…
Reference in New Issue
Block a user