OVERVIEW & QUICK START GUIDES

Documentation

Use our developer documentation guides to help get started using Reach®. You can also refer to these guides and access complete SDK documentation within your Cygnus portal.

How Reach℠ Works

Reach℠ is a remote support application for IoT devices. By leveraging a mobile device as a bridge between a customer’s IoT device and a support technician, the support technician can directly and securely interact with the customer’s device. Reach℠ runs on both the customer’s mobile app and the support technician’s computer. The support technician’s computer communicates with the customer’s mobile app, and the mobile app communicates with the IoT device via any communication protocol that the device supports (BLE, MQTT, HTTPS, etc.). This enables the support technician to control the device’s software in real-time, allowing them to diagnose and fix any issues with the device.

A customer requests a support session using their mobile app, and calls or chats with a customer support representative to initiate the Reach℠ support session. The support technician provides the customer with a unique pin to type into their mobile app, which acts as a security layer to prevent unauthorized access to the customer’s device.

Once the technician is allowed access, a two-way communication channel is created between the technician and the customer. The technician leverages the Reach℠ communication functionality to get and set information on the device, without the need for customer interaction. The customer can sit back and relax while the technician works, without the typical back-and-forth that is often required to identify the technical details that are affecting the customer’s device. Instead, the technician has access to all of the technical details of the device via the web-to-mobile and mobile-to-device communication channels.

Device Support

Reach℠ can support any device that can accept incoming communications of some kind. Common communication protocols include Bluetooth, Bluetooth Low Energy, and local WiFi, but a mobile app with Reach℠ may be customized for any kind of communication.

Reach℠ does not support devices that are unable to connect to a different device or to the internet. Reach℠ also doesn’t support existing IoT devices that are unable to accept incoming communications of any form (i.e. no HTTP, no MQTT, no BLE, etc.). However, if the existing device has the ability to update its firmware and/or software, it is likely that an update can enable the device to work with Reach℠.

The Reach℠ SDKs are provided in TypeScript/JavaScript, Kotlin, and Swift and can be integrated into your already-existing device interaction code.

Secure

Reach℠ maintains device security by encrypting all network traffic between devices.

Technicians can only gain access to the device via the customer’s express permission in the form of the customer entering in a pin that is unique to the remote support session requested by the customer.

If the customer’s mobile app connects to their device in the form of Bluetooth, this proximity-based security is maintained by Reach℠ because all communication to the device continues to occur over the Bluetooth connection.

Seamless Customer Interaction

Reach℠ is designed to interact seamlessly with a mobile or web app’s UI. UI elements can react to changes in both the device’s connection status and the device’s state via callbacks, which keeps the customer in the loop about what’s happening on their device. Reach℠ supports chat messages that include text, photos, and videos to help the customer and technician diagnose the issue.

Technicians can perform customer support for customers despite language barriers. Once the technician has access to the device, the customer likely does not need to be involved in the process of updating the device. In order to gain access, the customer only needs to enter a pin number into their mobile app; this is often possible despite language barriers, giving the technician access to the information they need.

If the internet connection between the customer support technician and the customer drops, Reach℠ will automatically reconnect the session when an internet connection is restored and the support session will pick up where it left off. All data transfer is paused on disconnect and resumed on reconnect, ensuring that frequent drops due to poor internet connections don’t prevent customers from getting their devices fixed.

The most common customer support requests are for connectivity issues. Reach℠ can sidestep the issue where technicians are unable to connect to the device remotely due to the customer’s device connectivity issues because, rather than connecting to the device, the technician can connect to the customer’s mobile device, which can, in turn, connect to the device over LAN or BLE technology. This provides better remote access to devices for technicians.

Fast Development Time

The Reach℠ SDKs are designed to facilitate fast development times and robust workflows. The SDKs are object-oriented, making the process of events both easy to follow and easy to write, even for asynchronous programming.

Three different asynchronous message protocols ensure that you can use the right tool for the job. Together the message protocols (including fire-and-forget and request-and-response) allow for fine-grained control over communication between the clients. Their object-oriented implementations make it easy to understand how to use the protocol and to choose which communication protocol is a good fit for the application at hand.

