Basic Scripting Guide ===================================================== Creating a Script ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Scripts can be added to a robot using a microcontroller part. Microcontrollers can be found in the parts inventory under the "Powered" category or by searching "microcontroller". If the part is still locked for you, go to the sandbox and it will be unlocked there. .. image:: Images/microcontroller_snap.png Once you've placed one on the robot, navigate over to the properties tab to see the interface for attaching scripts. .. image:: Images/microcontroller_properties_snap.png Use the "NEW" button to create and name a new script from the RoboCo scripting template. A script preview will then appear to the right of the properties panel to show that the script was attached. .. image::Images/ preview_snap.png To edit this script you may either select the "OPEN SCRIPT FOLDER" button and open the script from the folder or, if you followed the :doc:`/GettingStarted/idesetup`, you can open your scripts folder as a workspace in your IDE and open the script in VS Code. The Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once you have the script open in your IDE, it should look something like this: .. code-block:: python from inputs import * from controllables import * from sensors import * from scriptruntime import * from ports import * from color import * from routines import * from math import tau from time import sleep def main(): print('template script attached to ' + MicroController.self().name()) if __name__ == "__main__": main() The first 9 lines are import statements that tell python that you want to use code from the RoboCo API. Then there's a function called ``main`` whose body is example code to print a string that contains the name of this script's microcontroller. After ``main`` there is code that calls ``main``. The ``__name__ == "__main__"`` check is a pythonic way to determine if your script is being run as a script or being imported into another script. [#main]_ If you enter live mode with the script on a microcontroller you should see a print statement like this: .. image:: Images/template_print_log.png To get a more permanent look at the print statement, open it in the console tab: .. image:: Images/console.png To see what a script looks like without the boilerplate of the template, replace the whole script with just the following 1 line "Hello World" script. .. code-block:: python print("Hello, World!") The RoboCo API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ However, it takes more than print statements to program a robot. You'll want your scripts to spin motors and read sensors. To do that, you'll need the RoboCo API. An API is just a collection of types and functions written by someone else for you to use in your program. The RoboCo API will provide you with functions like :py:meth:`~controllables.DCMotor.spin` to spin a dc motor. In order to use this function you must to import the :py:class:`~controllables.DCMotor` class from the module :py:mod:`~controllables`. The template above imports a lot of classes, but in its simplest form importing and using those classes can look like either of the following examples: .. code-block:: python import controllables controllables.DCMotor(0).spin() .. code-block:: python from controllables import DCMotor DCMotor(0).spin() The first way imports the whole module :py:mod:`~controllables` and then accesses the class from it, while the second way just imports the class that it wants and accesses it directly. Another option is to import **all** the classes from the module like this: .. code-block:: python from controllables import * DCMotor(0).spin() Piston(1).move() Ports and Controllables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you want to do anything with these functions, you'll need a motor to spin. Let's start by adding a DC motor to the robot with something for it to spin. .. image:: Images/motor_png.png In order for the script to access the motor, the motor needs to be assigned to a "port" on the script's microcontroller. To do that, select the microcontroller in the properties tab. Then click the "ASSIGN" button for "PORT 0", and then click the on either the motor itself or the motor's button on the leftmost panel. .. image:: Images/port_assignment.PNG When you're done it should look like this: .. image:: Images/assigned.PNG Now replace your script with this code: .. code-block:: python from controllables import DCMotor # get the motor from port zero motor = DCMotor(0) # make it spin motor.spin() First you assign ``DCMotor(0)`` to a variable named ``motor``. The ``0`` here means that you want the part assigned to port 0. Next, you call the ``spin`` method on the motor which will make it start spinning. If you go into live mode the motor should start spinning right away. Sleep ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to write a script that doesn't finish right away, you can make use of the :py:mod:`~time.sleep` function. It causes the script to stop running for some number of seconds, before resuming where it left off. This can be handy for sequencing actions over time. .. code-block:: python from time import sleep from controllables import DCMotor # get the motor from port 0 motor = DCMotor(0) motor.spin()# makes the motor start spinning sleep(1)# our code waits for 1 second motor.stop()# then stops the motor sleep(1)# then the code waits again motor.spin(power = -1)# spins the motor in reverse sleep(1)# let the motor spin for 1 second motor.stop()# finally stop the motor When you enter play mode the above code should make the motor spin forward for one second, then stop for another second, then spin in reverse for a second before stopping again. Input and "The Loop" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To make the robot a bit more interesting we'd like to get it to respond to user input. For that, import the :py:mod:`~inputs` module. In the above script, your code ran for a few seconds before finishing. To get the code to run forever write a while loop that never terminates. .. code-block:: python from controllables import DCMotor from inputs import Input # spin the motor on port 0 motor = DCMotor(0) motor.spin() # repeat this code forever while True: # spin the motor only when x is pressed if Input.pressed("x"): motor.spin() else: motor.stop() This script spins the motor when the ``x`` key is pressed and stops the motor when it is released. It runs constantly rechecking the status of the ``x`` key and calling :py:meth:`~controllables.DCMotor.spin` or :py:meth:`~controllables.DCMotor.stop`. Because the loop condition is ``True``, it will run until the robot is shutdown as you exit live mode. The Text Screen ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you're programming, you'll often want to print out data so that you can understand what is happening in your code. If you do this inside a loop it can be really hard to actually read the data as it spams the console. To make things easier (and more fun) we recommend using the :py:class:`~controllables.TextScreen` part to output useful information. After placing and appropriately sizing the Text Screen part, you can assign it to port ``1`` of your microcontroller. .. image:: Images/text_screen.png Now you can output text to it in the script when you access it on port ``1``. .. code-block:: python from controllables import DCMotor, TextScreen from inputs import Input # spin the motor on port 0 motor = DCMotor(0) motor.spin() # get the text screen on port 1 screen = TextScreen(1) while True: # spin the motor only when x is pressed if Input.pressed("x"): motor.spin() else: motor.stop() # set the text value of the screen screen.text = "rpm = " + str(motor.rpm()) If you want to format the text and output multiple data points you might want to consider `string formatting`_. .. _`string formatting`: https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals .. code-block:: python from controllables import DCMotor, TextScreen from inputs import Input # spin the motor on port 0 motor = DCMotor(0) motor.spin() # setup the screen on port 2 with left alignment screen = TextScreen(1) screen.horizontal_alignment = TextScreen.HorizontalAlignment.LEFT while True: # spin the motor only when x is pressed if Input.pressed("x"): motor.spin() else: motor.stop() # set the screen to a multiline formatted string screen.text = ( f""" rpm = {motor.rpm():03.0f} degrees = {motor.degrees():03.0f} """ ) Sensors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To get something more interesting to print we can grab a :py:class:`~sensors.DistanceSensor` from the inventory. Try placing a distance sensor on a piston and assigning them to ports 0 and 1 like this: .. image:: Images/distance_sensor.png Then you can write a script that outputs the sensor data to the text screen as the piston pushes it in and out. .. code-block:: python from sensors import DistanceSensor from controllables import TextScreen, Piston # get the sensor off of port 0 sensor = DistanceSensor(0) # this just oscillates the piston on port 1 piston = Piston(1) piston.oscillate = True piston.move() # setup the screen on port 2 with left alignment screen = TextScreen(2) screen.horizontal_alignment = TextScreen.HorizontalAlignment.LEFT while True: # print out the distance with 3 places of precision screen.text = f'distance = {sensor.distance():.3f}' And Beyond! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following example is somewhat more complicated than the previous ones, but it is composed out of all those ideas put together. Setup your robot like in the image below, and paste the following code into the attached script. The robot will attempt to wander without hitting any walls. See if you can understand what the code is doing and play around with changing or improving it. .. image:: Images/automaton.png .. code-block:: python from sensors import DistanceSensor from controllables import DCMotor, TextScreen from time import time # get all the motors motors = [DCMotor(0),DCMotor(1),DCMotor(2),DCMotor(3)] right_motors = motors[:2] left_motors = motors[2:] # get the distance sensors, "whiskers", on ports 4 and 5 sensors = [DistanceSensor(4),DistanceSensor(5)] # define distance as the minimum of both whiskers def distance(): return min(sensors[0].distance(),sensors[1].distance()) # get the text screen from port 6 screen = TextScreen(6) screen.horizontal_alignment = TextScreen.HorizontalAlignment.LEFT # if the robot gets within 3 meters of # something it will turn around MIN_DISTANCE = 3 # make sure the left motors are flipped for motor in left_motors: # "flipped" is the same as in game property "flip" motor.flipped = True # and the right motors aren't flipped for motor in right_motors: motor.flipped = False # send the robot forward! for motor in motors: motor.spin() def turn_around(): # slow down for motor in motors: motor.spin(.25) # start turning around for motor in left_motors: motor.flipped = False # keep track of the average distance that we see distances = [] # turn for 5 seconds start_time = time() while time() - start_time < 5: distances.append(distance()) screen.text = ( f"""surveying for a better direction distance: {distance():.0f} time: {time() - start_time:.0f} """ ) # turn back the other way for motor in left_motors: motor.flipped = True for motor in right_motors: motor.flipped = True # calculate the average to use a min threshold # Q: Is there a better threshold you can think of? average_distance = sum(distances) / len(distances) start_time = time() while distance() < average_distance: if time() - start_time > 5: # it took too long just give up break # Q: What can we do to make this # less likely? screen.text = ( f"""done surveying trying to reach: {average_distance:.0f} current distance: {distance():.0f} time: {time() - start_time:.0f} """ ) # go forward for motor in left_motors: motor.flipped = True for motor in right_motors: motor.flipped = False # speed up for motor in motors: motor.spin(1) while True: screen.text = "charging ahead" if distance() < MIN_DISTANCE: turn_around() .. rubric:: Footnotes .. [#main] While the template is written in the standard form for python scripts, none of the boilerplater is actually required. You don't need to write your code in a function if you don't want to and the name ``main`` is just a convention. Unless you are planning on importing this script from elsewhere, you don't need to check the value of ``__name__``.