Last year, we experimented with the Roomba self-propelled vacuum cleaner platform. A new vacuum cleaner cost us around £ 300 (a used one can be found for £ 100 and even cheaper), and it consists of two electric drives for wheels, two touch sensors in front, an infrared sensor at the bottom (for detecting steps) and at the top (for searching for the station) . The exact list of sensors depends on the model: the protocol provides for up to four infrared sensors from the bottom, each of which returns one bit (“the floor is visible / not visible”). In any case, no range finders: all available sensors are one-bit. In addition, there are no “programmable arduins” in Roomba, and in order to control it, you need to install a laptop (or arduin) on top and communicate with the robot via RS-232. Having played enough with a vacuum cleaner, we left it gathering dust on one of the shelves of the warehouse.
This year we decided to try Microsoft Robotics Development Studio (MRDS) , to promote which Microsoft formulated the “MRDS Reference Platform” specification - a set of equipment and a control protocol for a “standard” robot. This specification would allow robots to create compatible robots and transfer programs between them. Compared with the hardware of the vacuum cleaner, the Reference Platform is much more complicated and powerful: Kinect is included in the specification, three IR rangefinders and two ultrasonic, and also wheel rotation sensors (encoders). The implementation of the MRDS RP is currently offered only by the company Parallax called Eddie (about £ 1000, not including Kinect). The extraordinary similarity of Eddie with the photos of the prototype robot in the MRDS RP specification suggests that the specification was created in close cooperation with Parallax, in other words, Parallax managed to achieve that Microsoft took their platform as a reference.
The British Channel 4 included a two-minute piece with the Eddie demo integrated into a shopping cart in one of the releases of the Gadget Man program. Unfortunately, only the owners of British IP addresses can view the program on Channel 4, and I did not manage to grab it and reload it (maybe someone of the readers will succeed?) - therefore I bring only freeze-frames from there.
The first navigation attempt was based on rangefinders: the robot “gropes” the shelf and travels along it, maintaining a predetermined distance, just as the tottering drinker walks, leaning against the wall through the step. The similarity was really huge: the delay in receiving the signal from the range finders, processing them in the MRDS, forming a command for the motors, and overcoming their inertia - amounted to tenths of a second. During this time, the robot “led away” to the side, so that the “course correction” each time was tens of degrees, and the trajectory was obtained in a wide zigzag. In addition to this, the Eddie rangefinders were not too accurate - the error was up to ± 5 cm - and rather narrowly focused, i.e. the shelves were recognized only on one, in advance of a given height from the floor.