Serialization and deserialization of data often consume a large portion of development time for projects that communicate between IoT devices. The SDKs provided by Reach℠ make it straightforward to identify data types that are encoded in bytes, which in turn makes deserialization straightforward. Reach℠ also has built-in functionality for encoding and decoding JSON messages, so any object that can be encoded to and decoded from JSON is supported by Reach℠ with very minimal development efforts.

Battle Tested

Reach℠ is well-tested across different network conditions, including in rural locations with poor internet access. If a connection is dropped, reconnection occurs automatically and the remote support session picks up where it left off. We use Reach℠ internally at i3 Product Development, and we work closely with our clients using Reach℠ to ensure the communication between their devices and support technicians is robust.

Given a standard home or work internet connection, Reach℠ can seamlessly send tens of thousands of small messages per second and large messages up to GBs in sizes without interrupting the web and mobile app UIs, ensuring an excellent customer experience.

Code Examples

We provide clients with code examples illustrating the many use-cases of Reach℠. All examples include code for TypeScript/JavaScript, Kotlin, and Swift. The examples cover all SDK features and how to implement them, along with best practices from our extensive experience working with our customers.

Logging and Tracking

A custom logger can be attached to each SDK to support usage tracking, helping you gain insights into how your customers utilize the platform for their remote support needs. Session IDs are provided to allow for insights and tracking on a session-by-session basis, giving you fine-grained information to work from.

An Example

Below is a simple example of using the JavaScript SDK to restart a device and query its temperature.

// Technician (UI interaction)

const CHAT_MESSAGE = 0;
const REBOOT = 1;
const GET_TEMPERATURE = 2;

function onConnect() {
changeUIConnectionStatus("GREEN");
}

function onDisconnect() {
changeUIConnectionStatus("RED");
}

function onNotification(notification) {
switch(notification.category) {
case CHAT_MESSAGE {
let message = decode_text(notification.data);
pushChatMessage(message);
}
}
}

function onCommand(command) {
switch(command.category) {
case REBOOT {
pushLogMessage("Device is restarting...");
}
}
}

let client = new reachClient.RemoteSupportClient();
client.onConnect.subscribe(onConnect);
client.onDisconnect.subscribe(onDisconnect);
// etc

// Customer's mobile app (device and UI interaction)

const CHAT_MESSAGE = 0;
const REBOOT = 1;
const GET_TEMPERATURE = 2;

function onCommand(command) {
switch(command.category) {
case REBOOT {
device.reboot();
}
}
}

function onQueryReceived(query) {
switch(query.category) {
case GET_TEMPERATURE {
let temperature = device.get_temperature();
query.respond(new TaggedData(data=new Uint8Array(temperature), tag="float"));
}
}
}

let client = new reachClient.RemoteSupportClient();
client.onCommand.subscribe(onCommand);
client.onQuery.subscribe(onQueryReceived);
// etc

Quickstart for iOS

Create a Product

Within your web portal instance, head to the products page and create a product. You’ll use the API key of this product in your iOS app.

Install SDK

CocoaPods

To install via CocoaPods, add the following to your Podfile

pod 'CygnusReach', '~> 1.1.0'

Manual Installation

Download the Reach iOS and WebRTC frameworks here. Drag both frameworks into your Xcode project’s Frameworks directory. Open up the project file to the General tab. Under the Frameworks, Libraries, and Embedded Content section, select Embed & Sign for both frameworks.

You’ll also have to install the PromiseKit frameworkCocoaMQTT framework and Reachability framework.

 

Set Up Client

To set up remote support, you have to create an instance of the RemoteSupportClient class, which acts as the main interface for handling a session. You’ll instantiate it with the API key of the product you created.

import RemoteSupport
class SupportService {
var rs: RemoteSupportClient?
init() {
self.rs = RemoteSupportClient(
apiUrlBase: "https://api.dev.cygnusreach.com",
apiKey: Config.apiKey,
retainLogs: true,
timeout: 5
)
}
}

 

Starting a Session

You can choose to either host a session or join an existing session given a 5 digit numeric PIN. Hosting a session looks like this:

func hostSession() {
// Subscribe to the connection event publisher to react to a connection to the session
remoteSupport?.onConnect
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.handleConnect() }
.store(in: &bag)
remoteSupport.initiateSupportSession().done { pin in
// Session is created but not connected. Get PIN to peer
}.catch { error in
// Session has failed to initialize
}
}

