SandCastleIcon.png 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 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):
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, 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:

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

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

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

External Links

Cookies help us deliver our services. By using our services, you agree to our use of cookies.