koalazak/dorita980

Cloud API firmware 2.x.x

koalazak opened this issue · 57 comments

Hi guys,
I have almost everything ready to make the Cloud API possible:

  • cloud discovery
  • cloud aws login
  • suscribe to topics in the cloud to receive state updates
  • make changes in the state to set preferences ( schedule, bust, etc)

But i dont found the correct topic and message content to send basic commands like start or stop.
Anybody sniff that data? or has that data?

My sniff data is weired and malformed, i dont know if my sslsplit is showingme the info in the correct encoding. When I send a command with my phone over the cloud I see some bytes in the comunication but no one string like topic o json message.

can anybody help?

here is the working snippet:

const AWSIoTData = require('aws-iot-device-sdk');
const AWS = require('aws-sdk');
const request = require('request-promise');
// install with: npm install aws-iot-device-sdk aws-sdk request-promise request

const ROBOT_BLID = ''; // same as local api
const ROBOT_PASSWORD = ''; // same as local api
const APP_ID = ''; // like IOS-12345678-1234-1234-1234-123456789098

function cloudDiscovery () {
  var requestOptions = {
    'method': 'GET',
    'uri': `https://disc-prod.iot.irobotapi.com/v1/robot/discover/${ROBOT_BLID}`,
    'json': true
  };
  return request(requestOptions);
}

function cloudLogin () {
  return cloudDiscovery().then(function (discoveryData) {
    var postData = {
      'associations': {
        '0': {
          'robot_id': ROBOT_BLID,
          'deleted': false,
          'password': ROBOT_PASSWORD
        }
      },
      'app_id': APP_ID
    };

    var requestOptions = {
      'method': 'POST',
      'headers': {
        'Content-Type': 'application/json',
        'User-Agent': 'aspen/1.9.1.184.1 CFNetwork/808.2.16 Darwin/16.3.0'
      },
      'uri': `${discoveryData.httpBase}/v1/login`,
      'body': postData,
      'json': true
    };

    return request(requestOptions).then((rawLoginResponse) => {
      return {login: rawLoginResponse.associations['0'], credentials: rawLoginResponse.credentials, discovery: discoveryData};
    });
  });
}

function initMQTT (amazonData) {
  var awsConfiguration = {
    poolId: amazonData.credentials.CognitoId,
    region: amazonData.discovery.awsRegion
  };

  var AWSConfiguration = awsConfiguration;

  var clientId = ROBOT_BLID + '-' + (Math.floor((Math.random() * 100000) + 1));

  AWS.config.region = AWSConfiguration.region;

  AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: AWSConfiguration.poolId
  });

  const mqttClient = AWSIoTData.device({
    region: AWS.config.region,
    clientId: clientId,
    protocol: 'wss',
    maximumReconnectTimeMs: 8000,
    debug: true,
    accessKeyId: amazonData.credentials.AccessKeyId,
    secretKey: amazonData.credentials.SecretKey,
    sessionToken: amazonData.credentials.SessionToken
  });

  mqttClient.on('connect', function (e) {
    console.log('connect!', e);

    mqttClient.subscribe('$aws/things/' + ROBOT_BLID + '/shadow/#'); // all subtopics

    // const cmd = {'state': {'desired': {'cleanSchedule': {'cycle': ['none', 'none', 'none', 'none', 'none', 'none', 'none'], 'h': [17, 10, 10, 12, 10, 13, 17], 'm': [0, 30, 30, 0, 30, 30, 0]}}}};
    // mqttClient.publish('$aws/things/' + ROBOT_BLID + '/shadow/update', JSON.stringify(cmd));
  });

  mqttClient.on('reconnect', function () {
    console.log('reconnect!');
  });

  mqttClient.on('message', function (t, m) {
    console.log('message:');
    console.log('topic:', t);
    console.log(m.toString());
  });

  mqttClient.on('delta', function (m) {
    console.log('delta:');
    console.log(m);
  });
  mqttClient.on('status', function (m) {
    console.log('status:');
    console.log(m);
  });

  mqttClient.on('data', function (m) {
    console.log('data:');
    console.log(m);
  });

  mqttClient.on('error', function (e) {
    console.log('error:');
    console.log(e);
  });

  mqttClient.on('packetreceive', function (m) {
    console.log('packetreceive:');
    console.log(m);
  });
}