The SDK makes heavy use of PromiseKit, so this is a paradigm you’ll see a lot. The initiateSupportSession function call returns a Promise which you can then use to react to when the work is finished. The pin variable is a String that the other side of the connection will have to use in order to begin a session.

If you need to connect to a remote support session that someone else is hosting, that would look something like this:

func connectToSession(pin: String) {
// Subscribe to the connection event publisher to react to a connection to the session
remoteSupport?.onConnect
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.handleConnect() }
.store(in: &bag)
remoteSupport.connectToSupportSession(pin: pin).done {
// Connection attempt succeeded, but connection is not yet open
}.catch { error in
// Failed to connect to peer
}
}

 

Sending Data

Once you have a connection set up, you’ll be able to send whatever data you want back and forth. There are three main message interfaces: notifications, commands, and queries. Let’s look at what these mean.

  • Notification: A message sent to a peer without an expected response.
  • Command: A message sent to a peer with either an acknowledgement or an error sent back in response
  • Query: A message sent to a peer with either data or an error sent back in response
 

Reacting to these messages requires subscribing to the RemoteSupportClient’s event publishers:

extension SupportService {
func setup() {
rs.onNotification
.receive(on: DispatchQueue.main)
.sink { [weak self] args in
// React to received notification
}
.store(in: &bag)
.receive(on: DispatchQueue.main)
.sink { args in
// Do work
args.context.complete()
}
.store(in: &bag)
.receive(on: DispatchQueue.main)
.sink { args in
// Do work
let data = RSTaggedData(tag: DefaultTag.Bytes, data: Data())
args.context.respond(data, isLastMessage: true)
}
.store(in: &bag)
}
}

To send messages, there are three main methods for sending the three message types:

let notification = RSNotification(category: DefaultCategory.Chat, data: nTagged)
remoteSupport.sendNotification(notification: notification).done {
// Notification sent
}.catch { error in
// Notification failed to send
}
let command = RSCommand(category: DefaultCategory.Bytes, data: cTagged, acknowledgeOn: .finished)
remoteSupport.sendCommand(command: command).done {
// Command sent
}.catch { error in
// Command failed to send
}
let qTagged = RSTaggedData(tag: DefaultTag.Bytes, data: Data())
let query = RSQuery(category: DefaultCategory.Bytes, data: qTagged)
let receipt = remoteSupport.sendQuery(query: query) { tagged, isFinal in
// Do something with response
}
receipt.complete.done {
// Final response for query received
}.catch { error in
// Either query failed to send or peer sent an error in response
}
rs.onCommand
rs.onQuery
let nTagged = RSTaggedData(tag: DefaultTag.Chat, data: "Hello".data(using: .utf8)!)
let cTagged = RSTaggedData(tag: DefaultTag.Bytes, data: Data())

 

Disconnecting

To disconnect, you can call the client’s disconnect method

 

remoteSupport.disconnect()

If the peer intentionally disconnects from the session, the client’s onDisconnect publisher will fire an event with expected set to true.

 

rs.onDisconnect
.receive(on: DispatchQueue.main)
.sink { args in
if expected { ... }
}
.store(in: &bag)

If expected is false, the client will automatically attempt to reconnect to the session.

If you want to see the SDK in action, check out our sample project.

You can access the full documentation here.

Quickstart for Android

Create a Product

Within your web portal instance, head to the products page and create a product. You’ll use the API key of this product in your Android app.

Install SDK

You can install the SDK via Gradle

repositories {
maven {
url 'https://pkgs.dev.azure.com/i3-iot/CygnusReach/_packaging/Reach/maven/v1'
name 'Reach'
}
}

dependencies {
implementation ('com.github.100mslive:webrtc:m97')
implementation ('com.cygnusreach:sdk-android:0.0.49-beta38@aar') { transitive = true }
implementation ('com.cygnusreach:sdk-androidx:0.0.49-beta38@aar') { transitive = true }
}

This will pull in all of the necessary dependencies along with the Cygnus SDK.

Set Up Client

The remote support client needs to be initialized on app startup with the application context.

