Roomba 980 Wifi Connectivity Reverse engineering

I figured it out.

I was using lowercase characters in my BLID. Changed them to uppercase and now it works.

You guys do some great work on here. Keep it up!

Thanks for putting this together. I got this installed and working, but I am struggling to understand how to get this added to my devices in the smartthings app. I was able to get the vacuum running from the smartthings ide, but it isn’t appearing in the app. Do I need to have a smartapp set up? sorry for the noob question. Thanks for your help!

I’m not sure to understand your issue but maybe this will help (and my apologies if you already knew this) : once you created a device handler, you have to create a virtual device (in the devices menu in IDE, then click “new device”). You will be given the opportunity to pick the “driver” i.e. “device handler” you just created in a list of hundreds of devices (providing you didn’t forget to hit the publish button when you created the device handler). Once this is done, you should see it as a new device to be configured in the app, just like any other device. You might need to hit the “find new device” button, but most of the times now it shows itself in the list of devices directly (in the iphone app at least).
Hope this helps

Got it! Awesome! Thanks so much!

This worked great for me.

Questions: How responsive did everyone find this solution? I felt like it lagged really bad… (Not blaming the app itself, I realize there are many layers its passing through)

And lastly, has anyone looked into updating the app to show the “status” of what is running/happening to the Roomba? This implementation is purely sending commands. But doesn’t show us anything.

Just curious. Love it though, thanks guys!

This looks like great work thanks for all your efforts it is terrible that iRobot does not provide an API for this model.

For another alternative you can get a Roomba 805 from Costco (I was told by iRobot that 8xx series are the same they just have different accessories) for $400 (currently $350 on sales) and the Thinking Cleaner wifi module for $99. That makes the solution a few hundred cheaper and below is the latest 2.x code for smart things integration:

Not sure if you can modify the above code for the UI and status tile as @darrylb has suggested.

Hi all, I’ve improved upon Elfege’s device type to make a larger state machine out of the Roomba virtual switch so that it more closely follows the iRobot App experience.

Here is my unpublished github repo and here is the code block for copy/paste consumption:
Update 1: Included a ‘bin full’ status
Update 2: More error handling, included some additional valueTile, made pollingInterval a setting (default - 4min)
Update 3: Added the Consumable capability in order to allow CORE SmartApps to be used for notifications!

