728x90

Peer-to-Peer Connectivity

The GKSession class allows your application to create and manage an ad-hoc Bluetooth network, as shown in Figure 1. Copies of your application running on multiple devices can discover each other and exchange information, providing a simple and powerful way to create multiplayer games on the iPhone. Further, sessions offer all applications an exciting new way to allow users to collaborate with each other.

Figure 1  Bluetooth networking

Bluetooth Connection

Bluetooth networking is not supported on the original iPhone or the first-generation iPod Touch. It is also not supported in Simulator.

When you develop a peer-to-peer application, you can either implement your own user interface to show other users discovered by the session or you can use a GKPeerPickerController object to present a standard user interface to configure a session between two iPhones.

Once the network between the devices is established, the GKSession class does not dictate format for the data transmitted over it. You are free to design data formats that are optimal for your application.

Note: This guide discusses the infrastructure provided by the peer-to-peer connectivity classes. It does not cover the design and implementation of networked games or applications.

Sessions

Sessions are created, they discover each other, and they are connected into a network. Your application uses the connected session to transmit data to other iPhones. Your application provides a delegate to handle connection requests and a data handler to receive data directed to your application from another iPhone.

Peers

iPhones connected to the ad-hoc network are known as peers. A peer is synonymous with a session running inside your application. Each session creates a unique peer identification string, or peerID, used to identify them to other users on the network. Interactions with other peers on the network are done through their peer ID. For example, if your application knows the peer ID of another peer, it can retrieve a user-readable name for that peer by calling the session’s displayNameForPeer: method, as shown in Figure 2

Figure 2  Peer IDs are used to interact with other peers

Peer IDs are used to communicate with other peers.

Other peers on the network can appear in a variety of states relative to the local session. Peers can appear or disappear from the network, be connected to the session, or disconnect from the session. Your application implements the delegate’s session:peer:didChangeState: method to be notified when peers change their state.

Discovering Other Peers

Every session implements its own specific type of service. This might be a specific game or a feature like swapping business cards. You are responsible for determining the needs of your service type and the data it needs to exchange between peers.

Sessions discover other peers on the network based on a session mode which is set when the session is initialized. Your application can configure the session to be a server, which advertises a service type on the network; a client, which searches for advertising servers; or a peer, which advertises like a server and searches like a client simultaneously. Figure 3 illustrates the session mode.

Servers advertise their service type with a session identification string, or sessionID. Clients find only servers with a matching session ID.

Figure 3  Servers, clients, and peers

Servers, Clients and Peers

The session ID is the short name of a registered Bonjour service. For more information on Bonjour services, see Bonjour Networking. If you do not specify a session ID when creating a session, the session generates one using the application’s bundle identifier.

To establish a connection, at least one iPhone must advertise as a server and another must search for it. Your application provides code for both modes. Peers, which advertise and search simultaneously, are the most flexible way to implement this. However, because they both advertise and search, it takes longer for other devices to be detected by the session.

Implementing a Server

A copy of your application acting as a server initializes the session by calling initWithSessionID:displayName:sessionMode: with a session mode of either GKSessionModeServer or GKSessionModePeer. After the application configures the session, it advertises the service by setting the session’s isAvailable property to YES.

Servers are notified when a client requests a connection. When the client sends a connection request, the session:didReceiveConnectionRequestFromPeer: method on the delegate is called. A typical behavior of the delegate should be to use the peerID string to retrieve a user-readable name by calling displayNameForPeer:. It can then present an interface that lets users decide whether to accept the connection.

The delegate accepts the request by calling the session’s acceptConnectionFromPeer:error: or rejects it by calling denyConnectionFromPeer:.

When the connection is successfully created, the delegate’s session:peer:didChangeState: method is called to inform the delegate that a new peer is connected.

Connecting to a Service

A copy of your application acting as a client initializes the session by calling initWithSessionID:displayName:sessionMode: with a session mode of either GKSessionModeClient or GKSessionModePeer. After configuring the session, your application searches the network for advertising servers by setting the session’s isAvailable property to YES. If the session is configured with the GKSessionModePeer session mode it also advertises itself as a server, as described above.

