From Test-Scratch-Wiki
![]() | Only change code if you consider yourself advanced with computers. |
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
![]() | To use an iOS device rather than a Mac to control the project, you will require some patches as iOS does not support the NSHost Cocoa class. |
Set up the interface
First, open up Xcode and create an 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 we will be using in our application.
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 you can 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:
![]() | When adding or modifying this code, be extremely careful. System crashes and/or memory leaks may occur. |
// If something goes wrong while using this code, // such as system crashes and/or memory leaks, // the Scratch Wiki is not responsible. // Use this code at your own risk. #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 our given IP address NSLog(@"Connecting..."); [NSStream getStreamsToHost:host port:42001 inputStream:&inputstr outputStream:&outputstr];// Set up streams to port 42001 (Scratch) on our 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 our stream is open... NSData *myData = [[NSString stringWithString:[message stringValue]] dataUsingEncoding:NSASCIIStringEncoding];// Get NSData from message NSMutableData *toSend;// What we will transfer 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
Let's 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
All we do here is allocate memory for our streams.
-(IBAction)connect:(id)sender
Here, we connect our two streams to Scratch.
![]() | Streams (NSStreams, NSInputstreams, and NSOutputstreams ) are connections to an IP address, which can read and write data |
First, we set up an NSHost
with our IP address. Then we use the getStreamsToHost
convenience method to connect the streams. Finally we schedule and open the streams.
-(IBAction)broadcast:(id)sender
Here we broadcast the message to Scratch. First, we check if the stream is open. If it is, we create an NSData
with our string. We also make one empty data for our final message. We 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.
![]() | We do this because the format for a message sent to Scratch is [len][len][len][len](message). |
Finally, we send this data with the NSOutputstream write:maxLength:
message.
-(void)dealloc
Here we just free up memory.
- (void) stream: (NSStream *) stream handleEvent: (NSStreamEvent) eventCode
This event is triggered by a stream. First, we take care of some events like errors, which need status bar updates. Then we take care of the hasBytesAvailible
event. This event is triggered by an incoming message. In this event, we use the inputstream's read:maxLength:
message to read the given data, then cut off the first bytes (containing size), then finally display it.