This article or section documents something not included in the current version of Scratch (3.0). It is only useful from a historical perspective. |
This article has links to websites or programs outside of Scratch and Wikipedia. Remember to stay safe while using the internet, as we cannot guarantee the safety of other websites. |
This article or section may not have content matching Scratch Wiki editing standards. Please improve it according to Scratch Wiki:Guidelines and Scratch Wiki:Editing Conventions. (April 2018) Reason: This is written in book form rather than wiki form. |
ActionScript 3.0 (AS3) is a language developed by Adobe for use on the Flash platform. This tutorial teaches how to communicate to and from Scratch with AS3.
Note: | This tutorial does not include information about how to run the ActionScript code that is written. |
Note: | Remote Sensor Connections is a feature only available in Scratch 1.4. |
Scratch Setup
Scratch originally had a sensors block which allowed users to connect to a PicoBoard, but in Scratch 1.3, a feature was added to let any application interact with Scratch. To enable this feature, one must enter the Sensing Blocks palette, right-click the sensors block and choose "enable remote sensor connections". Users could enable this feature by choosing the Sensing Blocks palette and with the sensors block, choose the enable remote sensor connection option. The application would then start listening to port 42001.
Note: | If using Windows Vista or a similar operating system, one may have to confirm this action through a system dialog. |
ActionScript Setup
To program with ActionScript, a user needs an ActionScript editor. This tutorial was made with Flash CS4; however free alternatives, such as: FlashDevelop, are available that can achieve the same result.
Communicating
Scratch allows other applications to communicate with it on port 42001. It is an outlet for an application that Scratch can send and receive data through. Here is what Scratch and other languages can send to each other:
- Scratch is able to send a message through the socket whenever a global variable is updated or a broadcast is issued.
- Other programs on the same socket can broadcast a message to Scratch or can add custom values to the sensor block below the built-in ones.
However, other applications cannot set Scratch global variables through the port. For the purposes of this tutorial, an overview of the Scratch Extension Protocol is given. This basically describes what an application needs to do to send messages that Scratch will understand.
Protocol
All messages to Scratch must be sent to port 42001 in this format:
- After the zero bytes is the length of the string message to pass, given as a 32-bit big-Endian number (4 bytes).
- Lastly, there is the string message, whose length is the length of the message size in bytes.
For example:
[size][size][size][size][string message (size bytes long)]
Receiving Data From Scratch
To communicate with Scratch, it will be necessary use the Socket class. Create a new Socket and connect it to port 42001.
var host:String = "localhost" //This script connects to the computer this being programmed on (127.0.0.1) var sock:Socket = new Socket() //This is the socket
Now that there is a Socket to work with, one will need to add some event listeners with the socket's addEventListener() method. When the socket dispatches an event the user is listening for, it will call a handler function as specified.
sock.addEventListener(Event.CONNECT,onConnect) //Called when the socket connects sock.addEventListener(Event.CLOSE,onClose) //Called when the socket is closed sock.addEventListener(IOErrorEvent.IO_ERROR,onError) //Called on a connection problem sock.addEventListener(ProgressEvent.SOCKET_DATA,onDataIn) //Called when data is received
Now, write the event handlers for these listeners:
function onConnect(e:Event):void { trace("Connected!") } function onClose(e:Event):void { trace("Socket has been closed.") } function onError(e:IOErrorEvent):void { trace("Oh no! Trouble connecting!") } function onDataIn(e:ProgressEvent):void { //This will just call another function to do the work getData() }
Now, there are the functions to handle when the socket connects, closes, and throws an error. The last thing to do is to connect.
sock.connect(host,42001) //Pass the host variable from above and 42001 to connect()
Test the movie. If everything was done right, and Scratch is open with remote sensor connections enabled, one should see "Connected!" trace to the output panel after a few seconds. Now that it is connected, it can send and receive data with Scratch!
Remember the getData() function that was called from the onDataIn() event handler? This is what is next to write.
function getData():void { var data:String="" while (sock.bytesAvailable) { data+=sock.readUTF(); } trace(data) //More code here in a bit! }
Everything Scratch sent is now in the variable named data. That can be traced if one wants to, but instead, one will write some code to see what Scratch is sending.
- If a variable was updated, the program will get a message in this form:
sensor-update "variable name" value
- If a broadcast was dispatched, this will be received:
broadcast "broadcast name"
To see this in action, test the movie again, then head over to Scratch and change some global variables or broadcast some things. The messages will show up in the output panel. Next, it is needed to use the messages from Scratch. For this, Regular Expressions (RegExps) will be used. Basically, RegExps allows one to define a certain pattern a string must follow and can be checked against a string to extract data from it. Two RegExps are used here:
/broadcast\s\"(.*)"/ (This matches a string that follows the 'broadcast' pattern) /sensor-update\s\"(.*?)\"\s\"(.*?)\"/ (This matches a string that follows the 'sensor-update' pattern)
Information on RegExps can be found here.
/* -- THIS GOES WITHIN THE getData() FUNCTION! -- */ //One is still within the previous getData() function here. Place this code after 'trace(data)' var broadcast_pattern:RegExp=/broadcast\s\"(.*)"/; var sensor_pattern:RegExp=/sensor-update\s\"(.*?)\"\s\"(.*?)\"/; var sensor_num_pattern:RegExp=/sensor-update\s\"(.*?)\"\s([-|\d|.]+)/; var result:Object; //See those parentheses in the regexes above? The values within those are actually stored //in the 'result' object below at indices 1,2,etc. result=broadcast_pattern.exec(data); if (result != null) { //It's a broadcast! Call the broadcasted() function with the name of the broadcast broadcasted(result[1]) } result=sensor_pattern.exec(data); if (result != null) { //It's a sensor-update! Call the updated() function with the variable name and the new value updated(result[1],result[2]) } result=sensor_num_pattern.exec(data) if(result != null){ //It's...the same as before, only Scratch passed a numerical value. updated(result[1],result[2]) }
This is what this code does:
- If Scratch broadcasted something, it calls the broadcasted() function with the broadcast name.
- If Scratch updated a variable, it calls the updated() function with the updated variable and the new value.
Now, of course, one will have to write these functions:
function broadcasted(broadcast:String):void { trace("Scratch broadcasted",broadcast) } function updated(variable:String,value:String):void { trace("Scratch set",variable,"to",value) }
Test the movie and start playing with Scratch! As one broadcasts an update, they should see this happen in the output panel.
Doing Something With This Data
Now that one can understand Scratch's messages, it would be nice to be able to do something with them. The following explains how to launch a browser window from Scratch!
First, for the Scratch side. Create a new project and add a script like this:
when gf clicked forever ask [URL?] and wait if <not <(length of (answer)) = [0]>> ::control set [new url v] to (answer)
This repeatedly asks for a URL. Now on the AS3 side one has to modify the updated() function to look like this:
function updated(variable:String,value:String):void { trace("Scratch set",variable,"to",value) if(variable=="new url"){ navigateToURL(new URLRequest(value)) } }
Now when the "new URL" Scratch variable is updated, Flash will open a browser window at the specified URL.
Sending Data to Scratch
Next look at how to send data to Scratch. If one recalls from above, all messages must be formatted like so:
[0][0][0][message size](string message)
Look at the (string message) part. The way one sends these messages is identical to the way one receives them:
- If a remote sensor is going to be updated, it will look like this:
sensor-update "variable name" value
- If broadcasting, the script is this:
broadcast "broadcast name"
This is simple enough. Knowing how to format a message to Scratch, a user can then write the following function:
function sendScratchMessage(s:String):void { var bytes:ByteArray=new ByteArray(); bytes[0]=0; bytes[1]=0; bytes[2]=0; bytes[3]=s.length; for (var i:Number=0; i<4; i++) { sock.writeByte(bytes[i]); } sock.writeMultiByte(s,"us-ascii"); sock.flush(); }
Basically, this takes whatever is in the string "s" and wraps it in a ByteArray with the appropriate procedures. Notice the three zero bytes and the length of the message all go before the message itself. It then pushes these into the socket. With this function, users can write two more that make broadcasts and sensor updates a breeze:
function update(variable:String,value:*):void { if(!sock.connected) return value=String(value); sendScratchMessage('sensor-update "'+variable+'" "'+value+'"'); } function broadcast(broadcast:String):void { if(!sock.connected) return sendScratchMessage('broadcast "'+broadcast+'"'); }
Now all left to do is call these functions to send things through the socket.
AS3 to Scratch Example: Time
Like the URL block, something Scratch users often request is a time block. Replicate that functionality. This bit of code will create a 'time' item in the sensor block dropdown.
var timer:Timer = new Timer(1000) timer.addEventListener(TimerEvent.TIMER,everySecond) timer.start() function everySecond(e:TimerEvent):void { var d:Date=new Date() update('time',d.toTimeString()) }
Now head over to Scratch and on a new sprite do something like this:
when gf clicked forever say ([time v] sensor value)
And...that's it! Access to time functionality is available in Scratch (at least with this Flash file running).
Conclusion
The entire script can be copied into a Flash movie:
var sock:Socket = new Socket(); sock.addEventListener(ProgressEvent.SOCKET_DATA,onDataIn); sock.addEventListener(Event.CONNECT,onConnect) sock.addEventListener(Event.CLOSE,onClose) sock.addEventListener(IOErrorEvent.IO_ERROR,onError) sock.connect('localhost',42001) function onDataIn(e:ProgressEvent):void { getData(); } function onConnect(e:Event):void { trace("Connected!") } function onClose(e:Event):void { trace("Socket has been closed.") } function onError(e:IOErrorEvent):void { trace("Oh no! Trouble connecting!") } function getData():void { var data:String=""; while (sock.bytesAvailable) { data+=sock.readUTF(); } trace(data) var broadcast_pattern:RegExp=/broadcast\s\"(.*)"/; var sensor_pattern:RegExp=/sensor-update\s\"(.*)\"\s\"(.*)\"/; var sensor_num_pattern:RegExp=/sensor-update\s\"(.*?)\"\s([-|\d|.]+)/; var result:Object; //See those parentheses in the regexes above? The values within those are actually stored //in the 'result' object below at indices 1,2,etc. result=broadcast_pattern.exec(data); if (result!=null) { //It's a broadcast! Call the broadcasted() function with the name of the broadcast broadcasted(result[1]); } result=sensor_pattern.exec(data); if (result!=null) { //It's a sensor-update! Call the updated() function with the variable name and the new value updated(result[1],result[2]); } result=sensor_num_pattern.exec(data) if(result != null){ updated(result[1],result[2]) } } function broadcasted(broadcast:String):void { trace("Scratch broadcasted",broadcast) } function updated(variable:String,value:String):void { trace("Scratch set",variable,"to",value) if(variable=="new url"){ navigateToURL(new URLRequest(value)) } } function sendScratchMessage(s:String):void { var bytes:ByteArray=new ByteArray(); bytes[0]=0; bytes[1]=0; bytes[2]=0; bytes[3]=s.length; for (var i:Number=0; i<4; i++) { sock.writeByte(bytes[i]); } sock.writeMultiByte(s,"us-ascii"); sock.flush(); } function update(variable:String,value:*):void { if(!sock.connected) return value=String(value); sendScratchMessage('sensor-update "'+variable+'" "'+value+'"'); } function broadcast(broadcast:String):void { if(!sock.connected) return sendScratchMessage('broadcast "'+broadcast+'"'); } var timer:Timer=new Timer(1000) timer.addEventListener(TimerEvent.TIMER,everySecond) timer.start() function everySecond(e:TimerEvent):void { var d:Date=new Date() update('time',d.toTimeString()) }