When a client discovers an available server, the delegate’s session:peer:didChangeState: method is called to provide the peerID string of the discovered server. Your application can call displayNameForPeer: to retrieve a user-readable name to display to the user. When the user selects a peer to connect to, your application calls the session’s connectToPeer:withTimeout: method to request the connection.

When the connection is successfully created, the delegate’s session:peer:didChangeState: method is called to inform the application that a new peer is connected.

Exchanging Data

Peers connected to the session can exchange data with other connected peers. Your application sends data to all connected peers by calling the sendDataToAllPeers:withDataMode:error: method or to a subset of the peers by calling the sendData:toPeers:withDataMode:error: method. The data is an arbitrary block of memory encapsulated in an NSData object. Your application can design and use any data formats it wishes for its data. Your application is free to create its own data format. For best performance, it is recommended that the size of the data objects be kept small (under 1000 bytes in length). Larger messages (up to 95 kilobytes) may need to be split into smaller chunks and reassembled at the destination, incurring additional latency and overhead.

You can choose to send data reliably, where the session retransmits data that fails to reach its destination, or unreliably, where it sends it only once. Unreliable messages are appropriate when the data must arrive in real time to be useful to other peers, and where sending an updated packet is more important than resending stale data (for example, dead reckoning information).

Reliable messages are received by participants in the order they were sent by the sender.

To receive data sent by other peers, your application implements the receiveData:fromPeer:inSession:context: method on an object. Your application provides this object to the session by calling the setDataReceiveHandler:withContext: method. When data is received from connected peers, the data handler is called on your application’s main thread.

Important: All data received from other peers should be treated as untrusted data. Be sure to validate the data you receive from other peers and write your code carefully to avoid security vulnerabilities. See the Secure Coding Guide for more information.

Disconnecting Peers

When your application is ready to end a session, it should call the disconnectFromAllPeers method.

Your application can call the disconnectPeerFromAllPeers: method to disconnect a particular peer from the connection.

Networks are inherently unreliable. If a peer is non responsive for a period of time, it is automatically disconnected from the session. Your application can modify the disconnectTimeout property to control how long the session waits for another peer before disconnecting it.

Your application can detect when another peer disconnects inside the delegate’s session:peer:didChangeState: method.

Cleaning Up

When your application is ready to dispose of the session, your application should disconnect from other peers, set the isAvailable flag to NO, remove the data handler and delegate, and then release the session.

The Peer Picker

While you may choose to implement your own user interface using the GKSession's delegate, Game Kit offers a standard user interface to the discovery and connection process. A GKPeerPickerController object presents the user interface and responds to the user’s actions, resulting in a fully configured GKSession that connects the two peers. Figure 4 illustrates how the peer picker works..

Figure 4  The peer picker creates a session connecting two peers on the network

Peer Picker

Configuring the Peer Picker Controller

Your application provides a delegate that the controller calls as the user interacts with the peer picker.

The peer picker controller’s setConnectionTypesMask: property is used to configure the list of available connection methods the application offers to the user. In iPhone OS 3.0, the peer picker can select between local Bluetooth networking and Internet networking. When your application sets the mask to include more than one form of network, the peer picker controller displays an additional dialog to allow users to choose which network they want to use. When a user picks a network, the controller calls the delegate’s peerPickerController:didSelectConnectionType: method.

Important: In iPhone OS 3.0, the peer picker does not configure Internet connections. If your application provides Internet connections, when the user selects an Internet connection, your application must dismiss the peer picker and present its own user interface to configure the Internet connection.

If your application wants to customize the session created by the peer picker, it can implement the delegate’s peerPickerController:sessionForConnectionType: method. If your application does not implement this method, the peer picker creates a default session for your application.

Displaying the Peer Picker

When your application has configured the peer picker controller, it shows the user interface by calling the controller’s show method. If the user connects to another peer, the delegate’s peerPickerController:didConnectPeer:toSession: method is called. Your application should take ownership of the session and call the controller’s dismiss method to hide the dialog.

If the user cancels the connection attempt, the delegate’s peerPickerControllerDidCancel: method is called.

