Using kivent module complex physics, graphics, and game environment was created using only Python language the low-level parts of physics interactions are written as wrappers in Cython utilizing the cymunk - python port of 2d physics engine Chipmunk2D.
The compiling and installation of the kivy and kivent modules from source codes can be done using the playbook.yml file (using ansible-playbook is an installer program) or getting help from online forums and official pages.
The game-like environment consists of different categories of entities. Static walls around the arena. Randomly generated blue polyonal obstacles and rectagonal obstacles. Candy - the star which represents the target position of the robots. And finally robots which are composed of multiple entities.
The robot body consists of a colored rectangle representing the physical parts. Then there are 3 colored triangles representing ultrasound senesors sensing cone. The cone color is green, if obstacle, robot or wall is detected it turns red. The robot movement control is simulated as if it has been equipped with holonomic mecanum drive. The simulaiton is described in the chapter Robot control simulation.
Interactive program and GUI
The whole scene is interactive, the user can touch, move and rotate any physical object, but the walls. Objects can be also pinned to the place by a right mouse click. The whole application can be controled via mouse or any touch device with controlling multiple physical entities at the same time on the multi-touch devices (Tested on Dell XPS 15 touch-screen). As kivy and kivent are multiplatform, the
krv application can be ported and runned on different devices including operation systems such as linux, windows, ios and Android. The krv can therefore be a great simulation program running on your touch-screen smartphone.
The navigation loop of robots can be toggled by a button. There are also buttons for obstacle spawning and its removal. Info about object counts, current ultrasound states and their history is displayed under the buttons.
Robot control simulation
The robot control is simulated as a holonomic mecanum drive. Overview of different mobile robot undercarriages can be found in this article. In the control loop the robot gets informations about the target in a form of relative vector
rel_vec where (0, 0) is the center of robot and its tip is the star position. From the
rel_vec vector the normalized vector of wanted direction
vel_vec (relative to the robot) and wanted angular velocity
ang_vel is calculated in a function
goto_target (in robot.py in Robot class).
ang_vel is afterwards send to the robot control simulation function
control.go(). This function recalculates the wanted control into forces (impulses) which are going to be applied to individual wheels.
The equations used are from :
def calc_wheel_speed(self, vel_vec, ang_vel): """Simple mecanum wheel control algorithm""" vd = vel_vec.length th = vel_vec.angle dth = ang_vel th45 = th + radians(45) wheel_speeds = [ vd * sin(th45) + dth, vd * cos(th45) - dth, vd * cos(th45) + dth, vd * sin(th45) - dth, ] max_s = max(wheel_speeds) if max_s > 1: wheel_speeds = [s/max_s for s in wheel_speeds] return wheel_speeds
Each wheel impulse is than applied to the robot body, but not at the center but at the wheel position - generating rotational moment. Wheel positions are defined in the corners of the robot and their apply force direction is as it is in the mecanum drive (45 degrees to the rollers axis).
#wheel = [position_of_wheel, vector_when_moving_wheel_in_frontal_direction] self.wheels = [ [Vec2d(-w/2, +h/2), Vec2d(+1, +1)], # lf [Vec2d(+w/2, +h/2), Vec2d(-1, +1)], # rf [Vec2d(-w/2, -h/2), Vec2d(-1, +1)], # lb [Vec2d(+w/2, -h/2), Vec2d(+1, +1)], # rb ]
The impulses are applied afterwards where
imp_value is scalar which represents the power of motors:
for v, wheel_speed in zip(self.wheels, wheel_speeds): wheel_pos, wheel_force_dir = v imp_vec = wheel_force_dir.normalized() * imp_value * wheel_speed loc_wheel_pos = Vec2d(wheel_pos) loc_imp_vec = Vec2d(imp_vec) [v.rotate(ori) for v in [loc_wheel_pos, loc_imp_vec]] #print('wheel_pos') #print(loc_wheel_pos, wheel_pos) b.apply_impulse(loc_imp_vec, loc_wheel_pos)
The ultrasound sensors are simulated very roughly as they only gives robot the information whereas the obstacle is inside its sensing cone. The code for distance measurement was not yet designed with stability due to the physics module used.
Robot navigation system
The robot has two sources about the environment. He knows the relative vector of the target from the simulated camera. The second input is 3 ultrasounds which gives a boolean value for each simulated ultrasound sensor.
The navigation step
goto_target() is run every 50 ms. Inside the loop the input data are converted into the wanted vector of traverse and angular velocity (as stated in Robot control simulation section). The control loop does contain also a list of previous 20 ultrasounds states. Each time the loop is run the ultrasound states (
states list of 3 booleans) are pushed into the
states list and the oldest one is popped out. The same principle is applied to the robot velocities whose history is also stored in a list.
The current state is generated from the ultrasound states as follows
dets = self.ultrasound_detections() LR = dets==[True, False, True] # left and right L = dets==[True, False, False] # only left R = dets==[False, False, True] # etc LM = dets==[True, True, False] RM = dets==[False, True, True] M = dets==[False, True, False] # middle ALL = all(dets) NONE = not any(dets)
Another set of variables accompanies the current state.
is_ALL is true if at least half of the states are
ALL. This means at least half of the last 20 step all of the robot ultrasound sensors were blocked. There is also
is_M with the similar meaning.
If the robot is stucked, it is detected and a
STUCK state is added into the
states list. There is also
is_stuck if there is any
STUCK state in states. Also
is_near if the robot is very near the target = 1.2 * ultrasound range. and
is_close which is defined as 3 times the
The stuck detection is derived from velocity history. If the sum of all velocity vectors in velocity list is below the defined value (
stuck_sum) another conditions must be met. The robot must not be near the target (
is_near != True) and there must the
is_NONE must be also False. The robot should not be considered stucked if there are at least half of the latest states
NONE = no ultrasound sensor is sensing anything.
If the robot is not stucked nor the ultrasounds are sensing anything, it just follows the star. This means the robot is going into the relative direction to the target and also orienting itself in the target direction. This can be seen in a video easy from middle.
If the robot is stucked the the target vector is rotated with the
stuck_angle, which defaults to slightly more then 90 degrees (110 deg). This is due to the fact the robot may be stucked in a "hole" or dead-end. This is functional in a way as can be seen in a shallow dead-end avoidance video.
More information can be obtained by reading the code of
goto_target() function here.
Sorry for the code clumsiness I was aiming at the functionality and was learning the kivent module on the run, but though it is mainly without docstrings I tried to be as easy-to-understand as possible. If you want to follow in my path I recommend going through the kivent examples and introduction first.
Non-holonomic undercarriage control can be added with only creating new class for robot control which definitions of functions used by the
 Simplistic Control of Mecanum Drive, Ian McInerey, FRC Team 2022 link
 Slides and notes from DRE2 subject BUT.