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 Note: 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):
Scratch Connect IB Interface.png
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:

Note Warning: When adding or modifying this code, be careful. 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.

Note Tip: 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.

Note Tip: 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.

External Links