728x90

'BlueTooth > 기본기' 카테고리의 다른 글

Bluetooth Packet Type  (0) 2013.11.06
quoted-printable decoder  (0) 2013.06.03
synergy MessageSendLater  (0) 2011.04.19
bluelab stereo 2009.R2 Inquiry시 iPhone이 검색되면 panic  (0) 2011.03.30
Apple 개발문서  (4) 2010.03.15
iphone간 bluetooth 연결 sample  (1) 2010.03.04
아이폰 블루투스 프로그래밍  (3) 2010.03.03
bluetooth keypad  (0) 2010.02.10
Bluetooth 2.0 under에서 Pincode없이 연결  (0) 2009.12.28
bluetooth hfp 정리  (0) 2009.07.24
728x90
iphone간 bluetooth 연결 sample

Sameple Code : need GameKit Framework

BluetoothSampleAppDelegate.h
//
// BluetoothSampleAppDelegate.h
// BluetoothSample
//

#import "EAGLView.h"
#import <GameKit/GameKit.h>

@interface BluetoothSampleAppDelegate : NSObject  {
    UIWindow *window;
    EAGLView *glView;

    GKPeerPickerController *picker;
    GKSession *session;

    int myNumber;
    NSData *myData;
    UILabel *textView;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet EAGLView *glView;
@property (nonatomic, retain) GKPeerPickerController *picker;
@property (nonatomic, retain) GKSession *session;

- (void)mySendData;

@end

BluetoothSampleAppDelegate.m
//
// BluetoothSampleAppDelegate.m
// BluetoothSample
//

#import "BluetoothSampleAppDelegate.h"

@implementation BluetoothSampleAppDelegate

@synthesize picker;
@synthesize session;
@synthesize window;
@synthesize glView;

- (void)applicationDidFinishLaunching:(UIApplication *)application
{

    // setup the text view
    myNumber = 0;
    textView = [[UILabel alloc] initWithFrame:CGRectMake(10.0f, 10.0f, 640.0f, 12.0f)];
    textView.text = [NSString stringWithFormat:@"myNumber: %i\n", myNumber];
    [window addSubview:textView];
    [window bringSubviewToFront:textView];

    // start the EAGLView
    glView.animationInterval = 1.0 / 60.0;
    [glView startAnimation];

    // allocate the NSData

    myData = [[NSData alloc] initWithBytes:&myNumber length:sizeof(int)];

    // allocate and setup the peer picker controller
    picker = [[GKPeerPickerController alloc] init];
    picker.delegate = self;
    picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby |
                                 GKPeerPickerConnectionTypeOnline;

    [picker show];
}

- (void)applicationWillResignActive:(UIApplication *)application
{

    glView.animationInterval = 1.0 / 5.0;
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{

    glView.animationInterval = 1.0 / 60.0;
}

- (void)peerPickerController:(GKPeerPickerController *)picker didSelectConnectionType:
(GKPeerPickerConnectionType)type
{

    if(type == GKPeerPickerConnectionTypeOnline)
    {

        [self.picker dismiss];
        [self.picker release];
        self.picker = nil;

       
// Display your own UI here.

    }
}

- (GKSession *) peerPickerController:(GKPeerPickerController *)picker
sessionForConnectionType:(GKPeerPickerConnectionType)type
{

    session = [[GKSession alloc] initWithSessionID:@"FR" displayName:nil
                                            sessionMode:GKSessionModePeer];

    session.delegate = self;
    return session;
}

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:
                    (GKPeerConnectionState)state
{

    switch (state)
    {

        case GKPeerStateConnected:
            [self.session setDataReceiveHandler :self withContext:nil];
            [self mySendData]; // start off by sending data upon connection
            break;
        case GKPeerStateDisconnected:
            break;
    }
}

- (void)peerPickerController:(GKPeerPickerController *)picker didConnectToPeer:
                (NSString *)peerID
{

    printf("connection was successful! start the game.\n");
}

- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker
{

    printf("connection attempt was canceled\n");
}

- (void)mySendData
{

    // allocate the NSData
    myNumber++;
    myData = [[NSData alloc] initWithBytes:&myNumber length:sizeof(int)];
    [session sendDataToAllPeers :myData withDataMode:GKSendDataReliable error:nil];
    printf("send data: %i\n", myNumber);
    textView.text = [NSString stringWithFormat:@"myNumber: %i\n", myNumber];
}

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session
                                 context:(void *)context
{
    // Read the bytes in data and perform an application-specific action,
    // then free the NSData object

    [data getBytes:&myNumber length:sizeof(int)];

    printf("received data: %i from: %s\n", myNumber, [peer UTF8String]);
    textView.text = [NSString stringWithFormat:@"myNumber: %i\n", myNumber];
    [self mySendData];
}

- (void)dealloc
{
    [picker release];
    [session release];
    [textView release];
    [glView release];
    [window release];
    [super dealloc];
}

@end


728x90

'BlueTooth > 기본기' 카테고리의 다른 글

quoted-printable decoder  (0) 2013.06.03
synergy MessageSendLater  (0) 2011.04.19
bluelab stereo 2009.R2 Inquiry시 iPhone이 검색되면 panic  (0) 2011.03.30
Apple 개발문서  (4) 2010.03.15
Peer-to-Peer Connectivity  (0) 2010.03.04
아이폰 블루투스 프로그래밍  (3) 2010.03.03
bluetooth keypad  (0) 2010.02.10
Bluetooth 2.0 under에서 Pincode없이 연결  (0) 2009.12.28
bluetooth hfp 정리  (0) 2009.07.24
BlueTooth 관련용어  (0) 2009.07.20
728x90
원본 링크를 찾지 못했습니다. 죄송합니다. 불펌이네요;;

ne of the neat features available in iPhone OS 3.0 is the GameKit framework. The GameKit framework contains APIs to allow communications over a Bluetooth network. Using these APIs, you can create peer-to-peer games and applications with ease. Unlike other mobile platforms, using Bluetooth as a communication channel in iPhone is way easier than expected. Hence, in this article, I will show you how to build a simple application that allows two iPhone or iPod Touch devices to communicate with each other.


Author's Note: To test the concepts covered in this article, you would need two iPhones (3G or 3GS), or iPod Touches (second generation or later) running iPhone OS 3.0 or later.

Creating the Project

Using Xcode, create a new View-based Application project and name it as Bluetooth.

All the various APIs for accessing the Bluetooth is located in the GameKit framework. Hence, you need to add this framework to your project. Add a new Framework to the project by right-clicking on the Frameworks group in Xcode and selecting Add, Existing Frameworks. Select GameKit.framework (see Figure 1).

Figure 1. GameKit: Add the GameKit framework to the project in Xcode.

In the BluetoothViewController.h file, declare the following object, outlets, and actions:

<PRE>#import #import @interface BluetoothViewController : UIViewController { GKSession *currentSession; IBOutlet UITextField *txtMessage; IBOutlet UIButton *connect; IBOutlet UIButton *disconnect;}@property (nonatomic, retain) GKSession *currentSession;@property (nonatomic, retain) UITextField *txtMessage;@property (nonatomic, retain) UIButton *connect;@property (nonatomic, retain) UIButton *disconnect;-(IBAction) btnSend:(id) sender;-(IBAction) btnConnect:(id) sender;-(IBAction) btnDisconnect:(id) sender;@end</PRE>

</pre>

The GKSession object is used to represent a session between two connected Bluetooth devices. You will make use of it to send and receive data between the two devices.

In the BluetoothViewController.m file, add in the following statements in bold:

<PRE>#import "BluetoothViewController.h"#import @implementation BluetoothViewController@synthesize currentSession;@synthesize txtMessage;@synthesize connect;@synthesize disconnect;</PRE>

Double-click on BluetoothViewController.xib to edit it in Interface Builder. Add the following views to the View window (see also Figure 2):

  • Text Field
  • Round Rect Button
Figure 2. Nice view: Populate the View window with the various views.

Perform the following actions:

  • Control-click on the File’s Owner item and drag and drop it over the Text Field view. Select txtMessage.
  • Control-click on the File’s Owner item and drag and drop it over the Connect button. Select connect.
  • Control-click on the File’s Owner item and drag and drop it over the Disconnect button. Select disconnect.
  • Control-click on the Send button and drag and drop it over the File’s Owner item. Select btnSend:.
  • Control-click on the Connect button and drag and drop it over the File’s Owner item. Select btnConnect:.
  • Control-click on the Disconnect button and drag and drop it over the File’s Owner item. Select btnDisconnect:.
Right-click on the File’s Owner item to verify that all the connections are made correctly (see Figure 3).
Figure 3. Check it: Verify the connections made for the various outlets and actions.

Back in Xcode, in the BluetoothViewController.m file, add in the following statements in bold:

<PRE>- (void)viewDidLoad { [connect setHidden:NO]; [disconnect setHidden:YES]; [super viewDidLoad];}- (void)dealloc { [txtMessage release]; [currentSession release]; [super dealloc];}</PRE>

Searching for Peer Devices

Now that all the plumbings for the project have been done, you can now focus on the APIs for accessing other Bluetooth devices.

In the BluetoothViewController.h file, declare a GKPeerPickerController object:

<PRE>#import "BluetoothViewController.h"#import @implementation BluetoothViewController@synthesize currentSession;@synthesize txtMessage;@synthesize connect;@synthesize disconnect;GKPeerPickerController *picker;</PRE>

The GKPeerPickerController class provides a standard UI to let your application discover and connect to another Bluetooth device. This is the easiest way to connect to another Bluetooth device.

To discover and connect to another Bluetooth device, implement the btnConnect: method as follows:

<PRE>-(IBAction) btnConnect:(id) sender { picker = [[GKPeerPickerController alloc] init]; picker.delegate = self; picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby; [connect setHidden:YES]; [disconnect setHidden:NO]; [picker show]; }</PRE>




The connectionTypesMask property indicates the types of connections that the user can choose from. There are two types available: GKPeerPickerConnectionTypeNearby and GKPeerPickerConnectionTypeOnline. For Bluetooth communication, use the GKPeerPickerConnectionTypeNearby constant. The GKPeerPickerConnectionTypeOnline constant indicates an Internet-based connection.

When remote Bluetooth devices are detected and the user has selected and connected to one of them, the peerPickerController:didConnectPeer:toSession: method will be called. Hence, implement this method as follows: <PRE>- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *) session { self.currentSession = session; session.delegate = self; [session setDataReceiveHandler:self withContext:nil];picker.delegate = nil; [picker dismiss]; [picker autorelease];}</PRE>

When the user has connected to the peer Bluetooth device, you save the GKSession object to the currentSession property. This will allow you to use the GKSession object to communicate with the remote device.

If the user cancels the Bluetooth Picker, the peerPickerControllerDidCancel: method will be called. Define this method as follows: <PRE>- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker{ picker.delegate = nil; [picker autorelease]; [connect setHidden:NO]; [disconnect setHidden:YES];}</PRE>

To disconnect from a connected device, use the disconnectFromAllPeers method from the GKSession object. Define the btnDisconnect: method as follows: <PRE>-(IBAction) btnDisconnect:(id) sender { [self.currentSession disconnectFromAllPeers]; [self.currentSession release]; currentSession = nil; [connect setHidden:NO]; [disconnect setHidden:YES];}</PRE>

When a device is connected or disconnected, the session:peer:didChangeState: method will be called. Implement the method as follows: <PRE>- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { switch (state) { case GKPeerStateConnected: NSLog(@"connected"); break; case GKPeerStateDisconnected: NSLog(@"disconnected"); [self.currentSession release]; currentSession = nil; [connect setHidden:NO]; [disconnect setHidden:YES]; break; }}</PRE>

Handling this event will allow you to know when a connection is established, or ended. For example, when the connection is established, you might want to immediately start sending data over to the other device.

Figure 4. On the lookout: The GKPeerPickerController looking for other devices.

Sending Data

To send data to the connected Bluetooth device, use the sendDataToAllPeers: method of the GKSession object. The data that you send is transmitted via an NSData object; hence you are free to define your own application protocol to send any types of data (e.g. binary data such as images). Define the mySendDataToPeers: method as follows: <PRE>- (void) mySendDataToPeers:(NSData *) data{ if (currentSession) [self.currentSession sendDataToAllPeers:data withDataMode:GKSendDataReliable error:nil]; }</PRE>

Define the btnSend: method as follows so that the text entered by the user will be sent to the remote device: <PRE>-(IBAction) btnSend:(id) sender{ //---convert an NSString object to NSData--- NSData* data; NSString *str = [NSString stringWithString:txtMessage.text]; data = [str dataUsingEncoding: NSASCIIStringEncoding]; [self mySendDataToPeers:data]; }</PRE>

Receiving Data

Figure 5. Name it: Display the names of devices found.

When data is received from the other device, the receiveData:fromPeer:inSession:context: method will be called. Implement this method as follows: <PRE>- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context { //---convert the NSData to NSString--- NSString* str; str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Data received" message:str delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; [alert release]; }</PRE>

Here, the received data is in the NSData format. To display it using the UIAlertView class, you need to convert it to an NSString object.

Testing the Application

Figure 6. Connections: A remote device is prompting to connect to you.

That’s it! You are now ready to test the application. Press Command-R in Xcode to deploy the application onto two iPhones / iPod Touches. For this article, I assume you have two devices -- either iPhones or iPod Touches. In order to run this application, they both need to run at least iPhone OS 3.0.

Once the application is deployed to the two devices, launch the application on both devices. On each device, tap the Connect button. The GKPeerPickerController will display the standard UI to discover other devices (see Figure 4).

After a while, both application should be able to find each other (see Figure 5). When you tap on the name of the found device, the application will attempt to connect to it.

728x90

'BlueTooth > 기본기' 카테고리의 다른 글

quoted-printable decoder  (0) 2013.06.03
synergy MessageSendLater  (0) 2011.04.19
bluelab stereo 2009.R2 Inquiry시 iPhone이 검색되면 panic  (0) 2011.03.30
Apple 개발문서  (4) 2010.03.15
Peer-to-Peer Connectivity  (0) 2010.03.04
iphone간 bluetooth 연결 sample  (1) 2010.03.04
bluetooth keypad  (0) 2010.02.10
Bluetooth 2.0 under에서 Pincode없이 연결  (0) 2009.12.28
bluetooth hfp 정리  (0) 2009.07.24
BlueTooth 관련용어  (0) 2009.07.20
728x90
현재 개발중인 keypad.

자판 배열에 대한 소유권은 Mobience사에 있고 다만 Bluetooth Keypad device를 만들고 있다. QWERTY와 비슷한 배열을 가지고 있어 익숙해지면 사용하기 편리할 것으로 보이네.
DEMO중인 폰은 T옴니아2.

기사원문 : http://us.aving.net/news/view.php?articleId=146111

Mobience to develop 'smallQWERTY' keyboard for MID and smartphone

SEOUL, Korea (AVING) -- <Visual News> Mobience(www.mobience.com) announced the development of a new concept of 'smallQWERTY' keyboard in Korea market. According to the company, the smallQWERTY keyboard is designed to help enjoy touching big buttons with efficient text input and full keyboard support for every need at small mobile devices such as smartphone and MID

728x90

'BlueTooth > 기본기' 카테고리의 다른 글

quoted-printable decoder  (0) 2013.06.03
synergy MessageSendLater  (0) 2011.04.19
bluelab stereo 2009.R2 Inquiry시 iPhone이 검색되면 panic  (0) 2011.03.30
Apple 개발문서  (4) 2010.03.15
Peer-to-Peer Connectivity  (0) 2010.03.04
iphone간 bluetooth 연결 sample  (1) 2010.03.04
아이폰 블루투스 프로그래밍  (3) 2010.03.03
Bluetooth 2.0 under에서 Pincode없이 연결  (0) 2009.12.28
bluetooth hfp 정리  (0) 2009.07.24
BlueTooth 관련용어  (0) 2009.07.20

+ Recent posts