www.gusucode.com > Mobile Robotics Simulation Toolbox > src/environment/MultiRobotEnv.m
classdef MultiRobotEnv < matlab.System % MULTIROBOTENV 2D Multi-Robot Environment % % Displays the pose (position and orientation) of multiple objects in a % 2D environment. Additionally has the option to display a map as a % robotics.OccupancyGrid or robotics.BinaryOccupancyGrid, object % trajectories, waypoints, lidar scans, and/or objects. % % For more information, see <a href="matlab:edit mrsDocMultiRobotEnv">the documentation page</a> % % Copyright 2018-2019 The MathWorks, Inc. %% PROPERTIES % Public (user-visible) properties properties(Nontunable) numRobots = 1; % Number of robots robotRadius = 0; % Robot radii [m] showTrajectory = false; % Show trajectory mapName = ''; % Map hasWaypoints = false; % Accept waypoints hasLidar = false; % Accept lidar inputs hasObjDetector = false; % Accept object detections hasRobotDetector = false; % Accept robot detections plotSensorLines = true; % Plot sensor lines showRobotIds = true; % Show robot IDs robotColors = []; % Robot colors % Lidar sensorOffset = {[0 0]}; % Lidar sensor offset (x,y) [m] scanAngles = {[-pi/4,0,pi/4]}; % Scan angles [rad] % Object detectors objDetectorOffset = {[0 0]}; % Object detector offset (x,y) [m] objDetectorAngle = 0; % Object detector angle [rad] objDetectorFOV = pi/4; % Object detector field of view [rad] objDetectorMaxRange = 5; % Object detector maximum range [m] objectColors = [1 0 0;0 1 0;0 0 1]; % Object label colors [RGB rows] objectMarkers = 's'; % Object markers [character array] % Robot detectors robotDetectorOffset = {[0 0]}; % Robot detector offset (x,y) [m] robotDetectorAngle = 0; % Robot detector angle [rad] robotDetectorFOV = pi/4; % Robot detector field of view [rad] robotDetectorMaxRange = 5; % Robot detector maximum range [m] end properties % Tunable Poses; % Robot poses (x,y,theta) [m,m,rad] end % Private properties properties(Access = private) map; % Occupancy grid representing the map fig; % Figure window ax; % Axes for plotting RobotHandle; % Handle to robot body marker or circle OrientationHandle; % Handle to robot orientation line LidarHandles; % Handle array to lidar lines TrajHandle; % Handle to trajectory plot trajX; % X Trajectory points trajY; % Y Trajectory points WaypointHandle; % Handle to waypoints ObjectHandles; % Handle to objects ObjDetectorHandles = {}; % Handle array to object detector lines RobotDetectorHandles = {}; % Handle array to robot detector lines IdHandles; % Handle array to robot IDs end %% METHODS methods % Constructor: Takes number of robots as mandatory argument function obj = MultiRobotEnv(N) obj.numRobots = N; end end methods(Access = protected) % Setup method: Initializes all necessary graphics objects function setupImpl(obj) % Setup the visualization setupVisualization(obj); end % Step method: Updates visualization based on inputs function stepImpl(obj,robotIndices,poses,varargin) % Check for closed figure if ~isvalid(obj.fig) return; end % Unpack the optional arguments idx = 1; % Waypoints if obj.hasWaypoints waypoints = varargin{idx}; idx = idx + 1; else waypoints = []; end % Lidar ranges if any(obj.hasLidar) ranges = varargin{idx}; idx = idx + 1; else ranges = cell(1,obj.numRobots); end % Objects if any(obj.hasObjDetector) objects = varargin{idx}; else objects = []; end % Draw the waypoints and objects drawWaypointsAndObjects(obj,waypoints,objects); % Draw the robots if ~isempty(robotIndices) drawRobots(obj,robotIndices,poses,ranges); end % Update the figure drawnow('limitrate') end end methods (Access = public) % Performs all the visualization setup. It is separate from the % setup method, since it can be called independently as well. function setupVisualization(obj) % Convert scalar flags to arrays based on number of robots if numel(obj.robotRadius) ~= obj.numRobots obj.robotRadius = repmat(obj.robotRadius,[1,obj.numRobots]); end if numel(obj.showTrajectory) ~= obj.numRobots obj.showTrajectory = repmat(obj.showTrajectory,[1,obj.numRobots]); end if numel(obj.hasLidar) ~= obj.numRobots obj.hasLidar = repmat(obj.hasLidar,[1,obj.numRobots]); end if numel(obj.hasObjDetector) ~= obj.numRobots obj.hasObjDetector = repmat(obj.hasObjDetector,[1,obj.numRobots]); end if numel(obj.hasRobotDetector) ~= obj.numRobots obj.hasRobotDetector = repmat(obj.hasRobotDetector,[1,obj.numRobots]); end % Initialize poses obj.Poses = nan(3,obj.numRobots); % Create figure FigureName = 'Multi-Robot Environment'; FigureTag = 'MultiRobotEnvironment'; existingFigures = findobj('type','figure','tag',FigureTag); if ~isempty(existingFigures) obj.fig = figure(existingFigures(1)); % bring figure to the front clf; else obj.fig = figure('Name',FigureName,'tag',FigureTag); end % Create global axes obj.ax = axes('parent',obj.fig); hold(obj.ax,'on'); % Show the map obj.map = internal.createMapFromName(obj.mapName); if ~isempty(obj.map) show(obj.map,'Parent',obj.ax); end % Initialize robot plot obj.OrientationHandle = cell(obj.numRobots,1); obj.RobotHandle = cell(obj.numRobots,1); if isempty(obj.robotColors) obj.robotColors = [0 0 1]; end for rIdx = 1:obj.numRobots if size(obj.robotColors,1) == 1 rColor = obj.robotColors; else rColor = obj.robotColors(rIdx,:); end obj.OrientationHandle{rIdx} = plot(obj.ax,0,0,'Color',rColor,'LineWidth',1.5); if obj.robotRadius(rIdx) > 0 % Finite size robot [x,y] = internal.circlePoints(0,0,obj.robotRadius(rIdx),17); obj.RobotHandle{rIdx} = plot(obj.ax,x,y,'Color',rColor,'LineWidth',1.5); else % Point robot obj.RobotHandle{rIdx} = plot(obj.ax,0,0,'bo', ... 'LineWidth',1.5,'MarkerFaceColor',[1 1 1]); end % Initialize robot IDs, if enabled if obj.showRobotIds obj.IdHandles{rIdx} = text(0,0,num2str(rIdx), ... 'Color',rColor,'FontWeight','bold'); end end % Initialize trajectory obj.TrajHandle = cell(obj.numRobots,1); obj.trajX = cell(obj.numRobots,1); obj.trajY = cell(obj.numRobots,1); for rIdx = 1:obj.numRobots if obj.showTrajectory(rIdx) obj.TrajHandle{rIdx} = plot(obj.ax,0,0,'b.-'); obj.trajX{rIdx} = []; obj.trajY{rIdx} = []; end end % Initialize waypoints if obj.hasWaypoints obj.WaypointHandle = plot(obj.ax,0,0, ... 'rx','MarkerSize',10,'LineWidth',2); end % Initialize lidar lines obj.LidarHandles = cell(obj.numRobots,1); for rIdx = 1:obj.numRobots if obj.hasLidar(rIdx) for idx = 1:numel(obj.scanAngles{rIdx}) obj.LidarHandles{rIdx}(idx) = plot(obj.ax,0,0,'b--'); end end end % Initialize objects and object detector lines if numel(obj.objectMarkers) == 1 obj.ObjectHandles = scatter(obj.ax,[],[],75,... obj.objectMarkers,'filled','LineWidth',2); else for idx = 1:numel(obj.objectMarkers) obj.ObjectHandles(idx) = scatter(obj.ax,[],[],75,... obj.objectMarkers(idx),'filled','LineWidth',2); end end obj.ObjDetectorHandles = cell(obj.numRobots,1); for rIdx = 1:obj.numRobots if obj.hasObjDetector(rIdx) objDetectorFormat = 'g-.'; obj.ObjDetectorHandles{rIdx}(1) = plot(obj.ax,0,0,objDetectorFormat); % Left line obj.ObjDetectorHandles{rIdx}(2) = plot(obj.ax,0,0,objDetectorFormat); % Right line end end % Initialize robot detector lines obj.RobotDetectorHandles = cell(obj.numRobots,1); for rIdx = 1:obj.numRobots robotDetectorFormat = 'm:'; obj.RobotDetectorHandles{rIdx} = [ plot(obj.ax,0,0,robotDetectorFormat,'LineWidth',1.5), ... % Left line plot(obj.ax,0,0,robotDetectorFormat,'LineWidth',1.5) ]; % Right line end % Final setup title(obj.ax,'Multi-Robot Visualization'); hold(obj.ax,'off'); axis equal end % Helper method to draw the waypoints and objects, % which are independent of the robot function drawWaypointsAndObjects(obj,waypoints,objects) % Check for closed figure if ~isvalid(obj.fig) return; end % Update waypoints if obj.hasWaypoints && (numel(waypoints) > 1) set(obj.WaypointHandle,'xdata',waypoints(:,1), ... 'ydata',waypoints(:,2)); else set(obj.WaypointHandle,'xdata',[], ... 'ydata',[]); end % Update the objects if numel(objects) <= 1 for idx = 1:numel(obj.ObjectHandles) set(obj.ObjectHandles(idx),'xdata',[],'ydata',[],'cdata',[]); end else % Find the corresponding colors of each object if size(obj.objectColors,1) == 1 colorData = obj.objectColors; % Use the single color specified else colorData = obj.objectColors(objects(:,3),:); % Use the color based on labels end % If single marker, plot markers in the single object handle if numel(obj.objectMarkers) == 1 set(obj.ObjectHandles,'xdata',objects(:,1),... 'ydata',objects(:,2),'cdata',colorData); % If multiple markers, plot markers in the single object handle else for idx = 1:numel(obj.objectMarkers) indices = (objects(:,3) == idx); set(obj.ObjectHandles(idx),'xdata',objects(indices,1),... 'ydata',objects(indices,2),'cdata',colorData(indices,:)); end end end end % Helper method to draw all robots (calls drawRobot) function drawRobots(obj,robotIndices,poses,ranges) % Check for closed figure if ~isvalid(obj.fig) return; end % Draw each robot and its sensors % Single-robot case if numel(robotIndices) == 1 obj.Poses(:,robotIndices) = poses; drawRobot(obj,robotIndices,poses,ranges); % Multi-robot case else for rIdx = robotIndices pose = poses(:,rIdx); obj.Poses(:,rIdx) = pose; drawRobot(obj,rIdx,pose,ranges{rIdx}); end end end % Helper method to draw the robot and its sensors at each step function drawRobot(obj,rIdx,pose,ranges) % Unpack the pose input into (x, y, theta) x = pose(1); y = pose(2); theta = pose(3); % Update the trajectory if obj.showTrajectory(rIdx) obj.trajX{rIdx}(end+1) = x; obj.trajY{rIdx}(end+1) = y; set(obj.TrajHandle{rIdx},'xdata',obj.trajX{rIdx}, ... 'ydata',obj.trajY{rIdx}); end % Update the robot pose xAxesLim = get(obj.ax,'XLim'); lineLength = diff(xAxesLim)/20; r = obj.robotRadius(rIdx); if r > 0 % Finite radius case [xc,yc] = internal.circlePoints(x,y,r,17); set(obj.RobotHandle{rIdx},'xdata',xc,'ydata',yc); len = max(lineLength,2*r); % Plot orientation based on radius unless it's too small xp = [x, x+(len*cos(theta))]; yp = [y, y+(len*sin(theta))]; set(obj.OrientationHandle{rIdx},'xdata',xp,'ydata',yp); else % Point robot case xp = [x, x+(lineLength*cos(theta))]; yp = [y, y+(lineLength*sin(theta))]; set(obj.RobotHandle{rIdx},'xdata',x,'ydata',y); set(obj.OrientationHandle{rIdx},'xdata',xp,'ydata',yp); end % Show robot IDs, if enabled if obj.showRobotIds set(obj.IdHandles{rIdx},'Position',[x y] - max(1.25*r,lineLength*0.5)); end % Update lidar lines if obj.hasLidar(rIdx) && obj.plotSensorLines % Find the sensor location(s) offsetVec = [cos(theta) -sin(theta); ... sin(theta) cos(theta)]*obj.sensorOffset{rIdx}'; sensorLoc = [x,y] + offsetVec'; scanAngs = obj.scanAngles{rIdx}; for idx = 1:numel(ranges) if ~isnan(ranges(idx)) % If there is a single sensor offset, use that value if size(sensorLoc,1) == 1 sensorPos = sensorLoc; % Else, use the specific index's sensor location else sensorPos = sensorLoc(idx,:); end alpha = theta + scanAngs(idx); lidarX = sensorPos(1) + [0, ranges(idx)*cos(alpha)]; lidarY = sensorPos(2) + [0, ranges(idx)*sin(alpha)]; set(obj.LidarHandles{rIdx}(idx),'xdata',lidarX, ... 'ydata',lidarY); else set(obj.LidarHandles{rIdx}(idx),'xdata',[],'ydata',[]); end end end % Update object and object detector lines if obj.hasObjDetector(rIdx) && obj.plotSensorLines % Find the sensor location(s) offsetVec = [cos(theta) -sin(theta); ... sin(theta) cos(theta)]*obj.objDetectorOffset{rIdx}'; sensorLoc = [x,y] + offsetVec'; % Plot the object detector lines maxRange = obj.objDetectorMaxRange(rIdx); % Left line alphaLeft = theta + obj.objDetectorAngle(rIdx) + obj.objDetectorFOV(rIdx)/2; if ~isempty(obj.map) intPtsLeft = rayIntersection(obj.map,[sensorLoc alphaLeft],0,maxRange); else intPtsLeft = NaN; end if ~isnan(intPtsLeft) objLeftX = [sensorLoc(1) intPtsLeft(1)]; objLeftY = [sensorLoc(2) intPtsLeft(2)]; else objLeftX = sensorLoc(1) + [0, maxRange*cos(alphaLeft)]; objLeftY = sensorLoc(2) + [0, maxRange*sin(alphaLeft)]; end set(obj.ObjDetectorHandles{rIdx}(1), ... 'xdata',objLeftX,'ydata',objLeftY); % Right line alphaRight = theta + obj.objDetectorAngle(rIdx) - obj.objDetectorFOV(rIdx)/2; if ~isempty(obj.map) intPtsRight = rayIntersection(obj.map,[sensorLoc alphaRight],0,maxRange); else intPtsRight = NaN; end if ~isnan(intPtsRight) objRightX = [sensorLoc(1) intPtsRight(1)]; objRightY = [sensorLoc(2) intPtsRight(2)]; else objRightX = sensorLoc(1) + [0, maxRange*cos(alphaRight)]; objRightY = sensorLoc(2) + [0, maxRange*sin(alphaRight)]; end set(obj.ObjDetectorHandles{rIdx}(2), ... 'xdata',objRightX,'ydata',objRightY); end % Update robot detector lines if obj.hasRobotDetector(rIdx) && obj.plotSensorLines % Find the sensor location(s) offsetVec = [cos(theta) -sin(theta); ... sin(theta) cos(theta)]*obj.robotDetectorOffset{rIdx}'; sensorLoc = [x,y] + offsetVec'; % Plot the robot detector lines maxRange = obj.robotDetectorMaxRange(rIdx); % Left line alphaLeft = theta + obj.robotDetectorAngle(rIdx) + obj.robotDetectorFOV(rIdx)/2; if ~isempty(obj.map) intPtsLeft = rayIntersection(obj.map,[sensorLoc alphaLeft],0,maxRange); else intPtsLeft = NaN; end if ~isnan(intPtsLeft) objLeftX = [sensorLoc(1) intPtsLeft(1)]; objLeftY = [sensorLoc(2) intPtsLeft(2)]; else objLeftX = sensorLoc(1) + [0, maxRange*cos(alphaLeft)]; objLeftY = sensorLoc(2) + [0, maxRange*sin(alphaLeft)]; end set(obj.RobotDetectorHandles{rIdx}(1), ... 'xdata',objLeftX,'ydata',objLeftY); % Right line alphaRight = theta + obj.robotDetectorAngle(rIdx) - obj.robotDetectorFOV(rIdx)/2; if ~isempty(obj.map) intPtsRight = rayIntersection(obj.map,[sensorLoc alphaRight],0,maxRange); else intPtsRight = NaN; end if ~isnan(intPtsRight) objRightX = [sensorLoc(1) intPtsRight(1)]; objRightY = [sensorLoc(2) intPtsRight(2)]; else objRightX = sensorLoc(1) + [0, maxRange*cos(alphaRight)]; objRightY = sensorLoc(2) + [0, maxRange*sin(alphaRight)]; end set(obj.RobotDetectorHandles{rIdx}(2), ... 'xdata',objRightX,'ydata',objRightY); end end % Attaches all properties associated with a LidarSensor object function attachLidarSensor(obj,rIdx,lidar) if numel(obj.hasLidar) ~= obj.numRobots obj.hasLidar = repmat(obj.hasLidar,[1,obj.numRobots]); end obj.hasLidar(rIdx) = true; obj.sensorOffset{rIdx} = lidar.sensorOffset; obj.scanAngles{rIdx} = lidar.scanAngles; % Ensure to use the same map as the visualizer release(lidar); lidar.mapName = obj.mapName; lidar.isMultiRobot = true; lidar.robotIdx = rIdx; setEnvironment(lidar,obj); end % Attaches all properties associated with an ObjectDetector object function attachObjectDetector(obj,rIdx,detector) if numel(obj.hasObjDetector) ~= obj.numRobots obj.hasObjDetector = repmat(obj.hasObjDetector,[1,obj.numRobots]); end obj.hasObjDetector(rIdx) = true; obj.objDetectorOffset{rIdx} = detector.sensorOffset; obj.objDetectorAngle(rIdx) = detector.sensorAngle; obj.objDetectorFOV(rIdx) = detector.fieldOfView; obj.objDetectorMaxRange(rIdx) = detector.maxRange; % Ensure to use the same map as the visualizer release(detector); detector.mapName = obj.mapName; end % Attaches all properties associated with a RobotDetector object function attachRobotDetector(obj,detector) release(obj); if numel(obj.hasRobotDetector) ~= obj.numRobots obj.hasRobotDetector = repmat(obj.hasRobotDetector,[1,obj.numRobots]); end rIdx = detector.robotIdx; obj.hasRobotDetector(rIdx) = true; obj.robotDetectorOffset{rIdx} = detector.sensorOffset; obj.robotDetectorAngle(rIdx) = detector.sensorAngle; obj.robotDetectorFOV(rIdx) = detector.fieldOfView; obj.robotDetectorMaxRange(rIdx) = detector.maxRange; end end methods (Access = protected) % Define total number of inputs for system with optional inputs function n = getNumInputsImpl(obj) n = 2; if obj.hasWaypoints n = n + 1; end if any(obj.hasLidar) n = n + 1; end if any(obj.hasObjDetector) n = n + 1; end end end end