/**
*  Roomba 9xx - Virtual Switch
*
*  Copyright 2016 Steve-Gregory
*
*  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
*  in compliance with the License. You may obtain a copy of the License at:
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
*  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
*  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
*  for the specific language governing permissions and limitations under the License.
*
*/
metadata {
    definition (name: "Roomba 9xx - Virtual Switch", namespace: "Steve-Gregory", author: "Steve-Gregory") {
        capability "Switch"
        capability "Refresh"
        capability "Polling"
        capability "Consumable"
        capability "Configuration"

        command "dock"
        command "refresh"
        command "resume"
        command "pause"

        attribute "batteryLevel", "number"
        attribute "totalJobs", "number"
        attribute "totalJobHrs", "number"
        attribute "headline", "string"
        attribute "robotName", "string"
        attribute "preferences_set", "string"
        attribute "status", "string"
    }
}
// simulator metadata
simulator {
}
//Preferences
preferences {
        section("Roomba Credentials") {
            input title: "Roomba Credentials", description: "The username/password can be retrieved via node.js & dorita980", displayDuringSetup: true, type: "paragraph", element: "paragraph"
            input "roomba_username", "text", title: "Roomba username/blid", required: true, displayDuringSetup: true
            input "roomba_password", "password", title: "Roomba password", required: true, displayDuringSetup: true
            input "roomba_host", "string", title:"Roomba Host (Default: Use the Cloud)", defaultValue:""
        }
        section("Misc.") {
            input title: "Polling Interval", description: "This feature allows you to change the frequency of polling for the robot in minutes (1-59)", displayDuringSetup: true, type: "paragraph", element: "paragraph"
            input "pollInterval", "number", title: "Polling Interval", description: "Change polling frequency (in minutes)", defaultValue:4, range: "1..59", required: true, displayDuringSetup: true
        }
}
// Settings updated
def updated() {
    //log.debug "Updated settings ${settings}..
    schedule("0 0/${settings.pollInterval} * * * ?", poll)  // 4min polling is normal for irobots
    poll()
}
// Configuration
def configure() {
    log.debug "Configuring.."
    poll()
}
//Consumable
def setConsumableStatus(statusString) {
    log.debug "User requested setting the Consumable Status - ${statusString}"
    def status = device.latestValue("status")
    log.debug "Setting value based on last roomba state - ${status}"
    if(status == "bin-full") {
        return "maintenance_required"
    } else {
        return "good"
    }
}
//Refresh
def refresh() {
    log.debug "Executing 'refresh'"
    poll()
}
//Polling
def poll() {
    log.debug "Polling for status ----"
    sendEvent(name: "headline", value: "Polling the API", displayed: false)
    state.RoombaCmd = "getStatus"
    apiGet()
}
// UI tile definitions
tiles {

    multiAttributeTile(name:"CLEAN", type:"lighting", width: 6, height: 4, canChangeIcon: true) {
        tileAttribute("device.status", key: "PRIMARY_CONTROL") {
            attributeState "error", label: 'Error', icon: "st.switches.switch.off", backgroundColor: "#bc2323" // No action allowed here
            attributeState "bin-full", label: 'Bin Full', icon: "st.switches.switch.off", backgroundColor: "#bc2323" // No action allowed here
            attributeState "docked", label: 'Start Clean', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "starting"
            attributeState "docking", label: 'Docking', icon: "st.switches.switch.off", backgroundColor: "#ffa81e" // No action allowed here
            attributeState "starting", label: 'Starting Clean', icon: "st.switches.switch.off", backgroundColor: "#ffffff"
            attributeState "cleaning", label: 'Stop Clean', action: "stop", icon: "st.switches.switch.on", backgroundColor: "#79b821"
            attributeState "pausing", label: 'Stop Clean', icon: "st.switches.switch.on", backgroundColor: "#79b821" // No action allowed here
            attributeState "paused", label: 'Send Home', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "docking"
            attributeState "resuming", label: 'Stop Clean', icon: "st.switches.switch.on", backgroundColor: "#79b821" // No action allowed here
        }
        tileAttribute("device.headline", key: "SECONDARY_CONTROL") {
           attributeState "default", label:'${currentValue}'
        }
        tileAttribute("device.batteryLevel", key: "SLIDER_CONTROL") {
        }
    }
    standardTile("DOCK", "device.status", width: 2, height: 2) {
        state "docked", label: 'Docked', backgroundColor: "#79b821" // No action allowed here
        state "docking", label: 'Docking', backgroundColor: "#ffa81e" // No action allowed here
        state "starting", label: 'UnDocking', backgroundColor: "#ffa81e" // No action allowed here
        state "cleaning", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking"
        state "pausing", label: 'Not on Dock', backgroundColor: "#ffffff", nextState: "docking" // No action allowed here
        state "paused", label: 'Dock', action: "dock", backgroundColor: "#ffffff", nextState: "docking"
        state "resuming", label: 'Not on Dock', backgroundColor: "#ffffff", defaultState: true // No action allowed here
    }
    standardTile("PAUSE", "device.status", width: 2, height: 2) {
        state "docked", label: 'Pause', backgroundColor: "#ffffff", defaultState: true // No action allowed here
        state "docking", label: 'Pause', backgroundColor: "#ffffff" // No action allowed here
        state "starting", label: 'Pause', backgroundColor: "#ffffff" // No action allowed here
        state "cleaning", label: 'Pause', action: "pause", backgroundColor: "#ffffff"
        state "pausing", label: 'Pausing..', backgroundColor: "#79b821" // No action allowed here
        state "paused", label: 'Paused', backgroundColor: "#79b821" // No action allowed here
        state "resuming", label: 'Pause', backgroundColor: "#ffffff" // No action allowed here
    }
    standardTile("RESUME", "device.status", width: 2, height: 2) {
        state "docked", label: 'Resume', backgroundColor: "#ffffff", defaultState: true // No action allowed here
        state "docking", label: 'Resume', backgroundColor: "#ffffff" // No action allowed here
        state "starting", label: 'Resume', backgroundColor: "#ffffff" // No action allowed here
        state "cleaning", label: 'Resume', backgroundColor: "#ffffff" // No action allowed here
        state "pausing", label: 'Resume', backgroundColor: "#79b821" // No action allowed here
        state "paused", label: 'Resume', action: "resume", backgroundColor: "#ffffff"
        state "resuming", label: 'Resuming..', backgroundColor: "#79b821" // No action allowed here
    }
    standardTile("refresh", "device.status", width: 6, height: 2, decoration: "flat") {
        state "default", label:'Refresh', action:"refresh.refresh", icon:"st.secondary.refresh"
    }
    valueTile("job_count", "device.totalJobs", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Number of Cleaning Jobs:\n${currentValue} jobs'
    }
    valueTile("job_hr_count", "device.totalJobHrs", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Total Job Time:\n${currentValue} hours'
    }
    valueTile("current_job_time", "device.runtimeMins", width: 3, height: 1, decoration: "flat") {
        state "default", label:'Current Job Runtime:\n${currentValue} minutes'
    }
    main "CLEAN"
    details(["STATUS",
             "CLEAN", "DOCK", "PAUSE", "RESUME",
             "refresh", "job_count", "job_hr_count", "current_job_time"])
}
// Switch methods
def on() {
    def status = device.latestValue("status")
    log.debug "On based on state - ${status}"
    if(status == "paused") {
        resume()
    } else {
        start()
    }
}
def off() {
    def status = device.latestValue("status")
    log.debug "Off based on state - ${status}"
    if(status == "paused") {
        dock()
    } else {
        stop()
    }
}
// Actions
def start() {
    sendEvent(name: "status", value: "starting")
    state.RoombaCmd = "start"
    apiGet()
    runIn(30, poll)
}
def stop() {
    sendEvent(name: "status", value: "stopping")
    state.RoombaCmd = "stop"
    apiGet()
    runIn(30, poll)
}
def dock() {
    sendEvent(name: "status", value: "docking")
    state.RoombaCmd = "dock"
    apiGet()
    runIn(30, poll)
}
def pause() {
    sendEvent(name: "status", value: "pausing")
    state.RoombaCmd = "pause"
    apiGet()
    runIn(30, poll)
}
def resume() {
    sendEvent(name: "status", value: "resuming")
    state.RoombaCmd = "resume"
    apiGet()
    runIn(30, poll)
}
// API methods
def parse(description) {
    def msg = parseLanMessage(description)
    def headersAsString = msg.header // => headers as a string
    def headerMap = msg.headers      // => headers as a Map
    def body = msg.body              // => request body as a string
    def status = msg.status          // => http status code of the response
    def json = msg.json              // => any JSON included in response body, as a data structure of lists and maps
    def xml = msg.xml                // => any XML included in response body, as a document tree structure
    def data = msg.data              // => either JSON or XML in response body (whichever is specified by content-type header in response)
}
def apiGet() {
    def request_query = ""
    def request_host = ""
    def encoded_str = "${roomba_username}:${roomba_password}".bytes.encodeBase64()

    //Handle prefrences
    if("${roomba_host}" == "" || "${roomba_host}" == "null") {
        request_host = "https://irobot.axeda.com"
    } else {
        log.debug "Using Roomba Host: ${roomba_host}"
        request_host = "${roomba_host}"
    }

    //Validation before calling the API
    if(!roomba_username || !roomba_password) {
        def new_status = "Username/Password not set. Configure required before using device."
        sendEvent(name: "headline", value: new_status, displayed: false)
        sendEvent(name: "preferences_set", value: "missing", displayed: false)
        return
    } else if(state.preferences_set != "missing") {
        sendEvent(name: "preferences_set", value: "ready", displayed: false)
    }

    state.AssetID = "ElPaso@irobot!${roomba_username}"
    state.Authorization = "${encoded_str}"

    // Path (No changes required)
    def request_path = "/services/v1/rest/Scripto/execute/AspenApiRequest"
    // Query manipulation
    if( state.RoombaCmd == "getStatus" || state.RoombaCmd == "accumulatedHistorical" || state.RoombaCmd == "missionHistory") {
        request_query = "?blid=${roomba_username}&robotpwd=${roomba_password}&method=${state.RoombaCmd}"
    } else {
        request_query = "?blid=${roomba_username}&robotpwd=${roomba_password}&method=multipleFieldSet&value=%7B%0A%20%20%22remoteCommand%22%20:%20%22${state.RoombaCmd}%22%0A%7D"
    }

    def requestURI = "${request_host}${request_path}${request_query}"
    def httpRequest = [
        method:"GET",
        uri: "${requestURI}",
        headers: [
            'User-Agent': 'aspen%20production/2618 CFNetwork/758.3.15 Darwin/15.4.0',
            Accept: '*/*',
            'Accept-Language': 'en-us',
            'ASSET-ID': state.AssetID,
        ]
    ]
    try {
        httpGet(httpRequest) { resp ->
            resp.headers.each {
                log.debug "${it.name} : ${it.value}"
            }
            log.debug "response contentType: ${resp.contentType}"
            log.debug "response data: ${resp.data}"
            parseResponseByCmd(resp, state.RoombaCmd)
        }
    } catch (e) {
        log.error "something went wrong: $e"
    }
}
def parseResponseByCmd(resp, command) {
    //Parsing
    def data = resp.data
    if(command == "getStatus") {
        setStatus(data)
    } else if(command == "accumulatedHistorical" ) {
        //readSummaryInfo -- same as getStatus but easier to parse
    } else if(command == "missionHistory") {
        //readMissionHistory -- get results about last 30 jobs -- Out of scope for device-type?
    }
}
def setStatus(data) {
    //TODO: Mine other data here later? add support for "percent completion"?
    def rstatus = data.robot_status
    def robotName = data.robotName
    def mission = data.mission
    def runstats = data.bbrun
    def cschedule = data.cleanSchedule
    def pmaint = data.preventativeMaintenance
    def robot_status = new groovy.json.JsonSlurper().parseText(rstatus)
    def robot_history = new groovy.json.JsonSlurper().parseText(mission)
    def runtime_stats = new groovy.json.JsonSlurper().parseText(runstats)
    def schedule = new groovy.json.JsonSlurper().parseText(cschedule)
    def maintenance = new groovy.json.JsonSlurper().parseText(pmaint)
    log.debug "Robot Status = ${robot_status}"
    log.debug "Robot History = ${robot_history}"
    log.debug "Runtime stats= ${runtime_stats}"
    log.debug "Robot schedule= ${schedule}"
    log.debug "Robot Maintenance= ${maintenance}"
    def current_cycle = robot_status['cycle']
    def current_charge = robot_status['batPct']
    def current_phase = robot_status['phase']
    def num_mins_running = robot_status['mssnM']
    def flags = robot_status['flags']  // Unknown what 'Flags' 0/1/2/5 mean?
    def readyCode = robot_status['notReady']
    def num_cleaning_jobs = robot_history['nMssn']
    def num_dirt_detected = runtime_stats['nScrubs']
    def total_job_time = runtime_stats['hr']
    

    def new_status = get_robot_status(current_phase, current_cycle, current_charge, readyCode)
    def roomba_value = get_robot_enum(current_phase, readyCode)

    log.debug("Robot updates -- ${roomba_value} + ${new_status}")
    //Set the state object
    if(roomba_value == "cleaning") {
        state.switch = "on"
    } else {
        state.switch = "off"
    }

    if(status == "bin-full") {
        state.consumableStatus = "maintenance_required"
    } else {
        state.consumableStatus = "good"
    }

    //send events, display final event
    sendEvent(name: "robotName", value: robotName, displayed: false)
    sendEvent(name: "totalJobHrs", value: total_job_time, displayed: false)
    sendEvent(name: "totalJobs", value: num_cleaning_jobs, displayed: false)
    sendEvent(name: "runtimeMins", value: num_mins_running, displayed: false)
    sendEvent(name: "batteryLevel", value: current_charge, displayed: false)
    sendEvent(name: "headline", value: new_status, displayed: false)
    sendEvent(name: "status", value: roomba_value)
    sendEvent(name: "switch", value: state.switch)
    sendEvent(name: "consumableStatus", value: state.consumableStatus)
}
def get_robot_enum(current_phase, readyCode) {
    if(readyCode != 0) {
        if(readyCode == 16) {
            return "bin-full"
        } else {
            return "error"
        }
    } else if(current_phase == "charge") {
        return "docked"
    } else if(current_phase == "hmUsrDock") {
        return "docking"
    } else if(current_phase == "pause" || current_phase == "stop") {
        return "paused"
    } else if(current_phase == "run") {
        return "cleaning"
    } else {
        //"Stuck" phase falls into this category.
        log.error "Unknown phase - Raw 'robot_status': ${status}. Add to 'get_robot_enum'"
        return "error"
    }
}
def parse_not_ready_status(readyCode) {
    def robotName = device.latestValue("robotName")

    if(readyCode == 16) {
      return "${robotName} bin is full. Empty bin to continue."
    } else if(readyCode == 7) {
      return "${robotName} is not upright. Place robot on flat surface to continue."
    } else if (readyCode == 1) {
      return "${robotName} is stuck. Move robot to continue."
    } else {
      return "${robotName} returned notReady=${readyCode}. See iRobot app for details."
    }
}
def get_robot_status(current_phase, current_cycle, current_charge, readyCode) {
    log.debug "Enter get_robot_status"

    def robotName = device.latestValue("robotName")

    if(readyCode != 0) {
      return parse_not_ready_status(readyCode)
    } else if(current_phase == "charge") {
        if (current_charge == 100) {
            return "${robotName} is Docked/Fully Charged"
        } else {
            return "${robotName} is Docked/Charging"
        }
    } else if(current_phase == "hmUsrDock") {
        return "${robotName} is returning home"
    } else if(current_phase == "run") {
        return "${robotName} is cleaning (${current_cycle} cycle)"
    } else if(current_phase == "pause" || current_phase == "stop") {
        return "Paused - 'Dock' or 'Resume'?"
    }

    log.error "Unknown phase - ${current_phase}."
    return "Error - refresh to continue. Code changes required if problem persists."
}

