diff --git a/doc/rest.md b/doc/rest.md index 3c5d1f54..f90554a7 100644 --- a/doc/rest.md +++ b/doc/rest.md @@ -60,7 +60,7 @@ If a DotBot is connected, this script should give an output similar to: "mode": 0, "last_seen": 1701244665.8099585, "waypoints": [], - "waypoints_threshold": 40, + "waypoints_threshold": 50, "position_history": [] } ] diff --git a/dotbot/__init__.py b/dotbot/__init__.py index e1b4b724..167ea244 100644 --- a/dotbot/__init__.py +++ b/dotbot/__init__.py @@ -15,6 +15,7 @@ CONTROLLER_ADAPTER_DEFAULT = "serial" MQTT_HOST_DEFAULT = "localhost" MQTT_PORT_DEFAULT = 1883 +MAP_SIZE_DEFAULT = "2000x2000" # in mm unit SIMULATOR_INIT_STATE_PATH_DEFAULT = "simulator_init_state.toml" diff --git a/dotbot/controller.py b/dotbot/controller.py index de5e9644..2478d4ed 100644 --- a/dotbot/controller.py +++ b/dotbot/controller.py @@ -8,6 +8,7 @@ """Interface of the Dotbot controller.""" import asyncio +import dataclasses import json import math import os @@ -33,6 +34,7 @@ CONTROLLER_HTTP_PORT_DEFAULT, DOTBOT_ADDRESS_DEFAULT, GATEWAY_ADDRESS_DEFAULT, + MAP_SIZE_DEFAULT, MQTT_HOST_DEFAULT, MQTT_PORT_DEFAULT, NETWORK_ID_DEFAULT, @@ -53,6 +55,7 @@ MAX_POSITION_HISTORY_SIZE, DotBotGPSPosition, DotBotLH2Position, + DotBotMapSizeModel, DotBotModel, DotBotMoveRawCommandModel, DotBotNotificationCommand, @@ -92,20 +95,31 @@ CONTROLLERS = {} INACTIVE_DELAY = 5 # seconds LOST_DELAY = 60 # seconds -LH2_POSITION_DISTANCE_THRESHOLD = 0.01 +LH2_POSITION_DISTANCE_THRESHOLD = 20 # mm GPS_POSITION_DISTANCE_THRESHOLD = 5 # meters CALIBRATION_PATH = Path.home() / ".dotbot" / "calibration.out" -def load_calibration() -> PayloadLh2CalibrationHomography: +@dataclass +class CalibrationHomography: + """Dataclass that holds computed LH2 homography for a basestation indicated by index.""" + + homography_matrix: bytes = dataclasses.field(default_factory=lambda: bytearray) + + +def load_calibration() -> list[CalibrationHomography]: if not os.path.exists(CALIBRATION_PATH): - return None + return [] with open(CALIBRATION_PATH, "rb") as calibration_file: - index = int.from_bytes(calibration_file.read(4), "little", signed=False) - homography_matrix = calibration_file.read(36) - return PayloadLh2CalibrationHomography( - index=index, homography_matrix=homography_matrix - ) + homographies: list[CalibrationHomography] = [] + homographies_num = int.from_bytes( + calibration_file.read(1), "little", signed=False + ) + for _ in range(homographies_num): + homographies.append( + CalibrationHomography(homography_matrix=calibration_file.read(36)) + ) + return homographies class ControllerException(Exception): @@ -126,6 +140,7 @@ class ControllerSettings: gw_address: str = GATEWAY_ADDRESS_DEFAULT network_id: str = NETWORK_ID_DEFAULT controller_http_port: int = CONTROLLER_HTTP_PORT_DEFAULT + map_size: str = MAP_SIZE_DEFAULT webbrowser: bool = False verbose: bool = False log_level: str = "info" @@ -193,8 +208,12 @@ def __init__(self, settings: ControllerSettings): self.settings = settings self.adapter: GatewayAdapterBase = None self.websockets = [] - self.lh2_calibration = load_calibration() + self.lh2_calibration: list[CalibrationHomography] = load_calibration() self.api = api + self.map_size = DotBotMapSizeModel( + width=int(settings.map_size.split("x")[0]), + height=int(settings.map_size.split("x")[1]), + ) api.controller = self self.qrkey = None @@ -358,9 +377,9 @@ def on_command_waypoints(self, topic, payload): count=len(command.waypoints), waypoints=[ PayloadLH2Location( - pos_x=int(waypoint.x * 1e6), - pos_y=int(waypoint.y * 1e6), - pos_z=int(waypoint.z * 1e6), + pos_x=int(waypoint.x), + pos_y=int(waypoint.y), + pos_z=int(waypoint.z), ) for waypoint in command.waypoints ], @@ -423,6 +442,14 @@ def on_request(self, payload): data=data, ).model_dump(exclude_none=True) self.qrkey.publish(reply_topic, message) + elif request.request == DotBotRequestType.MAP_SIZE: + logger.info("Publish map size") + data = self.map_size.model_dump(exclude_none=True) + message = DotBotReplyModel( + request=DotBotRequestType.MAP_SIZE, + data=data, + ).model_dump(exclude_none=True) + self.qrkey.publish(reply_topic, message) else: logger.warning("Unsupported request command") @@ -547,21 +574,37 @@ def handle_received_frame( if frame.packet.payload_type == PayloadType.DOTBOT_ADVERTISEMENT: logger = logger.bind(application=ApplicationType.DotBot.name) - dotbot.calibrated = bool(frame.packet.payload.calibrated) - logger.info("Advertisement received", calibrated=bool(dotbot.calibrated)) + dotbot.calibrated = int(frame.packet.payload.calibrated) + logger.info("Advertisement received", calibrated=hex(dotbot.calibrated)) # Send calibration to dotbot if it's not calibrated and the localization system has calibration need_update = False - if dotbot.calibrated is False and self.lh2_calibration is not None: + is_fully_calibrated = all( + [ + dotbot.calibrated >> index & 0x01 + for index in range(len(self.lh2_calibration)) + ] + ) + if is_fully_calibrated is False and self.lh2_calibration: # Send calibration to new dotbot if the localization system is calibrated self.logger.info("Send calibration data", payload=self.lh2_calibration) self.dotbots.update({dotbot.address: dotbot}) - self.send_payload(int(source, 16), payload=self.lh2_calibration) - elif dotbot.calibrated is True: + for index, homography in enumerate(self.lh2_calibration): + self.logger.info( + "Sending calibration homography", + index=index, + matrix=homography.homography_matrix, + ) + payload = PayloadLh2CalibrationHomography( + index=index, + homography_matrix=homography.homography_matrix, + ) + self.send_payload(int(source, 16), payload=payload) + elif is_fully_calibrated is True: if frame.packet.payload.direction != 0xFFFF: dotbot.direction = frame.packet.payload.direction new_position = DotBotLH2Position( - x=frame.packet.payload.pos_x / 1e6, - y=frame.packet.payload.pos_y / 1e6, + x=frame.packet.payload.pos_x, + y=frame.packet.payload.pos_y, z=0.0, ) if new_position.x != 0xFFFFFFFF and new_position.y != 0xFFFFFFFF: diff --git a/dotbot/controller_app.py b/dotbot/controller_app.py index feceebca..de7538bf 100644 --- a/dotbot/controller_app.py +++ b/dotbot/controller_app.py @@ -19,6 +19,7 @@ CONTROLLER_HTTP_PORT_DEFAULT, DOTBOT_ADDRESS_DEFAULT, GATEWAY_ADDRESS_DEFAULT, + MAP_SIZE_DEFAULT, MQTT_HOST_DEFAULT, MQTT_PORT_DEFAULT, NETWORK_ID_DEFAULT, @@ -120,6 +121,12 @@ type=click.Path(exists=True, dir_okay=False), help="Path to a .toml configuration file.", ) +@click.option( + "-m", + "--map-size", + type=str, + help=f"Map size in mm. Defaults to '{MAP_SIZE_DEFAULT}'", +) def main( adapter, port, @@ -131,6 +138,7 @@ def main( gw_address, network_id, controller_http_port, + map_size, webbrowser, verbose, log_level, @@ -153,6 +161,7 @@ def main( "gw_address": gw_address, "network_id": network_id, "controller_http_port": controller_http_port, + "map_size": map_size, "webbrowser": webbrowser, "verbose": verbose, "log_level": log_level, diff --git a/dotbot/dotbot_simulator.py b/dotbot/dotbot_simulator.py index b3ccc047..a15c6250 100644 --- a/dotbot/dotbot_simulator.py +++ b/dotbot/dotbot_simulator.py @@ -23,9 +23,10 @@ from dotbot.logger import LOGGER from dotbot.protocol import PayloadDotBotAdvertisement, PayloadType -R = 1 -L = 2 -SIMULATOR_STEP_DELTA_T = 0.002 +R = 50 +L = 75 +MOTOR_SPEED = 70 +SIMULATOR_STEP_DELTA_T = 0.002 # 2 ms INITIAL_BATTERY_VOLTAGE = 3000 # mV MAX_BATTERY_DURATION = 300 # 5 minutes @@ -61,8 +62,8 @@ class SimulatedDotBotSettings(BaseModel): pos_x: int pos_y: int theta: float - calibrated: bool = True - motor_left_error: float = 0.5 + calibrated: int = 0xFF + motor_left_error: float = 0 motor_right_error: float = 0 @@ -112,9 +113,9 @@ def _diff_drive_bot(self, x_pos_old, y_pos_old, theta_old, v_right, v_left): """Execute state space model of a rigid differential drive robot.""" v_right_real = v_right * (1 - self.motor_right_error) v_left_real = v_left * (1 - self.motor_left_error) - x_dot = R / 2 * (v_right_real + v_left_real) * cos(theta_old - pi) * 50000 - y_dot = R / 2 * (v_right_real + v_left_real) * sin(theta_old - pi) * 50000 - theta_dot = R / L * (-v_right_real + v_left_real) + x_dot = (R / 2) * ((v_left_real + v_right_real) / L) * cos(theta_old - pi) * 100 + y_dot = (R / 2) * ((v_left_real + v_right_real) / L) * sin(theta_old - pi) * 100 + theta_dot = R / L * (v_left_real - v_right_real) x_pos = x_pos_old + x_dot * SIMULATOR_STEP_DELTA_T y_pos = y_pos_old + y_dot * SIMULATOR_STEP_DELTA_T @@ -166,9 +167,9 @@ def update(self, dt: float): error_angle=error_angle, ) - angular_speed = error_angle * 200 - self.v_left = 100 + angular_speed - self.v_right = 100 - angular_speed + angular_speed = error_angle * MOTOR_SPEED + self.v_left = MOTOR_SPEED + angular_speed + self.v_right = MOTOR_SPEED - angular_speed if self.v_left > 100: self.v_left = 100 @@ -223,7 +224,7 @@ def handle_frame(self, frame: Frame): self.v_left = 0 self.v_right = 0 self.controller_mode = DotBotSimulatorMode.MANUAL - self.waypoint_threshold = frame.packet.payload.threshold * 1000 + self.waypoint_threshold = frame.packet.payload.threshold self.waypoints = frame.packet.payload.waypoints if self.waypoints: self.controller_mode = DotBotSimulatorMode.AUTOMATIC diff --git a/dotbot/examples/charging_station.py b/dotbot/examples/charging_station.py index 05f97b7b..0e18dd6a 100644 --- a/dotbot/examples/charging_station.py +++ b/dotbot/examples/charging_station.py @@ -22,23 +22,23 @@ from dotbot.rest import RestClient, rest_client from dotbot.websocket import DotBotWsClient -THRESHOLD = 30 # Acceptable distance error to consider a waypoint reached +THRESHOLD = 50 # Acceptable distance error to consider a waypoint reached DT = 0.05 # Control loop period (seconds) # TODO: Measure these values for real dotbots -BOT_RADIUS = 0.03 # Physical radius of a DotBot (unit), used for collision avoidance -MAX_SPEED = 0.075 # Maximum allowed linear speed of a bot +BOT_RADIUS = 40 # Physical radius of a DotBot (unit), used for collision avoidance +MAX_SPEED = 300 # Maximum allowed linear speed of a bot (mm/s) (QUEUE_HEAD_X, QUEUE_HEAD_Y) = ( - 0.1, - 0.8, + 500, + 1500, ) # World-frame (X, Y) position of the charging queue head QUEUE_SPACING = ( - 0.1 # Spacing between consecutive bots in the charging queue (along X axis) + 200 # Spacing between consecutive bots in the charging queue (along X axis) ) -(PARK_X, PARK_Y) = (0.8, 0.1) # World-frame (X, Y) position of the parking area origin -PARK_SPACING = 0.1 # Spacing between parked bots (along Y axis) +(PARK_X, PARK_Y) = (1500, 500) # World-frame (X, Y) position of the parking area origin +PARK_SPACING = 200 # Spacing between parked bots (along Y axis) async def queue_robots( @@ -236,8 +236,8 @@ def assign_charge_goals( # Send the first one to the charger head = ordered[0] goals[head.address] = { - "x": 0.2, - "y": 0.2, + "x": 200, + "y": 200, } # Remaining bots shift left in the queue @@ -257,9 +257,8 @@ def preferred_vel(dotbot: DotBotModel, goal: Vec2 | None) -> Vec2: dy = goal["y"] - dotbot.lh2_position.y dist = math.sqrt(dx * dx + dy * dy) - dist1000 = dist * 1000 # If close to goal, stop - if dist1000 < THRESHOLD: + if dist < THRESHOLD: return Vec2(x=0, y=0) # Right-hand rule bias diff --git a/dotbot/examples/charging_station_init_state.toml b/dotbot/examples/charging_station_init_state.toml index be2b633c..6ef74e1e 100644 --- a/dotbot/examples/charging_station_init_state.toml +++ b/dotbot/examples/charging_station_init_state.toml @@ -1,6 +1,6 @@ [[dotbots]] address = "BADCAFE111111111" # DotBot unique address -calibrated = true # optional, defaults to true +calibrated = 0x01 # optional, defaults to only first lighthouse calibrated pos_x = 400_000 # [0, 1_000_000] pos_y = 200_000 # [0, 1_000_000] theta = 0.0 # [0.0, 2pi] diff --git a/dotbot/frontend/config-overrides.js b/dotbot/frontend/config-overrides.js index 794f408a..b61bba76 100644 --- a/dotbot/frontend/config-overrides.js +++ b/dotbot/frontend/config-overrides.js @@ -19,5 +19,8 @@ module.exports = function override(config) { Buffer: ['buffer', 'Buffer'] }) ]) + config.watchOptions = { + ignored: /node_modules/, + }; return config; } diff --git a/dotbot/frontend/src/App.js b/dotbot/frontend/src/App.js index 76c26be6..e2f5c301 100644 --- a/dotbot/frontend/src/App.js +++ b/dotbot/frontend/src/App.js @@ -14,6 +14,7 @@ const log = logger.child({module: 'app'}); const App = () => { const [searchParams, setSearchParams] = useSearchParams(); const [message, setMessage] = useState(null); + const [areaSize, setAreaSize] = useState({height: 2000, width: 2000}); const [dotbots, setDotbots] = useState([]); const [ready, clientId, mqttData, setMqttData, publish, publishCommand, sendRequest] = useQrKey({ @@ -30,6 +31,8 @@ const App = () => { // Received the list of dotbots if (payload.request === RequestType.DotBots) { setDotbots(payload.data); + } else if (payload.request === RequestType.AreaSize) { + setAreaSize(payload.data); } } else if (message.topic === `/notify`) { // Process notifications @@ -45,7 +48,9 @@ const App = () => { x: payload.data.lh2_position.x, y: payload.data.lh2_position.y }; - if (dotbotsTmp[idx].lh2_position && (dotbotsTmp[idx].position_history.length === 0 || lh2_distance(dotbotsTmp[idx].lh2_position, newPosition) > lh2_distance_threshold)) { + console.log('distance threshold:', lh2_distance_threshold, lh2_distance(dotbotsTmp[idx].lh2_position, newPosition)); + if (dotbotsTmp[idx].lh2_position && (dotbotsTmp[idx].position_history.length === 0 || lh2_distance(dotbotsTmp[idx].lh2_position, newPosition) >= lh2_distance_threshold)) { + console.log('Adding to position history'); dotbotsTmp[idx].position_history.push(newPosition); } dotbotsTmp[idx].lh2_position = newPosition; @@ -72,13 +77,14 @@ const App = () => { } } setMessage(null); - },[clientId, dotbots, setDotbots, sendRequest, message, setMessage] + },[clientId, dotbots, setDotbots, setAreaSize, sendRequest, message, setMessage] ); useEffect(() => { if (clientId) { // Ask for the list of dotbots at startup setTimeout(sendRequest, 100, ({request: RequestType.DotBots, reply: `${clientId}`})); + setTimeout(sendRequest, 200, ({request: RequestType.AreaSize, reply: `${clientId}`})); } }, [sendRequest, clientId] ); @@ -98,6 +104,7 @@ const App = () => {
-
{dotbot.address}
+
{dotbot.address.slice(-6)}
-  {`${parseFloat(dotbot.battery).toFixed(1)}V`} +  {`${parseFloat(dotbot.battery).toFixed(1)}V`}
{dotbotStatuses[dotbot.status]} @@ -107,7 +107,7 @@ export const DotBotItem = ({dotbot, publishCommand, updateActive, applyWaypoints

{`Target threshold: ${dotbot.waypoints_threshold}`}

- +
diff --git a/dotbot/frontend/src/DotBots.js b/dotbot/frontend/src/DotBots.js index f775dde1..a4c9d262 100644 --- a/dotbot/frontend/src/DotBots.js +++ b/dotbot/frontend/src/DotBots.js @@ -10,7 +10,7 @@ import { XGOItem } from "./XGOItem"; import { ApplicationType, inactiveAddress, maxWaypoints, maxPositionHistory } from "./utils/constants"; -const DotBots = ({ dotbots, updateDotbots, publishCommand, publish }) => { +const DotBots = ({ dotbots, areaSize, updateDotbots, publishCommand, publish }) => { const [ activeDotbot, setActiveDotbot ] = useState(inactiveAddress); const [ showDotBotHistory, setShowDotBotHistory ] = useState(true); const [ dotbotHistorySize, setDotbotHistorySize ] = useState(maxPositionHistory); @@ -159,7 +159,7 @@ const DotBots = ({ dotbots, updateDotbots, publishCommand, publish }) => { applyWaypoints, clearWaypoints, activeDotbot ]); - let needDotBotMap = dotbots.filter(dotbot => dotbot.application === ApplicationType.DotBot).some((dotbot) => dotbot.calibrated); + let needDotBotMap = dotbots.filter(dotbot => dotbot.application === ApplicationType.DotBot).some((dotbot) => dotbot.calibrated > 0x00); return ( <> @@ -183,7 +183,7 @@ const DotBots = ({ dotbots, updateDotbots, publishCommand, publish }) => { <> {dotbots.filter(dotbot => dotbot.application === ApplicationType.DotBot).length > 0 &&
-
+
Available DotBots
@@ -208,7 +208,7 @@ const DotBots = ({ dotbots, updateDotbots, publishCommand, publish }) => {
{needDotBotMap && -
+
dotbot.application === ApplicationType.DotBot)} @@ -220,6 +220,7 @@ const DotBots = ({ dotbots, updateDotbots, publishCommand, publish }) => { setHistorySize={setDotbotHistorySize} mapClicked={mapClicked} mapSize={350} + areaSize={areaSize} publish={publish} />
@@ -233,7 +234,8 @@ const DotBots = ({ dotbots, updateDotbots, publishCommand, publish }) => { historySize={dotbotHistorySize} setHistorySize={setDotbotHistorySize} mapClicked={mapClicked} - mapSize={650} + mapSize={1000} + areaSize={areaSize} publish={publish} />
diff --git a/dotbot/frontend/src/DotBotsMap.js b/dotbot/frontend/src/DotBotsMap.js index 72198047..4dc768b1 100644 --- a/dotbot/frontend/src/DotBotsMap.js +++ b/dotbot/frontend/src/DotBotsMap.js @@ -1,6 +1,6 @@ import React from "react"; import { useState } from "react"; -import { ApplicationType, inactiveAddress } from "./utils/constants"; +import { ApplicationType, inactiveAddress, dotbotRadius } from "./utils/constants"; const DotBotsWaypoint = (props) => { @@ -8,8 +8,8 @@ const DotBotsWaypoint = (props) => { <> {(props.index === 0) ? ( { ) : ( <> @@ -49,8 +49,8 @@ const DotBotsPosition = (props) => { <> {(props.index === 0) ? ( { ) : ( <> { rgbColor = `rgb(${props.dotbot.rgb_led.red}, ${props.dotbot.rgb_led.green}, ${props.dotbot.rgb_led.blue})` } - const posX = props.mapSize * parseFloat(props.dotbot.lh2_position.x); - const posY = props.mapSize * parseFloat(props.dotbot.lh2_position.y); + const posX = props.mapSize * parseInt(props.dotbot.lh2_position.x) / props.areaSize.width; + const posY = props.mapSize * parseInt(props.dotbot.lh2_position.y) / props.areaSize.width; + const rotation = (props.dotbot.direction) ? props.dotbot.direction : 0; - const radius = (props.dotbot.address === props.active || hovered) ? 8: 5; + const radius = (props.dotbot.address === props.active || hovered) ? props.mapSize * (dotbotRadius + 5) / props.areaSize.width : props.mapSize * dotbotRadius / props.areaSize.width; const directionShift = (props.dotbot.address === props.active || hovered) ? 2: 1; - const directionSize = (props.dotbot.address === props.active || hovered) ? 8: 5; + const directionSize = (props.dotbot.address === props.active || hovered) ? props.mapSize * (dotbotRadius + 5) / props.areaSize.width : props.mapSize * dotbotRadius / props.areaSize.width; const opacity = `${props.dotbot.status === 0 ? "80%" : "20%"}` const waypointOpacity = `${props.dotbot.status === 0 ? "50%" : "10%"}` @@ -120,7 +121,7 @@ const DotBotsMapPoint = (props) => { color={rgbColor} opacity={waypointOpacity} waypoints={props.dotbot.waypoints} - threshold={props.dotbot.waypoints_threshold / 1000} + threshold={props.dotbot.waypoints_threshold} {...props} /> )) @@ -147,7 +148,7 @@ const DotBotsMapPoint = (props) => { onMouseLeave={onMouseLeave} > {`${props.dotbot.address}@${posX}x${posY}`} - {(props.dotbot.direction) && } + {(props.dotbot.direction) && } ) @@ -163,7 +164,7 @@ export const DotBotsMap = (props) => { const dim = event.target.getBoundingClientRect(); const x = event.clientX - dim.left; const y = event.clientY - dim.top; - props.mapClicked(x / props.mapSize, y / props.mapSize); + props.mapClicked(x * props.areaSize.width / props.mapSize, y * props.areaSize.height / props.mapSize); }; const updateDisplayGrid = (event) => { @@ -171,21 +172,22 @@ export const DotBotsMap = (props) => { }; const mapSize = props.mapSize; - const gridSize = `${mapSize + 1}px`; + const gridWidth = `${mapSize + 1}px`; + const gridHeight = `${mapSize * props.areaSize.height / props.areaSize.width + 1}px`; return (
0 ? "visible" : "invisible"}`}>
-
- +
+ - + {/* - - - - + */} + + + {/* Map grid */} diff --git a/dotbot/frontend/src/utils/constants.js b/dotbot/frontend/src/utils/constants.js index fe43a749..e47dba42 100644 --- a/dotbot/frontend/src/utils/constants.js +++ b/dotbot/frontend/src/utils/constants.js @@ -15,12 +15,11 @@ export const NotificationType = { Reload: 1, Update: 2, PinCodeUpdate: 3, - LH2CalibrationState: 4, }; export const RequestType = { DotBots: 0, - LH2CalibrationState: 1, + AreaSize: 1, }; export const inactiveAddress = "0000000000000000"; @@ -28,9 +27,11 @@ export const inactiveAddress = "0000000000000000"; export const maxWaypoints = 16; export const maxPositionHistory = 100; -export const lh2_distance_threshold = 0.01; +export const lh2_distance_threshold = 20; // 20 mm export const gps_distance_threshold = 5; // 5 meters +export const dotbotRadius = 40; // in mm + export const dotbotStatuses = ["active", "inactive", "lost"]; export const dotbotBadgeStatuses = ["success", "secondary", "danger"]; diff --git a/dotbot/models.py b/dotbot/models.py index 2a88c145..331ce9f0 100644 --- a/dotbot/models.py +++ b/dotbot/models.py @@ -82,6 +82,13 @@ class DotBotWaypoints(BaseModel): waypoints: List[Union[DotBotLH2Position, DotBotGPSPosition]] +class DotBotMapSizeModel(BaseModel): + """Map size model.""" + + width: int # in mm unit + height: int # in mm unit + + class DotBotStatus(IntEnum): """Status of a DotBot.""" @@ -134,6 +141,7 @@ class DotBotRequestType(IntEnum): """Request received from MQTT client.""" DOTBOTS: int = 0 + MAP_SIZE: int = 1 class DotBotRequestModel(BaseModel): @@ -168,9 +176,9 @@ class DotBotModel(BaseModel): lh2_position: Optional[DotBotLH2Position] = None gps_position: Optional[DotBotGPSPosition] = None waypoints: List[Union[DotBotLH2Position, DotBotGPSPosition]] = [] - waypoints_threshold: int = 40 + waypoints_threshold: int = 100 # in mm position_history: List[Union[DotBotLH2Position, DotBotGPSPosition]] = [] - calibrated: bool = False + calibrated: int = 0x00 # Bitmask: first lighthouse = 0x01, second lighthouse = 0x02 battery: float = 0.0 # Voltage in Volts diff --git a/dotbot/protocol.py b/dotbot/protocol.py index 199eda40..5b312c12 100644 --- a/dotbot/protocol.py +++ b/dotbot/protocol.py @@ -78,7 +78,7 @@ class PayloadDotBotAdvertisement(Payload): ] ) - calibrated: bool = False + calibrated: int = 0x00 # Bitmask: first lighthouse = 0x01, second lighthouse = 0x02 direction: int = 0xFFFF pos_x: int = 0xFFFFFFFF pos_y: int = 0xFFFFFFFF @@ -259,7 +259,7 @@ class PayloadLH2Waypoints(Payload): metadata: list[PayloadFieldMetadata] = dataclasses.field( default_factory=lambda: [ - PayloadFieldMetadata(name="threshold", disp="thr."), + PayloadFieldMetadata(name="threshold", disp="thr.", length=2), PayloadFieldMetadata(name="count", disp="len."), PayloadFieldMetadata(name="waypoints", type_=list, length=0), ] diff --git a/dotbot/server.py b/dotbot/server.py index 269c5be0..aab28c81 100644 --- a/dotbot/server.py +++ b/dotbot/server.py @@ -19,6 +19,7 @@ from dotbot import pydotbot_version from dotbot.logger import LOGGER from dotbot.models import ( + DotBotMapSizeModel, DotBotModel, DotBotMoveRawCommandModel, DotBotNotificationCommand, @@ -193,9 +194,9 @@ async def _dotbots_waypoints( count=len(waypoints.waypoints), waypoints=[ PayloadLH2Location( - pos_x=int(waypoint.x * 1e6), - pos_y=int(waypoint.y * 1e6), - pos_z=int(waypoint.z * 1e6), + pos_x=int(waypoint.x), + pos_y=int(waypoint.y), + pos_z=int(waypoint.z), ) for waypoint in waypoints.waypoints ], @@ -248,6 +249,18 @@ async def dotbots(query: DotBotQueryModel = Depends()): return api.controller.get_dotbots(query) +@api.get( + path="/controller/map_size", + response_model=DotBotMapSizeModel, + response_model_exclude_none=True, + summary="Return the map size of the controller", + tags=["controller"], +) +async def map_size(): + """Map size HTTP GET handler.""" + return api.controller.map_size + + @api.websocket("/controller/ws/status") async def websocket_endpoint(websocket: WebSocket): """Websocket server endpoint.""" diff --git a/dotbot/tests/test_controller_app.py b/dotbot/tests/test_controller_app.py index 9347d6f8..5069d907 100644 --- a/dotbot/tests/test_controller_app.py +++ b/dotbot/tests/test_controller_app.py @@ -42,6 +42,7 @@ Logging level. Defaults to info --log-output PATH Filename where logs are redirected --config-path FILE Path to a .toml configuration file. + -m, --map-size TEXT Map size in mm. Defaults to '2000x2000' --help Show this message and exit. """ diff --git a/dotbot/tests/test_experiment_charging_station.py b/dotbot/tests/test_experiment_charging_station.py index c9339e0f..191559c1 100644 --- a/dotbot/tests/test_experiment_charging_station.py +++ b/dotbot/tests/test_experiment_charging_station.py @@ -196,9 +196,9 @@ def fake_bot(address: str, x: float, y: float) -> DotBotModel: @patch("asyncio.sleep", new_callable=AsyncMock) async def test_queue_robots_converges_to_queue_positions(_): bots = [ - fake_bot("B", x=0.5, y=0.0), - fake_bot("A", x=0.1, y=0.0), - fake_bot("C", x=0.9, y=0.0), + fake_bot("B", x=500, y=0), + fake_bot("A", x=100, y=0), + fake_bot("C", x=900, y=0), ] client = FakeRestClient(bots) @@ -210,8 +210,8 @@ async def test_queue_robots_converges_to_queue_positions(_): # Bots should be ordered A, B, C along the queue expected = { - "A": QUEUE_HEAD_X + 0 * QUEUE_SPACING, - "B": QUEUE_HEAD_X + 1 * QUEUE_SPACING, + "B": QUEUE_HEAD_X + 0 * QUEUE_SPACING, + "A": QUEUE_HEAD_X + 1 * QUEUE_SPACING, "C": QUEUE_HEAD_X + 2 * QUEUE_SPACING, } @@ -219,11 +219,11 @@ async def test_queue_robots_converges_to_queue_positions(_): bot = client._dotbots[address] # X, Y coordinate matches queue spacing - assert math.isclose(bot.lh2_position.x, expected_x, abs_tol=0.05) - assert math.isclose(bot.lh2_position.y, QUEUE_HEAD_Y, abs_tol=0.05) + assert math.isclose(bot.lh2_position.x, expected_x, abs_tol=100) + assert math.isclose(bot.lh2_position.y, QUEUE_HEAD_Y, abs_tol=100) # Waypoints were actually sent - assert len(client.waypoint_commands) == 39 + assert len(client.waypoint_commands) @pytest.mark.asyncio @@ -255,8 +255,8 @@ async def test_charge_robots_moves_all_bots_to_parking(_): bot = client._dotbots[address] # X, Y coordinate matches queue spacing - assert math.isclose(bot.lh2_position.x, PARK_X, abs_tol=0.05) - assert math.isclose(bot.lh2_position.y, expected_y, abs_tol=0.05) + assert math.isclose(bot.lh2_position.x, PARK_X, abs_tol=100) + assert math.isclose(bot.lh2_position.y, expected_y, abs_tol=100) # LEDs were used during charging assert len(client.rgb_commands) >= 2 * len(bots) diff --git a/dotbot/tests/test_protocol.py b/dotbot/tests/test_protocol.py index 03f60790..0d84f7c1 100644 --- a/dotbot/tests/test_protocol.py +++ b/dotbot/tests/test_protocol.py @@ -192,7 +192,7 @@ def test_parse_header(bytes_, expected): ), pytest.param( b"\x04\x02\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x08" - b"\x0a\x02" + b"\x0a\x00\x02" b"\xe8\x03\x00\x00\xe8\x03\x00\x00\x02\x00\x00\x00" b"\xe8\x03\x00\x00\xe8\x03\x00\x00\x02\x00\x00\x00", Header( @@ -410,7 +410,7 @@ def test_frame_parser(bytes_, header, payload_type, payload): ) ), ), - b"\x04\x02\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x08\x0a\x02" + b"\x04\x02\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x08\x0a\x00\x02" b"\xe8\x03\x00\x00\xe8\x03\x00\x00\x02\x00\x00\x00" b"\xe8\x03\x00\x00\xe8\x03\x00\x00\x02\x00\x00\x00", id="PayloadLH2Waypoints", @@ -656,12 +656,12 @@ def test_payload_to_bytes(frame, expected): ( " +------+------+--------------------+--------------------+------+\n" " LH2_WAYPOINTS | ver. | type | dst | src | type |\n" - " (45 Bytes) | 0x04 | 0x02 | 0x1122334455667788 | 0x1222122212221221 | 0x08 |\n" + " (46 Bytes) | 0x04 | 0x02 | 0x1122334455667788 | 0x1222122212221221 | 0x08 |\n" " +------+------+--------------------+--------------------+------+\n" - " +------+------+------------+------------+------------+------------+------------+------------+\n" - " | thr. | len. | x | y | z | x | y | z |\n" - " | 0x0a | 0x02 | 0x000003e8 | 0x000003e8 | 0x00000002 | 0x000003e8 | 0x000003e8 | 0x00000002 |\n" - " +------+------+------------+------------+------------+------------+------------+------------+\n" + " +--------+------+------------+------------+------------+------------+------------+------------+\n" + " | thr. | len. | x | y | z | x | y | z |\n" + " | 0x000a | 0x02 | 0x000003e8 | 0x000003e8 | 0x00000002 | 0x000003e8 | 0x000003e8 | 0x00000002 |\n" + " +--------+------+------------+------------+------------+------------+------------+------------+\n" "\n" ), id="LH2Waypoints", diff --git a/dotbot/tests/test_server.py b/dotbot/tests/test_server.py index fd46fba7..35fb71b2 100644 --- a/dotbot/tests/test_server.py +++ b/dotbot/tests/test_server.py @@ -164,7 +164,7 @@ async def test_set_dotbots_rgb_led(dotbots, code, found): }, False, ApplicationType.DotBot, - {"threshold": 10, "waypoints": [{"x": 0.5, "y": 0.1, "z": 0}]}, + {"threshold": 100, "waypoints": [{"x": 500, "y": 100, "z": 0}]}, 200, True, id="dotbot_found", @@ -176,12 +176,12 @@ async def test_set_dotbots_rgb_led(dotbots, code, found): application=ApplicationType.DotBot, swarm="0000", last_seen=123.4, - lh2_position=DotBotLH2Position(x=0.1, y=0.5, z=0), + lh2_position=DotBotLH2Position(x=100, y=500, z=0), ), }, True, ApplicationType.DotBot, - {"threshold": 10, "waypoints": [{"x": 0.5, "y": 0.1, "z": 0}]}, + {"threshold": 100, "waypoints": [{"x": 500, "y": 100, "z": 0}]}, 200, True, id="dotbot_with_position_found", @@ -197,7 +197,7 @@ async def test_set_dotbots_rgb_led(dotbots, code, found): }, False, ApplicationType.DotBot, - {"threshold": 10, "waypoints": [{"x": 0.5, "y": 0.1, "z": 0}]}, + {"threshold": 100, "waypoints": [{"x": 500, "y": 100, "z": 0}]}, 404, False, id="dotbot_not_found", @@ -274,18 +274,18 @@ async def test_set_dotbots_waypoints( expected_waypoints = [DotBotGPSPosition(latitude=0.5, longitude=0.1)] else: # DotBot application payload = PayloadLH2Waypoints( - threshold=10, + threshold=100, count=1, - waypoints=[PayloadLH2Location(pos_x=500000, pos_y=100000, pos_z=0)], + waypoints=[PayloadLH2Location(pos_x=500, pos_y=100, pos_z=0)], ) - expected_threshold = 10 + expected_threshold = 100 if has_position is True: expected_waypoints = [ - DotBotLH2Position(x=0.1, y=0.5, z=0), - DotBotLH2Position(x=0.5, y=0.1, z=0), + DotBotLH2Position(x=100, y=500, z=0), + DotBotLH2Position(x=500, y=100, z=0), ] else: - expected_waypoints = [DotBotLH2Position(x=0.5, y=0.1, z=0)] + expected_waypoints = [DotBotLH2Position(x=500, y=100, z=0)] response = await client.put( f"/controller/dotbots/{address}/{application.value}/waypoints", @@ -658,13 +658,13 @@ def mock_async_client(*args, **kwargs): application=ApplicationType.DotBot, data=DotBotWaypoints( threshold=10, - waypoints=[DotBotLH2Position(x=0.5, y=0.1, z=0)], + waypoints=[DotBotLH2Position(x=500, y=100, z=0)], ), ), PayloadLH2Waypoints( threshold=10, count=1, - waypoints=[PayloadLH2Location(pos_x=500000, pos_y=100000, pos_z=0)], + waypoints=[PayloadLH2Location(pos_x=500, pos_y=100, pos_z=0)], ), True, id="waypoints_valid", diff --git a/simulator_init_state.toml b/simulator_init_state.toml index fa1a2679..6c09d66f 100644 --- a/simulator_init_state.toml +++ b/simulator_init_state.toml @@ -3,25 +3,25 @@ [[dotbots]] address = "BADCAFE111111111" # DotBot unique address -calibrated = true # optional, defaults to true -pos_x = 200_000 # [0, 1_000_000] -pos_y = 200_000 # [0, 1_000_000] +calibrated = 0xff # optional, defaults to only first lighthouse calibrated +pos_x = 400 # [0, 2_000] in mm +pos_y = 400 # [0, 2_000] theta = 0.0 # [0.0, 2pi] [[dotbots]] address = "DEADBEEF22222222" -pos_x = 200_000 -pos_y = 100_000 +pos_x = 400 +pos_y = 200 theta = 1.57 [[dotbots]] address = "B0B0F00D33333333" -pos_x = 1_000_000 -pos_y = 1_000_000 +pos_x = 1_500 +pos_y = 1_500 theta = 1.57 [[dotbots]] address = "BADC0DE444444444" -pos_x = 500_000 -pos_y = 500_000 +pos_x = 1_000 +pos_y = 1_000 theta = 3.14