cloudLogin().then((credentialData) => {
  console.log(credentialData);
  initMQTT(credentialData);
}).catch(console.log);

Hey @koalazak!

I'd like to help. Can you provide some instructions?

How do I find the app id to run your snippet? I guess it's from the mobile app?

Cheers

I think you can use any string with that format. I get my app ID sniffing the trafic.

OK, it seems to do something. Which is the relevant output? :)

So i did run a start stop cycle through the cloud and got readable output from your script.

Is this what you're looking for?

the output is all the messages sent to the topic $aws/things/' + ROBOT_BLID + '/shadow (and subtopics) by the robot or by the official mobile app.
When you say through the cloud and got readable output you mean using the official mobile app? or using this script (publishing to a topic with this script)?

What im looking for is what message and what topic we suppose to use in mqttClient.publish() method in this script to start/stop the robot via this script.

Running this script, then triggering the bot via the official mobile app.

When I start the script, the output ends with "undefined".

One sec, I'm sanitizing the output.

ok, you are looking the messages published by the mobile app or robot to that topics. that is what i get too. and using the mqttClient.publish() method I can set new schedule for example. But What im looking for is what message and what topic we suppose to use in mqttClient.publish() method in this script to start/stop the robot via this script.

So do you have some instructions to obtain them?

I have a ton of JSON output. But only after triggering something via the official mobile app.

you can start making a MITM attack to sniff the data between the official mobile app and the cloud, getting the mqtt packets and decode them. Google about MITM

No TLS on that?

No chance to reconstruct the command from something like this?
message: topic: $aws/things/ROBOT_BLID_HERE/shadow/update/accepted {"state":{"reported":{"lastCommand":{"command":"start","time":1488399056,"initiator":"rmtApp"}}},"metadata":{"reported":{"lastCommand":{"command":{"timestamp":1488399056},"time":{"timestamp":1488399056},"initiator":{"timestamp":1488399056}}}},"version":1617,"timestamp":1488399057}

yes, TLS on that. bypassing with sslsplit.
I try some variants of that message but no way.

Is there a way to trigger the cloud api when you're on wifi?

I have a proxy running to monitor the traffic.

Never mind, i isolated my networks, now it tries to go through the cloud.

I see a message with the app id, i also get the cloud icon in the app. But now my roomba doesn't react when i press start.

Reset everything, now the roomba reacts, but I don't see the commands in the proxy. Only the requests for mission history, login, etc. Will debug a bit more...

Its like the start/stop commands are in other format not json, like raw mqtt packets, a few bytes...

Make sure you are creating certs with a valid CommonName.
The mobile app is validating the CN field in the roomba cert with the format 'Roomba-{number16}' if it is not valid, then the mobile app disconnect. (the validation is in the mobile app, so you need a selfisgned cert with this CN if you want to perform a sslsplit to sniff the trafic

but if you are seeing the mission commands and commands when you set preferences, there is no problem with the cert.

Hmm, I used mitmproxy instead of sslsplit. I'm a bit too tired to read through how to use sslsplit right now.

There should be a option to see raw tcp traffic in mitmproxy. I'm seeing the http traffic, but I'm guessing mqtt gets lost.

I think i have a working setup now redirecting to sslsplit. A test port 80 request got logged. I tried redirecting 8883 but get no traffic. Any tips to debug the problem?

OK, finally got a start message :)

Can I send it to you by email? It's quite long. Seems like it came over an upgraded ssl websocket carrying mqtt. Theres the upgrade, some garbage (probably mqtt), then start message, garbage, pause message.

yes sure.
I got the message too. But I cant figure out how to reproduce it in my test snippet. Can you?

I got to a version mismatch. It seems to be a sequence number. The question is where from.

well, that is a step forward! There is not in state object?

version is in every message received:

{
"state":{"reported":{"svcEndpoints":{"svcDeplId":"v007"}}},
"metadata":{"reported":{"svcEndpoints":{"svcDeplId":{"timestamp":1488553359}}}},
"version":3345,
"timestamp":1488553359
}

you can parse all the messages and store the version to use in the next call.

can you share your code? zaktu.x@gmail.com

Will send it later

Sent the log.

Btw, sending the previous command without the version actually gets a valid reply. But the robot doesn't start.