Here are some screenshots of the app in action:
Switch (Text alternates between status’ and 'battery level’


Running roomba

Pausing Roomba

Docking Roomba

Docking Roomba via master-button (doesn’t always work)

Recent Activity

Settings

CoRE SmartApp integration via Consumable Capability:

ToDo before release:

- Handle common errors (Bin full, stuck, “not upright”)
- CoRE SmartApp example for ‘bin full’ notifications
- Work on “refresh after actions” Update:30sec after running command, API will poll, in addition to the schedule() poll configurable in settings, this should work for most people
- Work on the main button being able to start/pause/dock Update: Fixed
- Use ‘password’ input for the password in preferences
- Include historical information as valueTile
- Including settings manipulation from the device

#Notes for future contributions/contributors:
Including Settings manipulation: (API scraped from iRobot using Fiddler+proxy)

#Edge cleaning OFF
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":1,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}
#Edge Cleaning ON
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}


#Finish cleaning when bin is full
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":1,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

#Don't finish job, wait for bin to be emptied, then press clean to resume

blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

#Cleaning passes
##One pass
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":1}
##Two pass
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":1,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":1}
##Automatic
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

#Carpet Boost
##Automatic
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":0,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}##Performance
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":1,"binPause":0,"vacHigh":1,"noPP":0,"ecoCharge":0,"noAutoPasses":0}
##ECO mode
blid=...&robotpwd=...&method=multipleFieldSet&value={"twoPass":0,"openOnly":0,"schedHold":0,"manualUpdate":0,"carpetBoost":1,"binPause":0,"vacHigh":0,"noPP":0,"ecoCharge":0,"noAutoPasses":0}