class App : Application() {
override fun onCreate() {
RemoteSupportClient.initialize(applicationContext)
super.onCreate()
}
}

 

To set up a session, there’s a separate implementation for Kotlin and Java users. The Kotlin implementation takes advantage of coroutines for running its processes on background threads and the Java implementation uses a Java friendly async pattern.

class MainViewModel : ViewModel() {
    private var rs: IRemoteSupportClient? = null

    fun connectToSupport(pin: String) {
        var client = this.rs
        if (client != null) return
        this.rs = RemoteSupportClient.createKotlin(
                App.app.baseContext,
                "https://api.cygnusreach.com",
                Config.apiKey,
                Logger())

        viewModelScope.launch(Dispatchers.Default) {
            client.onConnect.subscribe(::handleConnect)
            client.onDisconnect.subscribe(::handleDisconnect)
            client.onPartialMessage.subscribe(::handlePartialMessage)
            client.onNotification.subscribe(::handleNotification)
            client.onCommand.subscribe(::handleCommand)
            client.onQuery.subscribe(::handleQuery)
            client.onMessageError.subscribe(::handleMessageError)
        }
    }
}
class MainViewModel extends ViewModel {

    @Nullable
    private IRemoteSupportClientJava rs;

    void connectToSupport(String pin) {
        if (rs != null) return;
        IRemoteSupportClientJava client = RemoteSupportClient.createJava(
                App.Companion.getApp().getBaseContext(),
                App.Companion.getApp().getRemoteSupportUrl(),
                "",
                new Logger());
        this.rs = client;

        client.setOnConnectFromHandler(() -> { ... });
        client.setOnDisconnectFromHandler(expected -> { ... });
        client.setOnNotificationHandler(notification -> { ... });
        client.setOnCommandHandler((command, context) -> { ... });
        client.setOnQueryHandler((query, context) -> { ... });
    }
}

Starting a Session

If you want to connect to an existing session, you can do this:

client.connectToSupportSession(pin)
client.connectToSupportSessionAsync(pin);

If you want to host a session, you can do this:

val pin = client.initiateSupportSession()?: return
String pin = client.initiateSupportSessionAsync().get();

Functions to set up a connection are asynchronous, but you shouldn’t consider a session connected until the onConnect handler is called.

Sending Data

Once you have a connection set up, you’ll be able to send whatever data you want back and forth. There are three main message interfaces: notifications, commands, and queries.

  • Notification: A message sent to a peer without an expected response.
  • Command: A message sent to a peer with either an acknowledge message or an error sent back in response
  • Query: A message sent to a peer with either data or an error sent back in response

 

To send messages, there are three main methods for sending the three message types:

try {
    client.sendNotification(Notification(data, tag, category)).wait()
    client.sendCommand(Command(data, tag, category, acknowledgeOn)).wait()
    val r = client.sendQuery(Query(data, tag, category)).wait()
    r.onResponse.subscribe { }
} catch (e: Exception) { }
try {
    client.sendNotificationAsync(new Notification(data, tag, category)).get();
    client.sendCommandAsync(new Command(data, tag, category, acknowledgeOn)).get().get();
    JavaQueryReceipt r = client.sendQueryAsync(new Query(data, tag, category)).get().get();
    r.setOnResponseReceivedHandler(queryResponse -> { });
} catch (Exception e) { }

Commands will have a single response, either an ack or an error. Queries can have one or more responses. To look at each response of a query, you can subscribe to its onResponse event and look at the final field to see if it’s the last message or not.

Disconnecting

If you want to end a session, you can call the client’s close method.

client.close()
client.closeAsync()

If the peer closes the session, the client’s onDisconnect event handler will be called with expected set to true. If the session loses connection without either side initiating it, the onDisconnect event handler will be called with expected set to false and will automatically attempt to reconnect.

Setting Up Service

Running a session in a service enables users to keep a session alive while the app is not in the foreground.

In order to set up the remote support client in a session, you need to use the IClientProxy interface.

RemoteSupport.configureRemoteSupport(
    "https://api.cygnusreach.com",
    productKey)

