Skip to content

Robots

B-bot kinematic models. See Robot Models for a conceptual overview and command dictionaries.

Factory

robots

HolonomicRobot

HolonomicRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/holonomic.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]
    self.initial_pose = np.array(c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.radius = float(c.get("radius", 0.3))
    self.max_speed = float(c.get("max_speed", 2.0))
    self.path_max_length = int(c.get("path_max_length", 4000))
    self.vx = 0.0
    self.vy = 0.0
    self.colors = config["colors"]

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

radius instance-attribute

radius = float(c.get('radius', 0.3))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

vx instance-attribute

vx = 0.0

vy instance-attribute

vy = 0.0

colors instance-attribute

colors = config['colors']

position property

position

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/holonomic.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/holonomic.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.vx = 0.0
    self.vy = 0.0
    self.path.clear()

state

state()
Source code in src/hivebr/engine/robots/holonomic.py
def state(self):
    return {"type": "holonomic", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "vx": self.vx, "vy": self.vy}

step

step(command, dt)
Source code in src/hivebr/engine/robots/holonomic.py
def step(self, command: dict, dt: float):
    vx = float(command.get("vx", 0.0))
    vy = float(command.get("vy", 0.0))
    norm = math.hypot(vx, vy)
    if norm > self.max_speed:
        scale = self.max_speed / norm
        vx *= scale
        vy *= scale
    self.vx, self.vy = vx, vy
    self.x += self.vx * dt
    self.y += self.vy * dt
    if norm > 1e-8:
        self.theta = wrap_to_pi(math.atan2(self.vy, self.vx))
    self.path.append(self.position.copy())
    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/holonomic.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/holonomic.py
def draw(self, screen, camera):
    center = camera.world_to_screen(self.x, self.y)
    radius_px = max(5, int(self.radius * camera.scale))
    pygame.draw.circle(screen, tuple(self.colors["robot"]), center, radius_px)
    front = self.position + np.array([math.cos(self.theta), math.sin(self.theta)]) * self.radius * 1.6
    pygame.draw.line(screen, tuple(self.colors["white"]), center, camera.world_to_screen(*front), 3)

    # =========================
    # Rodas (4 omni wheels)
    # =========================
    wheel_len = self.radius * 0.6
    wheel_width = self.radius * 0.3

    rot = np.array([
        [math.cos(self.theta), -math.sin(self.theta)],
        [math.sin(self.theta),  math.cos(self.theta)]
    ])

    # Posições locais: 4 rodas nas extremidades
    offset = self.radius * 0.6
    wheels = [
        (offset, offset),    # Frente direita
        (offset, -offset),   # Frente esquerda
        (-offset, offset),   # Trás direita
        (-offset, -offset)   # Trás esquerda
    ]

    for wx, wy in wheels:
        # centro da roda no mundo
        wheel_center = rot @ np.array([wx, wy]) + self.position

        # retângulo da roda (orientada localmente)
        wheel_corners = np.array([
            [ wheel_len/2,  wheel_width/2],
            [ wheel_len/2, -wheel_width/2],
            [-wheel_len/2, -wheel_width/2],
            [-wheel_len/2,  wheel_width/2]
        ])

        # Rotacionar a roda
        wheel_pts = [
            camera.world_to_screen(*(rot @ c + wheel_center))
            for c in wheel_corners
        ]

        pygame.draw.polygon(
            screen,
            (139, 69, 19),
            wheel_pts
        )

DifferentialRobot

DifferentialRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/differential.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]
    self.initial_pose = np.array(c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.radius = float(c.get("radius", 0.3))
    self.max_speed = float(c.get("max_speed", 2.0))
    self.max_omega = float(c.get("max_omega", 2.0))
    self.path_max_length = int(c.get("path_max_length", 4000))
    self.v = 0.0
    self.omega = 0.0
    self.colors = config["colors"]
    self.battery_percent = 100.0
    self.is_charging = False
    self.battery_charge_rate = float(c.get("battery_charge_rate", 20.0))
    self.enable_battery = bool(c.get("enable_battery", False))
    if self.enable_battery:
        self.c_idle = float(c.get("c_idle", 0.01))
        self.c_v = float(c.get("c_v", 0.05))
        self.c_w = float(c.get("c_w", 0.02))
        self.critical_battery  = float(c.get("critical_battery", 40.0))
        self.working_battery  = float(c.get("critical_battery", 90.0))
    self.going_to_recharge = False

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

radius instance-attribute

radius = float(c.get('radius', 0.3))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

max_omega instance-attribute

max_omega = float(c.get('max_omega', 2.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

v instance-attribute

v = 0.0

omega instance-attribute

omega = 0.0

colors instance-attribute

colors = config['colors']

battery_percent instance-attribute

battery_percent = 100.0

is_charging instance-attribute

is_charging = False

battery_charge_rate instance-attribute

battery_charge_rate = float(
    c.get("battery_charge_rate", 20.0)
)

c_idle instance-attribute

c_idle = float(c.get('c_idle', 0.01))

c_v instance-attribute

c_v = float(c.get('c_v', 0.05))

c_w instance-attribute

c_w = float(c.get('c_w', 0.02))

critical_battery instance-attribute

critical_battery = float(c.get('critical_battery', 40.0))

working_battery instance-attribute

working_battery = float(c.get('critical_battery', 90.0))

going_to_recharge instance-attribute

going_to_recharge = False

position property

position

enable_battery

enable_battery(enable=True)
Source code in src/hivebr/engine/robots/differential.py
def enable_battery(self, enable=True):
    self.enable_battery = enable

set_charging

set_charging()
Source code in src/hivebr/engine/robots/differential.py
def set_charging(self):
    self.is_charging = True
    self.going_to_recharge = False

set_battery_parameters

set_battery_parameters(
    c_idle,
    c_v,
    c_w,
    critical_battery=20,
    working_battery=90,
    battery_charge_rate=20,
)
Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters(self, c_idle, c_v, c_w, critical_battery=20, working_battery=90, battery_charge_rate=20):
    self.c_idle = float(c_idle)
    self.c_v = float(c_v)
    self.c_w = float(c_w)
    self.critical_battery = float(critical_battery)
    self.working_battery  = float(working_battery)
    self.enable_battery = True
    self.is_charging = False
    self.battery_charge_rate = battery_charge_rate

    print(f"Battery model enabled: c_idle={self.c_idle:.4f}, c_v={self.c_v:.4f}, c_w={self.c_w:.4f}, critical_battery={critical_battery:.2f} ")

set_battery_parameters_from_specs

set_battery_parameters_from_specs(
    voltage,
    ah,
    power_electronics,
    power_motors_cruise,
    v_cruise,
    omega_cruise=0.0,
)

Calculates battery consumption coefficients from physical specifications. E_total = V * Ah * 3600 (Joules) Consumption (%/s) = (Power (W) / E_total) * 100

Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters_from_specs(self, voltage, ah, power_electronics, power_motors_cruise, v_cruise, omega_cruise=0.0):
    """
    Calculates battery consumption coefficients from physical specifications.
    E_total = V * Ah * 3600 (Joules)
    Consumption (%/s) = (Power (W) / E_total) * 100
    """
    energy_joules = voltage * ah * 3600

    # Power idle (electronics)
    p_idle = power_electronics
    c_idle = (p_idle / energy_joules) * 100

    # Power motors (cruise)
    # Assuming power_motors_cruise = c_v_watts * v^2 + c_w_watts * omega^2
    # For simplicity, let's assume c_v_watts * v_cruise^2 takes a share, 
    # and we can estimate c_v and c_w. 
    # A simple approach: split motor power evenly between v and omega if both provided.

    motor_power = power_motors_cruise
    c_v = 0.0
    c_w = 0.0

    if v_cruise > 0 and omega_cruise > 0:
        c_v_watts = (motor_power / 2) / (v_cruise**2)
        c_w_watts = (motor_power / 2) / (omega_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
        c_w = (c_w_watts / energy_joules) * 100
    elif v_cruise > 0:
        c_v_watts = motor_power / (v_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
    elif omega_cruise > 0:
        c_w_watts = motor_power / (omega_cruise**2)
        c_w = (c_w_watts / energy_joules) * 100

    self.set_battery_parameters(c_idle, c_v, c_w)
    return self.battery_percent

update_battery

update_battery(dt, charge_rate=10.0)

Updates battery level. If charging is True, increases battery at charge_rate (%/s).

Source code in src/hivebr/engine/robots/differential.py
def update_battery(self, dt: float, charge_rate: float = 10.0):
    """
    Updates battery level. If charging is True, increases battery at charge_rate (%/s).
    """
    if not self.enable_battery:
        return

    if self.is_charging:
        self.battery_percent = min(100.0, self.battery_percent + charge_rate * dt)
        if self.battery_percent >= self.working_battery:
            self.is_charging = False
    else:
        consumption = (
            self.c_idle
            + self.c_v * self.v**2
            + self.c_w * self.omega**2
        )
        self.battery_percent = max(0.0, self.battery_percent - consumption * dt)

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/differential.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/differential.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.v = 0.0
    self.omega = 0.0
    self.path.clear()
    self.battery_percent = 100

state

state()
Source code in src/hivebr/engine/robots/differential.py
def state(self):
    return {"type": "differential", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "v": self.v, "omega": self.omega}

step

step(command, dt)
Source code in src/hivebr/engine/robots/differential.py
def step(self, command: dict, dt: float):
    if self.enable_battery and self.battery_percent <= 0:
        self.v = 0.0
        self.omega = 0.0
    else:
        self.v = clamp(float(command.get("v", 0.0)), -self.max_speed, self.max_speed)
        self.omega = clamp(float(command.get("omega", 0.0)), -self.max_omega, self.max_omega)

    self.update_battery(dt)

    self.x += self.v * math.cos(self.theta) * dt
    self.y += self.v * math.sin(self.theta) * dt
    self.theta = wrap_to_pi(self.theta + self.omega * dt)
    self.path.append(self.position.copy())
    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/differential.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/differential.py
def draw(self, screen, camera):
    center = camera.world_to_screen(self.x, self.y)
    radius_px = max(5, int(self.radius * camera.scale))
    pygame.draw.circle(screen, tuple(self.colors["robot"]), center, radius_px)
    front = self.position + np.array([math.cos(self.theta), math.sin(self.theta)]) * self.radius * 1.6
    pygame.draw.line(screen, tuple([192,192,186]), center, camera.world_to_screen(*front), 3)

    # =========================
    # Rodas
    # =========================
    wheel_len = self.radius * 0.8
    wheel_width = self.radius * 0.3

    rot = np.array([
        [math.cos(self.theta), -math.sin(self.theta)],
        [math.sin(self.theta),  math.cos(self.theta)]
    ])

    # Posições locais: centro das rodas em relação ao centro do robô
    # (x, y) = (0, +/- self.radius)
    # Vamos ajustar para ficarem um pouco para dentro
    offset = self.radius * 0.8

    wheels = [
        (0, offset),  # direita
        (0, -offset)  # esquerda
    ]

    for wx, wy in wheels:
        # centro da roda no mundo
        wheel_center = rot @ np.array([wx, wy]) + self.position

        # retângulo da roda
        # orientada na mesma direção que o robô para facilitar, ou perpendicular?
        # Para diferencial, rodas são paralelas ao eixo principal
        wheel_corners = np.array([
            [ wheel_len/2,  wheel_width/2],
            [ wheel_len/2, -wheel_width/2],
            [-wheel_len/2, -wheel_width/2],
            [-wheel_len/2,  wheel_width/2]
        ])

        # Rotacionar a roda (aqui alinhada com o corpo)
        wheel_pts = [
            camera.world_to_screen(*(rot @ c + wheel_center))
            for c in wheel_corners
        ]

        pygame.draw.polygon(
            screen,
            (139, 69, 19),
            wheel_pts
        )

    if self.enable_battery:
        # Draw battery indicator
        bar_width = radius_px * 1.2
        bar_height = radius_px * 0.4
        bar_x = center[0] - bar_width / 2
        bar_y = center[1] - bar_height / 2

        # Draw border
        pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 1)

        # Draw level
        fill_width = (self.battery_percent / 100.0) * (bar_width - 2)
        color = (0, 255, 0) if self.battery_percent > self.critical_battery else (255, 165, 0)
        if fill_width > 0:
            pygame.draw.rect(screen, color, (bar_x + 1, bar_y + 1, fill_width, bar_height - 2))

        # Draw text
        font = pygame.font.SysFont("Arial", int(radius_px * 0.5), bold=True)
        text = font.render(f"{int(self.battery_percent)}", True, (0, 0, 0))
        text_rect = text.get_rect(center=(center[0], center[1]))
        screen.blit(text, text_rect)

AckermannRobot

AckermannRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/ackermann.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]
    self.initial_pose = np.array(c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.wheel_base = float(c.get("wheel_base", 0.5))
    self.length = float(c.get("length", 0.8))
    self.width = float(c.get("width", 0.4))
    self.radius = float(c.get("radius", 0.4))
    self.max_speed = float(c.get("max_speed", 3.0))
    self.max_steer = float(c.get("max_steer", 0.6))
    self.path_max_length = int(c.get("path_max_length", 4000))
    self.speed = 0.0
    self.steer = 0.0
    self.colors = config["colors"]

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

wheel_base instance-attribute

wheel_base = float(c.get('wheel_base', 0.5))

length instance-attribute

length = float(c.get('length', 0.8))

width instance-attribute

width = float(c.get('width', 0.4))

radius instance-attribute

radius = float(c.get('radius', 0.4))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 3.0))

max_steer instance-attribute

max_steer = float(c.get('max_steer', 0.6))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

speed instance-attribute

speed = 0.0

steer instance-attribute

steer = 0.0

colors instance-attribute

colors = config['colors']

position property

position

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/ackermann.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/ackermann.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.speed = 0.0
    self.steer = 0.0
    self.path.clear()

state

state()
Source code in src/hivebr/engine/robots/ackermann.py
def state(self):
    return {"type": "ackermann", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "speed": self.speed, "steer": self.steer}

step

step(command, dt)
Source code in src/hivebr/engine/robots/ackermann.py
def step(self, command: dict, dt: float):
    self.speed = clamp(float(command.get("speed", 0.0)), -self.max_speed, self.max_speed)
    self.steer = clamp(float(command.get("steer", 0.0)), -self.max_steer, self.max_steer)
    self.x += self.speed * math.cos(self.theta) * dt
    self.y += self.speed * math.sin(self.theta) * dt
    self.theta += (self.speed / self.wheel_base) * math.tan(self.steer) * dt
    self.theta = wrap_to_pi(self.theta)
    self.path.append(self.position.copy())
    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/ackermann.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/ackermann.py
def draw(self, screen, camera):
    # =========================
    # Corpo do robô
    # =========================
    corners = np.array([
        [ self.length/2,  self.width/2],
        [ self.length/2, -self.width/2],
        [-self.length/2, -self.width/2],
        [-self.length/2,  self.width/2]
    ])

    rot = np.array([
        [math.cos(self.theta), -math.sin(self.theta)],
        [math.sin(self.theta),  math.cos(self.theta)]
    ])

    pts = [
        camera.world_to_screen(*(rot @ c + self.position))
        for c in corners
    ]

    pygame.draw.polygon(screen, tuple(self.colors["robot"]), pts)

    # =========================
    # Círculo transparente
    # =========================
    radius_px = max(5, int(self.radius * camera.scale))

    circle_surface = pygame.Surface(
        (radius_px * 2, radius_px * 2),
        pygame.SRCALPHA
    )

    color = (*self.colors["robot"], 120)

    pygame.draw.circle(
        circle_surface,
        color,
        (radius_px, radius_px),
        radius_px
    )

    screen_pos = camera.world_to_screen(self.x, self.y)

    screen.blit(
        circle_surface,
        (screen_pos[0] - radius_px,
        screen_pos[1] - radius_px)
    )

    # =========================
    # Rodas
    # =========================

    wheel_length = self.length * 0.18
    wheel_width  = self.width * 0.12

    half_track = self.width / 2
    half_wb = self.wheel_base / 2

    # posições locais das rodas
    wheels = [
        (+half_wb, +half_track, self.theta + self.steer), # dianteira esquerda
        (+half_wb, -half_track, self.theta + self.steer), # dianteira direita
        (-half_wb, +half_track, self.theta),              # traseira esquerda
        (-half_wb, -half_track, self.theta),              # traseira direita
    ]

    for wx, wy, wtheta in wheels:

        # centro da roda no mundo
        wheel_center = rot @ np.array([wx, wy]) + self.position

        # rotação da roda
        wrot = np.array([
            [math.cos(wtheta), -math.sin(wtheta)],
            [math.sin(wtheta),  math.cos(wtheta)]
        ])

        # retângulo da roda
        wheel_corners = np.array([
            [ wheel_length/2,  wheel_width/2],
            [ wheel_length/2, -wheel_width/2],
            [-wheel_length/2, -wheel_width/2],
            [-wheel_length/2,  wheel_width/2]
        ])

        wheel_pts = [
            camera.world_to_screen(*(wrot @ c + wheel_center))
            for c in wheel_corners
        ]

        pygame.draw.polygon(
            screen,
            (139, 69, 19),
            wheel_pts
        )

    # =========================
    # Direção frontal
    # =========================
    front = self.position + np.array([
        math.cos(self.theta),
        math.sin(self.theta)
    ]) * (self.length / 2)

    pygame.draw.line(
        screen,
        tuple([192,192,186]),
        camera.world_to_screen(self.x, self.y),
        camera.world_to_screen(*front),
        3
    )

BoatRobot

BoatRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/boat.py
def __init__(self, config: dict):
    super().__init__(config)

    c = config["robot"]

    self.initial_pose = np.array(
        c.get("initial_pose", [2.0, 2.0, 0.0]),
        dtype=float
    )

    self.x, self.y, self.theta = self.initial_pose.tolist()

    self.radius = float(c.get("radius", 0.35))

    self.max_speed = float(c.get("max_speed", 2.0))
    self.max_omega = float(c.get("max_omega", 1.0))

    self.path_max_length = int(
        c.get("path_max_length", 4000)
    )

    self.v = 0.0
    self.omega = 0.0

    self.colors = config["colors"]

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

radius instance-attribute

radius = float(c.get('radius', 0.35))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

max_omega instance-attribute

max_omega = float(c.get('max_omega', 1.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

v instance-attribute

v = 0.0

omega instance-attribute

omega = 0.0

colors instance-attribute

colors = config['colors']

position property

position

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/boat.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/boat.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.v = 0.0
    self.omega = 0.0
    self.path.clear()

state

state()
Source code in src/hivebr/engine/robots/boat.py
def state(self):
    return {
        "type": "boat",
        "x": self.x,
        "y": self.y,
        "theta": self.theta,
        "position": self.position.copy(),
        "v": self.v,
        "omega": self.omega
    }

step

step(command, dt)
Source code in src/hivebr/engine/robots/boat.py
def step(self, command: dict, dt: float):
    self.v = clamp(
        float(command.get("v", 0.0)),
        -self.max_speed,
        self.max_speed
    )

    self.omega = clamp(
        float(command.get("omega", 0.0)),
        -self.max_omega,
        self.max_omega
    )

    self.x += self.v * math.cos(self.theta) * dt
    self.y += self.v * math.sin(self.theta) * dt

    self.theta = wrap_to_pi(
        self.theta + self.omega * dt
    )

    self.path.append(self.position.copy())

    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/boat.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return

    pygame.draw.lines(
        screen,
        tuple(self.colors["path"]),
        False,
        [camera.world_to_screen(*p) for p in self.path],
        2
    )

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/boat.py
def draw(self, screen, camera):
    center = np.array([self.x, self.y])

    color = tuple(self.colors["robot"])
    outline = tuple(self.colors["black"])

    # =====================================
    # Geometria do casco
    # =====================================

    W = 0.9 * self.radius
    L = math.sqrt(4*self.radius**2 - W**2)

    hull_local = np.array([
        [-0.50 * L, -0.50 * W],
        [ 0.25 * L, -0.50 * W],
        [ 0.50 * L,  0.00 * W],
        [ 0.25 * L,  0.50 * W],
        [-0.50 * L,  0.50 * W],
    ])

    bow_local = np.array([
        [ 0.50 * L, 0.0],
        [ 0.20 * L, -0.25 * W],
        [ 0.20 * L,  0.25 * W],
    ])

    # =====================================
    # Rotação
    # =====================================

    c = math.cos(self.theta)
    s = math.sin(self.theta)

    R = np.array([
        [c, -s],
        [s,  c]
    ])

    hull_world = (R @ hull_local.T).T + center
    bow_world = (R @ bow_local.T).T + center

    hull_screen = [
        camera.world_to_screen(*p)
        for p in hull_world
    ]

    bow_screen = [
        camera.world_to_screen(*p)
        for p in bow_world
    ]

    # =====================================
    # Desenho
    # =====================================

    pygame.draw.polygon(
        screen,
        color,
        hull_screen
    )

    pygame.draw.polygon(
        screen,
        color,
        bow_screen
    )

    pygame.draw.polygon(
        screen,
        outline,
        hull_screen,
        2
    )

    # =========================
    # Círculo transparente
    # =========================
    radius_px = max(5, int(self.radius * camera.scale))

    circle_surface = pygame.Surface(
        (radius_px * 2, radius_px * 2),
        pygame.SRCALPHA
    )

    color = (*self.colors["robot"], 120)

    pygame.draw.circle(
        circle_surface,
        color,
        (radius_px, radius_px),
        radius_px
    )
    screen_pos = camera.world_to_screen(self.x, self.y)

    screen.blit(
        circle_surface,
        (screen_pos[0] - radius_px,
        screen_pos[1] - radius_px)
    )

    # linha central (opcional)

    front = center + np.array([
        math.cos(self.theta),
        math.sin(self.theta)
    ]) * L * 0.6

    pygame.draw.line(
        screen,
        tuple(self.colors["white"]),
        camera.world_to_screen(*center),
        camera.world_to_screen(*front),
        2
    )

DifferentialRobotFirstOrder

DifferentialRobotFirstOrder(config)

Bases: DifferentialRobot

Source code in src/hivebr/engine/robots/differential_firstorder.py
def __init__(self, config: dict):
    super().__init__(config)

    c = config["robot"]

    # constante de tempo do motor
    self.tau_v = float(c.get("tau_v", 0.5))

    # velocidade desejada
    self.v_ref = 0.0
    self.v = 0.0

    self.type = "differential_firstorder"

tau_v instance-attribute

tau_v = float(c.get('tau_v', 0.5))

v_ref instance-attribute

v_ref = 0.0

v instance-attribute

v = 0.0

type instance-attribute

type = 'differential_firstorder'

reset

reset()
Source code in src/hivebr/engine/robots/differential_firstorder.py
def reset(self):
    super().reset()
    self.v_ref = 0.0
    self.v = 0.0

state

state()
Source code in src/hivebr/engine/robots/differential_firstorder.py
def state(self):
    return {"type": "differential_firstorder", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "v": self.v, "omega": self.omega, "v_ref": self.v_ref, "tau_v": self.tau_v}

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/differential_firstorder.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

step

step(command, dt)
Source code in src/hivebr/engine/robots/differential_firstorder.py
def step(self, command: dict, dt: float):

    # comandos vindos do controlador
    self.v_ref = clamp(
        float(command.get("v", 0.0)),
        -self.max_speed,
        self.max_speed
    )

    self.omega = clamp(
        float(command.get("omega", 0.0)),
        -self.max_omega,
        self.max_omega
    )

    # ------------------------------------
    # Dinâmica de primeira ordem:
    #
    # tau_v * dv/dt + v = v_ref
    # ------------------------------------
    dv = (self.v_ref - self.v) / self.tau_v

    self.v += dv * dt

    # proteção numérica
    self.v = clamp(
        self.v,
        -self.max_speed,
        self.max_speed
    )

    # cinemática
    self.x += self.v * math.cos(self.theta) * dt
    self.y += self.v * math.sin(self.theta) * dt

    self.theta = wrap_to_pi(
        self.theta + self.omega * dt
    )

    self.path.append(self.position.copy())

    if len(self.path) > self.path_max_length:
        self.path.pop(0)

LineFollowerRobot

LineFollowerRobot(config)

Bases: DifferentialRobot

Robô diferencial com um arranjo de sensores analógicos de linha.

Mantém a mesma cinemática do :class:DifferentialRobot (comandos {"v": ..., "omega": ...}), mas adiciona um conjunto de sensores TCRT5000 montados numa barra horizontal na frente do corpo. O corpo é um quadrado de length x width (padrão 10cm x 10cm).

Os sensores são amostrados a partir de um :class:LineTrackMap através de :meth:read_line_sensors, que retorna valores analógicos em [0, 1] (1 = linha preta, 0 = fundo claro) e calcula o erro de posição da linha em relação ao centro do robô.

Configuração (bloco robot):

num_sensors Número de sensores (tipicamente 2, 5 ou 8). Padrão 5. sensor_offset Distância à frente do centro do robô, em metros. sensor_span Largura total ocupada pelo arranjo, em metros. sensor_aperture Raio da área amostrada por cada sensor, em metros. sensor_noise Desvio-padrão do ruído analógico adicionado à leitura. line_threshold Limiar acima do qual o sensor é considerado "sobre a linha".

Source code in src/hivebr/engine/robots/line_follower.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]

    self.body_length = float(c.get("length", 0.10))
    self.body_width = float(c.get("width", 0.10))
    self.radius = float(c.get("radius", 0.05))

    self.num_sensors = int(c.get("num_sensors", 5))
    if self.num_sensors < 1:
        self.num_sensors = 1

    self.sensor_offset = float(
        c.get("sensor_offset", self.body_length * 0.5)
    )
    line_width = float(c.get("line_width", 0.02))
    if "sensor_span" in c:
        self.sensor_span = float(c["sensor_span"])
    else:
        # Espaçamento entre sensores ligeiramente menor que a largura da
        # linha para que a pista sempre cruze sensores adjacentes
        # (funciona para 2, 5 ou 8 sensores).
        default_span = min(
            self.body_width * 0.9,
            max(line_width, line_width * 0.75 * (self.num_sensors - 1)),
        )
        self.sensor_span = float(default_span)
    self.sensor_aperture = float(c.get("sensor_aperture", 0.004))
    self.sensor_noise = float(c.get("sensor_noise", 0.0))
    self.line_threshold = float(c.get("line_threshold", 0.5))

    self.sensor_local = self._build_sensor_positions()
    if self.num_sensors > 1:
        self.sensor_weights = np.linspace(-1.0, 1.0, self.num_sensors)
    else:
        self.sensor_weights = np.zeros(1)

    self.sensor_readings = np.zeros(self.num_sensors)
    self.line_error = 0.0
    self.line_position = 0.0
    self.line_lost = True
    self._last_sign = 0.0

    # ---- cronometragem de voltas / percurso ----
    self.lap_detection_radius = float(c.get("lap_detection_radius", 0.15))
    self.min_lap_distance = float(c.get("min_lap_distance", 1.0))
    self.min_lap_time = float(c.get("min_lap_time", 1.0))
    self.lost_timeout = float(c.get("lost_timeout", 1.0))
    self.reset_laps()

body_length instance-attribute

body_length = float(c.get('length', 0.1))

body_width instance-attribute

body_width = float(c.get('width', 0.1))

radius instance-attribute

radius = float(c.get('radius', 0.05))

num_sensors instance-attribute

num_sensors = int(c.get('num_sensors', 5))

sensor_offset instance-attribute

sensor_offset = float(
    c.get("sensor_offset", self.body_length * 0.5)
)

sensor_span instance-attribute

sensor_span = float(c['sensor_span'])

sensor_aperture instance-attribute

sensor_aperture = float(c.get('sensor_aperture', 0.004))

sensor_noise instance-attribute

sensor_noise = float(c.get('sensor_noise', 0.0))

line_threshold instance-attribute

line_threshold = float(c.get('line_threshold', 0.5))

sensor_local instance-attribute

sensor_local = self._build_sensor_positions()

sensor_weights instance-attribute

sensor_weights = np.linspace(-1.0, 1.0, self.num_sensors)

sensor_readings instance-attribute

sensor_readings = np.zeros(self.num_sensors)

line_error instance-attribute

line_error = 0.0

line_position instance-attribute

line_position = 0.0

line_lost instance-attribute

line_lost = True

lap_detection_radius instance-attribute

lap_detection_radius = float(
    c.get("lap_detection_radius", 0.15)
)

min_lap_distance instance-attribute

min_lap_distance = float(c.get('min_lap_distance', 1.0))

min_lap_time instance-attribute

min_lap_time = float(c.get('min_lap_time', 1.0))

lost_timeout instance-attribute

lost_timeout = float(c.get('lost_timeout', 1.0))

read_sensors class-attribute instance-attribute

read_sensors = read_line_sensors

reset_laps

reset_laps()

Zera o cronômetro e as estatísticas de voltas/percurso.

Source code in src/hivebr/engine/robots/line_follower.py
def reset_laps(self):
    """Zera o cronômetro e as estatísticas de voltas/percurso."""
    self.time = 0.0
    self.distance_traveled = 0.0
    self.lap_times: list[float] = []
    self.lap_start_time = 0.0
    self.lap_origin = self.position.copy()
    self._left_start = False
    self.finished = False
    self.finish_time = None
    self._lost_time = 0.0

reset

reset()
Source code in src/hivebr/engine/robots/line_follower.py
def reset(self):
    super().reset()
    self.sensor_readings = np.zeros(self.num_sensors)
    self.line_error = 0.0
    self.line_position = 0.0
    self.line_lost = True
    self._last_sign = 0.0
    self.reset_laps()

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/line_follower.py
def set_initial_pose(self, x, y, theta=0.0):
    super().set_initial_pose(x, y, theta)
    self.reset_laps()

state

state()
Source code in src/hivebr/engine/robots/line_follower.py
def state(self):
    state = super().state()
    current_lap = (
        self.finish_time
        if self.finished
        else (self.time - self.lap_start_time)
    )
    state.update(
        {
            "line_follower": True,
            "num_sensors": self.num_sensors,
            "sensors": self.sensor_readings.tolist(),
            "line_error": self.line_error,
            "line_position": self.line_position,
            "line_lost": self.line_lost,
            "time": self.time,
            "finished": self.finished,
            "finish_time": self.finish_time,
            "lap_count": len(self.lap_times),
            "lap_times": list(self.lap_times),
            "last_lap": self.lap_times[-1] if self.lap_times else None,
            "lap_avg": float(np.mean(self.lap_times)) if self.lap_times else None,
            "current_lap": current_lap,
        }
    )
    return state

lap_statistics

lap_statistics()

Resumo das voltas (contagem, última, média, melhor, pior).

Source code in src/hivebr/engine/robots/line_follower.py
def lap_statistics(self) -> dict:
    """Resumo das voltas (contagem, última, média, melhor, pior)."""
    if not self.lap_times:
        return {"count": 0, "last": None, "mean": None, "best": None, "worst": None}
    times = np.asarray(self.lap_times, dtype=float)
    return {
        "count": int(times.size),
        "last": float(times[-1]),
        "mean": float(times.mean()),
        "best": float(times.min()),
        "worst": float(times.max()),
    }

sensor_world_position

sensor_world_position(index)

Posição no mundo do sensor index.

Source code in src/hivebr/engine/robots/line_follower.py
def sensor_world_position(self, index: int) -> np.ndarray:
    """Posição no mundo do sensor ``index``."""
    lx, ly = self.sensor_local[index]
    cos_t = math.cos(self.theta)
    sin_t = math.sin(self.theta)
    return np.array(
        [
            self.x + lx * cos_t - ly * sin_t,
            self.y + lx * sin_t + ly * cos_t,
        ]
    )

read_line_sensors

read_line_sensors(line_map)

Amostra os sensores e atualiza line_error/line_lost.

Parameters

line_map: Um :class:LineTrackMap. Se None, devolve leituras nulas.

Returns

numpy.ndarray Leituras analógicas em [0, 1] (1 = linha preta).

Source code in src/hivebr/engine/robots/line_follower.py
def read_line_sensors(self, line_map) -> np.ndarray:
    """Amostra os sensores e atualiza ``line_error``/``line_lost``.

    Parameters
    ----------
    line_map:
        Um :class:`LineTrackMap`. Se ``None``, devolve leituras nulas.

    Returns
    -------
    numpy.ndarray
        Leituras analógicas em ``[0, 1]`` (``1`` = linha preta).
    """
    if line_map is None:
        self.sensor_readings = np.zeros(self.num_sensors)
        self.line_lost = True
        return self.sensor_readings

    cos_t = math.cos(self.theta)
    sin_t = math.sin(self.theta)
    values = np.empty(self.num_sensors)

    for i, (lx, ly) in enumerate(self.sensor_local):
        wx = self.x + lx * cos_t - ly * sin_t
        wy = self.y + lx * sin_t + ly * cos_t
        value = line_map.sample(wx, wy, self.sensor_aperture)
        if self.sensor_noise > 0.0:
            value += float(np.random.normal(0.0, self.sensor_noise))
        values[i] = clamp(value, 0.0, 1.0)

    self.sensor_readings = values

    total = float(values.sum())
    if total > 1e-6:
        self.line_error = float(np.dot(self.sensor_weights, values) / total)
        self.line_lost = False
    else:
        self.line_lost = True
        if abs(self.line_error) < 1e-6:
            self.line_error = self._last_sign

    if abs(self.line_error) > 1e-6:
        self._last_sign = 1.0 if self.line_error > 0.0 else -1.0

    self.line_position = self.line_error * (self.sensor_span / 2.0)
    return values

step

step(command, dt)
Source code in src/hivebr/engine/robots/line_follower.py
def step(self, command: dict, dt: float):
    if self.finished:
        command = {"v": 0.0, "omega": 0.0}
    super().step(command, dt)
    self._update_progress(dt)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/line_follower.py
def draw(self, screen, camera):
    cos_t = math.cos(self.theta)
    sin_t = math.sin(self.theta)
    rot = np.array([[cos_t, -sin_t], [sin_t, cos_t]])

    half_l = self.body_length / 2.0
    half_w = self.body_width / 2.0
    corners = [
        (-half_l, -half_w),
        (half_l, -half_w),
        (half_l, half_w),
        (-half_l, half_w),
    ]
    body_pts = [
        camera.world_to_screen(*(rot @ np.array(c) + self.position))
        for c in corners
    ]
    pygame.draw.polygon(screen, tuple(self.colors["robot"]), body_pts)
    pygame.draw.polygon(screen, (20, 20, 20), body_pts, 2)

    front = self.position + np.array([cos_t, sin_t]) * half_l
    pygame.draw.line(
        screen,
        tuple(self.colors.get("silver", [192, 192, 186])),
        camera.world_to_screen(self.x, self.y),
        camera.world_to_screen(*front),
        2,
    )

    sensor_radius = max(2, int(0.006 * camera.scale))
    for (lx, ly), value in zip(self.sensor_local, self.sensor_readings):
        pos = rot @ np.array([lx, ly]) + self.position
        screen_pos = camera.world_to_screen(*pos)
        pygame.draw.circle(
            screen, self._sensor_color(float(value)), screen_pos, sensor_radius
        )
        pygame.draw.circle(screen, (0, 0, 0), screen_pos, sensor_radius, 1)

create_robot

create_robot(config)
Source code in src/hivebr/engine/robots/__init__.py
def create_robot(config: dict):
    robot_type = config["robot"].get("type", "ackermann").lower()
    if robot_type in ["holonomic", "omni", "omnidirectional"]:
        return HolonomicRobot(config)    
    elif robot_type in ["ackermann", "car", "racecar"]:
        return AckermannRobot(config)
    elif robot_type == "boat":
        return BoatRobot(config)
    elif robot_type in ["line_follower", "linefollower", "line", "line_tracker", "tcrt5000",
                        "differential_line_follower"]:
        return LineFollowerRobot(config)
    elif robot_type in ["differential_firstorder", "firstorder", "diff_tau"]:            
        return DifferentialRobotFirstOrder(config)
    elif robot_type in ["differential", "diff", "unicycle", "nonholonomic"]:
        return DifferentialRobot(config)
    else:
        print(f"Tipo de robô desconhecido: {robot_type}, using differential as default.")
        return DifferentialRobot(config)


    raise ValueError(f"Tipo de robô desconhecido: {robot_type}")

create_fleet

create_fleet(config)

Build the list of B-bots from config['robots'].

Falls back to a single robot built from config['robot'] when no fleet is declared, so mono-robot scenarios are just N=1. Each entry may override any field of config['robot'] (type, initial_pose, radius, ...) and may carry an optional color used for rendering and path tracing.

Source code in src/hivebr/engine/robots/__init__.py
def create_fleet(config: dict) -> list:
    """Build the list of B-bots from ``config['robots']``.

    Falls back to a single robot built from ``config['robot']`` when no fleet
    is declared, so mono-robot scenarios are just N=1. Each entry may override
    any field of ``config['robot']`` (type, initial_pose, radius, ...) and may
    carry an optional ``color`` used for rendering and path tracing.
    """
    specs = config.get("robots")
    if not specs:
        return [create_robot(config)]

    palette = list(config.get("colors", {}).get("fleet", []))
    robots = []
    for index, spec in enumerate(specs):
        merged = deepcopy(config)
        robot_cfg = dict(config.get("robot", {}))
        robot_cfg.update({k: v for k, v in spec.items() if k != "id" and k != "color"})
        merged["robot"] = robot_cfg

        color = spec.get("color")
        if color is None and palette:
            color = palette[index % len(palette)]
        if color is not None:
            merged["colors"] = dict(config.get("colors", {}))
            merged["colors"]["robot"] = list(color)
            merged["colors"]["path"] = list(color)

        robots.append(create_robot(merged))
    return robots

Base class

BaseRobot

BaseRobot(config)

Bases: ABC

Source code in src/hivebr/engine/robots/base.py
7
8
9
def __init__(self, config: dict):
    self.config = config
    self.path = []

config instance-attribute

config = config

path instance-attribute

path = []

position abstractmethod property

position

reset abstractmethod

reset()
Source code in src/hivebr/engine/robots/base.py
@abstractmethod
def reset(self):
    pass

state abstractmethod

state()
Source code in src/hivebr/engine/robots/base.py
@abstractmethod
def state(self) -> dict:
    pass

step abstractmethod

step(command, dt)
Source code in src/hivebr/engine/robots/base.py
@abstractmethod
def step(self, command: dict, dt: float):
    pass

draw abstractmethod

draw(screen, camera)
Source code in src/hivebr/engine/robots/base.py
@abstractmethod
def draw(self, screen, camera):
    pass

clear_path

clear_path()
Source code in src/hivebr/engine/robots/base.py
def clear_path(self):
    self.path.clear()

Differential drive

DifferentialRobot

DifferentialRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/differential.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]
    self.initial_pose = np.array(c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.radius = float(c.get("radius", 0.3))
    self.max_speed = float(c.get("max_speed", 2.0))
    self.max_omega = float(c.get("max_omega", 2.0))
    self.path_max_length = int(c.get("path_max_length", 4000))
    self.v = 0.0
    self.omega = 0.0
    self.colors = config["colors"]
    self.battery_percent = 100.0
    self.is_charging = False
    self.battery_charge_rate = float(c.get("battery_charge_rate", 20.0))
    self.enable_battery = bool(c.get("enable_battery", False))
    if self.enable_battery:
        self.c_idle = float(c.get("c_idle", 0.01))
        self.c_v = float(c.get("c_v", 0.05))
        self.c_w = float(c.get("c_w", 0.02))
        self.critical_battery  = float(c.get("critical_battery", 40.0))
        self.working_battery  = float(c.get("critical_battery", 90.0))
    self.going_to_recharge = False

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

radius instance-attribute

radius = float(c.get('radius', 0.3))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

max_omega instance-attribute

max_omega = float(c.get('max_omega', 2.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

v instance-attribute

v = 0.0

omega instance-attribute

omega = 0.0

colors instance-attribute

colors = config['colors']

battery_percent instance-attribute

battery_percent = 100.0

is_charging instance-attribute

is_charging = False

battery_charge_rate instance-attribute

battery_charge_rate = float(
    c.get("battery_charge_rate", 20.0)
)

c_idle instance-attribute

c_idle = float(c.get('c_idle', 0.01))

c_v instance-attribute

c_v = float(c.get('c_v', 0.05))

c_w instance-attribute

c_w = float(c.get('c_w', 0.02))

critical_battery instance-attribute

critical_battery = float(c.get('critical_battery', 40.0))

working_battery instance-attribute

working_battery = float(c.get('critical_battery', 90.0))

going_to_recharge instance-attribute

going_to_recharge = False

position property

position

config instance-attribute

config = config

path instance-attribute

path = []

set_charging

set_charging()
Source code in src/hivebr/engine/robots/differential.py
def set_charging(self):
    self.is_charging = True
    self.going_to_recharge = False

set_battery_parameters

set_battery_parameters(
    c_idle,
    c_v,
    c_w,
    critical_battery=20,
    working_battery=90,
    battery_charge_rate=20,
)
Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters(self, c_idle, c_v, c_w, critical_battery=20, working_battery=90, battery_charge_rate=20):
    self.c_idle = float(c_idle)
    self.c_v = float(c_v)
    self.c_w = float(c_w)
    self.critical_battery = float(critical_battery)
    self.working_battery  = float(working_battery)
    self.enable_battery = True
    self.is_charging = False
    self.battery_charge_rate = battery_charge_rate

    print(f"Battery model enabled: c_idle={self.c_idle:.4f}, c_v={self.c_v:.4f}, c_w={self.c_w:.4f}, critical_battery={critical_battery:.2f} ")

set_battery_parameters_from_specs

set_battery_parameters_from_specs(
    voltage,
    ah,
    power_electronics,
    power_motors_cruise,
    v_cruise,
    omega_cruise=0.0,
)

Calculates battery consumption coefficients from physical specifications. E_total = V * Ah * 3600 (Joules) Consumption (%/s) = (Power (W) / E_total) * 100

Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters_from_specs(self, voltage, ah, power_electronics, power_motors_cruise, v_cruise, omega_cruise=0.0):
    """
    Calculates battery consumption coefficients from physical specifications.
    E_total = V * Ah * 3600 (Joules)
    Consumption (%/s) = (Power (W) / E_total) * 100
    """
    energy_joules = voltage * ah * 3600

    # Power idle (electronics)
    p_idle = power_electronics
    c_idle = (p_idle / energy_joules) * 100

    # Power motors (cruise)
    # Assuming power_motors_cruise = c_v_watts * v^2 + c_w_watts * omega^2
    # For simplicity, let's assume c_v_watts * v_cruise^2 takes a share, 
    # and we can estimate c_v and c_w. 
    # A simple approach: split motor power evenly between v and omega if both provided.

    motor_power = power_motors_cruise
    c_v = 0.0
    c_w = 0.0

    if v_cruise > 0 and omega_cruise > 0:
        c_v_watts = (motor_power / 2) / (v_cruise**2)
        c_w_watts = (motor_power / 2) / (omega_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
        c_w = (c_w_watts / energy_joules) * 100
    elif v_cruise > 0:
        c_v_watts = motor_power / (v_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
    elif omega_cruise > 0:
        c_w_watts = motor_power / (omega_cruise**2)
        c_w = (c_w_watts / energy_joules) * 100

    self.set_battery_parameters(c_idle, c_v, c_w)
    return self.battery_percent

update_battery

update_battery(dt, charge_rate=10.0)

Updates battery level. If charging is True, increases battery at charge_rate (%/s).

Source code in src/hivebr/engine/robots/differential.py
def update_battery(self, dt: float, charge_rate: float = 10.0):
    """
    Updates battery level. If charging is True, increases battery at charge_rate (%/s).
    """
    if not self.enable_battery:
        return

    if self.is_charging:
        self.battery_percent = min(100.0, self.battery_percent + charge_rate * dt)
        if self.battery_percent >= self.working_battery:
            self.is_charging = False
    else:
        consumption = (
            self.c_idle
            + self.c_v * self.v**2
            + self.c_w * self.omega**2
        )
        self.battery_percent = max(0.0, self.battery_percent - consumption * dt)

enable_battery

enable_battery(enable=True)
Source code in src/hivebr/engine/robots/differential.py
def enable_battery(self, enable=True):
    self.enable_battery = enable

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/differential.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/differential.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.v = 0.0
    self.omega = 0.0
    self.path.clear()
    self.battery_percent = 100

state

state()
Source code in src/hivebr/engine/robots/differential.py
def state(self):
    return {"type": "differential", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "v": self.v, "omega": self.omega}

step

step(command, dt)
Source code in src/hivebr/engine/robots/differential.py
def step(self, command: dict, dt: float):
    if self.enable_battery and self.battery_percent <= 0:
        self.v = 0.0
        self.omega = 0.0
    else:
        self.v = clamp(float(command.get("v", 0.0)), -self.max_speed, self.max_speed)
        self.omega = clamp(float(command.get("omega", 0.0)), -self.max_omega, self.max_omega)

    self.update_battery(dt)

    self.x += self.v * math.cos(self.theta) * dt
    self.y += self.v * math.sin(self.theta) * dt
    self.theta = wrap_to_pi(self.theta + self.omega * dt)
    self.path.append(self.position.copy())
    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/differential.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/differential.py
def draw(self, screen, camera):
    center = camera.world_to_screen(self.x, self.y)
    radius_px = max(5, int(self.radius * camera.scale))
    pygame.draw.circle(screen, tuple(self.colors["robot"]), center, radius_px)
    front = self.position + np.array([math.cos(self.theta), math.sin(self.theta)]) * self.radius * 1.6
    pygame.draw.line(screen, tuple([192,192,186]), center, camera.world_to_screen(*front), 3)

    # =========================
    # Rodas
    # =========================
    wheel_len = self.radius * 0.8
    wheel_width = self.radius * 0.3

    rot = np.array([
        [math.cos(self.theta), -math.sin(self.theta)],
        [math.sin(self.theta),  math.cos(self.theta)]
    ])

    # Posições locais: centro das rodas em relação ao centro do robô
    # (x, y) = (0, +/- self.radius)
    # Vamos ajustar para ficarem um pouco para dentro
    offset = self.radius * 0.8

    wheels = [
        (0, offset),  # direita
        (0, -offset)  # esquerda
    ]

    for wx, wy in wheels:
        # centro da roda no mundo
        wheel_center = rot @ np.array([wx, wy]) + self.position

        # retângulo da roda
        # orientada na mesma direção que o robô para facilitar, ou perpendicular?
        # Para diferencial, rodas são paralelas ao eixo principal
        wheel_corners = np.array([
            [ wheel_len/2,  wheel_width/2],
            [ wheel_len/2, -wheel_width/2],
            [-wheel_len/2, -wheel_width/2],
            [-wheel_len/2,  wheel_width/2]
        ])

        # Rotacionar a roda (aqui alinhada com o corpo)
        wheel_pts = [
            camera.world_to_screen(*(rot @ c + wheel_center))
            for c in wheel_corners
        ]

        pygame.draw.polygon(
            screen,
            (139, 69, 19),
            wheel_pts
        )

    if self.enable_battery:
        # Draw battery indicator
        bar_width = radius_px * 1.2
        bar_height = radius_px * 0.4
        bar_x = center[0] - bar_width / 2
        bar_y = center[1] - bar_height / 2

        # Draw border
        pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 1)

        # Draw level
        fill_width = (self.battery_percent / 100.0) * (bar_width - 2)
        color = (0, 255, 0) if self.battery_percent > self.critical_battery else (255, 165, 0)
        if fill_width > 0:
            pygame.draw.rect(screen, color, (bar_x + 1, bar_y + 1, fill_width, bar_height - 2))

        # Draw text
        font = pygame.font.SysFont("Arial", int(radius_px * 0.5), bold=True)
        text = font.render(f"{int(self.battery_percent)}", True, (0, 0, 0))
        text_rect = text.get_rect(center=(center[0], center[1]))
        screen.blit(text, text_rect)

clear_path

clear_path()
Source code in src/hivebr/engine/robots/base.py
def clear_path(self):
    self.path.clear()

Differential drive (first order)

DifferentialRobotFirstOrder

DifferentialRobotFirstOrder(config)

Bases: DifferentialRobot

Source code in src/hivebr/engine/robots/differential_firstorder.py
def __init__(self, config: dict):
    super().__init__(config)

    c = config["robot"]

    # constante de tempo do motor
    self.tau_v = float(c.get("tau_v", 0.5))

    # velocidade desejada
    self.v_ref = 0.0
    self.v = 0.0

    self.type = "differential_firstorder"

tau_v instance-attribute

tau_v = float(c.get('tau_v', 0.5))

v_ref instance-attribute

v_ref = 0.0

v instance-attribute

v = 0.0

type instance-attribute

type = 'differential_firstorder'

config instance-attribute

config = config

path instance-attribute

path = []

position property

position

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

radius instance-attribute

radius = float(c.get('radius', 0.3))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

max_omega instance-attribute

max_omega = float(c.get('max_omega', 2.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

omega instance-attribute

omega = 0.0

colors instance-attribute

colors = config['colors']

battery_percent instance-attribute

battery_percent = 100.0

is_charging instance-attribute

is_charging = False

battery_charge_rate instance-attribute

battery_charge_rate = float(
    c.get("battery_charge_rate", 20.0)
)

c_idle instance-attribute

c_idle = float(c.get('c_idle', 0.01))

c_v instance-attribute

c_v = float(c.get('c_v', 0.05))

c_w instance-attribute

c_w = float(c.get('c_w', 0.02))

critical_battery instance-attribute

critical_battery = float(c.get('critical_battery', 40.0))

working_battery instance-attribute

working_battery = float(c.get('critical_battery', 90.0))

going_to_recharge instance-attribute

going_to_recharge = False

reset

reset()
Source code in src/hivebr/engine/robots/differential_firstorder.py
def reset(self):
    super().reset()
    self.v_ref = 0.0
    self.v = 0.0

state

state()
Source code in src/hivebr/engine/robots/differential_firstorder.py
def state(self):
    return {"type": "differential_firstorder", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "v": self.v, "omega": self.omega, "v_ref": self.v_ref, "tau_v": self.tau_v}

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/differential_firstorder.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

step

step(command, dt)
Source code in src/hivebr/engine/robots/differential_firstorder.py
def step(self, command: dict, dt: float):

    # comandos vindos do controlador
    self.v_ref = clamp(
        float(command.get("v", 0.0)),
        -self.max_speed,
        self.max_speed
    )

    self.omega = clamp(
        float(command.get("omega", 0.0)),
        -self.max_omega,
        self.max_omega
    )

    # ------------------------------------
    # Dinâmica de primeira ordem:
    #
    # tau_v * dv/dt + v = v_ref
    # ------------------------------------
    dv = (self.v_ref - self.v) / self.tau_v

    self.v += dv * dt

    # proteção numérica
    self.v = clamp(
        self.v,
        -self.max_speed,
        self.max_speed
    )

    # cinemática
    self.x += self.v * math.cos(self.theta) * dt
    self.y += self.v * math.sin(self.theta) * dt

    self.theta = wrap_to_pi(
        self.theta + self.omega * dt
    )

    self.path.append(self.position.copy())

    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/differential.py
def draw(self, screen, camera):
    center = camera.world_to_screen(self.x, self.y)
    radius_px = max(5, int(self.radius * camera.scale))
    pygame.draw.circle(screen, tuple(self.colors["robot"]), center, radius_px)
    front = self.position + np.array([math.cos(self.theta), math.sin(self.theta)]) * self.radius * 1.6
    pygame.draw.line(screen, tuple([192,192,186]), center, camera.world_to_screen(*front), 3)

    # =========================
    # Rodas
    # =========================
    wheel_len = self.radius * 0.8
    wheel_width = self.radius * 0.3

    rot = np.array([
        [math.cos(self.theta), -math.sin(self.theta)],
        [math.sin(self.theta),  math.cos(self.theta)]
    ])

    # Posições locais: centro das rodas em relação ao centro do robô
    # (x, y) = (0, +/- self.radius)
    # Vamos ajustar para ficarem um pouco para dentro
    offset = self.radius * 0.8

    wheels = [
        (0, offset),  # direita
        (0, -offset)  # esquerda
    ]

    for wx, wy in wheels:
        # centro da roda no mundo
        wheel_center = rot @ np.array([wx, wy]) + self.position

        # retângulo da roda
        # orientada na mesma direção que o robô para facilitar, ou perpendicular?
        # Para diferencial, rodas são paralelas ao eixo principal
        wheel_corners = np.array([
            [ wheel_len/2,  wheel_width/2],
            [ wheel_len/2, -wheel_width/2],
            [-wheel_len/2, -wheel_width/2],
            [-wheel_len/2,  wheel_width/2]
        ])

        # Rotacionar a roda (aqui alinhada com o corpo)
        wheel_pts = [
            camera.world_to_screen(*(rot @ c + wheel_center))
            for c in wheel_corners
        ]

        pygame.draw.polygon(
            screen,
            (139, 69, 19),
            wheel_pts
        )

    if self.enable_battery:
        # Draw battery indicator
        bar_width = radius_px * 1.2
        bar_height = radius_px * 0.4
        bar_x = center[0] - bar_width / 2
        bar_y = center[1] - bar_height / 2

        # Draw border
        pygame.draw.rect(screen, (255, 255, 255), (bar_x, bar_y, bar_width, bar_height), 1)

        # Draw level
        fill_width = (self.battery_percent / 100.0) * (bar_width - 2)
        color = (0, 255, 0) if self.battery_percent > self.critical_battery else (255, 165, 0)
        if fill_width > 0:
            pygame.draw.rect(screen, color, (bar_x + 1, bar_y + 1, fill_width, bar_height - 2))

        # Draw text
        font = pygame.font.SysFont("Arial", int(radius_px * 0.5), bold=True)
        text = font.render(f"{int(self.battery_percent)}", True, (0, 0, 0))
        text_rect = text.get_rect(center=(center[0], center[1]))
        screen.blit(text, text_rect)

clear_path

clear_path()
Source code in src/hivebr/engine/robots/base.py
def clear_path(self):
    self.path.clear()

enable_battery

enable_battery(enable=True)
Source code in src/hivebr/engine/robots/differential.py
def enable_battery(self, enable=True):
    self.enable_battery = enable

set_charging

set_charging()
Source code in src/hivebr/engine/robots/differential.py
def set_charging(self):
    self.is_charging = True
    self.going_to_recharge = False

set_battery_parameters

set_battery_parameters(
    c_idle,
    c_v,
    c_w,
    critical_battery=20,
    working_battery=90,
    battery_charge_rate=20,
)
Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters(self, c_idle, c_v, c_w, critical_battery=20, working_battery=90, battery_charge_rate=20):
    self.c_idle = float(c_idle)
    self.c_v = float(c_v)
    self.c_w = float(c_w)
    self.critical_battery = float(critical_battery)
    self.working_battery  = float(working_battery)
    self.enable_battery = True
    self.is_charging = False
    self.battery_charge_rate = battery_charge_rate

    print(f"Battery model enabled: c_idle={self.c_idle:.4f}, c_v={self.c_v:.4f}, c_w={self.c_w:.4f}, critical_battery={critical_battery:.2f} ")

set_battery_parameters_from_specs

set_battery_parameters_from_specs(
    voltage,
    ah,
    power_electronics,
    power_motors_cruise,
    v_cruise,
    omega_cruise=0.0,
)

Calculates battery consumption coefficients from physical specifications. E_total = V * Ah * 3600 (Joules) Consumption (%/s) = (Power (W) / E_total) * 100

Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters_from_specs(self, voltage, ah, power_electronics, power_motors_cruise, v_cruise, omega_cruise=0.0):
    """
    Calculates battery consumption coefficients from physical specifications.
    E_total = V * Ah * 3600 (Joules)
    Consumption (%/s) = (Power (W) / E_total) * 100
    """
    energy_joules = voltage * ah * 3600

    # Power idle (electronics)
    p_idle = power_electronics
    c_idle = (p_idle / energy_joules) * 100

    # Power motors (cruise)
    # Assuming power_motors_cruise = c_v_watts * v^2 + c_w_watts * omega^2
    # For simplicity, let's assume c_v_watts * v_cruise^2 takes a share, 
    # and we can estimate c_v and c_w. 
    # A simple approach: split motor power evenly between v and omega if both provided.

    motor_power = power_motors_cruise
    c_v = 0.0
    c_w = 0.0

    if v_cruise > 0 and omega_cruise > 0:
        c_v_watts = (motor_power / 2) / (v_cruise**2)
        c_w_watts = (motor_power / 2) / (omega_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
        c_w = (c_w_watts / energy_joules) * 100
    elif v_cruise > 0:
        c_v_watts = motor_power / (v_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
    elif omega_cruise > 0:
        c_w_watts = motor_power / (omega_cruise**2)
        c_w = (c_w_watts / energy_joules) * 100

    self.set_battery_parameters(c_idle, c_v, c_w)
    return self.battery_percent

update_battery

update_battery(dt, charge_rate=10.0)

Updates battery level. If charging is True, increases battery at charge_rate (%/s).

Source code in src/hivebr/engine/robots/differential.py
def update_battery(self, dt: float, charge_rate: float = 10.0):
    """
    Updates battery level. If charging is True, increases battery at charge_rate (%/s).
    """
    if not self.enable_battery:
        return

    if self.is_charging:
        self.battery_percent = min(100.0, self.battery_percent + charge_rate * dt)
        if self.battery_percent >= self.working_battery:
            self.is_charging = False
    else:
        consumption = (
            self.c_idle
            + self.c_v * self.v**2
            + self.c_w * self.omega**2
        )
        self.battery_percent = max(0.0, self.battery_percent - consumption * dt)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/differential.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

Differential drive with line sensors (TCRT5000)

LineFollowerRobot

LineFollowerRobot(config)

Bases: DifferentialRobot

Robô diferencial com um arranjo de sensores analógicos de linha.

Mantém a mesma cinemática do :class:DifferentialRobot (comandos {"v": ..., "omega": ...}), mas adiciona um conjunto de sensores TCRT5000 montados numa barra horizontal na frente do corpo. O corpo é um quadrado de length x width (padrão 10cm x 10cm).

Os sensores são amostrados a partir de um :class:LineTrackMap através de :meth:read_line_sensors, que retorna valores analógicos em [0, 1] (1 = linha preta, 0 = fundo claro) e calcula o erro de posição da linha em relação ao centro do robô.

Configuração (bloco robot):

num_sensors Número de sensores (tipicamente 2, 5 ou 8). Padrão 5. sensor_offset Distância à frente do centro do robô, em metros. sensor_span Largura total ocupada pelo arranjo, em metros. sensor_aperture Raio da área amostrada por cada sensor, em metros. sensor_noise Desvio-padrão do ruído analógico adicionado à leitura. line_threshold Limiar acima do qual o sensor é considerado "sobre a linha".

Source code in src/hivebr/engine/robots/line_follower.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]

    self.body_length = float(c.get("length", 0.10))
    self.body_width = float(c.get("width", 0.10))
    self.radius = float(c.get("radius", 0.05))

    self.num_sensors = int(c.get("num_sensors", 5))
    if self.num_sensors < 1:
        self.num_sensors = 1

    self.sensor_offset = float(
        c.get("sensor_offset", self.body_length * 0.5)
    )
    line_width = float(c.get("line_width", 0.02))
    if "sensor_span" in c:
        self.sensor_span = float(c["sensor_span"])
    else:
        # Espaçamento entre sensores ligeiramente menor que a largura da
        # linha para que a pista sempre cruze sensores adjacentes
        # (funciona para 2, 5 ou 8 sensores).
        default_span = min(
            self.body_width * 0.9,
            max(line_width, line_width * 0.75 * (self.num_sensors - 1)),
        )
        self.sensor_span = float(default_span)
    self.sensor_aperture = float(c.get("sensor_aperture", 0.004))
    self.sensor_noise = float(c.get("sensor_noise", 0.0))
    self.line_threshold = float(c.get("line_threshold", 0.5))

    self.sensor_local = self._build_sensor_positions()
    if self.num_sensors > 1:
        self.sensor_weights = np.linspace(-1.0, 1.0, self.num_sensors)
    else:
        self.sensor_weights = np.zeros(1)

    self.sensor_readings = np.zeros(self.num_sensors)
    self.line_error = 0.0
    self.line_position = 0.0
    self.line_lost = True
    self._last_sign = 0.0

    # ---- cronometragem de voltas / percurso ----
    self.lap_detection_radius = float(c.get("lap_detection_radius", 0.15))
    self.min_lap_distance = float(c.get("min_lap_distance", 1.0))
    self.min_lap_time = float(c.get("min_lap_time", 1.0))
    self.lost_timeout = float(c.get("lost_timeout", 1.0))
    self.reset_laps()

body_length instance-attribute

body_length = float(c.get('length', 0.1))

body_width instance-attribute

body_width = float(c.get('width', 0.1))

radius instance-attribute

radius = float(c.get('radius', 0.05))

num_sensors instance-attribute

num_sensors = int(c.get('num_sensors', 5))

sensor_offset instance-attribute

sensor_offset = float(
    c.get("sensor_offset", self.body_length * 0.5)
)

sensor_span instance-attribute

sensor_span = float(c['sensor_span'])

sensor_aperture instance-attribute

sensor_aperture = float(c.get('sensor_aperture', 0.004))

sensor_noise instance-attribute

sensor_noise = float(c.get('sensor_noise', 0.0))

line_threshold instance-attribute

line_threshold = float(c.get('line_threshold', 0.5))

sensor_local instance-attribute

sensor_local = self._build_sensor_positions()

sensor_weights instance-attribute

sensor_weights = np.linspace(-1.0, 1.0, self.num_sensors)

sensor_readings instance-attribute

sensor_readings = np.zeros(self.num_sensors)

line_error instance-attribute

line_error = 0.0

line_position instance-attribute

line_position = 0.0

line_lost instance-attribute

line_lost = True

lap_detection_radius instance-attribute

lap_detection_radius = float(
    c.get("lap_detection_radius", 0.15)
)

min_lap_distance instance-attribute

min_lap_distance = float(c.get('min_lap_distance', 1.0))

min_lap_time instance-attribute

min_lap_time = float(c.get('min_lap_time', 1.0))

lost_timeout instance-attribute

lost_timeout = float(c.get('lost_timeout', 1.0))

read_sensors class-attribute instance-attribute

read_sensors = read_line_sensors

config instance-attribute

config = config

path instance-attribute

path = []

position property

position

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

max_omega instance-attribute

max_omega = float(c.get('max_omega', 2.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

v instance-attribute

v = 0.0

omega instance-attribute

omega = 0.0

colors instance-attribute

colors = config['colors']

battery_percent instance-attribute

battery_percent = 100.0

is_charging instance-attribute

is_charging = False

battery_charge_rate instance-attribute

battery_charge_rate = float(
    c.get("battery_charge_rate", 20.0)
)

c_idle instance-attribute

c_idle = float(c.get('c_idle', 0.01))

c_v instance-attribute

c_v = float(c.get('c_v', 0.05))

c_w instance-attribute

c_w = float(c.get('c_w', 0.02))

critical_battery instance-attribute

critical_battery = float(c.get('critical_battery', 40.0))

working_battery instance-attribute

working_battery = float(c.get('critical_battery', 90.0))

going_to_recharge instance-attribute

going_to_recharge = False

reset_laps

reset_laps()

Zera o cronômetro e as estatísticas de voltas/percurso.

Source code in src/hivebr/engine/robots/line_follower.py
def reset_laps(self):
    """Zera o cronômetro e as estatísticas de voltas/percurso."""
    self.time = 0.0
    self.distance_traveled = 0.0
    self.lap_times: list[float] = []
    self.lap_start_time = 0.0
    self.lap_origin = self.position.copy()
    self._left_start = False
    self.finished = False
    self.finish_time = None
    self._lost_time = 0.0

reset

reset()
Source code in src/hivebr/engine/robots/line_follower.py
def reset(self):
    super().reset()
    self.sensor_readings = np.zeros(self.num_sensors)
    self.line_error = 0.0
    self.line_position = 0.0
    self.line_lost = True
    self._last_sign = 0.0
    self.reset_laps()

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/line_follower.py
def set_initial_pose(self, x, y, theta=0.0):
    super().set_initial_pose(x, y, theta)
    self.reset_laps()

state

state()
Source code in src/hivebr/engine/robots/line_follower.py
def state(self):
    state = super().state()
    current_lap = (
        self.finish_time
        if self.finished
        else (self.time - self.lap_start_time)
    )
    state.update(
        {
            "line_follower": True,
            "num_sensors": self.num_sensors,
            "sensors": self.sensor_readings.tolist(),
            "line_error": self.line_error,
            "line_position": self.line_position,
            "line_lost": self.line_lost,
            "time": self.time,
            "finished": self.finished,
            "finish_time": self.finish_time,
            "lap_count": len(self.lap_times),
            "lap_times": list(self.lap_times),
            "last_lap": self.lap_times[-1] if self.lap_times else None,
            "lap_avg": float(np.mean(self.lap_times)) if self.lap_times else None,
            "current_lap": current_lap,
        }
    )
    return state

lap_statistics

lap_statistics()

Resumo das voltas (contagem, última, média, melhor, pior).

Source code in src/hivebr/engine/robots/line_follower.py
def lap_statistics(self) -> dict:
    """Resumo das voltas (contagem, última, média, melhor, pior)."""
    if not self.lap_times:
        return {"count": 0, "last": None, "mean": None, "best": None, "worst": None}
    times = np.asarray(self.lap_times, dtype=float)
    return {
        "count": int(times.size),
        "last": float(times[-1]),
        "mean": float(times.mean()),
        "best": float(times.min()),
        "worst": float(times.max()),
    }

sensor_world_position

sensor_world_position(index)

Posição no mundo do sensor index.

Source code in src/hivebr/engine/robots/line_follower.py
def sensor_world_position(self, index: int) -> np.ndarray:
    """Posição no mundo do sensor ``index``."""
    lx, ly = self.sensor_local[index]
    cos_t = math.cos(self.theta)
    sin_t = math.sin(self.theta)
    return np.array(
        [
            self.x + lx * cos_t - ly * sin_t,
            self.y + lx * sin_t + ly * cos_t,
        ]
    )

read_line_sensors

read_line_sensors(line_map)

Amostra os sensores e atualiza line_error/line_lost.

Parameters

line_map: Um :class:LineTrackMap. Se None, devolve leituras nulas.

Returns

numpy.ndarray Leituras analógicas em [0, 1] (1 = linha preta).

Source code in src/hivebr/engine/robots/line_follower.py
def read_line_sensors(self, line_map) -> np.ndarray:
    """Amostra os sensores e atualiza ``line_error``/``line_lost``.

    Parameters
    ----------
    line_map:
        Um :class:`LineTrackMap`. Se ``None``, devolve leituras nulas.

    Returns
    -------
    numpy.ndarray
        Leituras analógicas em ``[0, 1]`` (``1`` = linha preta).
    """
    if line_map is None:
        self.sensor_readings = np.zeros(self.num_sensors)
        self.line_lost = True
        return self.sensor_readings

    cos_t = math.cos(self.theta)
    sin_t = math.sin(self.theta)
    values = np.empty(self.num_sensors)

    for i, (lx, ly) in enumerate(self.sensor_local):
        wx = self.x + lx * cos_t - ly * sin_t
        wy = self.y + lx * sin_t + ly * cos_t
        value = line_map.sample(wx, wy, self.sensor_aperture)
        if self.sensor_noise > 0.0:
            value += float(np.random.normal(0.0, self.sensor_noise))
        values[i] = clamp(value, 0.0, 1.0)

    self.sensor_readings = values

    total = float(values.sum())
    if total > 1e-6:
        self.line_error = float(np.dot(self.sensor_weights, values) / total)
        self.line_lost = False
    else:
        self.line_lost = True
        if abs(self.line_error) < 1e-6:
            self.line_error = self._last_sign

    if abs(self.line_error) > 1e-6:
        self._last_sign = 1.0 if self.line_error > 0.0 else -1.0

    self.line_position = self.line_error * (self.sensor_span / 2.0)
    return values

step

step(command, dt)
Source code in src/hivebr/engine/robots/line_follower.py
def step(self, command: dict, dt: float):
    if self.finished:
        command = {"v": 0.0, "omega": 0.0}
    super().step(command, dt)
    self._update_progress(dt)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/line_follower.py
def draw(self, screen, camera):
    cos_t = math.cos(self.theta)
    sin_t = math.sin(self.theta)
    rot = np.array([[cos_t, -sin_t], [sin_t, cos_t]])

    half_l = self.body_length / 2.0
    half_w = self.body_width / 2.0
    corners = [
        (-half_l, -half_w),
        (half_l, -half_w),
        (half_l, half_w),
        (-half_l, half_w),
    ]
    body_pts = [
        camera.world_to_screen(*(rot @ np.array(c) + self.position))
        for c in corners
    ]
    pygame.draw.polygon(screen, tuple(self.colors["robot"]), body_pts)
    pygame.draw.polygon(screen, (20, 20, 20), body_pts, 2)

    front = self.position + np.array([cos_t, sin_t]) * half_l
    pygame.draw.line(
        screen,
        tuple(self.colors.get("silver", [192, 192, 186])),
        camera.world_to_screen(self.x, self.y),
        camera.world_to_screen(*front),
        2,
    )

    sensor_radius = max(2, int(0.006 * camera.scale))
    for (lx, ly), value in zip(self.sensor_local, self.sensor_readings):
        pos = rot @ np.array([lx, ly]) + self.position
        screen_pos = camera.world_to_screen(*pos)
        pygame.draw.circle(
            screen, self._sensor_color(float(value)), screen_pos, sensor_radius
        )
        pygame.draw.circle(screen, (0, 0, 0), screen_pos, sensor_radius, 1)

clear_path

clear_path()
Source code in src/hivebr/engine/robots/base.py
def clear_path(self):
    self.path.clear()

enable_battery

enable_battery(enable=True)
Source code in src/hivebr/engine/robots/differential.py
def enable_battery(self, enable=True):
    self.enable_battery = enable

set_charging

set_charging()
Source code in src/hivebr/engine/robots/differential.py
def set_charging(self):
    self.is_charging = True
    self.going_to_recharge = False

set_battery_parameters

set_battery_parameters(
    c_idle,
    c_v,
    c_w,
    critical_battery=20,
    working_battery=90,
    battery_charge_rate=20,
)
Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters(self, c_idle, c_v, c_w, critical_battery=20, working_battery=90, battery_charge_rate=20):
    self.c_idle = float(c_idle)
    self.c_v = float(c_v)
    self.c_w = float(c_w)
    self.critical_battery = float(critical_battery)
    self.working_battery  = float(working_battery)
    self.enable_battery = True
    self.is_charging = False
    self.battery_charge_rate = battery_charge_rate

    print(f"Battery model enabled: c_idle={self.c_idle:.4f}, c_v={self.c_v:.4f}, c_w={self.c_w:.4f}, critical_battery={critical_battery:.2f} ")

set_battery_parameters_from_specs

set_battery_parameters_from_specs(
    voltage,
    ah,
    power_electronics,
    power_motors_cruise,
    v_cruise,
    omega_cruise=0.0,
)

Calculates battery consumption coefficients from physical specifications. E_total = V * Ah * 3600 (Joules) Consumption (%/s) = (Power (W) / E_total) * 100

Source code in src/hivebr/engine/robots/differential.py
def set_battery_parameters_from_specs(self, voltage, ah, power_electronics, power_motors_cruise, v_cruise, omega_cruise=0.0):
    """
    Calculates battery consumption coefficients from physical specifications.
    E_total = V * Ah * 3600 (Joules)
    Consumption (%/s) = (Power (W) / E_total) * 100
    """
    energy_joules = voltage * ah * 3600

    # Power idle (electronics)
    p_idle = power_electronics
    c_idle = (p_idle / energy_joules) * 100

    # Power motors (cruise)
    # Assuming power_motors_cruise = c_v_watts * v^2 + c_w_watts * omega^2
    # For simplicity, let's assume c_v_watts * v_cruise^2 takes a share, 
    # and we can estimate c_v and c_w. 
    # A simple approach: split motor power evenly between v and omega if both provided.

    motor_power = power_motors_cruise
    c_v = 0.0
    c_w = 0.0

    if v_cruise > 0 and omega_cruise > 0:
        c_v_watts = (motor_power / 2) / (v_cruise**2)
        c_w_watts = (motor_power / 2) / (omega_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
        c_w = (c_w_watts / energy_joules) * 100
    elif v_cruise > 0:
        c_v_watts = motor_power / (v_cruise**2)
        c_v = (c_v_watts / energy_joules) * 100
    elif omega_cruise > 0:
        c_w_watts = motor_power / (omega_cruise**2)
        c_w = (c_w_watts / energy_joules) * 100

    self.set_battery_parameters(c_idle, c_v, c_w)
    return self.battery_percent

update_battery

update_battery(dt, charge_rate=10.0)

Updates battery level. If charging is True, increases battery at charge_rate (%/s).

Source code in src/hivebr/engine/robots/differential.py
def update_battery(self, dt: float, charge_rate: float = 10.0):
    """
    Updates battery level. If charging is True, increases battery at charge_rate (%/s).
    """
    if not self.enable_battery:
        return

    if self.is_charging:
        self.battery_percent = min(100.0, self.battery_percent + charge_rate * dt)
        if self.battery_percent >= self.working_battery:
            self.is_charging = False
    else:
        consumption = (
            self.c_idle
            + self.c_v * self.v**2
            + self.c_w * self.omega**2
        )
        self.battery_percent = max(0.0, self.battery_percent - consumption * dt)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/differential.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

Holonomic

HolonomicRobot

HolonomicRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/holonomic.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]
    self.initial_pose = np.array(c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.radius = float(c.get("radius", 0.3))
    self.max_speed = float(c.get("max_speed", 2.0))
    self.path_max_length = int(c.get("path_max_length", 4000))
    self.vx = 0.0
    self.vy = 0.0
    self.colors = config["colors"]

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

radius instance-attribute

radius = float(c.get('radius', 0.3))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

vx instance-attribute

vx = 0.0

vy instance-attribute

vy = 0.0

colors instance-attribute

colors = config['colors']

position property

position

config instance-attribute

config = config

path instance-attribute

path = []

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/holonomic.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/holonomic.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.vx = 0.0
    self.vy = 0.0
    self.path.clear()

state

state()
Source code in src/hivebr/engine/robots/holonomic.py
def state(self):
    return {"type": "holonomic", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "vx": self.vx, "vy": self.vy}

step

step(command, dt)
Source code in src/hivebr/engine/robots/holonomic.py
def step(self, command: dict, dt: float):
    vx = float(command.get("vx", 0.0))
    vy = float(command.get("vy", 0.0))
    norm = math.hypot(vx, vy)
    if norm > self.max_speed:
        scale = self.max_speed / norm
        vx *= scale
        vy *= scale
    self.vx, self.vy = vx, vy
    self.x += self.vx * dt
    self.y += self.vy * dt
    if norm > 1e-8:
        self.theta = wrap_to_pi(math.atan2(self.vy, self.vx))
    self.path.append(self.position.copy())
    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/holonomic.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/holonomic.py
def draw(self, screen, camera):
    center = camera.world_to_screen(self.x, self.y)
    radius_px = max(5, int(self.radius * camera.scale))
    pygame.draw.circle(screen, tuple(self.colors["robot"]), center, radius_px)
    front = self.position + np.array([math.cos(self.theta), math.sin(self.theta)]) * self.radius * 1.6
    pygame.draw.line(screen, tuple(self.colors["white"]), center, camera.world_to_screen(*front), 3)

    # =========================
    # Rodas (4 omni wheels)
    # =========================
    wheel_len = self.radius * 0.6
    wheel_width = self.radius * 0.3

    rot = np.array([
        [math.cos(self.theta), -math.sin(self.theta)],
        [math.sin(self.theta),  math.cos(self.theta)]
    ])

    # Posições locais: 4 rodas nas extremidades
    offset = self.radius * 0.6
    wheels = [
        (offset, offset),    # Frente direita
        (offset, -offset),   # Frente esquerda
        (-offset, offset),   # Trás direita
        (-offset, -offset)   # Trás esquerda
    ]

    for wx, wy in wheels:
        # centro da roda no mundo
        wheel_center = rot @ np.array([wx, wy]) + self.position

        # retângulo da roda (orientada localmente)
        wheel_corners = np.array([
            [ wheel_len/2,  wheel_width/2],
            [ wheel_len/2, -wheel_width/2],
            [-wheel_len/2, -wheel_width/2],
            [-wheel_len/2,  wheel_width/2]
        ])

        # Rotacionar a roda
        wheel_pts = [
            camera.world_to_screen(*(rot @ c + wheel_center))
            for c in wheel_corners
        ]

        pygame.draw.polygon(
            screen,
            (139, 69, 19),
            wheel_pts
        )

clear_path

clear_path()
Source code in src/hivebr/engine/robots/base.py
def clear_path(self):
    self.path.clear()

Ackermann

AckermannRobot

AckermannRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/ackermann.py
def __init__(self, config: dict):
    super().__init__(config)
    c = config["robot"]
    self.initial_pose = np.array(c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.wheel_base = float(c.get("wheel_base", 0.5))
    self.length = float(c.get("length", 0.8))
    self.width = float(c.get("width", 0.4))
    self.radius = float(c.get("radius", 0.4))
    self.max_speed = float(c.get("max_speed", 3.0))
    self.max_steer = float(c.get("max_steer", 0.6))
    self.path_max_length = int(c.get("path_max_length", 4000))
    self.speed = 0.0
    self.steer = 0.0
    self.colors = config["colors"]

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

wheel_base instance-attribute

wheel_base = float(c.get('wheel_base', 0.5))

length instance-attribute

length = float(c.get('length', 0.8))

width instance-attribute

width = float(c.get('width', 0.4))

radius instance-attribute

radius = float(c.get('radius', 0.4))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 3.0))

max_steer instance-attribute

max_steer = float(c.get('max_steer', 0.6))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

speed instance-attribute

speed = 0.0

steer instance-attribute

steer = 0.0

colors instance-attribute

colors = config['colors']

position property

position

config instance-attribute

config = config

path instance-attribute

path = []

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/ackermann.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/ackermann.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.speed = 0.0
    self.steer = 0.0
    self.path.clear()

state

state()
Source code in src/hivebr/engine/robots/ackermann.py
def state(self):
    return {"type": "ackermann", "x": self.x, "y": self.y, "theta": self.theta, "position": self.position.copy(), "speed": self.speed, "steer": self.steer}

step

step(command, dt)
Source code in src/hivebr/engine/robots/ackermann.py
def step(self, command: dict, dt: float):
    self.speed = clamp(float(command.get("speed", 0.0)), -self.max_speed, self.max_speed)
    self.steer = clamp(float(command.get("steer", 0.0)), -self.max_steer, self.max_steer)
    self.x += self.speed * math.cos(self.theta) * dt
    self.y += self.speed * math.sin(self.theta) * dt
    self.theta += (self.speed / self.wheel_base) * math.tan(self.steer) * dt
    self.theta = wrap_to_pi(self.theta)
    self.path.append(self.position.copy())
    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/ackermann.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return
    pygame.draw.lines(screen, tuple(self.colors["path"]), False, [camera.world_to_screen(*p) for p in self.path], 2)

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/ackermann.py
def draw(self, screen, camera):
    # =========================
    # Corpo do robô
    # =========================
    corners = np.array([
        [ self.length/2,  self.width/2],
        [ self.length/2, -self.width/2],
        [-self.length/2, -self.width/2],
        [-self.length/2,  self.width/2]
    ])

    rot = np.array([
        [math.cos(self.theta), -math.sin(self.theta)],
        [math.sin(self.theta),  math.cos(self.theta)]
    ])

    pts = [
        camera.world_to_screen(*(rot @ c + self.position))
        for c in corners
    ]

    pygame.draw.polygon(screen, tuple(self.colors["robot"]), pts)

    # =========================
    # Círculo transparente
    # =========================
    radius_px = max(5, int(self.radius * camera.scale))

    circle_surface = pygame.Surface(
        (radius_px * 2, radius_px * 2),
        pygame.SRCALPHA
    )

    color = (*self.colors["robot"], 120)

    pygame.draw.circle(
        circle_surface,
        color,
        (radius_px, radius_px),
        radius_px
    )

    screen_pos = camera.world_to_screen(self.x, self.y)

    screen.blit(
        circle_surface,
        (screen_pos[0] - radius_px,
        screen_pos[1] - radius_px)
    )

    # =========================
    # Rodas
    # =========================

    wheel_length = self.length * 0.18
    wheel_width  = self.width * 0.12

    half_track = self.width / 2
    half_wb = self.wheel_base / 2

    # posições locais das rodas
    wheels = [
        (+half_wb, +half_track, self.theta + self.steer), # dianteira esquerda
        (+half_wb, -half_track, self.theta + self.steer), # dianteira direita
        (-half_wb, +half_track, self.theta),              # traseira esquerda
        (-half_wb, -half_track, self.theta),              # traseira direita
    ]

    for wx, wy, wtheta in wheels:

        # centro da roda no mundo
        wheel_center = rot @ np.array([wx, wy]) + self.position

        # rotação da roda
        wrot = np.array([
            [math.cos(wtheta), -math.sin(wtheta)],
            [math.sin(wtheta),  math.cos(wtheta)]
        ])

        # retângulo da roda
        wheel_corners = np.array([
            [ wheel_length/2,  wheel_width/2],
            [ wheel_length/2, -wheel_width/2],
            [-wheel_length/2, -wheel_width/2],
            [-wheel_length/2,  wheel_width/2]
        ])

        wheel_pts = [
            camera.world_to_screen(*(wrot @ c + wheel_center))
            for c in wheel_corners
        ]

        pygame.draw.polygon(
            screen,
            (139, 69, 19),
            wheel_pts
        )

    # =========================
    # Direção frontal
    # =========================
    front = self.position + np.array([
        math.cos(self.theta),
        math.sin(self.theta)
    ]) * (self.length / 2)

    pygame.draw.line(
        screen,
        tuple([192,192,186]),
        camera.world_to_screen(self.x, self.y),
        camera.world_to_screen(*front),
        3
    )

clear_path

clear_path()
Source code in src/hivebr/engine/robots/base.py
def clear_path(self):
    self.path.clear()

Boat

BoatRobot

BoatRobot(config)

Bases: BaseRobot

Source code in src/hivebr/engine/robots/boat.py
def __init__(self, config: dict):
    super().__init__(config)

    c = config["robot"]

    self.initial_pose = np.array(
        c.get("initial_pose", [2.0, 2.0, 0.0]),
        dtype=float
    )

    self.x, self.y, self.theta = self.initial_pose.tolist()

    self.radius = float(c.get("radius", 0.35))

    self.max_speed = float(c.get("max_speed", 2.0))
    self.max_omega = float(c.get("max_omega", 1.0))

    self.path_max_length = int(
        c.get("path_max_length", 4000)
    )

    self.v = 0.0
    self.omega = 0.0

    self.colors = config["colors"]

initial_pose instance-attribute

initial_pose = np.array(
    c.get("initial_pose", [2.0, 2.0, 0.0]), dtype=float
)

radius instance-attribute

radius = float(c.get('radius', 0.35))

max_speed instance-attribute

max_speed = float(c.get('max_speed', 2.0))

max_omega instance-attribute

max_omega = float(c.get('max_omega', 1.0))

path_max_length instance-attribute

path_max_length = int(c.get('path_max_length', 4000))

v instance-attribute

v = 0.0

omega instance-attribute

omega = 0.0

colors instance-attribute

colors = config['colors']

position property

position

config instance-attribute

config = config

path instance-attribute

path = []

set_initial_pose

set_initial_pose(x, y, theta=0.0)
Source code in src/hivebr/engine/robots/boat.py
def set_initial_pose(self,x,y,theta=0.0):
    self.initial_pose = np.array([x, y, theta], dtype=float)
    self.x, self.y, self.theta = self.initial_pose.tolist()

reset

reset()
Source code in src/hivebr/engine/robots/boat.py
def reset(self):
    self.x, self.y, self.theta = self.initial_pose.tolist()
    self.v = 0.0
    self.omega = 0.0
    self.path.clear()

state

state()
Source code in src/hivebr/engine/robots/boat.py
def state(self):
    return {
        "type": "boat",
        "x": self.x,
        "y": self.y,
        "theta": self.theta,
        "position": self.position.copy(),
        "v": self.v,
        "omega": self.omega
    }

step

step(command, dt)
Source code in src/hivebr/engine/robots/boat.py
def step(self, command: dict, dt: float):
    self.v = clamp(
        float(command.get("v", 0.0)),
        -self.max_speed,
        self.max_speed
    )

    self.omega = clamp(
        float(command.get("omega", 0.0)),
        -self.max_omega,
        self.max_omega
    )

    self.x += self.v * math.cos(self.theta) * dt
    self.y += self.v * math.sin(self.theta) * dt

    self.theta = wrap_to_pi(
        self.theta + self.omega * dt
    )

    self.path.append(self.position.copy())

    if len(self.path) > self.path_max_length:
        self.path.pop(0)

draw_path

draw_path(screen, camera)
Source code in src/hivebr/engine/robots/boat.py
def draw_path(self, screen, camera):
    if len(self.path) < 2:
        return

    pygame.draw.lines(
        screen,
        tuple(self.colors["path"]),
        False,
        [camera.world_to_screen(*p) for p in self.path],
        2
    )

draw

draw(screen, camera)
Source code in src/hivebr/engine/robots/boat.py
def draw(self, screen, camera):
    center = np.array([self.x, self.y])

    color = tuple(self.colors["robot"])
    outline = tuple(self.colors["black"])

    # =====================================
    # Geometria do casco
    # =====================================

    W = 0.9 * self.radius
    L = math.sqrt(4*self.radius**2 - W**2)

    hull_local = np.array([
        [-0.50 * L, -0.50 * W],
        [ 0.25 * L, -0.50 * W],
        [ 0.50 * L,  0.00 * W],
        [ 0.25 * L,  0.50 * W],
        [-0.50 * L,  0.50 * W],
    ])

    bow_local = np.array([
        [ 0.50 * L, 0.0],
        [ 0.20 * L, -0.25 * W],
        [ 0.20 * L,  0.25 * W],
    ])

    # =====================================
    # Rotação
    # =====================================

    c = math.cos(self.theta)
    s = math.sin(self.theta)

    R = np.array([
        [c, -s],
        [s,  c]
    ])

    hull_world = (R @ hull_local.T).T + center
    bow_world = (R @ bow_local.T).T + center

    hull_screen = [
        camera.world_to_screen(*p)
        for p in hull_world
    ]

    bow_screen = [
        camera.world_to_screen(*p)
        for p in bow_world
    ]

    # =====================================
    # Desenho
    # =====================================

    pygame.draw.polygon(
        screen,
        color,
        hull_screen
    )

    pygame.draw.polygon(
        screen,
        color,
        bow_screen
    )

    pygame.draw.polygon(
        screen,
        outline,
        hull_screen,
        2
    )

    # =========================
    # Círculo transparente
    # =========================
    radius_px = max(5, int(self.radius * camera.scale))

    circle_surface = pygame.Surface(
        (radius_px * 2, radius_px * 2),
        pygame.SRCALPHA
    )

    color = (*self.colors["robot"], 120)

    pygame.draw.circle(
        circle_surface,
        color,
        (radius_px, radius_px),
        radius_px
    )
    screen_pos = camera.world_to_screen(self.x, self.y)

    screen.blit(
        circle_surface,
        (screen_pos[0] - radius_px,
        screen_pos[1] - radius_px)
    )

    # linha central (opcional)

    front = center + np.array([
        math.cos(self.theta),
        math.sin(self.theta)
    ]) * L * 0.6

    pygame.draw.line(
        screen,
        tuple(self.colors["white"]),
        camera.world_to_screen(*center),
        camera.world_to_screen(*front),
        2
    )

clear_path

clear_path()
Source code in src/hivebr/engine/robots/base.py
def clear_path(self):
    self.path.clear()