# Update cleaning schedule (All days set to "No Cleaning" at 9am)
blid=...&robotpwd=...&method=multipleFieldSet&value={ "cleanSchedule" : { "cycle" : ["none", "none", "none", "none", "none", "none", "none"], "h" : [9, 9, 9, 9, 9, 9, 9], "m" : [0, 0, 0, 0, 0, 0, 0] } }
5 Likes

Looks good.

Any way to get a full bin push notification or event?

Thanks!

Support for “bin full” as a state have been added above.

Edit: Also, I’ve got the schedule() call to work via update() so after a user sets (or re-sets) preferences you should continue to get updated every 4min – rather than refresh every time…

1 Like

Awesome!!! I got too lazy to do this myself but you did it! :slight_smile: great job @steve

Elfège Leylavergne

1 Like

If you use the latest revision of code above and refresh your device, you can write a CoRE SmartApp that will send a Push notification when roomba changes from "good" to "maintenance_required" – I’ve included an example screenshot as well!

2 Likes

Thanks!

I’m working on the shoulders of giants, I got on this band-wagon late (first time with smartthings, device-type writing, groovy!) and was super impressed with the level of detail found in this thread.

Big THANK YOU to everyone in this thread for keeping the effort going :grinning:

Push notification of maintenance

You have been able to address an obvious over site that iRobot has failed to resolve for over a year now.

