Robot Models¶
Every B-bot derives from BaseRobot and exposes a uniform interface:
| Member | Description |
|---|---|
robot.position |
numpy array [x, y]. |
robot.x, robot.y, robot.theta |
Scalar pose. |
robot.state() |
Dictionary with type, pose, velocity and (for differential) battery. |
robot.step(command, dt) |
Advance the kinematics by one timestep. |
robot.reset() |
Return to the initial pose. |
robot.draw(screen, camera) |
Render the body. |
The command you return from a controller depends on the model.
Supported models¶
type (aliases) |
Command keys | Notes |
|---|---|---|
differential (diff, unicycle, nonholonomic) |
{"v": …, "omega": …} |
Classic unicycle, with optional battery model. |
line_follower (line, linefollower, line_tracker, tcrt5000) |
{"v": …, "omega": …} |
Differential kinematics + 2/5/8 analog line sensors (TCRT5000). |
differential_firstorder (firstorder, diff_tau) |
{"v": …, "omega": …} |
Adds a first-order lag on the linear speed (tau_v). |
holonomic (omni, omnidirectional) |
{"vx": …, "vy": …} |
Velocity is set directly in world frame. |
ackermann (car, racecar) |
{"speed": …, "steer": …} |
Bicycle model with front steering. |
boat |
{"v": …, "omega": …} |
Like a differential drive, drawn as a hull. |
# differential
return {"v": 0.5, "omega": 0.1}
# holonomic
return {"vx": 0.5, "vy": 0.0}
# ackermann
return {"speed": 0.5, "steer": 0.2}
Selecting a model¶
or programmatically:
Line follower (TCRT5000)¶
LineFollowerRobot keeps the differential-drive kinematics, but the body is a
square (default 10cm x 10cm) carrying a horizontal bar of analog line sensors
in front. The line is not an obstacle: it is read from a line map — a
grayscale image whose dark pixels are the track (see
world.line_map_image).
world:
line_map_image: config/line_track.png # black line on white background
resolution: 0.005 # metres per pixel
robot:
type: line_follower
length: 0.10
width: 0.10
line_width: 0.02 # physical line width (used to space the sensors)
num_sensors: 5 # 2, 5 or 8 TCRT5000
sensor_offset: 0.05 # distance ahead of the body centre
# sensor_span: 0.08 # optional; derived from line_width when omitted
sensor_aperture: 0.004 # sampled area radius
sensor_noise: 0.0
line_threshold: 0.5 # above this a sensor counts as "on the line"
Read the sensors from a control callback; each reading is analog in [0, 1]
(1 = black line):
def controller(robot, world, goal, cfg, dt, lidar_readings=None):
robot.read_line_sensors(world.line_map) # updates readings/error
error = robot.line_error # -1 (right) .. +1 (left)
return {"v": 0.3, "omega": 3.0 * error}
robot.sensor_readings holds the per-sensor values, robot.line_position is the
lateral offset in metres and robot.line_lost is True when no sensor sees the
line. If sensor_span is omitted it is derived from line_width so that the
line always crosses neighbouring sensors. See
examples/line_follower.py for a full PID demo on a random track.
Lap timing and progress¶
The B-bot times itself while it follows the line. reset_laps() starts a new
measurement, and the running state is exposed through robot.state() and
robot.lap_statistics():
robot:
lap_detection_radius: 0.15 # "back to start" radius for a closed track (m)
min_lap_distance: 1.0 # min distance before a lap/finish is valid (m)
min_lap_time: 1.0 # min lap duration (s)
lost_timeout: 1.0 # line lost for this long => end of open track (s)
stats = robot.lap_statistics()
# {"count": 3, "last": 12.84, "mean": 13.02, "best": 12.41, "worst": 13.55}
On a closed track the B-bot records a lap every time it returns near the
start; on an open track it declares finished = True and sets finish_time
after the line has been lost for lost_timeout seconds. robot.state() adds:
line_error, line_lost, time, lap_count, lap_times, last_lap,
lap_avg, current_lap, finished and finish_time.
Comparing several line followers at once¶
You can run several line_follower B-bots on the same track from the same
start point, each with different gains, to compare techniques under identical
conditions. Because they start overlapped, turn off the robot–robot collision
evaluation so the B-bots do not stop on each other:
config["interaction"] = {"collision_policy": "none", "robot_collision": False}
config["show_times"] = True # show each B-bot's time in the HUD
Attach per-B-bot gains (the controller receives the B-bot object) and run one PID per robot:
for i, robot in enumerate(sim.robots):
robot.pid_params = GAIN_SETS[i] # e.g. {"kp": 4.0, "ki": 0.05, "kd": 0.9, ...}
After the run, each B-bot reports its own result:
The Toggle Times button (or config["show_times"]) hides/shows the per-B-bot
timing in the HUD. See the
Multi Line Follower example and the
collision flag for details. Pair this
with the data logger to save the comparison.
Battery model (differential)¶
Differential B-bots can model energy. It is disabled by default.
robot.set_battery_parameters(
c_idle=0.1, # %/s at rest
c_v=4.0, # %/s per (m/s)^2
c_w=1.0, # %/s per (rad/s)^2
critical_battery=50,
working_battery=90,
)
# or derive coefficients from hardware specs:
robot.set_battery_parameters_from_specs(
voltage=11.1, ah=5.0, power_electronics=1.0,
power_motors_cruise=15.0, v_cruise=0.5,
)
Charging spots come from world.recharge_places. The engine charges a B-bot
while robot.is_charging is True and it sits within goal_tolerance of a
spot, until battery_percent >= working_battery.
Building a fleet¶
create_fleet(config) reads config["robots"]; each spec overrides the base
robot block and may set a color.
If config["robots"] is absent, a single B-bot is built from config["robot"],
so mono-robot is simply a fleet with N = 1.
Custom models¶
Subclass BaseRobot and implement position, reset, state, step and
draw, then add a branch in hivebr.engine.robots.create_robot:
from hivebr.engine.robots.base import BaseRobot
class MyRobot(BaseRobot):
@property
def position(self):
return np.array([self.x, self.y])
def reset(self): ...
def state(self): ...
def step(self, command, dt): ...
def draw(self, screen, camera): ...
See the API reference for Robot classes.