The message I sent was:
{"state":{"reported":{"lastCommand":{"command":"start","time":1488399056,"initiator":"rmtApp"}}},"metadata":{"reported":{"lastCommand":{"command":{"timestamp":1488399056},"time":{"timestamp":1488399056},"initiator":{"timestamp":1488399056}}}},"timestamp":1488399057}

{ "state":{ "desired":{ "command":{ "command":"start","time":+new Date(),"initiator":"rmtApp" } } }, "timestamp":+new Date() }

Bam! Your working start command! :)

I think that string you see in logs are a post-execution message sent by the robot after receibe the command. And the real command is all the ugly bytes before that string

nice!
I tried something like that before but nothing. Maybe I dont use the time filed in my tests...
Testing now....

I actually got the robot to react with the last posted one. "start" for start and "stop" for stop.

There seems to be some kind of problems with the timestamps though, as it will start over and over again.

Yes, it seems to be important.

oh crap, that works but the robot now is receiving the start command every second :p.
let me kwno if you can stop it :p

Send the stop message. Same format. x)

you promise that i can not get a loop of start AND stop commands now? :p

I can't, but my hope is that a hard reset will be able to fix it. :)

Let me try if the app is still able to start it.

Actually it is in a start stop loop. I'm resetting mine.

Yep, a reset fixed it. But that means we have to find out where to get the right timestamps.

Or maybe it's not about the timestamps but some of the other parts of the messages. The messages in the app seem to contain more information.

reset as 10 second the start button?

Didn't try that. I used the "reset roomba" option in the app. But you have to pair it again.

reset with 10 second start button doest work. but yes with the App.

New firmware is rolling out (again). Does anybody know what's changed? Hopefully nothing broke.
img_4182

@iosdeveloper opened a new issue for that: #31

@Letier do you have any progress in the cloud api reverse engeniering?