val client = RemoteSupport.getProxy(viewModelScope).also { client ->
    client.onDisconnect.subscribe(viewModelScope) { handleDisconnect(it) }
    client.onPartialMessage.subscribe(viewModelScope) { handlePartialMessage(it) }
    client.onNotification.subscribe(viewModelScope) { handleNotification(it) }
    client.onCommand.subscribe(viewModelScope) { handleCommand(it) }
    client.onQuery.subscribe(viewModelScope) { handleQuery(it) }
    client.onMessageError.subscribe(viewModelScope) { handleMessageError(it) }
    client.onScreenCapture.subscribe(viewModelScope) { handleScreenCapture(it) }
    client.onVideoCapture.subscribe(viewModelScope) { handleVideoCapture(it) }
}

Connecting to a session uses the same function signature as the IRemoteSupportClient

client.connectToSupportSession(pin)

In order to start and stop the service, you can use

RemoteSupport.startService(applicationContext)
RemoteSupport.stopService(applicationContext)

If you want to see the SDK in action, check out our sample project.

You can access the full documentation here.

Quickstart for Web

Create a Product

You can install the SDK via NPM. In your package.json:

"dependencies": {
  @cygnus-technology/cygnus-reach
}

Set Up Client

To set up remote support, you have to create an instance of the RemoteSupportClient class, which acts as the main interface for handling a session. You’ll instantiate it with the API key of the product you created.

this.rs = new RemoteSupportClient(
environment.reach.api,
this.productService.key,
5000,
new ConsoleLogger()
);

 

Starting a Session

The SDK has asynchronous methods for creating a session and connecting to a session.

Creating a session

async createSession(): void {
const pin = await this.rs.initiateSupportSession();
}

Connecting to a session

async connect(pin: string): void {
await this.rs.connectToSupportSession(pin);
}

Sending Data

Once you have a connection set up, you’ll be able to send whatever data you want back and forth. There are three main message interfaces: notifications, commands, and queries.

  • Notification: A message sent to a peer without an expected response.
  • Command: A message sent to a peer with either an acknowledge message or an error sent back in response
  • Query: A message sent to a peer with either data or an error sent back in response

 

Reacting to these messages requires setting up the RemoteSupportClient’s event listeners:

private notificationHandler = this.onNotification.bind(this);
private commandHandler = this.onCommand.bind(this);
private queryHandler = this.onQuery.bind(this);

setupListeners(): void {
    this.rs.onNotification.subscribe(this.notificationHandler);
    this.rs.onCommand.subscribe(this.commandHandler);
    this.rs.onQuery.subscribe(this.queryHandler);
}

async onNotification(notification: RSNotification) { }
async onCommand(notification: ICommandEventArgs) { }
async onQuery(notification: IQueryEventArgs) { }

To send messages, there are three main methods for sending the three message types:

const encoder = new TextEncoder();
const encoded = encoder.encode('Hello');
const data = new RSTaggedData(DefaultTag.Chat, encoded);
const notification = RSNotification.create(DefaultCategory.Chat, data);
const command = RSCommand.create(DefaultCategory.Chat, data, AcknowledgeOn.Finished);
const query = RSQuery.create(DefaultCategory.Chat, data);

try {
    await this.rs.sendNotification(notification);
    await (await this.rs.sendCommand(command)).wait();
    const receipt = await this.rs.sendQuery(query);
    receipt.onResponse.subscribe(response => { });
    await receipt.wait();
} catch (e) { }

The method for sending each message type returns a promise that denotes when the SDK has successfully processed and sent the message.

The promises returned by the command and query methods are resolved with a receipt that contains a promise denoting when the peer has completed responding to the message.

Query receipts also have an onResponse event emitter that fires on each response received.

 

Disconnecting

If you want to end a session, you can use the client’s disconnect method

async close(): void {
    const sendDisconnect = true; // Sends a message to the peer that you are initiating the disconnect process
    if (this.rs.isConnected) { await this.rs.disconnect(sendDisconnect); }
    this.rs.close();
}

The disconnect method disconnects the client from the peer, and the close method unsubscribes all present subscriptions to the client’s events.

 

If the peer closes the session, the client’s onExpectedDisconnect event handler will be called. If the session loses connection without either side initiating it, the onDisconnect event handler will be called and the client will automatically attempt to reconnect.

 

You can access the full documentation here.