PositionOperations : PortSet<GetPosition, SetPosition, SetDestination> , and so that the service itself, transparently for the user, polls the wheel sensors and exposes the voltage on the motors. In fact, it did not come to this realization - only a muddy prototype was ready without sharing the responsibilities between the services. I will try, nevertheless, to show that I did it.System.Drawing.PointF . public struct Position { public readonly double x, y, heading; public Position(double x, double y, double heading) { this.x = x; this.y = y; this.heading = heading; } private static double Sqr(double d) { return d * d; } public double DistanceTo(double x, double y) { return Math.Sqrt(Sqr(this.y - y) + Sqr(this.x - x)); } public static double NormalizeHeading(double heading) { while (heading < -Math.PI) heading += 2 * Math.PI; while (heading > Math.PI) heading -= 2 * Math.PI; return heading; } // turn angle/2, go ahead distance, turn angle/2 again public Position advance(double distance, double angle) { double newHeading = heading + angle / 2, newX = x + distance * Math.Cos(newHeading), newY = y + distance * Math.Sin(newHeading); return new Position(newX, newY, NormalizeHeading(heading + angle)); } } advance method implements the recalculation of coordinates by a given angle of rotation φ and the distance covered | AB |, as in the drawing in the middle of the article.PositionKeeping , within which there are two instances of EncoderLog - the “ EncoderLog list”, a separate copy for each wheel. The list is SortedList in SortedList , because (theoretically) sensor data may not arrive in chronological order. The only non-trivial method in EncoderLog is linear approximation of the run to get its value at any time between “ticks” or after the last “tick”. private class PositionKeeping { private static readonly DateTime initialized = DateTime.Now; public class EncoderLog { private SortedList<DateTime, double> log = new SortedList<DateTime, double> { { initialized, 0 } }; public void Register(DateTime at, double reading) { log[at] = reading; } public void Reset(DateTime at, double reading) { log.Clear(); log.Add(at, reading); } public DateTime LastTick { get { return log.Last().Key; } } public double LastReading { get { return log.Last().Value; }} public double ReadingAt(DateTime at) { int index = log.Count - 1; while(index>=0 && log.Keys[index] > at) index--; if(index<0) return double.NaN; // before first reading; impossible // now, log.Keys[index] <= at, and log.Keys[index+1] > at DateTime preceding = log.Keys[index], following = index<log.Count-1 ? log.Keys[index+1] : DateTime.MaxValue; if(following == DateTime.MaxValue) { // last reading precedes at; extrapolate if(index==0) // there's only one reading return log[preceding]; else { DateTime nextPreceding = log.Keys[index-1]; return log[nextPreceding] + (at-nextPreceding).TotalSeconds * (log[preceding]-log[nextPreceding]) / (preceding-nextPreceding).TotalSeconds; } } else // both readings are available; interpolate return log[preceding] + (at-preceding).TotalSeconds * (log[following]-log[preceding]) / (following - preceding).TotalSeconds; } } public EncoderLog leftEnc = new EncoderLog(), rightEnc = new EncoderLog(); PositionKeeping at the time of each “tick” - we will measure the distance traveled from this saved position in order to get a new position. private SortedList<DateTime, Position> position = new SortedList<DateTime, Position> { { initialized, new Position(0, 0, Math.PI / 2) } }; public void Register(DateTime at, Position pos) { position.Add(at, pos); } public void Reset(DateTime at, Position pos) { // the position has changed => old ticks logs become obsolete leftEnc.Reset(at, leftEnc.ReadingAt(at)); rightEnc.Reset(at, rightEnc.ReadingAt(at)); position.Clear(); position.Add(at, pos); } public Position Current { get { return position.Last().Value; } } PositionKeeping class by calculating the new position using the data of both wheel sensors - in accordance with the formulas derived. Update can be called from the sensor check cycle ( ServiceHandlerBehavior.Concurrent ), and takes the service port argument, where it sends new coordinates if a tick is registered. This message changes the state of the service, so it should be treated as ServiceHandlerBehavior.Exclusive . private static readonly TimeSpan RegisterDelay = TimeSpan.FromSeconds(1); // register null-tick if no actual ticks for this long public void Update(DateTime at, double left, double right, PositionOperations mainPort) { DateTime prevRef = Min(leftEnc.LastTick, rightEnc.LastTick); SetPosition set = new SetPosition { Timestamp = at, LeftEncUpdated = left != leftEnc.LastReading || at > leftEnc.LastTick + RegisterDelay, LeftEncReading = left, RightEncUpdated = right != rightEnc.LastReading || at > rightEnc.LastTick + RegisterDelay, RightEncReading = right }; if(set.LeftEncUpdated || set.RightEncUpdated) { set.Position = Recalculate(prevRef, left, right); mainPort.Post(set); } } private Position Recalculate(DateTime prevRef, double left, double right) { double sLeft = left - leftEnc.ReadingAt(prevRef), sRight = right - rightEnc.ReadingAt(prevRef); Position refPos = position[prevRef]; // has to exist if the encoder reference exists if (Math.Abs(sRight - sLeft) < .5) // less then half-tick difference: go straight return refPos.advance(Constants.CmPerTick * (sRight + sLeft) / 2, 0); else { double angle = Constants.CmPerTick * (sRight - sLeft) / Constants.WheelsDist, distance = Constants.WheelsDist * Math.Sin(angle / 2) * (sRight + sLeft) / (sRight - sLeft); return refPos.advance(distance, angle); } } } SetPosition handler, which sets the voltage on the motors so that the robot moves toward the target. [ServiceHandler(ServiceHandlerBehavior.Exclusive)] public void SetPositionHandler(SetPosition set) { if (!set.LeftEncUpdated && !set.RightEncUpdated) { // position updated by an absolute reference. positionKeeping.Reset(set.Timestamp, set.Position); } else { if (set.LeftEncUpdated) positionKeeping.leftEnc.Register(set.Timestamp, set.LeftEncReading); if (set.RightEncUpdated) positionKeeping.rightEnc.Register(set.Timestamp, set.RightEncReading); positionKeeping.Register(set.Timestamp, set.Position); } // the navigator Destination dest = state.dest; double distance = set.Position.DistanceTo(dest.x, dest.y); if (distance < 5) // reached { drivePort.SetDrivePower(0, 0); SendNotification(submgrPort, new DriveDistance()); return; } double heading = Position.NormalizeHeading(Math.Atan2(dest.y - set.Position.y, dest.x - set.Position.x)), power = (distance < 50) ? .2 : .4; // a few magic numbers if (Math.Abs(heading) < .05) { // straight ahead drivePort.SetDrivePower(power, power); return; } double r = distance / (2 * Math.Sin(heading / 2)), hump = r * (1 - Math.Cos(heading / 2)); if (Math.Abs(heading) > Math.PI / 2 || Math.Abs(hump) > Constants.MaxHump) { // not reachable by an arc; rotate if (heading > 0) // rotate left drivePort.SetDrivePower(-.3, .3); else // rotate right drivePort.SetDrivePower(.3, -.3); } else { // go in arc double rLeft = Math.Abs(r - Constants.WheelsDist / 2), rRight = Math.Abs(r + Constants.WheelsDist / 2), rMax = Math.Max(rLeft, rRight); // <Patrician|Away> what does your robot do, sam // <bovril> it collects data about the surrounding environment, then discards it and drives into walls drivePort.SetDrivePower(power * Math.Pow(rLeft / rMax, 9), power * Math.Pow(rRight / rMax, 9)); } } SetPosition , and “restart” PositionKeeping calling SetPosition with LeftEncUpdated = RightEncUpdated = false . For a few meters between the readable barcodes, the error in determining the coordinates did not exceed 20 cm - just as much we left in reserve between the robot and the shelves.Source: https://habr.com/ru/post/161803/
All Articles