No luck. :(

It works with the iOS app at the moment, but doesn't do anything when I send the signals. The reset option in the app is greyed out, so I have no clue if I got it stuck in a strange state again.

Managed to reset and catch another conversation with the app. Saw the start message. But I'm suddenly not getting it to start anymore. And there seems to be all kind of state from my past trials saved. I'll need to find a way to clean it up. Posting via the snippet doesn't seem to work anymore.

So the situation seems to be that you somehow have to switch between certain states.

The following cycles work for me:
{ "state":{"desired":{"command":{"command":"start","time":Math.floor(new Date()/1000),"initiator":"rmtApp"}}}}
{ "state":{"desired":{"command":{"command":"clean","time":Math.floor(new Date()/1000),"initiator":"rmtApp"}}}}
{ "state":{"desired":{"command":null}}}

{ "state":{"desired":{"command":{"command":"stop","time":Math.floor(new Date()/1000),"initiator":"rmtApp"}}}}
{ "state":{"desired":{"command":null}}}

I'm not sure if clean is a real command. I just tried it and the robot started moving.

I dont like that aprouch :p may there is another fancy way haha. If i dont foudn the way y just implement that in next version.

Hi Guys, did you try to contact iRobot support to get some additional info?

Snippet doesn't work for me.
body: { errorMessage: 'Authentication failed', errorType: 'AspenError.AuthenticationFailed' }

I think it can be because I use fake APP_ID. Could you please share the correct one?

UPD1 Please ignore. It does login with APP_ID IOS-88888888-4444-4444-4444-121212121212

UPD2 We should pass host parameter when create a DeviceClient. I changed this part:

const mqttClient = AWSIoTData.device({
    host: amazonData.discovery.mqtt,
    region: AWS.config.region,
    clientId: clientId,
    protocol: 'wss',
    maximumReconnectTimeMs: 8000,
    debug: true,
    accessKeyId: amazonData.credentials.AccessKeyId,
    secretKey: amazonData.credentials.SecretKey,
    sessionToken: amazonData.credentials.SessionToken
  });

Nice, are you trying to send commands with mqttClient.publish() ?

I use AWSIoTData.thingShadow instead of AWSIoTData.device to create the object.
This is my code:

    const mqttClient = AWSIoTData.thingShadow(connectionOptions);
    var clientTokenUpdate;

    mqttClient.on('connect', function (e) {
        console.log('connect!', e);

        mqttClient.register(ROBOT_BLID, {debug: true}, function () {
            var startCmd = {command: {command: 'start'}};
            // this is to reset command
            //var startCmd = null;
            clientTokenUpdate = mqttClient.update(ROBOT_BLID, {'state': {'desired': startCmd}});

            // this is to get the full thing shadow state
            //clientTokenUpdate = mqttClient.get(ROBOT_BLID);
            if (clientTokenUpdate === null) {
                console.log('update shadow failed, operation still in progress');
            }
        });
    });

    mqttClient.on('status',
        function (thingName, stat, clientToken, stateObject) {
            console.log('received ' + stat + ' on ' + thingName + ': ' +
                JSON.stringify(stateObject));

    mqttClient.on('delta',
        function (thingName, stateObject) {
            console.log('received delta on ' + thingName + ': ' +
                JSON.stringify(stateObject));
        });

    mqttClient.on('timeout',
        function (thingName, clientToken) {
            console.log('received timeout on ' + thingName +
                ' with token: ' + clientToken);

When I set the 'desired' state to 'command' it doesn't appear in the shadow state but instead it acts on my device.
It doesn't clear the 'desired' state and I think this is why it's looping constantly trying to set this property.
The same behavior was when I tried to set the boolean property to string value. It keeps this property in 'desired' section and constantly sends the 'delta' events. You can check if you get full state.
I think we should clean the 'desired' command manually. Right after it acts.

Hi, I'm implementing ur library in C for an IoT School Project, https://github.com/roombavacuum/libroomba
and when I sent a command to my roomba e5 v3 in local, I got this:

MQTT Message: Topic wifistat, Qos 0, Len 163
Payload (0 - 80): {"state":{"reported":{"netinfo":{"dhcp":true,"addr":3232235870,"mask":4294967040
MQTT Message: Done
MQTT Message: Topic wifistat, Qos 0, Len 68
Payload (0 - 68): {"state":{"reported":{"wifistat":{"wifi":1,"uap":false,"cloud":1}}}}
MQTT Message: Done
MQTT Message: Topic wifistat, Qos 0, Len 163
Payload (0 - 80): {"state":{"reported":{"netinfo":{"dhcp":true,"addr":3232235870,"mask":4294967040
MQTT Message: Done
MQTT Message: Topic wifistat, Qos 0, Len 82
Payload (0 - 80): {"state":{"reported":{"wlcfg":{"sec":7,"ssid":"566F6461666F6E652D304134413632"}}
MQTT Message: Done
MQTT Message: Topic wifistat, Qos 0, Len 50
Payload (0 - 50): {"state":{"reported":{"mac":"d0:c5:d3:bd:21:31"}}}
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 40
Payload (0 - 40): {"state":{"reported":{"country": "PT"}}}
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 43
Payload (0 - 43): {"state":{"reported":{"cloudEnv": "prod"}}}
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 61
Payload (0 - 61): {"state":{"reported":{"svcEndpoints":{"svcDeplId": "v011"}}}}
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 37
Payload (0 - 37): {"state":{"reported":{"name": "e5"}}}
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 470
Payload (0 - 80): {"state":{"reported":{"lastDisconnect":4,"cap":{"ota":1,"eco":1,"svcConf":1},"ba
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 378
Payload (0 - 80): {"state":{"reported":{"bbmssn":{"nMssn":254,"nMssnOK":219,"nMssnF":35,"aMssnM":1
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 475
Payload (0 - 80): {"state":{"reported":{"cleanSchedule":{"cycle":["none","none","none","none","non
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 460
Payload (0 - 80): {"state":{"reported":{"langs":[{"en-US":0},{"en-GB":15},{"fr-FR":1},{"de-DE":2},
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 244
Payload (0 - 80): {"state":{"reported":{"tz":{"ver":7,"events":[{"dt":1564675200,"off":60},{"dt":1
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 294
Payload (0 - 80): {"state":{"reported":{"lastCommand":{
   "command": "start",
   "time": 15785928
MQTT Message: Done
MQTT Message: Topic $aws/things/3178480822035600/shadow/update, Qos 0, Len 201
Payload (0 - 80): {"state":{"reported":{"cleanMissionStatus":{"cycle":"clean","phase":"run","expir
MQTT Message: Done
MQTT Message: Topic wifistat, Qos 0, Len 55
Payload (0 - 55): {"state":{"reported":{"signal":{"rssi":-67,"snr":23}}}}

Is that helpful to ur could implementation?

will close this issue and reopen if start with the research again