Peripheral Creation
This fifty-minute-long video walks you through the process of creating a peripheral. The video is in WebM format. If it does not appear below, you may download it (223 MB) and take a look at this page for how to watch it.
This page assumes that you are fairly new to using a Squeak-based system
Existing peripherals can be found in the Enchanting-NXT Peripherals category in your System Browser. It is helpful to use the class heirarchy to your advantage. In this example, we'll create blocks for different kinds of motors. I envision the following heirarchy:
NXTPeripheral (all peripheral devices (sensors, motors, etc) are derived from this
|
NXTDCMotor (motors that have no idea how far they've spun)
|
NXTTachoMotor (a motor with encoders that knows how far it turns).
The reason for this is that a DC motor does everything a generic peripheral does, and more, while a tacho motor does everything a DC motor does, and more.
Notes:
- if we were creating a sensor, we would subclass NXTSensor (or one of its subclasses)
- please start your class name with 'NXT' — there are no name spaces in Squeak.
Bring up your base class — NXTPeripheral in this case — in your system browser. Middle click on it and choose 'subclass template'. This gives you a template, as follows, for creating a subclass.
NXTPeripheral subclass: #NameOfSubclass instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Enchanting-NXT Peripherals'
Where it says 'NameOfSubclass' put in your subclass name. In this case, we'll use NXTDCMotor. Hit Command-S (Alt-S?) to save, or middle-click and choose 'Accept' from the menu. If you get an error, especially 'Message not understood: subclass: instanceVariableNames:classVariableNames:poolDictionaries:category:' be sure you have 'instance' selected and not 'class' on the bottom of the second pane.
It is helpful to look at another class as a reference. As I am doing this, I am referencing the NXTLightDetector class. Looking at that class I see that it has no instance methods, but it does have class methods. (Instance methods are like normal methods you can call on an object; class methods are methods you call on a class. They are like 'static' methods in C++).
First, we want to create categories for the different methods. With the NXTDCMotor class selected, and, being sure we are looking at 'class' methods and not 'instance' methods, middle click on '
Now, click on 'accessing'. In the bottom half of the browser window, a template for creating a new method shows up. Right now it says:
message selector and argument names "comment stating purpose of message" | temporary variable names | statements
I will cut and paste from another class (the NXTLightDetector), make changes, paste the code here, and add some additional comments where necessary. Most of the functions are really short.
accessing functions
With 'accessing' selected (so the new function will go into the 'accessing' category, copy, paste, and tweak this code, and then save it (by pressing Command-S/Alt-S/middle-clicking and choosing 'Accept (s)'
baseName ^ 'DC motor'
The baseName function gives a name that should be given to devices of this type when they are created. When you create one, it has the base name, ex: 'DC motor'. When you create another one, it will be the base name followed by a number, ex: 'DC motor 2'. The 'caret' symbol (which appears as an arrow with a hat on it in Squeak) means 'return'.
After that is saved, you can copy, paste, and edit this function and save it, right on top. As the function name differs, it will create a new function.
concreteTypes "Returns a list of physical devices that implement the functions of this class." ^ #( 'RCX Motor' 'NXT Motor' ).
The concreteTypes function returns a list of all the different types a given block can work with. In this case, we are saying that this function will work with RCX motors and with NXT motors. The first option in the list is the default.
The function is called concreteTypes. The line right after it, in double quotes, is a comment.
The
#(
starts an array, and )
ends it. In the array,
we have two strings, each within single quotes. (Double quotes are always for
comments). Note that there is nothing between the elements in the array. It
could, in fact, be written on one line, like this:^ #('RCX Motor' 'NXT Motor').
but it is a little easier to edit on multiple lines.
block specs
Select block specs in your System Browser under NXTDCMotor, and copy, paste and modify these functions:
configBlockSpec ^ 'Drives a %p named %s'.
providesBlocks "Returns a list of block specifications to be used in the main user interface if there is any device configured that can use these blocks. Note that the 'selectors' are all unique, start with the class name followed by a colon, but do not actually have functions that go with them." ^ #( 'motion' ('turn %P %J' - NXTDCMotor:turnMotorDirection) ('stop %P by %K' - NXTDCMotor:stopMotor) ('%P is moving?' b NXTDCMotor:isMoving) ).
These functions both provide block specifications ('block specs' for short) that are used for creating blocks.
configBlockSpec
returns a string that specifies the specification
for DC motor blocks that will appear in the configuration dialog. The
%p
is replaced by a list in which the user can choose the concrete
type they are using for this motor. The %s
(for string) is where
the user can type in a name for the motor.providesBlocks
is similar. It is an array defining blocks that will
show up under the 'motion' palette if there are any motors of this sort (or its
subclasses) defined. Each block entry is an array with three fields: the
specification, the block type, and a symbol or selector.These specifications are a little hard to make out; they become a little more obvious when you see the result. The
%P
is replaced with a list of
all the names of peripherals that this block applies to. Thus, if you've
configured two DC motors, named 'DC motor 1' and 'DC motor 2', then it will show
a list that lets you choose 'DC motor 1' and 'DC motor 2'. %J is replaced by a
list specific to this peripheral. In this case, it will give the options
'forward' and 'backward'. Thus, the first spec lets you choose a motor and tell
it to turn forward or backward. %K
is replaced by a second list
(which will be 'braking' and 'coasting'), and, if a third list was needed, we'd
use %Q
.The second field defines the block type. A
-
is a regular block.
b
is a boolean reporter. r
is a reporter that returns
a numerical or textual value. There are a few other special block types.The last field, in Scratch, is a selector (a symbol naming a method) that tells Scratch which method to call when a block is active. In Enchanting, it needs to be a unique symbol (that doesn't change after you set it, or you'll break saving and loading!) with the class name first, followed by a colon, and then some other characters to make it unique.
The DC Motor interface exposes the following methods:
Method Summary
void backward()
Causes motor to rotate backwards until stop is called.
void flt()
Motor loses all power, causing the rotor to float freely to a stop.
void forward()
Causes motor to rotate forward until stop is called.
boolean isMoving()
Return if the motor is moving.
void stop()
Causes motor to stop immediately.
My intention is to expose as many of these functions as possible to the end user. We will see what we can do to translate them shortly.
dropdown lists
Click on the dropdown list category, copy, paste, modify and save this function:
customListNumber: aNumber "Peripherals can return custom lists (or nil)." (aNumber = 1) ifTrue: [ "A direction to turn the motor." ^ #('forward', 'backward') ]. (aNumber = 2) ifTrue: [ "Ways the motor can stop." ^ #('braking' 'coasting') ]. ^ nil.
Remember how the block specs refered to two lists (as replacements for
%J
and %K
?) Here is where we provide those
lists.The function starts off with a comment, it checks if we are interested in list number 1, to replace a
%J
, and if so, it returns the list —
two directions the motor can turn, forward or backward. If that wasn't it, we
check for list 2, to replace a %K
, a method of stoppping the motor.
We could likewise check for list 3 (to replace a %Q
). If none of
the lists is right, we return a nil
.Registering Your Class
At this point, you'd probably like the blocks to show up in the user interface so that you can see them.
First, open up
NXTConfiguration class>>orderedPeripheralClasses
in your
System Browser. (With a System Browser open, hover over the upper-left pane of
the window, type Command-F, and you'll be asked to enter in part of a class
name. Type in "NXTC" and hit accept. As only one class starts with
that, you are taken directly to it. (Otherwise, you would've been asked to pick
a specific class that matched the pattern). In the second pane, click on 'class'
to highlight it instead of 'instance'. In the third pane, click on the
'accessing' category, and in the fourth, click on the orderedPeripheralClasses
method.)The method looks like this at the time of writing:
orderedPeripheralClasses "Returns a list of all peripherals that might be set up for this device. The order is important, as it will be followed when placing blocks in the palettes in the main user interface. (In other words, if you return { WhizBang . SawBuzz } the WhizBang blocks will always appear before the SawBuzz blocks." ^ { NXTLightDetector . NXTRangeDetector }
This function returns an array of all known peripheral classes for your device (ie. this tells Enchanting everything you can attach to an NXT), and the order it does so affects where they show up in the user interface. Note that the array notation is different. (Technical reason: the other format changes the classe names to symbols, where they are not as easy to use as classes.) It uses curly braces, and class names separated by periods.
We want the NXTDCMotor class to show up first in the list. Go ahead and modify it like this:
^ { NXTDCMotor . NXTLightDetector . NXTRangeDetector }
Next, go to
NXTConfiguration class>>peripheralsFor:
. The
function currently reads:peripheralsFor: dialogName "Returns a list of all the peripherals for the specified dialog" (dialogName = 'sensors') ifTrue: [ ^ {NXTLightDetector . NXTRangeDetector} ]. ^ #()
It looks to see if the dialog is named sensors and then puts up which sort of sensor blocks should be user configurable. Not all classes need to go here. If I didn't want a user to directly create objects of the NXTDCMotor class, I wouldn't put it in here. (Why on earth might I not want them to create objects of that type, after all the work we just went to?! Well, perhaps the class has a subclass that I'd rather they use instead. The blocks for NXTDCMotor will still show up in the main user interface if a user configured a subclass of that type.)
We want NXTDCMotor to be an option when the user brings up the 'motion' dialog. I've changed the method to read:
peripheralsFor: dialogName "Returns a list of all the peripherals for the specified dialog" (dialogName = 'sensors') ifTrue: [ ^ {NXTLightDetector . NXTRangeDetector} ]. (dialogName = 'motors') ifTrue: [ ^ {NXTDCMotor} ]. ^ #()
As an aside, here's how I changed the buttons to launch the new dialog. I opened a new Workspace window (middle click on the background somewhere to bring up the World menu, choose open -> workspace) and types in the text on the current button, 'Configure Motors/Sensors', without the quotes, in the window. I then right-clicked on the line, and choose "Method Strings with it (E)" from the second menu. This brought up a blue window that showed me a function called ScriptableScratchMorph>>viewerPageForMotion. I looked at the code, highlighted the function name in the top pane, hit Command-B to bring up a System Browser on the function, and made changes to it and to ScriptableScratchMorph>>viewerPageForSensing. I also has to create a method called ScriptableScratchMorph>>showConfigureMotorsDialog and edit ScriptableScratchMorph>>showConfigureSensorsDialog.
Now, if you open up the configuration dialog (go to the main user interface, choose the motion palette, click on the button at the top, currently named 'Configure Motors/Sensors'), you'll see your new block shows up as an option.
When you configure one, you'll see your new blocks show up in the motion palette.
Serialization
Serialization and deserialization refers to storing/loading an object into a file. Go ahead and put one of your new blocks out in a script. Choose File -> Save. A dialog comes up that says, "Save failed: unknown class NXTDCMotor."
We have to tell the system about NXTDCMotor. Bring ObjStream class>>userClasses up in a System Browser. This function returns a list that pairs a number to a class name. Right now it ends with:
(180 NXTConfiguration) (181 NXTLightDetector) (182 NXTRangeDetector) )
We need to add our new class here. The numbers can not exceed 255, and should not be changed after bring created (as it will break the file format, making it impossible to open any file that used the previous number.)
I change the method to end as:
(180 NXTConfiguration) (181 NXTLightDetector) (182 NXTRangeDetector) (183 NXTDCMotor) )
After I do that, if I go File -> Save, I still get the same error. The ObjStream class doesn't realize I've added this new entry. Shift-click on File and choose '- Register classes for serialization'. Now you can go File -> Save, and then File -> Open. It works.
Translations
Let's output the text that we will need to translate. Shift-click on the File menu and choose "Show text to translate for peripheral". A dialog will come up asking which peripheral. Type in the one you care about, NXTDCMotor in this case. Hit accept. You will then get a window showing all the text needing code translations.
Copying and pasting, it looks like this:
######################### # NXTDCMotor ######################### msgid "RCX Motor" msgstr "" msgid "NXT Motor" msgstr "" msgid "turn %P %J" msgstr "" msgid "stop %P by %K" msgstr "" msgid "%P is moving?" msgstr "" msgid "forward" msgstr "" msgid "backward" msgstr "" msgid "braking" msgstr "" msgid "coasting" msgstr ""
You'll want to add this text to the end of FrontEnd/locale/lejos.exporter. Note that no duplicate checking has occurred. If some of these strings exist elsewhere, you'll have to remove one entry, and be certain that the translation is suitable in both instances, in both code and human languages.
The first two entries, 'RCX Motor' and 'NXT Motor' are the concrete types. We need to specify which class they are in LeJOS, which are 'RCXMotor' and 'Motor', and we do so like this:
msgid "RCX Motor" msgstr "RCXMotor" msgid "NXT Motor" msgstr "Motor"
Now, how do we translate the blocks and list elements to form code?
Consider this spec:
'turn %P %J'
and the two list values:
'forward'
'backward'
Someone we need to turn this into function calls like this:
myMotor.forward();
and
myMotor.backward();
note the semicolons; these are complete statements.
We can do it like this.
'forward' -> 'forward'
'backward' -> 'backward'
'turn %P %J' -> '%P.%J();'
The %P will be translated to become the name of a variable representing the motor, now unlike 'myMotor', but more likely named 'MA' or 'M1'. The %J (and %K and %Q) will take the text, look up the translation, and replace it.
So, using 'turn %P %J', turning it into '%P.%J();' and substituting M1 for %P and, if %J was 'forward', substituting 'forward' in for it, we get: 'M1.forward();'. This is exactly what we want.
Now consider 'stop %P by %K' with %K initially being 'braking' or 'coasting', and us wanting 'myMotor.stop();' if braking was chosen, and 'myMotor.flt();' if coasting was chosen.
The following translations will do that:
'braking' -> 'stop'
'coasting' -> 'flt'
'stop %P by %K' -> '%P.%K();'
Lastly, we have '%P is moving?' and we want it to become something like 'myMotor.isMoving()' (note the absense of a semicolon; this is not a complete statement). The translations is:
'%P is moving?' -> '%P.isMoving()'
Substituting in those translations, we get:
######################### # NXTDCMotor ######################### msgid "RCX Motor" msgstr "RCXMotor" msgid "NXT Motor" msgstr "Motor" msgid "turn %P %J" msgstr "%P.%J();" msgid "stop %P by %K" msgstr "%P.%K();" msgid "%P is moving?" msgstr "%P.isMoving()" msgid "forward" msgstr "forward" msgid "backward" msgstr "backward" msgid "braking" msgstr "stop" msgid "coasting" msgstr "flt"
Add this section to lejos.exporter, save it, and try exporting code — either by hitting the green flag or by choosing File -> Export Code and compiling manually. It will immediately use the latest data. Try out all the blocks, check that they compile (and run, if you can) and tweak as necessary.