Thanks for the great work! It works even better when I enter the BLID as the username instead of the password and vice versa…!

Installation instructions:
Add new repo steve-gregory/irobot-manager/master to settings,
Select update from repo, and check the irobot-roomba.groovy file, publish and update.

Adding your Roomba:
Create a new device from the Graph website and select iRobot Roomba.
Once device is created, edit the preferences to include the required settings (blid and password collected from dorita980 api).

Open smartthings app and hit the refresh button. You should be fully online at this point.

3 Likes

It seems I cannot control Roomba via Smartthings after the Robot firmware is updated to v2.0.0-34. I also cannot get the username and password using dorita980 code. It seems Roomba no longer listening on port 443.

I can only control Roomba from the iRobot app. From the app, it is sending requests to https://disc-prod.iot.irobotapi.com

Is there anyone experience the same issue? Thanks for the help and time.

Interesting, no new firmware listed in the release notes yet.

http://homesupport.irobot.com/app/answers/detail/a_id/529/p/4116

I just double checked and am on 1.6.6 still :confused:

Try pushing and holding the clean button to reset.

Hi again

would you happen to know how to control the roomba’s movement? I think I remember that it’s about sending serial packets and therefore it must be a tcp to serial protocol, right? I think I remember from the old versions something like Straight = [a numerica value]. Any idea if there is a way to imp^lement this in the device handlers we created? Let me know.

Out of curiosity, what is it you want to try and do by having this ability?