Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
| ||||||
| ||||||
|
Teaching your child how to write can be both fun and frustrating. Making this activity cooler will go a long way in keeping your youngster more focused on the task. And what can be cooler than a robot?... A Lego robot!
Let's set up some goals for this project:
- Build a Lego printer. It should be able to use a normal black marker pen to write. The movement should be as close to a human's as possible.
- Program the printer to write the letters of the alphabet. The strokes should resemble the strokes a normal person would do.
- Integrate with Amazon Alexa. The indications on how to draw a letter should be given by Alexa by speech. Also create a quiz mode where the printer draws a random letter and the child tries to guess the correct answer.
- As the motors are loud, prevent running them at the same time with Alexa speaking.
The x and y movement is done using a similar build with the one of the extensible legs from the Lego 42097 set (Compact Crawler Crane):
Two of them are put together, one in the extension of the other:
Both worm gears are linked with the same shaft, which in turn is driven by a Large EV3 Motor:
The motor doesn't drive the shaft directly, but through a clutch gear (part number 6198486), then a normal gear (part 4514558) and then in a 90 degrees angle using two 4-tooth gears (part 4248204).
This protects the lego parts from any mistakes in driving the motors.
The pen is hold between two large wheels with tires on them:
The wheel that moves the pen is not driven directly but by a chain of four toothed gears to reduce the rpm and increase the torque:
This is necessary as I could not obtain a good result by driving it directly from the medium motor. Small errors in the rotation (even 1 degree) meant the pen is either too low or too high and not touching the paper. The first gear (from the motor) is a 12 tooth and the one driving the wheel is 36, so a 3x decrease in rpm.
The "y" part is on wheels as it needs to slide. The "x" part is fixed.
"Y", fully assembled:
And "X":
- Setup your EV3 Brick and VS Code: https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-setup-17300f
I have VS Code configured to upload to the EV3 these types of files;
"ev3devBrowser.download.include": "{**/*.py,**/*.ini,**/*.sh,,**/*.svg}",
Make sure you have *.svg in the list otherwise you will not be able to run the programs.
- Format for the letters:
We will use the SVG format to represent how to draw the letters of the alphabet.
Lets take letter "B" as an example:
The indications would be along the lines:
- Start at the top and go down to the bottom
- Go back to the top and draw a curved line to the middle
- Then draw a curved line to the bottom
We can represent this information in this format:
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<rect width="100" height="100" fill="beige"/>
<line x1="30" y1="10" x2="30" y2="90" stroke="black" >
<title>Start at the top and draw a straight line all the way to the bottom.</title>
</line>
<path d="M 30 10 A 35 20 0 1 1 30 50" fill="black" fill-opacity="0.1" stroke="black">
<title>Go back at the top and draw a curved line to the middle.</title>
</path>
<path d="M 30 50 A 40 20 0 1 1 30 90" fill="black" fill-opacity="0.1" stroke="black">
<title>Then draw a curved line to the bottom</title>
</path>
</svg>
I created a small program to simulate the printer. This records all the movements the printer would do and creates an image with what the printer would draw on a paper. It helps a lot with experimentation as it is much quicker to run. Please check the "Printer Simulation" file from the project's attachments.
Let's run it for letter B:
Note: The (0, 0)origin point is in the upper left corner with x growing to the right and y growing down. Some formulas have flipped signs than the ones from the normal cartesian coordinate system!
Anatomy of the program ("Printer Simulation" python file):
- The SVG file is parsed.
- The program only knows how to interpret <line/> and <path/>. From <path> it only knows M - move to, L - line and A - arc. More info here.
- The line (draw_line) is straight forward - it computes the derivative (slope) from the line's formula to compute the speed for the motors.
- For the arc the following things need to happen first:
1. Given two points ((x, y) and (dx, dy)) we have to find the ellipse that will pass through both of them and have the horizontal radius of rad_x and vertical radius of rad_y.
The format for M and A are:
"M x y"
"A rx ry x-axis-rotation large-arc-flag sweep-flag x y"
- The general solution (if exists) consists of two ellipses. These are found by solving a system of two quadratic equations (the ellipse's parametric equation for both points). The function solve_ellipse does that. I tried to use SymPy for solving it but it runs very slow on the EV3 brick (around 30s to solve one system of equations).
- From these two ellipses there are four possible paths (arcs). The correct one is chosen using large-arc-flag and sweep-flag attributes. More info here.
- Beginning from the "start" angle and until the "end" angle is reached we compute the derivative for both x and y for finding the speeds the motors need to run, run both of them of 0.1 seconds and repeat the process. This is similar with the PID algorithm, just without the derivative part of the error. We only use the integral part of the error to correct the path as the curves are pretty simple and the results are good enough.
Now lets run it on the real printer - see ("Draw letter B using EV3" python file)
Alexa IntegrationStep 0 - Copy everything from the attachments to a VS Code project.
It should look like this:
Make sure to unzip the SVG zip in the svg directory:
Ignore package-lock.json and node_modules/
Step 1 - Connect the EV3 Brick and Alexa: https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-mission-1-f31925
Step 2 - Create a new skill in Alexa developer console and choose custom skill and Alexa-Hosted (Node.js): https://www.hackster.io/alexagadgets/lego-mindstorms-voice-challenge-mission-3-4ed812
Step 3 - Go to "Build" tab and copy the model (is provided in attachments) to the JSON Editor and save. Also make sure you have the "Custom Interface Controller" and "Auto Delegation" settings on. You can find them in "Interfaces" tab. Save and build the model.
Step 4 - Go to "Code" tab and create the files like in this image. If they already exist, then copy the code from attachments over the hello world that Alexa created. Save and deploy.
Step 5 - Go to "Test" tab and enable skill testing.
Step 6 - Run the script3r.py file on the EV3 brick with VS Code. (Check the tutorial linked)
Details about the code:
Because the motors are quite noisy we don't want Alexa to speak when they are running. This is why we will not just enable custom events but also speech data in the ini file:
[GadgetSettings]
amazonId = YOUR_OWN_AMAZON_ID
alexaGadgetSecret = YOUR_OWN_SECRET
[GadgetCapabilities]
Custom.Mindstorms.Gadget = 1.0
Alexa.Gadget.SpeechData = 1.0 - viseme
After a "speak" command is sent from EV3 to Alexa, we will wait for silence. This is done like this:
def on_alexa_gadget_speechdata_speechmarks(self, directive):
try:
if self._waiting_for_speech:
if directive.payload.speechmarksData[-1].value == 'sil':
self._waiting_for_speech = False
except KeyError:
print("Missing expected parameters: {}".format(directive), file=stderr)
As we don't want the motors control to interfere with sending or receiving notifications, we will use different threads:
self._waiting_for_speech = True
threading.Thread(target=self.draw_letter, args=(
payload["letter"], False), daemon=True).start()
The first thing we do in the new thread is to wait for Alexa to finish speaking:
while self._waiting_for_speech:
sleep(0.1)
We do the same after each "speak" command we send to Alexa:
self._waiting_for_speech = True
self.send_custom_event('Custom.Mindstorms.Gadget', 'speak', {'txt': pos_title.text})
while self._waiting_for_speech:
sleep(0.1)
IntentsThere are three intents defined in model.js:
- LearnIntent - this has one slot of type "Letter". Can be started by uttering "draw letter {Letter}, learn letter {Letter}, write letter {Letter}". This intent will send a command to EV3 to draw the requested letter while giving voice indications on how to draw it.
- PlayGameIntent - no slot. Started by uttering "play game" or "quiz". It will randomly chose one of the letters of the alphabet and request EV3 to draw it. No voice indications are given.
- PlayGameAnswerIntent - has one slot of type "Letter". This is started by uttering the answer of the previous intent. The answer can be like: "{Letter}", "answer is {Letter}", "letter {Letter}" etc
Interactions between user, echo device, custom skill, lambda function and EV3 brick for the LearnIntent:
The interactions for the PlayGameIntent/PlayGameAnswerIntent are similar with the ones for the LearnIntent.
Attached source code:- script3r.py - This the main file that runs on the EV3 brick.
- script3r.ini - Contains the Amazon Gadget's keys and it's capabilities.
- SVG.zip - A zip file containing all the letters of the alphabet in svg format. It has to be extracted in the root of the project and copied on the EV3 brick along with script3r.py and scrip3r.ini files.
- model.json - this file contains the interaction model for the Alexa Custom Skill. Must be copied over in Alexa Developer Console -> Script3r -> Build -> Interaction Model -> JSON Editor. Save and Build after copy.
- package.json - Copy this in Alexa Developer Console -> Script3r -> Code -> Skill Code -> lambda -> package.json
- util.js - Contains some utilities used by the lambda function. Copy this in Alexa Developer Console -> Script3r -> Code -> Skill Code -> lambda -> util.js
- index.js - Main code for the Alexa Custom Skill. Copy this in Alexa Developer Console -> Script3r -> Code -> Skill Code -> lambda -> index.js
#!/usr/bin/env python3
import os
import logging
import json
import threading
import xml.etree.ElementTree as ET
from time import sleep
from math import cos, sin, pi, acos, asin, sqrt, atan
from sys import stderr, stdout
import traceback
from ev3dev2.motor import LargeMotor, MediumMotor, OUTPUT_A, OUTPUT_D, OUTPUT_B
from ev3dev2.sound import Sound
from ev3dev2.led import Leds
from agt import AlexaGadget
MAX_ROTATION_X = 16
MAX_ROTATION_Y = 16
MAX_SPEED_X = 80
MAX_SPEED_Y = 80
POSITION_PEN = 130
# set logger to display on both EV3 Brick and console
logging.basicConfig(level=logging.INFO, stream=stdout,
format='%(message)s')
logging.getLogger().addHandler(logging.StreamHandler(stderr))
logger = logging.getLogger(__name__)
class Printer(object):
def __init__(self):
self.motor_x = LargeMotor(OUTPUT_A)
self.motor_y = LargeMotor(OUTPUT_D)
self.motor_pen = MediumMotor(OUTPUT_B)
self._pen_down = False
self.solver = Solver()
def x(self):
return -self.motor_x.position / (MAX_ROTATION_X * self.motor_x.count_per_rot)
def y(self):
return self.motor_y.position / (MAX_ROTATION_Y * self.motor_y.count_per_rot)
def reset(self):
self.motor_x.on_to_position(speed=80, position=0, block=False)
self.motor_y.on_to_position(speed=80, position=0, block=False)
self.motor_x.wait_until_not_moving()
self.motor_y.wait_until_not_moving()
self.pen_up()
def move_wh(self, w, h, speed_x=MAX_SPEED_X, speed_y=MAX_SPEED_Y):
self.motor_x.on_for_rotations(
speed=speed_x, rotations=-w * MAX_ROTATION_X, block=False)
self.motor_y.on_for_rotations(
speed=speed_y, rotations=h * MAX_ROTATION_Y, block=False)
self.motor_x.wait_until_not_moving()
self.motor_y.wait_until_not_moving()
def pen_down(self):
if not self._pen_down:
self.motor_pen.on_for_degrees(
speed=25, degrees=POSITION_PEN, brake=True)
self.motor_pen.wait_until_not_moving()
self._pen_down = True
def pen_up(self):
if self._pen_down:
self.motor_pen.on_for_degrees(
speed=25, degrees=-POSITION_PEN, brake=True)
self.motor_pen.wait_until_not_moving()
self._pen_down = False
def draw_arc(self, rad_x, rad_y, large, sweep, sx, sy, ex, ey):
h, k, start, end = self.solver.solve_ellipse(
(sx, sy), (ex, ey), rad_x, rad_y, large, sweep)
print('The solution is:', h, k, start*180/pi, "->", end*180/pi)
if start < 0:
start += 2*pi
if start < 0:
end += 2*pi
angle = start
angle_to_do = 0
if sweep:
cclock = 1
angle_to_do = end - start
else:
cclock = -1
angle_to_do = start-end
if angle_to_do < 0:
angle_to_do += 2*pi
print('doing:', angle_to_do*180/pi)
step = 0
angle_done = 0
integral_error_x = 0
integral_error_y = 0
integral_weight = 0.15
while angle_done < angle_to_do:
dx = -sin(angle) * rad_x * cclock - \
integral_error_x * integral_weight
dy = cos(angle) * rad_y * cclock - \
integral_error_y * integral_weight
magnitude = sqrt(dx**2 + dy**2)
speed_x = (dx/magnitude) * MAX_SPEED_X
speed_y = (dy/magnitude) * MAX_SPEED_Y
self.motor_x.on(speed=-speed_x)
self.motor_y.on(speed=speed_y)
sleep(0.1)
old_angle = angle
angle = self.solver.solve_angle(
cx=h, cy=k, a=rad_x, b=rad_y, px=self.x(), py=self.y())
step_angle = abs(angle-old_angle)
if(step_angle > pi):
step_angle = 2*pi - step_angle
angle_done += step_angle
x_sol = cos(angle) * rad_x + h
y_sol = sin(angle) * rad_y + k
integral_error_x += (self.x()-x_sol)
integral_error_y += (self.y()-y_sol)
step += 1
if(step > 1000):
break
self.motor_x.stop()
self.motor_y.stop()
def draw_line(self, from_x, from_y, to_x, to_y, pen=True):
self.move_wh(from_x - self.x(), from_y - self.y())
if pen:
self.pen_down()
x = self.x()
y = self.y()
if (to_x-x) < 0.00001 and (to_x-x) > -0.00001:
self.move_wh(0, to_y-y)
else:
m = abs((to_y-y)/(to_x-x))
speed_x = 1
speed_y = m * speed_x
magnitude = sqrt(speed_x**2 + speed_y**2)
speed_x = (speed_x/magnitude) * MAX_SPEED_X
speed_y = (speed_y/magnitude) * MAX_SPEED_Y
self.move_wh(to_x-x, to_y-y,
speed_x=speed_x, speed_y=speed_y)
if pen:
self.pen_up()
def draw_svg_path(self, path, width, height):
path_list = path.split(" ")
index = 0
m_x = 0
m_y = 0
while index < len(path_list):
current = path_list[index]
if current == 'M':
x = float(path_list[index+1]) / width
y = float(path_list[index+2]) / height
m_x = x
m_y = y
self.move_wh(x-self.x(), y-self.y())
index += 3
self.pen_down()
elif current == 'L':
x = float(path_list[index+1]) / width
y = float(path_list[index+2]) / height
m_x = x
m_y = y
self.draw_line(m_x, m_y, x, y, pen=False)
index += 3
elif current == 'A':
rad_x = float(path_list[index+1]) / width
rad_y = float(path_list[index+2]) / height
large = int(path_list[index+4])
sweep = int(path_list[index+5])
dx = float(path_list[index+6]) / width
dy = float(path_list[index+7]) / width
index += 8
self.draw_arc(rad_x, rad_y, large, sweep, m_x, m_y, dx, dy)
m_x = dx
m_y = dy
else:
index += 1
self.pen_up()
class Solver(object):
def solve_angle(self, cx, cy, a, b, px, py):
if px-cx:
theta = atan(((py-cy)*a)/((px-cx)*b))
if px-cx < 0:
# quadrand 2 and 3
theta = theta + pi
else:
if py-cy < 0:
theta = -pi/2
else:
theta = pi/2
return theta
def solve_ellipse(self, p1, p2, a, b, large, sweep):
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
m = -2*(b**2)*x1 + 2*(b**2)*x2
n = -2*(a**2)*y1 + 2*(a**2)*y2
q = -(b**2)*x1**2 - (a**2)*(y1**2) + (b**2)*(x2**2) + (a**2)*(y2**2)
if n < -0.00001 or n > 0.00001:
aa = b**2 + (a**2)*(m**2)/(n**2)
bb = -2*(b**2)*x1 - (2*q*(a**2)*m)/(n**2) + (2*(a**2)*y1*m)/n
cc = ((a**2)*(q**2))/(n**2) - (2*(a**2)*y1*q)/n + \
(b**2)*(x1**2) + (a**2)*(y1**2) - (a**2)*(b**2)
if bb**2 - 4*aa*cc < -0.00001 or bb**2 - 4*aa*cc > 0.00001:
h1 = (-bb + sqrt(bb**2 - 4*aa*cc))/(2*aa)
h2 = (-bb - sqrt(bb**2 - 4*aa*cc))/(2*aa)
else:
h1 = h2 = (-bb)/(2*aa)
k1 = (q - m * h1)/n
k2 = (q - m * h2)/n
else:
aa = a**2
bb = -2*a**2*y1
if m < -0.0001 or m > 0.0001:
cc = b**2*q**2/m**2 - 2*b**2*x1*q/m + b**2*x1**2 + a**2*y1**2 - a**2*b**2
h1 = q/m
h2 = q/m
if bb**2 - 4*aa*cc < -0.00001 or bb**2 - 4*aa*cc > 0.00001:
k1 = (-bb + sqrt(bb**2 - 4*aa*cc))/(2*aa)
k2 = (-bb - sqrt(bb**2 - 4*aa*cc))/(2*aa)
else:
k1 = k2 = (-bb)/(2*aa)
else:
h1 = h2 = x1
k1 = k2 = (-bb)/(2*aa)
h = "h"
k = "k"
if h1 != h2 or k1 != k2:
results = [
{h: h1, k: k1},
{h: h2, k: k2}
]
else:
results = [{h: h1, k: k1}]
results_len = len(results)
for result in results:
theta1 = 0
theta2 = 0
if p1[0]-result[h]:
theta1 = atan(((p1[1]-result[k])*a)/((p1[0]-result[h])*b))
if p1[0]-result[h] < 0:
# quadrand 2 and 3
theta1 = theta1 + pi
else:
if p1[1]-result[k] < 0:
theta1 = -pi/2
else:
theta1 = pi/2
if p2[0]-result[h]:
theta2 = atan(((p2[1]-result[k])*a)/((p2[0]-result[h])*b))
if p2[0]-result[h] < 0:
# quadrand 2 and 3
theta2 = theta2 + pi
else:
if p2[1]-result[k] < 0:
theta2 = -pi/2
else:
theta2 = pi/2
if results_len == 1:
return result[h], result[k], theta1, theta2
angle = theta2 - theta1
if angle < 0:
angle += 2 * pi
if angle >= 2*pi:
angle -= 2 * pi
if large == 0:
if sweep:
if angle <= pi:
return result[h], result[k], theta1, theta2
else:
if angle >= pi:
return result[h], result[k], theta1, theta2
else:
if sweep:
if angle >= pi:
return result[h], result[k], theta1, theta2
else:
if angle <= pi:
return result[h], result[k], theta1, theta2
return None
class MindstormsGadget(AlexaGadget):
"""
An Mindstorms gadget that will react to the Alexa wake word.
"""
def __init__(self):
"""
Performs Alexa Gadget initialization routines and ev3dev resource allocation.
"""
super().__init__()
self.leds = Leds()
self.sound = Sound()
self._waiting_for_speech = False
self.lock = threading.Lock()
self.printer = Printer()
def on_connected(self, device_addr):
"""
Gadget connected to the paired Echo device.
:param device_addr: the address of the device we connected to
"""
self.leds.set_color("LEFT", "GREEN")
self.leds.set_color("RIGHT", "GREEN")
logger.info("{} connected to Echo device".format(self.friendly_name))
def on_disconnected(self, device_addr):
"""
Gadget disconnected from the paired Echo device.
:param device_addr: the address of the device we disconnected from
"""
self.leds.set_color("LEFT", "BLACK")
self.leds.set_color("RIGHT", "BLACK")
logger.info("{} disconnected from Echo device".format(
self.friendly_name))
def on_custom_mindstorms_gadget_learn(self, directive):
"""
Handles the Custom.Mindstorms.Gadget control directive.
:param directive: the custom directive with the matching namespace and name
"""
try:
payload = json.loads(directive.payload.decode("utf-8"))
print("Payload: {}".format(payload), file=stderr)
self._waiting_for_speech = True
threading.Thread(target=self.draw_letter, args=(
payload["letter"], True), daemon=True).start()
except KeyError:
print("Missing expected parameters: {}".format(
directive), file=stderr)
def on_custom_mindstorms_gadget_guess(self, directive):
"""
Handles the Custom.Mindstorms.Gadget control directive.
:param directive: the custom directive with the matching namespace and name
"""
try:
payload = json.loads(directive.payload.decode("utf-8"))
print("Payload: {}".format(payload), file=stderr)
self._waiting_for_speech = True
threading.Thread(target=self.draw_letter, args=(
payload["letter"], False), daemon=True).start()
except KeyError:
print("Missing expected parameters: {}".format(
directive), file=stderr)
def on_alexa_gadget_speechdata_speechmarks(self, directive):
"""
Alexa.Gadget.SpeechData Speechmarks directive received.
For more info, visit:
https://developer.amazon.com/docs/alexa-gadgets-toolkit/alexa-gadget-speechdata-interface.html#Speechmarks-directive
:param directive: Protocol Buffer Message that was send by Echo device.
"""
try:
if self._waiting_for_speech:
if directive.payload.speechmarksData[-1].value == 'sil':
# sleep until time
print('received silence', file=stderr)
# sleep(
# int(directive.payload.speechmarksData[-1].startOffsetInMilliSeconds)/1000)
self._waiting_for_speech = False
except KeyError:
print("Missing expected parameters: {}".format(
directive), file=stderr)
def draw_letter(self, letter, speak):
print('drawing letter ', letter, file=stderr)
while self._waiting_for_speech:
sleep(0.1)
file = "svg/"+letter+".svg"
tree = ET.parse(file)
root = tree.getroot()
width = int(root.attrib["width"])
height = int(root.attrib["height"])
self.lock.acquire()
try:
for child in root:
if child.tag == '{http://www.w3.org/2000/svg}path':
for pos_title in child:
if pos_title.tag == '{http://www.w3.org/2000/svg}title':
if speak:
self._waiting_for_speech = True
self.send_custom_event(
'Custom.Mindstorms.Gadget', 'speak', {'txt': pos_title.text})
while self._waiting_for_speech:
sleep(0.1)
self.printer.draw_svg_path(
child.attrib["d"], width, height)
elif child.tag == '{http://www.w3.org/2000/svg}line':
for pos_title in child:
if pos_title.tag == '{http://www.w3.org/2000/svg}title':
if speak:
self._waiting_for_speech = True
self.send_custom_event(
'Custom.Mindstorms.Gadget', 'speak', {'txt': pos_title.text})
while self._waiting_for_speech:
sleep(0.1)
self.printer.draw_line(
float(child.attrib["x1"])/width,
float(child.attrib["y1"])/height,
float(child.attrib["x2"])/width,
float(child.attrib["y2"])/height)
except Exception as e:
print(e, file=stderr)
traceback.print_exc(file=stderr)
self.printer.reset()
self.lock.release()
self.send_custom_event(
'Custom.Mindstorms.Gadget', 'done', {})
if __name__ == '__main__':
gadget = MindstormsGadget()
# Set LCD font and turn off blinking LEDs
os.system('setfont Lat7-Terminus12x6')
gadget.leds.set_color("LEFT", "BLACK")
gadget.leds.set_color("RIGHT", "BLACK")
# Startup sequence
# gadget.sound.play_song((('C4', 'e'), ('D4', 'e'), ('E5', 'q')))
gadget.leds.set_color("LEFT", "GREEN")
gadget.leds.set_color("RIGHT", "GREEN")
# Gadget main entry point
gadget.main()
# Shutdown sequence
# gadget.sound.play_song((('E5', 'e'), ('C4', 'e')))
gadget.leds.set_color("LEFT", "BLACK")
gadget.leds.set_color("RIGHT", "BLACK")
[GadgetSettings]
amazonId = YOUR_ID
alexaGadgetSecret = YOUR_SECRET
[GadgetCapabilities]
Custom.Mindstorms.Gadget = 1.0
Alexa.Gadget.SpeechData = 1.0 - viseme
{
"interactionModel": {
"languageModel": {
"invocationName": "lego scripter",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "LearnIntent",
"slots": [
{
"name": "Letter",
"type": "Letter",
"samples": [
"letter {Letter}",
"{Letter}"
]
}
],
"samples": [
"draw a letter {Letter}",
"write a letter {Letter}",
"write letter {Letter}",
"write {Letter}",
"learn letter {Letter}",
"draw letter {Letter}",
"draw {Letter}",
"learn {Letter}"
]
},
{
"name": "PlayGameIntent",
"slots": [],
"samples": [
"Quiz",
"Let's play a game",
"Play a game"
]
},
{
"name": "PlayGameAnswerIntent",
"slots": [
{
"name": "Letter",
"type": "Letter",
"samples": [
"Answer is {Letter}",
"Answer is letter {Letter}",
"{Letter}",
"Letter {Letter}"
]
}
],
"samples": [
"{Letter}",
"Answer is {Letter}",
"Answer is letter {Letter}",
"Letter {Letter}"
]
}
],
"types": [
{
"name": "Letter",
"values": [
{
"name": {
"value": "Z"
}
},
{
"name": {
"value": "Y"
}
},
{
"name": {
"value": "X"
}
},
{
"name": {
"value": "W"
}
},
{
"name": {
"value": "V"
}
},
{
"name": {
"value": "U"
}
},
{
"name": {
"value": "T"
}
},
{
"name": {
"value": "S"
}
},
{
"name": {
"value": "R"
}
},
{
"name": {
"value": "Q"
}
},
{
"name": {
"value": "P"
}
},
{
"name": {
"value": "O"
}
},
{
"name": {
"value": "N"
}
},
{
"name": {
"value": "M"
}
},
{
"name": {
"value": "L"
}
},
{
"name": {
"value": "K"
}
},
{
"name": {
"value": "J"
}
},
{
"name": {
"value": "I"
}
},
{
"name": {
"value": "H"
}
},
{
"name": {
"value": "G"
}
},
{
"name": {
"value": "F"
}
},
{
"name": {
"value": "E"
}
},
{
"name": {
"value": "D"
}
},
{
"name": {
"value": "C"
}
},
{
"name": {
"value": "B"
}
},
{
"name": {
"value": "A"
}
}
]
}
]
},
"dialog": {
"intents": [
{
"name": "LearnIntent",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "Letter",
"type": "Letter",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.1207405175159.184443735393"
}
}
]
},
{
"name": "PlayGameAnswerIntent",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "Letter",
"type": "Letter",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.551051320616.1560203168942"
}
}
]
}
],
"delegationStrategy": "ALWAYS"
},
"prompts": [
{
"id": "Elicit.Slot.1207405175159.184443735393",
"variations": [
{
"type": "PlainText",
"value": "What letter should it be?"
}
]
},
{
"id": "Elicit.Slot.551051320616.1560203168942",
"variations": [
{
"type": "PlainText",
"value": "What letter did you say?"
}
]
}
]
}
}
index.js
JavaScriptconst Alexa = require('ask-sdk-core');
const Util = require('./util');
// The namespace of the custom directive to be sent by this skill
const NAMESPACE = 'Custom.Mindstorms.Gadget';
const NAME_LEARN = 'learn'
const NAME_GUESS = 'guess'
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle: async function (handlerInput) {
let request = handlerInput.requestEnvelope;
let { apiEndpoint, apiAccessToken } = request.context.System;
let apiResponse = await Util.getConnectedEndpoints(apiEndpoint, apiAccessToken);
if ((apiResponse.endpoints || []).length === 0) {
return handlerInput.responseBuilder
.speak(`I couldn't find an EV3 Brick connected to this Echo device. Please check to make sure your EV3 Brick is connected, and try again.`)
.getResponse();
}
// Store the gadget endpointId to be used in this skill session
let endpointId = apiResponse.endpoints[0].endpointId || [];
Util.putSessionAttribute(handlerInput, 'endpointId', endpointId);
// Set skill duration to 2 minutes (4 30-seconds interval)
Util.putSessionAttribute(handlerInput, 'duration', 4);
// Set the token to track the event handler
const token = handlerInput.requestEnvelope.request.requestId;
Util.putSessionAttribute(handlerInput, 'token', token);
return handlerInput.responseBuilder
.speak("Welcome to Lego Scripter. I can teach you how to write the letters of the " +
"alphabet or we can play a game. What should it be?")
.withShouldEndSession(false)
.addDirective(buildStartEventHandler(token, 30000, {}))
.getResponse();
}
};
// The request interceptor is used for request handling testing and debugging.
// It will simply log the request in raw json format before any processing is performed.
const RequestInterceptor = {
process(handlerInput) {
let { attributesManager, requestEnvelope } = handlerInput;
let sessionAttributes = attributesManager.getSessionAttributes();
// Log the request for debug purposes.
console.log(`=====Request==${JSON.stringify(requestEnvelope)}`);
console.log(`=========SessionAttributes==${JSON.stringify(sessionAttributes, null, 2)}`);
}
};
// Generic error handling to capture any syntax or routing errors. If you receive an error
// stating the request handler chain is not found, you have not implemented a handler for
// the intent being invoked or included it in the skill builder below.
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
console.log(`~~~~ Error handled: ${error.stack}`);
const speakOutput = `Sorry, I had trouble doing what you asked. Please try again.`;
return handlerInput.responseBuilder
.speak(speakOutput)
.getResponse();
}
};
const HelpIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
},
handle(handlerInput) {
const speakOutput = 'I can draw a letter for your or we can play a game! How can I help?';
return handlerInput.responseBuilder
.speak(speakOutput)
.withShouldEndSession(false)
.getResponse();
}
};
const CancelAndStopIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
|| Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
},
handle(handlerInput) {
return handlerInput.responseBuilder
.addDirective(buildStopEventHandlerDirective(handlerInput))
.speak('Goodbye!')
.withShouldEndSession(true)
.getResponse();
}
};
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
},
handle(handlerInput) {
// Any cleanup logic goes here.
return handlerInput.responseBuilder
.addDirective(buildStopEventHandlerDirective(handlerInput))
.getResponse();
}
};
const LearnIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'LearnIntent';
},
handle: function (handlerInput) {
let letter = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Letter');
letter = letter.charAt(0).toUpperCase();
letter = letter.toUpperCase();
const attributesManager = handlerInput.attributesManager;
let endpointId = attributesManager.getSessionAttributes().endpointId || [];
// Set skill duration to 2 minutes (4 30-seconds interval)
Util.putSessionAttribute(handlerInput, 'duration', 4);
let directive = Util.build(endpointId, NAMESPACE, NAME_LEARN,
{
letter: letter
});
Util.putSessionAttribute(handlerInput, 'mode', 'learn');
console.log(directive);
return handlerInput.responseBuilder
.speak(`Let me draw letter ${letter} for you.`)
.addDirective(directive)
.getResponse();
}
};
const EventsReceivedRequestHandler = {
// Checks for a valid token and endpoint.
canHandle(handlerInput) {
let { request } = handlerInput.requestEnvelope;
console.log('Request type: ' + Alexa.getRequestType(handlerInput.requestEnvelope));
if (request.type !== 'CustomInterfaceController.EventsReceived') return false;
const attributesManager = handlerInput.attributesManager;
let sessionAttributes = attributesManager.getSessionAttributes();
let customEvent = request.events[0];
// if (sessionAttributes.token !== request.token) {
// console.log("Event token doesn't match. Ignoring this event");
// return false;
// }
// Validate endpoint
let requestEndpoint = customEvent.endpoint.endpointId;
if (requestEndpoint !== sessionAttributes.endpointId) {
console.log("Event endpoint id doesn't match. Ignoring this event");
return false;
}
return true;
},
handle(handlerInput) {
console.log("== Received Custom Event ==");
let customEvent = handlerInput.requestEnvelope.request.events[0];
let payload = customEvent.payload;
let name = customEvent.header.name;
const token = handlerInput.requestEnvelope.request.requestId;
Util.putSessionAttribute(handlerInput, 'token', token);
console.log(customEvent)
if (name === 'speak') {
return handlerInput.responseBuilder
.speak(payload.txt)
.getResponse();
}
if (name === 'done') {
let mode = handlerInput.attributesManager.getSessionAttributes().mode || 'learn';
if (mode === 'learn') {
return handlerInput.responseBuilder
.speak('Done. What to do next?')
.addDirective(buildStopEventHandlerDirective(handlerInput))
.withShouldEndSession(false)
.getResponse();
} else if (mode === 'guess') {
return handlerInput.responseBuilder
.speak('What letter is this?')
.addDirective(buildStopEventHandlerDirective(handlerInput))
.withShouldEndSession(false)
.getResponse();
}
}
return handlerInput.responseBuilder
.speak('unknown event')
.getResponse();
}
};
const ExpiredRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'CustomInterfaceController.Expired'
},
handle(handlerInput) {
console.log("== Custom Event Expiration Input ==");
const attributesManager = handlerInput.attributesManager;
let token = handlerInput.attributesManager.getSessionAttributes().token || '';
let duration = attributesManager.getSessionAttributes().duration || 0;
if (duration > 0) {
Util.putSessionAttribute(handlerInput, 'duration', --duration);
// Extends skill session
return handlerInput.responseBuilder
.addDirective(buildStartEventHandler(token, 30000, {}))
.getResponse();
}
else {
// End skill session
return handlerInput.responseBuilder
.withShouldEndSession(true)
.getResponse();
}
}
};
const PlayGameIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlayGameIntent';
},
handle(handlerInput) {
const attributesManager = handlerInput.attributesManager;
let endpointId = attributesManager.getSessionAttributes().endpointId || [];
// Set skill duration to 2 minutes (4 30-seconds interval)
Util.putSessionAttribute(handlerInput, 'duration', 4);
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXTZ";
var rnum = Math.floor(Math.random() * alphabet.length);
let letter = alphabet[rnum]
let directive = Util.build(endpointId, NAMESPACE, NAME_GUESS,
{
letter: letter
});
console.log(directive);
Util.putSessionAttribute(handlerInput, 'mode', 'guess');
Util.putSessionAttribute(handlerInput, 'letter', letter);
return handlerInput.responseBuilder
.speak(`I will draw a letter and you have to guess which one.`)
.addDirective(directive)
.getResponse();
}
};
const PlayGameAnswerIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'PlayGameAnswerIntent';
},
handle(handlerInput) {
let mode = handlerInput.attributesManager.getSessionAttributes().mode || 'learn';
let answer = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Letter');
answer = answer.charAt(0).toUpperCase();
answer = answer.toUpperCase();
if (mode === 'guess') {
let letter = handlerInput.attributesManager.getSessionAttributes().letter || '';
if (letter === answer) {
return handlerInput.responseBuilder
.speak(`Correct! What should we do next?`)
.withShouldEndSession(false)
.getResponse();
} else {
return handlerInput.responseBuilder
.speak(`Incorrect! Try again.`)
.withShouldEndSession(false)
.getResponse();
}
}
return handlerInput.responseBuilder
.speak(`How can I help?`)
.withShouldEndSession(false)
.getResponse();
}
};
// The SkillBuilder acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
LearnIntentHandler,
PlayGameIntentHandler,
PlayGameAnswerIntentHandler,
EventsReceivedRequestHandler,
ExpiredRequestHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler,
)
.addRequestInterceptors(RequestInterceptor)
.addErrorHandlers(
ErrorHandler,
)
.lambda();
function buildStartEventHandler(token, timeout = 30000, payload) {
return {
type: "CustomInterfaceController.StartEventHandler",
token: token,
expiration: {
durationInMilliseconds: timeout,
expirationPayload: payload
}
};
}
function buildStopEventHandlerDirective(handlerInput) {
let token = handlerInput.attributesManager.getSessionAttributes().token || '';
return {
"type": "CustomInterfaceController.StopEventHandler",
"token": token
}
}
util.js
JavaScript/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* You may not use this file except in compliance with the terms and conditions
* set forth in the accompanying LICENSE.TXT file.
*
* THESE MATERIALS ARE PROVIDED ON AN "AS IS" BASIS. AMAZON SPECIFICALLY DISCLAIMS, WITH
* RESPECT TO THESE MATERIALS, ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING
* THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
*/
'use strict';
const Https = require('https');
/**
* Build a custom directive payload to the gadget with the specified endpointId
* @param {string} endpointId - the gadget endpoint Id
* @param {string} namespace - the namespace of the skill
* @param {string} name - the name of the skill within the scope of this namespace
* @param {object} payload - the payload data
* @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/send-gadget-custom-directive-from-skill.html#respond}
*/
exports.build = function (endpointId, namespace, name, payload) {
// Construct the custom directive that needs to be sent
// Gadget should declare the capabilities in the discovery response to
// receive the directives under the following namespace.
return {
type: 'CustomInterfaceController.SendDirective',
header: {
name: name,
namespace: namespace
},
endpoint: {
endpointId: endpointId
},
payload
};
};
/**
* A convenience routine to add the a key-value pair to the session attribute.
* @param handlerInput - the context from Alexa Service
* @param key - the key to be added
* @param value - the value be added
*/
exports.putSessionAttribute = function (handlerInput, key, value) {
const attributesManager = handlerInput.attributesManager;
let sessionAttributes = attributesManager.getSessionAttributes();
sessionAttributes[key] = value;
attributesManager.setSessionAttributes(sessionAttributes);
};
/**
* To get a list of all the gadgets that meet these conditions,
* Call the Endpoint Enumeration API with the apiEndpoint and apiAccessToken to
* retrieve the list of all connected gadgets.
*
* @param {string} apiEndpoint - the Endpoint API url
* @param {string} apiAccessToken - the token from the session object in the Alexa request
* @see {@link https://developer.amazon.com/docs/alexa-gadgets-toolkit/send-gadget-custom-directive-from-skill.html#call-endpoint-enumeration-api}
*/
exports.getConnectedEndpoints = function (apiEndpoint, apiAccessToken) {
// The preceding https:// need to be stripped off before making the call
apiEndpoint = (apiEndpoint || '').replace('https://', '');
return new Promise(((resolve, reject) => {
const options = {
host: apiEndpoint,
path: '/v1/endpoints',
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiAccessToken
}
};
const request = Https.request(options, (response) => {
response.setEncoding('utf8');
let returnData = '';
response.on('data', (chunk) => {
returnData += chunk;
});
response.on('end', () => {
resolve(JSON.parse(returnData));
});
response.on('error', (error) => {
reject(error);
});
});
request.end();
}));
};
{
"name": "script3r",
"version": "1.0.0",
"description": "Learn the alphabet with Lego Mindstorms and Alexa",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Andrei Ciobanu",
"license": "ISC",
"dependencies": {
"ask-sdk-core": "^2.6.0",
"ask-sdk-model": "^1.18.0",
"aws-sdk": "^2.326.0",
"request": "^2.81.0"
}
}
Printer Simulation
Pythonfrom math import copysign, cos, sin, acos, asin, pi, sqrt, atan
from random import random
from sys import stderr
from PIL import Image, ImageDraw
import xml.etree.ElementTree as ET
MAX_ROTATION_X = 16
MAX_ROTATION_Y = 16
MAX_SPEED_X = 80
MAX_SPEED_Y = 80
DEGREES_PEN = 10
INTEGRAL_WEIGHT = 0.05
IMG_SIZE = 1000
class Motor(object):
def __init__(self):
self._count_per_rot = 360
self._position = 0
@property
def count_per_rot(self):
return self._count_per_rot
@property
def position(self):
return self._position
def reset(self):
self._position = 0
def on_for_rotations(self, speed, rotations, block=False):
self._position += rotations * \
self._count_per_rot * copysign(1, speed)
def on_for_seconds(self, speed, seconds, block=False):
# +- 5% erro
error = 1+(random()-0.5)/10
self._position += (speed/100) * seconds * 1050 * error
def on_for_degrees(self, speed, degrees, block=False):
self._position += (degrees/360) * \
self._count_per_rot * copysign(1, speed)
def wait_until_not_moving(self):
return
def stop(self):
return
class PrinterSimulation(object):
def __init__(self):
self.solver = Solver()
self.motor_x = Motor()
self.motor_y = Motor()
self.motor_pen = Motor()
self._pen_down = False
self.image = Image.new('RGBA', (IMG_SIZE, IMG_SIZE))
self.image_draw = ImageDraw.Draw(self.image)
def record_position(self):
return (self.x() * IMG_SIZE, self.y() * IMG_SIZE)
def draw(self, old_position):
self.image_draw.line(
[old_position, (self.x()*IMG_SIZE, self.y()*IMG_SIZE)], width=10)
def reset(self):
self.motor_x.reset()
self.motor_y.reset()
self.pen_up()
self.image.save('run.png', "PNG")
def x(self):
return -self.motor_x.position / (MAX_ROTATION_X * self.motor_x.count_per_rot)
def y(self):
return self.motor_y.position / (MAX_ROTATION_Y * self.motor_y.count_per_rot)
def move_wh(self, w, h, speed_x=MAX_SPEED_X, speed_y=MAX_SPEED_Y):
old_position = self.record_position()
self.motor_x.on_for_rotations(
speed=speed_x, rotations=-w * MAX_ROTATION_X, block=False)
self.motor_y.on_for_rotations(
speed=speed_y, rotations=h * MAX_ROTATION_Y, block=False)
self.motor_x.wait_until_not_moving()
self.motor_y.wait_until_not_moving()
if self._pen_down:
self.draw(old_position)
def pen_down(self):
if not self._pen_down:
self.motor_pen.on_for_degrees(speed=10, degrees=DEGREES_PEN)
self._pen_down = True
def pen_up(self):
if self._pen_down:
self.motor_pen.on_for_degrees(speed=10, degrees=-DEGREES_PEN)
self._pen_down = False
def draw_arc(self, rad_x, rad_y, large, sweep, sx, sy, ex, ey):
h, k, start, end = self.solver.solve_ellipse(
(sx, sy), (ex, ey), rad_x, rad_y, large, sweep)
print('The solution is:', h, k, start*180/pi, "->", end*180/pi)
if start < 0:
start += 2*pi
if start < 0:
end += 2*pi
angle = start
angle_to_do = 0
if sweep:
cclock = 1
angle_to_do = end - start
else:
cclock = -1
angle_to_do = start-end
if angle_to_do < 0:
angle_to_do += 2*pi
print('doing:', angle_to_do*180/pi)
step = 0
angle_done = 0
integral_error_x = 0
integral_error_y = 0
while angle_done < angle_to_do:
dx = -sin(angle) * rad_x * cclock - \
integral_error_x * INTEGRAL_WEIGHT
dy = cos(angle) * rad_y * cclock - \
integral_error_y * INTEGRAL_WEIGHT
magnitude = sqrt(dx**2 + dy**2)
speed_x = (dx/magnitude) * MAX_SPEED_X
speed_y = (dy/magnitude) * MAX_SPEED_Y
old_position = self.record_position()
self.motor_x.on_for_seconds(speed=-speed_x, seconds=0.1)
self.motor_y.on_for_seconds(speed=speed_y, seconds=0.1)
if self._pen_down:
self.draw(old_position)
old_angle = angle
angle = self.solver.solve_angle(
cx=h, cy=k, a=rad_x, b=rad_y, px=self.x(), py=self.y())
step_angle = abs(angle-old_angle)
if(step_angle > pi):
step_angle = 2*pi - step_angle
angle_done += step_angle
x_sol = cos(angle) * rad_x + h
y_sol = sin(angle) * rad_y + k
integral_error_x += (self.x()-x_sol)
integral_error_y += (self.y()-y_sol)
step += 1
if(step > 1000):
break
self.motor_x.stop()
self.motor_y.stop()
def draw_line(self, from_x, from_y, to_x, to_y, pen=True):
self.move_wh(from_x - self.x(), from_y - self.y())
if pen:
self.pen_down()
if (to_x-self.x()) < 0.000001 and (to_x-self.x()) > -0.000001:
self.move_wh(0, to_y-self.y())
else:
m = abs((to_y-self.y())/(to_x-self.x()))
speed_x = 1
speed_y = m * speed_x
magnitude = sqrt(speed_x**2 + speed_y**2)
speed_x = (speed_x/magnitude) * MAX_SPEED_X
speed_y = (speed_y/magnitude) * MAX_SPEED_Y
print(to_x-self.x(), to_y-self.y(), speed_x, speed_y)
self.move_wh(to_x-self.x(), to_y-self.y(),
speed_x=speed_x, speed_y=speed_y)
if pen:
self.pen_up()
def draw_svg_path(self, path, width, height):
path_list = path.split(" ")
index = 0
m_x = 0
m_y = 0
while index < len(path_list):
current = path_list[index]
if current == 'M':
x = float(path_list[index+1]) / width
y = float(path_list[index+2]) / height
m_x = x
m_y = y
self.move_wh(x-self.x(), y-self.y())
index += 3
self.pen_down()
elif current == 'L':
x = float(path_list[index+1]) / width
y = float(path_list[index+2]) / height
m_x = x
m_y = y
self.draw_line(m_x, m_y, x, y, pen=False)
index += 3
elif current == 'A':
rad_x = float(path_list[index+1]) / width
rad_y = float(path_list[index+2]) / height
large = int(path_list[index+4])
sweep = int(path_list[index+5])
dx = float(path_list[index+6]) / width
dy = float(path_list[index+7]) / width
index += 8
self.draw_arc(rad_x, rad_y, large, sweep, m_x, m_y, dx, dy)
m_x = dx
m_y = dy
else:
index += 1
self.pen_up()
def draw_svg(self, file):
tree = ET.parse(file)
root = tree.getroot()
width = int(root.attrib["width"])
height = int(root.attrib["height"])
for child in root:
if child.tag == '{http://www.w3.org/2000/svg}path':
for pos_title in child:
if pos_title.tag == '{http://www.w3.org/2000/svg}title':
print(pos_title.text, file=stderr)
self.draw_svg_path(child.attrib["d"], width, height)
elif child.tag == '{http://www.w3.org/2000/svg}line':
for pos_title in child:
if pos_title.tag == '{http://www.w3.org/2000/svg}title':
print(pos_title.text, file=stderr)
self.draw_line(
float(child.attrib["x1"])/width,
float(child.attrib["y1"])/height,
float(child.attrib["x2"])/width,
float(child.attrib["y2"])/height)
class Solver(object):
def solve_angle(self, cx, cy, a, b, px, py):
if px-cx:
theta = atan(((py-cy)*a)/((px-cx)*b))
if px-cx < 0:
# quadrand 2 and 3
theta = theta + pi
else:
if py-cy < 0:
theta = -pi/2
else:
theta = pi/2
return theta
def solve_ellipse(self, p1, p2, a, b, large, sweep):
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
m = -2*(b**2)*x1 + 2*(b**2)*x2
n = -2*(a**2)*y1 + 2*(a**2)*y2
q = -(b**2)*x1**2 - (a**2)*(y1**2) + (b**2)*(x2**2) + (a**2)*(y2**2)
if n < -0.000001 or n > 0.000001:
aa = b**2 + (a**2)*(m**2)/(n**2)
bb = -2*(b**2)*x1 - (2*q*(a**2)*m)/(n**2) + (2*(a**2)*y1*m)/n
cc = ((a**2)*(q**2))/(n**2) - (2*(a**2)*y1*q)/n + \
(b**2)*(x1**2) + (a**2)*(y1**2) - (a**2)*(b**2)
if bb**2 - 4*aa*cc < -0.000001 or bb**2 - 4*aa*cc > 0.000001:
h1 = (-bb + sqrt(bb**2 - 4*aa*cc))/(2*aa)
h2 = (-bb - sqrt(bb**2 - 4*aa*cc))/(2*aa)
else:
h1 = h2 = (-bb)/(2*aa)
k1 = (q - m * h1)/n
k2 = (q - m * h2)/n
else:
aa = a**2
bb = -2*a**2*y1
if m < -0.000001 or m > 0.000001:
cc = b**2*q**2/m**2 - 2*b**2*x1*q/m + b**2*x1**2 + a**2*y1**2 - a**2*b**2
h1 = q/m
h2 = q/m
if bb**2 - 4*aa*cc < -0.000001 or bb**2 - 4*aa*cc > 0.000001:
k1 = (-bb + sqrt(bb**2 - 4*aa*cc))/(2*aa)
k2 = (-bb - sqrt(bb**2 - 4*aa*cc))/(2*aa)
else:
k1 = k2 = (-bb)/(2*aa)
else:
h1 = h2 = x1
k1 = k2 = (-bb)/(2*aa)
h = "h"
k = "k"
if h1 != h2 or k1 != k2:
results = [
{h: h1, k: k1},
{h: h2, k: k2}
]
else:
results = [{h: h1, k: k1}]
results_len = len(results)
for result in results:
theta1 = 0
theta2 = 0
if p1[0]-result[h]:
theta1 = atan(((p1[1]-result[k])*a)/((p1[0]-result[h])*b))
if p1[0]-result[h] < 0:
# quadrand 2 and 3
theta1 = theta1 + pi
else:
if p1[1]-result[k] < 0:
theta1 = -pi/2
else:
theta1 = pi/2
if p2[0]-result[h]:
theta2 = atan(((p2[1]-result[k])*a)/((p2[0]-result[h])*b))
if p2[0]-result[h] < 0:
# quadrand 2 and 3
theta2 = theta2 + pi
else:
if p2[1]-result[k] < 0:
theta2 = -pi/2
else:
theta2 = pi/2
if results_len == 1:
return result[h], result[k], theta1, theta2
angle = theta2 - theta1
if angle < 0:
angle += 2 * pi
if angle >= 2*pi:
angle -= 2 * pi
if large == 0:
if sweep:
if angle <= pi:
return result[h], result[k], theta1, theta2
else:
if angle >= pi:
return result[h], result[k], theta1, theta2
else:
if sweep:
if angle >= pi:
return result[h], result[k], theta1, theta2
else:
if angle <= pi:
return result[h], result[k], theta1, theta2
return None
printer = PrinterSimulation()
printer.draw_svg("B.svg")
printer.reset()
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<rect width="100" height="100" fill="beige"></rect>
<line x1="30" x2="30" y1="10" y2="90" stroke="black" >
<title>Start at the top and go down to the bottom</title>
</line>
<path d="M 30 10 A 35 20 0 1 1 30 50" fill="black" fill-opacity="0.1" stroke="black">
<title>Go back to the top and draw a curved line to the middle</title>
</path>
<path d="M 30 50 A 40 20 0 1 1 30 90" fill="black" fill-opacity="0.1" stroke="black">
<title>Then draw a curved line to the bottom</title>
</path>
</svg>
#!/usr/bin/python3
from ev3dev2.motor import LargeMotor, MediumMotor, OUTPUT_A, OUTPUT_D, OUTPUT_B
from time import sleep
from math import cos, sin, pi, acos, asin, sqrt, atan
import xml.etree.ElementTree as ET
from sys import stderr
import traceback
MAX_ROTATION_X = 16
MAX_ROTATION_Y = 16
MAX_SPEED_X = 80
MAX_SPEED_Y = 80
POSITION_PEN = 115
class Printer(object):
def __init__(self):
self._pen_down = False
self.motor_x = LargeMotor(OUTPUT_A)
self.motor_y = LargeMotor(OUTPUT_D)
self.motor_pen = MediumMotor(OUTPUT_B)
self.solver = Solver()
def x(self):
return -self.motor_x.position / (MAX_ROTATION_X * self.motor_x.count_per_rot)
def y(self):
return self.motor_y.position / (MAX_ROTATION_Y * self.motor_y.count_per_rot)
def reset(self):
self.motor_x.on_to_position(speed=80, position=0, block=False)
self.motor_y.on_to_position(speed=80, position=0, block=False)
self.motor_x.wait_until_not_moving()
self.motor_y.wait_until_not_moving()
self.pen_up()
def move_wh(self, w, h, speed_x=MAX_SPEED_X, speed_y=MAX_SPEED_Y):
self.motor_x.on_for_rotations(
speed=speed_x, rotations=-w * MAX_ROTATION_X, block=False)
self.motor_y.on_for_rotations(
speed=speed_y, rotations=h * MAX_ROTATION_Y, block=False)
self.motor_x.wait_until_not_moving()
self.motor_y.wait_until_not_moving()
def pen_down(self):
if not self._pen_down:
self.motor_pen.on_for_degrees(
speed=10, degrees=POSITION_PEN, brake=True, block=True)
self._pen_down = True
def pen_up(self):
if self._pen_down:
self.motor_pen.on_for_degrees(
speed=10, degrees=-POSITION_PEN, brake=True, block=True)
self._pen_down = False
def draw_arc(self, rad_x, rad_y, large, sweep, sx, sy, ex, ey):
h, k, start, end = self.solver.solve_ellipse(
(sx, sy), (ex, ey), rad_x, rad_y, large, sweep)
print('The solution is:', h, k, start*180/pi, "->", end*180/pi)
if start < 0:
start += 2*pi
if start < 0:
end += 2*pi
angle = start
angle_to_do = 0
if sweep:
cclock = 1
angle_to_do = end - start
else:
cclock = -1
angle_to_do = start-end
if angle_to_do < 0:
angle_to_do += 2*pi
print('doing:', angle_to_do*180/pi)
step = 0
angle_done = 0
integral_error_x = 0
integral_error_y = 0
integral_weight = 0.15
while angle_done < angle_to_do:
dx = -sin(angle) * rad_x * cclock - \
integral_error_x * integral_weight
dy = cos(angle) * rad_y * cclock - \
integral_error_y * integral_weight
magnitude = sqrt(dx**2 + dy**2)
speed_x = (dx/magnitude) * MAX_SPEED_X
speed_y = (dy/magnitude) * MAX_SPEED_Y
self.motor_x.on(speed=-speed_x)
self.motor_y.on(speed=speed_y)
sleep(0.1)
old_angle = angle
angle = self.solver.solve_angle(
cx=h, cy=k, a=rad_x, b=rad_y, px=self.x(), py=self.y())
step_angle = abs(angle-old_angle)
if(step_angle > pi):
step_angle = 2*pi - step_angle
angle_done += step_angle
print('Step', angle*180/pi, 'done:',
step_angle*180/pi, 'total:', angle_done*180/pi)
x_sol = cos(angle) * rad_x + h
y_sol = sin(angle) * rad_y + k
integral_error_x += (self.x()-x_sol)
integral_error_y += (self.y()-y_sol)
step += 1
if(step > 1000):
break
self.motor_x.stop()
self.motor_y.stop()
def draw_line(self, from_x, from_y, to_x, to_y, pen=True):
self.move_wh(from_x - self.x(), from_y - self.y())
if pen:
self.pen_down()
if (to_x-self.x()) < 0.000001 and (to_x-self.x()) > -0.000001:
self.move_wh(0, to_y-self.y())
else:
m = abs((to_y-self.y())/(to_x-self.x()))
speed_x = 1
speed_y = m * speed_x
magnitude = sqrt(speed_x**2 + speed_y**2)
speed_x = (speed_x/magnitude) * MAX_SPEED_X
speed_y = (speed_y/magnitude) * MAX_SPEED_Y
print(to_x-self.x(), to_y-self.y(), speed_x, speed_y)
self.move_wh(to_x-self.x(), to_y-self.y(),
speed_x=speed_x, speed_y=speed_y)
if pen:
self.pen_up()
def draw_svg_path(self, path, width, height):
path_list = path.split(" ")
index = 0
m_x = 0
m_y = 0
while index < len(path_list):
current = path_list[index]
if current == 'M':
x = float(path_list[index+1]) / width
y = float(path_list[index+2]) / height
m_x = x
m_y = y
self.move_wh(x-self.x(), y-self.y())
index += 3
self.pen_down()
elif current == 'L':
x = float(path_list[index+1]) / width
y = float(path_list[index+2]) / height
m_x = x
m_y = y
self.draw_line(m_x, m_y, x, y, pen=False)
index += 3
elif current == 'A':
rad_x = float(path_list[index+1]) / width
rad_y = float(path_list[index+2]) / height
large = int(path_list[index+4])
sweep = int(path_list[index+5])
dx = float(path_list[index+6]) / width
dy = float(path_list[index+7]) / width
index += 8
self.draw_arc(rad_x, rad_y, large, sweep, m_x, m_y, dx, dy)
m_x = dx
m_y = dy
else:
index += 1
self.pen_up()
def draw_svg(self, file):
tree = ET.parse(file)
root = tree.getroot()
width = int(root.attrib["width"])
height = int(root.attrib["height"])
for child in root:
if child.tag == '{http://www.w3.org/2000/svg}path':
for pos_title in child:
if pos_title.tag == '{http://www.w3.org/2000/svg}title':
print(pos_title.text, file=stderr)
self.draw_svg_path(child.attrib["d"], width, height)
elif child.tag == '{http://www.w3.org/2000/svg}line':
for pos_title in child:
if pos_title.tag == '{http://www.w3.org/2000/svg}title':
print(pos_title.text, file=stderr)
self.draw_line(
float(child.attrib["x1"])/width,
float(child.attrib["y1"])/height,
float(child.attrib["x2"])/width,
float(child.attrib["y2"])/height)
class Solver(object):
def solve_angle(self, cx, cy, a, b, px, py):
if px-cx:
theta = atan(((py-cy)*a)/((px-cx)*b))
if px-cx < 0:
# quadrand 2 and 3
theta = theta + pi
else:
if py-cy < 0:
theta = -pi/2
else:
theta = pi/2
return theta
def solve_ellipse(self, p1, p2, a, b, large, sweep):
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
m = -2*(b**2)*x1 + 2*(b**2)*x2
n = -2*(a**2)*y1 + 2*(a**2)*y2
q = -(b**2)*x1**2 - (a**2)*(y1**2) + (b**2)*(x2**2) + (a**2)*(y2**2)
if n < -0.000001 or n > 0.000001:
aa = b**2 + (a**2)*(m**2)/(n**2)
bb = -2*(b**2)*x1 - (2*q*(a**2)*m)/(n**2) + (2*(a**2)*y1*m)/n
cc = ((a**2)*(q**2))/(n**2) - (2*(a**2)*y1*q)/n + \
(b**2)*(x1**2) + (a**2)*(y1**2) - (a**2)*(b**2)
if bb**2 - 4*aa*cc < -0.000001 or bb**2 - 4*aa*cc > 0.000001:
h1 = (-bb + sqrt(bb**2 - 4*aa*cc))/(2*aa)
h2 = (-bb - sqrt(bb**2 - 4*aa*cc))/(2*aa)
else:
h1 = h2 = (-bb)/(2*aa)
k1 = (q - m * h1)/n
k2 = (q - m * h2)/n
else:
aa = a**2
bb = -2*a**2*y1
if m < -0.000001 or m > 0.000001:
cc = b**2*q**2/m**2 - 2*b**2*x1*q/m + b**2*x1**2 + a**2*y1**2 - a**2*b**2
h1 = q/m
h2 = q/m
if bb**2 - 4*aa*cc < -0.000001 or bb**2 - 4*aa*cc > 0.000001:
k1 = (-bb + sqrt(bb**2 - 4*aa*cc))/(2*aa)
k2 = (-bb - sqrt(bb**2 - 4*aa*cc))/(2*aa)
else:
k1 = k2 = (-bb)/(2*aa)
else:
h1 = h2 = x1
k1 = k2 = (-bb)/(2*aa)
h = "h"
k = "k"
if h1 != h2 or k1 != k2:
results = [
{h: h1, k: k1},
{h: h2, k: k2}
]
else:
results = [{h: h1, k: k1}]
results_len = len(results)
for result in results:
theta1 = 0
theta2 = 0
if p1[0]-result[h]:
theta1 = atan(((p1[1]-result[k])*a)/((p1[0]-result[h])*b))
if p1[0]-result[h] < 0:
# quadrand 2 and 3
theta1 = theta1 + pi
else:
if p1[1]-result[k] < 0:
theta1 = -pi/2
else:
theta1 = pi/2
if p2[0]-result[h]:
theta2 = atan(((p2[1]-result[k])*a)/((p2[0]-result[h])*b))
if p2[0]-result[h] < 0:
# quadrand 2 and 3
theta2 = theta2 + pi
else:
if p2[1]-result[k] < 0:
theta2 = -pi/2
else:
theta2 = pi/2
if results_len == 1:
return result[h], result[k], theta1, theta2
angle = theta2 - theta1
if angle < 0:
angle += 2 * pi
if angle >= 2*pi:
angle -= 2 * pi
if large == 0:
if sweep:
if angle <= pi:
return result[h], result[k], theta1, theta2
else:
if angle >= pi:
return result[h], result[k], theta1, theta2
else:
if sweep:
if angle >= pi:
return result[h], result[k], theta1, theta2
else:
if angle <= pi:
return result[h], result[k], theta1, theta2
return None
printer = Printer()
try:
printer.draw_svg("S.svg")
except Exception as e:
print(e, file=stderr)
traceback.print_exc(file=stderr)
printer.reset()
# for i in range(0, 100):
# printer.pen_down()
# printer.pen_up()
# printer.move_wh(-0.0, 1)
# printer.motor_pen.on_for_degrees(speed=5, degrees=-10) # - pen down
Comments