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. |
The Remote Sensors Protocol can be used to connect Objective-C (that is, a Mac OS X or iOS application) to Scratch. This has many practical applications:
- Using the WebKit library to allow JavaScript processing
- This can be used to evaluate mathematical expressions like 5*(3-2/5)
- Also, quick string processing like String.replace is possible
- RegExp can be used to test strings
- OpenGL and Quartz 2D graphics calls
- Native actions like creating/reading files/urls and copying text to the clipboard
- Accelerated calculations (e.g. a factorial function)
- Using an iPhone or iPad to control a Scratch project
This tutorial assumes basic knowledge of:
- Xcode
- Interface Builder
- Cocoa
- Objective-C (especially memory management)
and will focus on:
- Using streams to connect to Scratch
- Manipulating bytes and NSData
- Using the remote sensors protocol
Note: | To use an iOS device rather than a Mac to control the project will require some patches as iOS does not support the NSHost Cocoa class. |
Set up the interface
First, open up Xcode and create a new Mac application. Call it ScratchConnect. Open the file ScratchConnectAppDelegate.h
(under classes) and replace the code with this:
#import <Cocoa/Cocoa.h> @interface ScratchConnectAppDelegate : NSObject <NSApplicationDelegate> { NSWindow *window;//Window NSOutputStream *outputstr;// Output stream NSInputStream *inputstr;// Input stream IBOutlet id ip;// Ip address text field IBOutlet id message;// Message text field IBOutlet id status;// Status bar } -(IBAction)connect:(id)sender;// Connect to Scratch -(IBAction)broadcast:(id)sender;// Send a message @property (assign) IBOutlet NSWindow *window; @end
The above code declares the global variables and functions the application will be using.
Save it, then open the file MainMenu.xib or MainMenu.nib
under Resources.
Create the following interface with the appropriate objects (2 text boxes, 2 buttons, 1 label):
Under the send button, add a label with no text.
Now bind the top text box to "ip", bottom text box to "message", and bottom label to "status". Then bind the "Connect" button to "connect:" and "Message" button to "broadcast".
At this point, it is allowed to save and quit interface builder. Now, in Xcode, open ScratchConnectAppDelegate.m
.
Add the code and try it out
Now, add replace the code in the open file with this:
Warning: | When adding or modifying this code, be careful. Memory leaks may occur. |
// Using this code poses a risk such as memory leaks. If this happens the Scratch Wiki is not responsible. #import "ScratchConnectAppDelegate.h" @implementation ScratchConnectAppDelegate @synthesize window; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { outputstr = [[NSOutputStream alloc] initToMemory];// Output stream inputstr = [NSInputStream alloc];// Input stream } -(IBAction)connect:(id)sender {// Connect to Scratch NSHost *host = [NSHost hostWithAddress:[ip stringValue]];// Get a host from the given IP address NSLog(@"Connecting..."); [NSStream getStreamsToHost:host port:42001 inputStream:&inputstr outputStream:&outputstr];// Set up streams to port 42001 (Scratch) on the computer's IP [outputstr scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];// Initialize output stream [outputstr setDelegate:self]; [outputstr open]; [inputstr scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];// Initialize input stream [inputstr setDelegate:self]; [inputstr open]; [status setStringValue:@"Connected!"];// Set status to connected } -(IBAction)broadcast:(id)sender {// Send a string message to Scratch if ([outputstr streamStatus] == 2) {// If the stream is open... NSData *myData = [[NSString stringWithString:[message stringValue]] dataUsingEncoding:NSASCIIStringEncoding];// Get NSData from message NSMutableData *toSend;// What will be transfered Byte *toAppend = (Byte*)malloc(4);// Size of message toAppend[0]=(([myData length] >> 24) & 0xFF);// Construct size from myData's size toAppend[1]=(([myData length] >> 16) & 0xFF); toAppend[2]=(([myData length] >> 8) & 0xFF); toAppend[3]=([myData length] & 0xFF); toSend = [NSMutableData dataWithBytes:toAppend length:4];// Append size to data [toSend appendData:myData];// Append string to data const uint8_t *bytes = (const uint8_t*)[toSend bytes];// Get bytes NSLog(@"%d bytes were sent.", [outputstr write:bytes maxLength:[toSend length]]);//Send it! } else {// Shut stream, error occurs NSBeep(); [status setStringValue:@"Oops! Not connected."]; } } -(void)dealloc {// Free up stream memory [outputstr close]; [outputstr release]; outputstr = nil; [inputstr close]; [inputstr release]; inputstr = nil; [super dealloc]; } - (void) stream: (NSStream *) stream handleEvent: (NSStreamEvent) eventCode {// Event handler NSLog(@"Event %d occurred:", eventCode); if (eventCode == NSStreamEventErrorOccurred) {// Error! NSLog(@"Error!"); [status setStringValue:@"Oops! A connection error!"]; } if (eventCode == NSStreamEventEndEncountered) {// Data transfer complete NSLog(@"End of transfer..."); } if (eventCode == NSStreamEventHasSpaceAvailable) {// Space available NSLog(@"Space left..."); } if (eventCode == NSStreamEventOpenCompleted) {// Stream opened NSLog(@"Opened..."); } if (eventCode == NSStreamEventHasBytesAvailable) {// Message received [status setStringValue:@"Message received!"]; uint8_t buffer[1024];// To read into uint8_t rec[1024];// Message received NSMutableData *data = [[NSMutableData alloc] init]; int length = [stream read:buffer maxLength:1024];// Read data received into buffer if (!length) {// Error! NSLog(@"No data"); [status setStringValue:@"Message received, but could not be read."]; } else { [data appendBytes:buffer length:length];// Append received bytes to data } [data getBytes:rec range:NSMakeRange(3, [data length]-3)];// Read bytes into rec (ie get rid of size prefix) data = [NSData dataWithBytes:rec length:length-3];// Read rec back into data NSString *messRec = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];// Get string from Data [status setStringValue:messRec];// Set status value [data release];// Free up memory [messRec release]; data = nil; messRec = nil; } } @end
This code will be explained later.
Now open up Scratch. Open the "Sensing" palette and scroll down. Open the drop-down menu of the ([] sensor value) block and click on "enable remote sensor connections". After noting down the IP address given, click on OK.
Now create these scripts in Scratch:
when I receive [hi v] broadcast (bye v) say [bye] wait (1) secs change [meetings v] by (1) when gf clicked set [meetings v] to (0)
Save the project.
Now build and run the Xcode project. In the IP text box, type the IP given by Scratch. Then click connect. The status label should read connected. Now type "broadcast hi" in the message text box and click send. Scratch should say "Bye". The status should read "broadcast "bye"". One second later, the status should change to 'sensor-update "meetings" 1'.
How it works
This section will examine it method-by-method (all lines are commented heavily so taking it apart line-by-line should not be too hard):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
Everything done here is allocate memory for the streams.
-(IBAction)connect:(id)sender
Here, the two streams must be connected to Scratch.
Tip: | Streams (NSStreams, NSInputstreams, and NSOutputstreams ) are connections to an IP address, which can read and write data |
First, set up a NSHost
with one's IP address. Then use this: getStreamsToHost
convenience method to connect the streams. Finally, schedule and open the streams.
-(IBAction)broadcast:(id)sender
Here the message will be to Scratch. First, check if the stream is open. If it is, create a NSData
with the string. Make one empty data for the final message. Create a byte array containing the size of the message, then construct the toSend data with a concatenation of the byte array and string's data.
Tip: | This is done because the format for a message sent to Scratch is [len][len][len][len](message). |
Finally, send this data with the NSOutputstream write:maxLength:
message.
-(void)dealloc
This script will free up memory.
- (void) stream: (NSStream *) stream handleEvent: (NSStreamEvent) eventCode
This event is triggered by a stream. First, take care of some events like errors, which need status bar updates. Then, take care of the hasBytesAvailible
event. This event is triggered by an incoming message. In this event, use the inputstream's read:maxLength:
message to read the given data, then cut off the first bytes (containing size), then finally display it.