Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
17 changes: 17 additions & 0 deletions src/basic_bot/commons/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,20 @@
The path to the directory containing the web application's static files.
The web server will serve files from this directory.
"""

BB_WEB_PORT = env.env_int("BB_WEB_PORT", 5080)
"""
The port that the web server listens on for incoming HTTP requests. This is the
port to use when serving the web application to users. Some Linux distributions
require root privileges to bind to ports below 1024.

To bind to port 80 while running as a non-root user, you can use a reverse proxy
or port forwarding. From bash, you can use the following command to forward port 80.

```bash
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 5080
# also redirect for localhost access
iptables -t nat -I OUTPUT -p tcp -d 127.0.0.1 --dport 80 -j REDIRECT --to-ports 5080
```

"""
3 changes: 2 additions & 1 deletion src/basic_bot/commons/hub_state_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from basic_bot.commons import constants as c, messages, log
from basic_bot.commons.hub_state import HubState


# TODO: This class should maybe be a singleton.
should_exit = False

Expand Down Expand Up @@ -134,6 +133,8 @@ async def parse_next_message(
if should_exit:
return

log.info("hub_state_monitor websocket closed by central_hub")

async def monitor_state(self) -> None:
while not should_exit:
try:
Expand Down
4 changes: 4 additions & 0 deletions src/basic_bot/commons/servo_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class ServoOptions(TypedDict):
max_angle: NotRequired[int]
min_pulse: NotRequired[int]
max_pulse: NotRequired[int]
step_delay: NotRequired[float]
step_degrees: NotRequired[float]


@dataclass
Expand All @@ -68,3 +70,5 @@ class ServoOptionsDefaults:
max_angle: int = 180
min_pulse: int = 500
max_pulse: int = 2500
step_delay: float = 0.0001
step_degrees: float = 1.0
12 changes: 12 additions & 0 deletions src/basic_bot/commons/servo_config_file_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@
# The maximum pulse width in microseconds that the servo will accept
# as specified by the manufacturer.
"max_pulse": {"type": "integer"},
#
# Delay between servo movement steps in seconds.
"step_delay": {
"type": "number",
"exclusiveMinimum": 0,
},
#
# Number of degrees to move per servo step.
"step_degrees": {
"type": "number",
"exclusiveMinimum": 0,
},
},
},
},
Expand Down
49 changes: 36 additions & 13 deletions src/basic_bot/commons/servo_pca9685.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ def __init__(self, channel: int, min_pulse: int = 500, max_pulse: int = 2500) ->
# env var to turn on console debug output
DEBUG_MOTORS = env.env_bool("DEBUG_MOTORS", False)

# how many deg to turn per step (abs)
STEP_DEGREES = 1
# how long to wait between steps (default; use step_delay setter to change at run time)
DEFAULT_STEP_DELAY = 0.0001

# the precision of the servo motors in degrees
SERVO_PRECISION = 0.5

Expand Down Expand Up @@ -82,24 +77,50 @@ def __init__(
self.pause_event = threading.Event()
self.stopped_event = threading.Event()
self.force_stop = False
self._step_delay = DEFAULT_STEP_DELAY

self.name: str = servo_options["name"]
self.channel: int = servo_options["channel"]
motor_range = servo_options.get("motor_range")
self.motor_range: int = (
servo_options.get("motor_range") or ServoOptionsDefaults.motor_range
ServoOptionsDefaults.motor_range
if motor_range is None
else motor_range
)
min_pulse = servo_options.get("min_pulse")
self.min_pulse: int = (
servo_options.get("min_pulse") or ServoOptionsDefaults.min_pulse
ServoOptionsDefaults.min_pulse
if min_pulse is None
else min_pulse
)
max_pulse = servo_options.get("max_pulse")
self.max_pulse: int = (
servo_options.get("max_pulse") or ServoOptionsDefaults.max_pulse
ServoOptionsDefaults.max_pulse
if max_pulse is None
else max_pulse
)
min_angle = servo_options.get("min_angle")
self.min_angle: int = (
servo_options.get("min_angle") or ServoOptionsDefaults.min_angle
ServoOptionsDefaults.min_angle
if min_angle is None
else min_angle
)
max_angle = servo_options.get("max_angle")
self.max_angle: int = (
servo_options.get("max_angle") or ServoOptionsDefaults.max_angle
ServoOptionsDefaults.max_angle
if max_angle is None
else max_angle
)
step_delay = servo_options.get("step_delay")
self._step_delay: float = (
ServoOptionsDefaults.step_delay
if step_delay is None
else step_delay
)
step_degrees = servo_options.get("step_degrees")
self.step_degrees: float = (
ServoOptionsDefaults.step_degrees
if step_degrees is None
else step_degrees
)
self.mid_angle: float = (
float(self.min_angle) + float(self.max_angle - self.min_angle) / 2
Expand Down Expand Up @@ -237,7 +258,7 @@ def _step_move(self, direction: int) -> bool:
# and then stop the movement loop
return False

new_angle = self.current_angle + (STEP_DEGREES * direction)
new_angle = self.current_angle + (self.step_degrees * direction)

self.servo.fraction = new_angle / self.motor_range
time.sleep(self._step_delay)
Expand All @@ -251,7 +272,7 @@ def _step_move(self, direction: int) -> bool:

def _step_would_overshoot_dest(self, direction: int) -> bool:
current_angle = self.current_angle
new_angle = current_angle + STEP_DEGREES * direction
new_angle = current_angle + self.step_degrees * direction
return (direction == -1 and new_angle < self.destination_angle) or (
direction == 1 and new_angle > self.destination_angle
)
Expand All @@ -278,6 +299,8 @@ def _step_would_overshoot_dest(self, direction: int) -> bool:
"max_angle": ServoOptionsDefaults.max_angle,
"min_pulse": ServoOptionsDefaults.min_pulse,
"max_pulse": ServoOptionsDefaults.max_pulse,
"step_delay": ServoOptionsDefaults.step_delay,
"step_degrees": ServoOptionsDefaults.step_degrees,
}
)

Expand Down
10 changes: 7 additions & 3 deletions src/basic_bot/commons/vid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,20 @@ def on_track_ended():
log.debug(f"{track.kind} track ended")

try:
# Add transceivers for receiving video and audio
log.debug("Creating transceivers for video and audio")
pc.addTransceiver("video", direction="recvonly")
pc.addTransceiver("audio", direction="recvonly")

# Create offer
log.debug("Creating offer and setting local description")
offer = await pc.createOffer()
await pc.setLocalDescription(offer)

log.debug("Waiting for ICE gathering to complete")
# Wait for ICE gathering to complete
while pc.iceGatheringState != "complete":
await asyncio.sleep(0.1)

log.debug("ICE gathering complete, sending offer to WebRTC endpoint")
# Send offer to server
async with aiohttp.ClientSession() as session:
offer_data = {
Expand All @@ -175,15 +177,17 @@ def on_track_ended():
}

headers = {"Content-Type": "application/json"}
log.debug(f"Sending offer data: {offer_data} to {webrtc_endpoint}")
async with session.post(
webrtc_endpoint, json=offer_data, headers=headers
) as response:
log.debug("Answer received from WebRTC endpoint")
if response.status != 200:
raise RuntimeError(
f"Failed to send offer to WebRTC endpoint: {response.status}"
)

answer_data = await response.json()
log.debug(f"Answer data: {answer_data}")

# Set remote description
answer = RTCSessionDescription(
Expand Down
13 changes: 13 additions & 0 deletions src/basic_bot/commons/vision_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ def send_record_video_request(duration: float) -> requests.Response:
return response


def send_record_video_async(duration: float):
"""
Send a request to the vision service to start recording video asynchronously.
"""
if c.BB_LOG_ALL_MESSAGES:
log.info(f"Sending record video request for {duration} seconds")
requests.get(
f"{c.BB_VISION_URI}/record_video",
params={"duration": duration},
timeout=0.001,
)


def fetch_recorded_videos() -> requests.Response:
"""
Send a request to the vision service to retrieve a list of recorded videos.
Expand Down
8 changes: 6 additions & 2 deletions src/basic_bot/commons/webrtc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ def _initialize_audio(self) -> Optional[MediaStreamTrack]:
stderr=subprocess.DEVNULL,
bufsize=0, # Unbuffered stdout
)
log.debug(
"arecord process started for audio capture. Initializing microphone."
)
self.microphone = MediaPlayer(
self.arecord_process.stdout,
format="s16le",
Expand All @@ -115,7 +118,7 @@ def _initialize_audio(self) -> Optional[MediaStreamTrack]:
# Linux with PulseAudio
self.microphone = MediaPlayer("default", format="pulse")

# Initialize audio relay for sharing between multiple peers
log.debug("Initializing MediaRelay for audio streaming")
self.audio_relay = MediaRelay()
log.info("Audio streaming initialized successfully")
return (
Expand Down Expand Up @@ -199,10 +202,11 @@ async def on_connectionstatechange() -> None:
# handle offer
await pc.setRemoteDescription(offer)

# send answer
log.debug("Creating answer and setting local description")
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)

log.debug(f"Sending answer to {request.remote} with client_id={client_id}")
return web.Response(
content_type="application/json",
text=json.dumps(
Expand Down
5 changes: 5 additions & 0 deletions src/basic_bot/created_files/restart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ bb_stop $@
echo "Sleeping for 5 seconds"
sleep 5

if [ $# -eq 0 ]; then
bb_killall
sleep 2
fi

echo "Starting services..."
bb_start $@
22 changes: 18 additions & 4 deletions src/basic_bot/services/central_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@


"""

import json
import asyncio
import websockets
Expand All @@ -111,7 +112,6 @@
from basic_bot.commons.hub_state import HubState
from basic_bot.commons.outbound_clients import OutboundClients


log.info("Initializing hub state")
hub_state = HubState(
{
Expand Down Expand Up @@ -263,15 +263,29 @@ async def unregister(websocket: WebSocketServerProtocol) -> None:
)
try:
connected_sockets.remove(websocket)
except KeyError:
pass

try:
star_subscribers.remove(websocket)
except KeyError:
pass

try:
for key in subscribers:
subscribers[key].remove(websocket)
try:
subscribers[key].remove(websocket)
except KeyError:
pass
subsystem_name = identities.pop(websocket, None)
log.info(f"{subsystem_name}: subscribers after unregister: {subscribers}")
if subsystem_name:
await update_online_status(subsystem_name, 0)
except:
pass
except Exception as e:
error_string = traceback.format_exc()
log.error(
f"error removing websocket from subscribers: {e}; error message: {error_string}"
)


async def handle_state_request(
Expand Down
10 changes: 9 additions & 1 deletion src/basic_bot/services/servo_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
max_angle: 180
min_pulse: 500
max_pulse: 2500
step_delay: 0.0001
step_degrees: 1.0
```

If you update the `servo_config.yml` file, you will need to restart the service.
Expand All @@ -29,6 +31,8 @@
be constrained.
- `min_pulse` and `max_pulse` are the minimum and maximum pulse widths in microseconds
that the servo will accept as specified by the manufacturer.
- `step_delay` is the delay in seconds between motor movement steps.
- `step_degrees` is the degrees moved per motor movement step.

The service listens for messages on the central_hub key: "servo_angles". The
message `data` should be a dictionary with keys that are the servo `names`
Expand Down Expand Up @@ -65,7 +69,9 @@
"min_angle": 0,
"max_angle": 180,
"min_pulse": 500,
"max_pulse": 2500
"max_pulse": 2500,
"step_delay": 0.0001,
"step_degrees": 1.0
}
]
}
Expand Down Expand Up @@ -118,6 +124,8 @@ async def send_servo_config(websocket: WebSocketClientProtocol) -> None:
"max_angle": servo.max_angle,
"min_pulse": servo.min_pulse,
"max_pulse": servo.max_pulse,
"step_delay": servo.step_delay,
"step_degrees": servo.step_degrees,
}
for servo in servos_by_name.values()
]
Expand Down
Loading
Loading