diff --git a/dotbot/controller.py b/dotbot/controller.py index 04e4bd7a..c7563733 100644 --- a/dotbot/controller.py +++ b/dotbot/controller.py @@ -362,9 +362,15 @@ def handle_received_payload( header, PayloadType.LH2_LOCATION, LH2Location( - int(dotbot.lh2_position.x * 1e6), - int(dotbot.lh2_position.y * 1e6), - int(dotbot.lh2_position.z * 1e6), + int( + dotbot.lh2_position.x + * self.lh2_manager.calibration_data.width + ), + int( + dotbot.lh2_position.y + * self.lh2_manager.calibration_data.height + ), + int(dotbot.lh2_position.z * 1e3), ), ) ) diff --git a/dotbot/frontend/src/DotBots.test.js b/dotbot/frontend/src/DotBots.test.js index 895198b4..08c52d88 100644 --- a/dotbot/frontend/src/DotBots.test.js +++ b/dotbot/frontend/src/DotBots.test.js @@ -84,6 +84,9 @@ const server = setupServer( rest.get('/controller/lh2/calibration', (req, res, ctx) => { return res(ctx.json({state: "done"})); }), + rest.get('/controller/lh2/calibration/size', (req, res, ctx) => { + return res(ctx.json({width: "100", height: "100"})); + }), rest.put('/controller/dotbots/:address/:application/move_raw', (req, res, ctx) => { return res(); }), diff --git a/dotbot/frontend/src/DotBotsMap.js b/dotbot/frontend/src/DotBotsMap.js index 0ccc142e..1a32fbd3 100644 --- a/dotbot/frontend/src/DotBotsMap.js +++ b/dotbot/frontend/src/DotBotsMap.js @@ -4,7 +4,8 @@ import { ApplicationType } from "./constants"; import { apiFetchLH2CalibrationState, apiApplyLH2Calibration, - apiAddLH2CalibrationPoint, inactiveAddress + apiAddLH2CalibrationPoint, apiFetchLH2MapSize, apiUpdateLH2MapSize, + inactiveAddress } from "./rest"; const referencePoints = [ @@ -169,6 +170,9 @@ export const DotBotsMap = (props) => { const [ displayGrid, setDisplayGrid ] = useState(true); const [ calibrationFetched, setCalibrationFetched ] = useState(false); const [ calibrationState, setCalibrationState ] = useState("unknown"); + const [ mapSizeFetched, setMapSizeFetched ] = useState(false); + const [ mapWidth, setMapWidth ] = useState(200); + const [ mapHeight, setMapHeight ] = useState(200); const [ pointsChecked, setPointsChecked ] = useState([false, false, false, false]); const fetchCalibrationState = useCallback(async () => { @@ -178,6 +182,14 @@ export const DotBotsMap = (props) => { }, [setCalibrationFetched, setCalibrationState] ); + const fetchMapSize = useCallback(async () => { + const size = await apiFetchLH2MapSize().catch((error) => console.error(error)); + setMapWidth(size.width); + setMapHeight(size.height); + setMapSizeFetched(true); + }, [setMapSizeFetched, setMapWidth, setMapHeight] + ); + const pointClicked = (index) => { let pointsCheckedTmp = pointsChecked.slice(); pointsCheckedTmp[index] = true; @@ -191,6 +203,7 @@ export const DotBotsMap = (props) => { setCalibrationState("running"); } else if (calibrationState === "ready") { setCalibrationState("done"); + apiUpdateLH2MapSize(mapWidth, mapHeight); apiApplyLH2Calibration(); } }; @@ -215,10 +228,17 @@ export const DotBotsMap = (props) => { if (!calibrationFetched) { fetchCalibrationState(); } + if (!mapSizeFetched) { + fetchMapSize(); + } if (pointsChecked.every(v => v === true)) { setCalibrationState("ready"); } - }, [calibrationFetched, fetchCalibrationState, pointsChecked, setCalibrationState]); + }, [ + calibrationFetched, fetchCalibrationState, + mapSizeFetched, fetchMapSize, + pointsChecked, setCalibrationState + ]); let calibrationButtonLabel = "Start calibration"; let calibrationButtonClass = "btn-primary"; @@ -292,6 +312,15 @@ export const DotBotsMap = (props) => { props.setHistorySize(event.target.value)}/> +
+ + setMapWidth(event.target.value)}/> + + setMapHeight(event.target.value)}/> +
+ {/*
+ +
*/}
diff --git a/dotbot/frontend/src/rest.js b/dotbot/frontend/src/rest.js index 0e0a8709..3188fbfe 100644 --- a/dotbot/frontend/src/rest.js +++ b/dotbot/frontend/src/rest.js @@ -86,3 +86,17 @@ export const apiAddLH2CalibrationPoint = async (index) => { `${process.env.REACT_APP_DOTBOTS_BASE_URL}/controller/lh2/calibration/${index}`, ); } + +export const apiFetchLH2MapSize = async (width, height) => { + return await axios.get( + `${process.env.REACT_APP_DOTBOTS_BASE_URL}/controller/lh2/calibration/size`, + ).then(res => res.data); +} + +export const apiUpdateLH2MapSize = async (width, height) => { + return await axios.put( + `${process.env.REACT_APP_DOTBOTS_BASE_URL}/controller/lh2/calibration/size`, + {width: width, height: height}, + { headers: { 'Content-Type': 'application/json' } } + ); +} diff --git a/dotbot/lighthouse2.py b/dotbot/lighthouse2.py index 42f543da..a2857d59 100644 --- a/dotbot/lighthouse2.py +++ b/dotbot/lighthouse2.py @@ -17,7 +17,11 @@ import numpy as np from dotbot.logger import LOGGER -from dotbot.models import DotBotLH2Position, DotBotCalibrationStateModel +from dotbot.models import ( + DotBotLH2Position, + DotBotCalibrationStateModel, + DotBotCalibrationSizeModel, +) from dotbot.protocol import Lh2RawData @@ -97,6 +101,8 @@ class CalibrationData: random_rodriguez: np.array normal: np.array m: np.array + width: int = 200 + height: int = 200 class LighthouseManagerState(Enum): @@ -115,6 +121,7 @@ def __init__(self): self.state = LighthouseManagerState.NotCalibrated self.reference_points = REFERENCE_POINTS_DEFAULT Path.mkdir(CALIBRATION_DIR, exist_ok=True) + self.calibration_size = DotBotCalibrationSizeModel() self.calibration_output_path = CALIBRATION_DIR / "calibration.out" self.calibration_data = self._load_calibration() self.calibration_points = np.zeros( @@ -245,13 +252,16 @@ def compute_calibration(self): # pylint: disable=too-many-locals ) self.calibration_data = CalibrationData(zeta, random_rodriguez, n, M) - - with open(self.calibration_output_path, "wb") as output_file: - pickle.dump(self.calibration_data, output_file) + self.save_calibration() self.state = LighthouseManagerState.Calibrated self.logger.info("Calibration done", data=self.calibration_data) + def save_calibration(self): + """Store the calibration data in a file.""" + with open(self.calibration_output_path, "wb") as output_file: + pickle.dump(self.calibration_data, output_file) + def compute_position(self, raw_data: Lh2RawData) -> Optional[DotBotLH2Position]: """Compute the position coordinates from LH2 raw data and available calibration.""" if self.state != LighthouseManagerState.Calibrated: diff --git a/dotbot/models.py b/dotbot/models.py index b8d7acf7..bae8e2b5 100644 --- a/dotbot/models.py +++ b/dotbot/models.py @@ -23,6 +23,13 @@ class DotBotCalibrationStateModel(BaseModel): state: str +class DotBotCalibrationSizeModel(BaseModel): + """Model that holds the width and height of the LH2 calibration whole map.""" + + width: int = 200 # size in millimeters + height: int = 200 + + class DotBotMoveRawCommandModel(BaseModel): """Model class that defines a move raw command.""" @@ -46,6 +53,8 @@ class DotBotLH2Position(BaseModel): x: float y: float z: float + width: int = 200 + height: int = 200 class DotBotControlModeModel(BaseModel): diff --git a/dotbot/protocol.py b/dotbot/protocol.py index 9e83bf8e..81372c45 100644 --- a/dotbot/protocol.py +++ b/dotbot/protocol.py @@ -11,7 +11,7 @@ from dataclasses import dataclass -PROTOCOL_VERSION = 8 +PROTOCOL_VERSION = 9 class PayloadType(Enum): @@ -208,17 +208,17 @@ class LH2Location(ProtocolData): @property def fields(self) -> List[ProtocolField]: return [ - ProtocolField(self.pos_x, name="x", length=4), - ProtocolField(self.pos_y, name="y", length=4), - ProtocolField(self.pos_z, name="z", length=4), + ProtocolField(self.pos_x, name="x", length=2), + ProtocolField(self.pos_y, name="y", length=2), + ProtocolField(self.pos_z, name="z", length=2), ] @staticmethod def from_bytes(bytes_) -> ProtocolData: return LH2Location( - int.from_bytes(bytes_[0:4], "little"), - int.from_bytes(bytes_[4:8], "little"), - int.from_bytes(bytes_[8:12], "little"), + pos_x=int.from_bytes(bytes_[0:2], "little"), + pos_y=int.from_bytes(bytes_[2:4], "little"), + pos_z=int.from_bytes(bytes_[4:6], "little"), ) @@ -402,7 +402,7 @@ def from_bytes(bytes_: bytes): elif payload_type == PayloadType.LH2_RAW_DATA: values = Lh2RawData.from_bytes(bytes_[25:45]) elif payload_type == PayloadType.LH2_LOCATION: - values = LH2Location.from_bytes(bytes_[25:37]) + values = LH2Location.from_bytes(bytes_[25:31]) elif payload_type == PayloadType.ADVERTISEMENT: values = Advertisement.from_bytes(None) elif payload_type == PayloadType.GPS_POSITION: diff --git a/dotbot/server.py b/dotbot/server.py index bdd02f7f..7d5b54aa 100644 --- a/dotbot/server.py +++ b/dotbot/server.py @@ -13,6 +13,7 @@ from dotbot.logger import LOGGER from dotbot.models import ( DotBotCalibrationStateModel, + DotBotCalibrationSizeModel, DotBotModel, DotBotQueryModel, DotBotAddressModel, @@ -312,6 +313,31 @@ async def controller_get_lh2_calibration(): return app.controller.lh2_manager.state_model +@app.put( + path="/controller/lh2/calibration/size", + summary="Set the size of the LH2 map.", + tags=["dotbots"], +) +async def controller_set_lh2_size(size: DotBotCalibrationSizeModel): + """Set the size of the LH2 map.""" + app.controller.lh2_manager.calibration_data.width = size.width + app.controller.lh2_manager.calibration_data.height = size.height + + +@app.get( + path="/controller/lh2/calibration/size", + response_model=DotBotCalibrationSizeModel, + summary="Get the size of the LH2 map.", + tags=["dotbots"], +) +async def controller_lh2_size(): + """Set the width of the LH2 map.""" + return DotBotCalibrationSizeModel( + width=app.controller.lh2_manager.calibration_data.width, + height=app.controller.lh2_manager.calibration_data.height, + ) + + @app.websocket("/controller/ws/status") async def websocket_endpoint(websocket: WebSocket): """Websocket server endpoint.""" diff --git a/dotbot/tests/test_protocol.py b/dotbot/tests/test_protocol.py index 894d27f6..fa40314d 100644 --- a/dotbot/tests/test_protocol.py +++ b/dotbot/tests/test_protocol.py @@ -27,7 +27,7 @@ "payload,expected", [ pytest.param( - b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x08\x00\x00\x00\x00\x00\x00\x42\x00\x42", + b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x09\x00\x00\x00\x00\x00\x00\x42\x00\x42", ProtocolPayload( ProtocolHeader( 0x1122221111111111, @@ -43,7 +43,7 @@ id="MoveRaw", ), pytest.param( - b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x08\x00\x00\x00\x00\x01\x42\x42\x42", + b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x09\x00\x00\x00\x00\x01\x42\x42\x42", ProtocolPayload( ProtocolHeader( 0x1122221111111111, @@ -59,7 +59,7 @@ id="RGBLed", ), pytest.param( - b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x08\x00\x00\x00\x00\x02" + b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x09\x00\x00\x00\x00\x02" b"\x12\x34\x56\x78\x9a\xbc\xde\xf1\x01\x02" b"\x12\x34\x56\x78\x9a\xbc\xde\xf1\x01\x02", ProtocolPayload( @@ -82,8 +82,8 @@ id="LH2RawData", ), pytest.param( - b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x08\x00\x00\x00\x00\x03" - b"\xe8\x03\x00\x00\xe8\x03\x00\x00\x02\x00\x00\x00", + b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x09\x00\x00\x00\x00\x03" + b"\xe8\x03\xe8\x03\x02\x00", ProtocolPayload( ProtocolHeader( 0x1122221111111111, @@ -99,7 +99,7 @@ id="LH2Location", ), pytest.param( - b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x08\x00\x00\x00\x00\x04", + b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x09\x00\x00\x00\x00\x04", ProtocolPayload( ProtocolHeader( 0x1122221111111111, @@ -115,7 +115,7 @@ id="Advertisement", ), pytest.param( - b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x08\x00\x00\x00\x00\x05" + b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x09\x00\x00\x00\x00\x05" b"&~\xe9\x02]\xe4#\x00", ProtocolPayload( ProtocolHeader( @@ -132,7 +132,7 @@ id="GPSPosition", ), pytest.param( - b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x08\x00\x00\x00\x00\x06" + b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x09\x00\x00\x00\x00\x06" b"-\x00" b"\x12\x34\x56\x78\x9a\xbc\xde\xf1\x01\x02" b"\x12\x34\x56\x78\x9a\xbc\xde\xf1\x01\x02", @@ -157,7 +157,7 @@ id="DotBotData", ), pytest.param( - b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x08\x00\x00\x00\x00\x07\x01", + b"\x11\x11\x11\x11\x11\x22\x22\x11\x12\x12\x12\x12\x12\x12\x12\x12\x34\x12\x00\x09\x00\x00\x00\x00\x07\x01", ProtocolPayload( ProtocolHeader( 0x1122221111111111, @@ -173,9 +173,8 @@ id="ControlMode", ), pytest.param( - b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x08\x00\x00\x00\x00\x08\x02\x0a" - 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", + b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x09\x00\x00\x00\x00\x08\x02\x0a" + b"\xe8\x03\xe8\x03\x02\x00\xe8\x03\xe8\x03\x02\x00", ProtocolPayload( ProtocolHeader( 0x1122334455667788, @@ -191,7 +190,7 @@ id="LH2Waypoints", ), pytest.param( - b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x08\x00\x00\x00\x00\x09\x02\x0a" + b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x09\x00\x00\x00\x00\x09\x02\x0a" b"&~\xe9\x02]\xe4#\x00&~\xe9\x02]\xe4#\x00", ProtocolPayload( ProtocolHeader( @@ -208,7 +207,7 @@ id="GPSWaypoints", ), pytest.param( - b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x08\x00\x00\x00\x00\x0a" + b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x09\x00\x00\x00\x00\x0a" b"-\x00&~\xe9\x02]\xe4#\x00", ProtocolPayload( ProtocolHeader( @@ -225,12 +224,12 @@ id="SailBotData", ), pytest.param( - b"\x11\x22\x22\x11\x11\x11\x11\x11\x12\x12\x12\x12\x12\x12\x12\x12\x00\x00\x00\x08\x00\x00\x00\x00\xff", + b"\x11\x22\x22\x11\x11\x11\x11\x11\x12\x12\x12\x12\x12\x12\x12\x12\x00\x00\x00\x09\x00\x00\x00\x00\xff", ValueError("255 is not a valid PayloadType"), id="invalid payload", ), pytest.param( - b"\x11\x22\x22\x11\x11\x11\x11\x11\x12\x12\x12\x12\x12\x12\x12\x12\x00\x00\x00\x08\x00\x00\x00\x00\x0b", + b"\x11\x22\x22\x11\x11\x11\x11\x11\x12\x12\x12\x12\x12\x12\x12\x12\x00\x00\x00\x09\x00\x00\x00\x00\x0b", ProtocolPayloadParserException("Unsupported payload type '11'"), id="unsupported payload type", ), @@ -333,7 +332,7 @@ def test_protocol_parser(payload, expected): LH2Location(1000, 1000, 2), ), b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x01\x00\x00\x00\x00\x03" - b"\xe8\x03\x00\x00\xe8\x03\x00\x00\x02\x00\x00\x00", + b"\xe8\x03\xe8\x03\x02\x00", id="LH2Location", ), pytest.param( @@ -392,8 +391,7 @@ def test_protocol_parser(payload, expected): ), ), b"\x88\x77\x66\x55\x44\x33\x22\x11\x21\x12\x22\x12\x22\x12\x22\x12\x42\x14\x00\x01\x00\x00\x00\x00\x08\x02\x0a" - 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", + b"\xe8\x03\xe8\x03\x02\x00\xe8\x03\xe8\x03\x02\x00", id="LH2Waypoints", ), pytest.param( @@ -495,14 +493,10 @@ def test_payload(payload, expected): LH2Location(1000, 1000, 2), ), ( - " +----------------------------------+----------------------------------+----------+------+------+------------------+------+\n" - " LH2_LOCATION | dst | src | swarm id | app. | ver. | msg id | type |\n" - " (37 Bytes) | 0x1122334455667788 | 0x1222122212221221 | 0x2442 | 0x00 | 0x01 | 0x00000000 | 0x03 |\n" - " +----------------------------------+----------------------------------+----------+------+------+------------------+------+\n" - " +------------------+------------------+------------------+\n" - " | x | y | z |\n" - " | 0x000003e8 | 0x000003e8 | 0x00000002 |\n" - " +------------------+------------------+------------------+\n" + " +----------------------------------+----------------------------------+----------+------+------+------------------+------+----------+----------+----------+\n" + " LH2_LOCATION | dst | src | swarm id | app. | ver. | msg id | type | x | y | z |\n" + " (31 Bytes) | 0x1122334455667788 | 0x1222122212221221 | 0x2442 | 0x00 | 0x01 | 0x00000000 | 0x03 | 0x03e8 | 0x03e8 | 0x0002 |\n" + " +----------------------------------+----------------------------------+----------+------+------+------------------+------+----------+----------+----------+\n" "\n" ), id="LH2Location", @@ -593,12 +587,12 @@ def test_payload(payload, expected): ( " +----------------------------------+----------------------------------+----------+------+------+------------------+------+\n" " LH2_WAYPOINTS | dst | src | swarm id | app. | ver. | msg id | type |\n" - " (51 Bytes) | 0x1122334455667788 | 0x1222122212221221 | 0x2442 | 0x00 | 0x01 | 0x00000000 | 0x08 |\n" + " (39 Bytes) | 0x1122334455667788 | 0x1222122212221221 | 0x2442 | 0x00 | 0x01 | 0x00000000 | 0x08 |\n" " +----------------------------------+----------------------------------+----------+------+------+------------------+------+\n" - " +------+------+------------------+------------------+------------------+------------------+------------------+------------------+\n" - " | len. | thr. | x | y | z | x | y | z |\n" - " | 0x02 | 0x0a | 0x000003e8 | 0x000003e8 | 0x00000002 | 0x000003e8 | 0x000003e8 | 0x00000002 |\n" - " +------+------+------------------+------------------+------------------+------------------+------------------+------------------+\n" + " +------+------+----------+----------+----------+----------+----------+----------+\n" + " | len. | thr. | x | y | z | x | y | z |\n" + " | 0x02 | 0x0a | 0x03e8 | 0x03e8 | 0x0002 | 0x03e8 | 0x03e8 | 0x0002 |\n" + " +------+------+----------+----------+----------+----------+----------+----------+\n" "\n" ), id="LH2Waypoints", diff --git a/dotbot/tests/test_server.py b/dotbot/tests/test_server.py index f3d20b89..d42bd3be 100644 --- a/dotbot/tests/test_server.py +++ b/dotbot/tests/test_server.py @@ -14,6 +14,7 @@ DotBotMoveRawCommandModel, DotBotRgbLedCommandModel, DotBotCalibrationStateModel, + DotBotCalibrationSizeModel, DotBotControlModeModel, DotBotGPSPosition, DotBotLH2Position, @@ -49,6 +50,9 @@ def controller(): app.controller.lh2_manager = MagicMock() app.controller.lh2_manager.state_model = DotBotCalibrationStateModel(state="test") app.controller.notify_clients = AsyncMock() + app.controller.lh2_manager.calibration_data = MagicMock() + app.controller.lh2_manager.calibration_data.width = 100 + app.controller.lh2_manager.calibration_data.height = 100 app.controller.send_payload = MagicMock() app.controller.settings = MagicMock() app.controller.settings.gw_address = "0000" @@ -684,6 +688,24 @@ async def test_lh2_calibration(): calibration.assert_called_once() +@pytest.mark.asyncio +async def test_lh2_calibration_size(): + response = await client.get("/controller/lh2/calibration/size") + assert ( + response.json() + == DotBotCalibrationSizeModel(width=100, height=100).model_dump() + ) + assert response.status_code == 200 + + response = await client.put( + "/controller/lh2/calibration/size", + json=DotBotCalibrationSizeModel(width=300, height=400).model_dump(), + ) + assert response.status_code == 200 + assert app.controller.lh2_manager.calibration_data.width == 300 + assert app.controller.lh2_manager.calibration_data.height == 400 + + @pytest.mark.asyncio async def test_ws_client(): with TestClient(app).websocket_connect("/controller/ws/status") as websocket: diff --git a/tox.ini b/tox.ini index 26e30de7..78241dde 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = tests,check +envlist = check,tests skip_missing_interpreters = true isolated_build = true