返回教程正文

配套源码

main.py

app/python/main.py
ROS 2 与 App 通讯 01:用四组 LED 跑通双向链路app/python/main.py
Python261 行
"""Connect the protocol-neutral WebSocket Brick to the MCU LED RPC."""

import json
import queue
import time

from arduino.app_utils import App, Bridge
from websocket_server import WebSocketServer


LED_COMMAND_TOPIC = "/my_ros2_02/led_command"
LED_STATE_TOPIC = "/my_ros2_02/led_state"
LED_MESSAGE_TYPE = "std_msgs/msg/UInt8MultiArray"
STRING_MESSAGE_TYPE = "std_msgs/msg/String"
ALL_OFF = (0, 0, 0, 0)

server = WebSocketServer()
events = queue.Queue(maxsize=32)
current_leds = ALL_OFF
mcu_ready = False
last_server_status = None


def make_envelope(direction, topic, ros_type, sequence, data):
    """Build one JSON-safe ROS WebSocket envelope."""
    return {
        "direction": direction,
        "topic": topic,
        "ros_type": ros_type,
        "seq": int(sequence),
        "timestamp": int(time.time() * 1000),
        "data": data,
    }


def send_envelope(client_id, message):
    """Encode and send one text-frame envelope."""
    payload = json.dumps(message, ensure_ascii=False, separators=(",", ":"))
    return server.send(client_id, payload)


def send_error(client_id, sequence, error_text):
    """Return an error as a normal ROS String envelope."""
    message = make_envelope(
        "ws_to_ros",
        "/my_ros2_02/error",
        STRING_MESSAGE_TYPE,
        sequence,
        {"data": str(error_text)},
    )
    send_envelope(client_id, message)


def mask_to_leds(mask):
    """Convert the MCU bit mask into four LED values."""
    if not isinstance(mask, int) or isinstance(mask, bool):
        raise ValueError("get_leds must return an integer")
    if mask < 0 or mask > 15:
        raise ValueError("get_leds returned a value outside 0..15")
    return tuple((mask >> index) & 1 for index in range(4))


def set_leds_on_mcu(leds):
    """Send four LED values to the MCU and read back the result."""
    values = tuple(int(value) for value in leds)
    if len(values) != 4 or any(value not in (0, 1) for value in values):
        raise ValueError("LED command must contain four 0/1 values")

    accepted = Bridge.call(
        "set_leds",
        bool(values[0]),
        bool(values[1]),
        bool(values[2]),
        bool(values[3]),
        timeout=3,
    )
    if accepted is False:
        raise RuntimeError("MCU rejected set_leds")
    return mask_to_leds(Bridge.call("get_leds", timeout=3))


def validate_envelope(message):
    """Validate the common fields and return the sequence number."""
    if not isinstance(message, dict):
        raise ValueError("message must be a JSON object")

    if message.get("direction") != "ros_to_ws":
        raise ValueError("direction must be ros_to_ws")

    if not isinstance(message.get("topic"), str):
        raise ValueError("topic must be a string")

    if not isinstance(message.get("ros_type"), str):
        raise ValueError("ros_type must be a string")

    sequence = message.get("seq")
    if (
        type(sequence) is not int
        or sequence < 0
    ):
        raise ValueError("seq must be a non-negative integer")

    timestamp = message.get("timestamp")
    if (
        type(timestamp) is not int
        or timestamp <= 0
    ):
        raise ValueError(
            "timestamp must be a positive Unix millisecond integer"
        )

    if not isinstance(message.get("data"), dict):
        raise ValueError(
            "data must contain the serialized ROS message object"
        )

    return sequence


def process_text_message(client_id, payload):
    """Decode one text frame and dispatch the ROS message by topic and type."""
    global current_leds

    message = json.loads(payload)
    sequence = validate_envelope(message)
    print(
        f"Received topic={message['topic']} ros_type={message['ros_type']} "
        f"seq={sequence}",
        flush=True,
    )

    if (
        message["topic"] != LED_COMMAND_TOPIC
        or message["ros_type"] != LED_MESSAGE_TYPE
    ):
        raise ValueError(
            f"unsupported route: {message['topic']} ({message['ros_type']})"
        )

    current_leds = set_leds_on_mcu(message["data"].get("data"))
    response = make_envelope(
        "ws_to_ros",
        LED_STATE_TOPIC,
        LED_MESSAGE_TYPE,
        sequence,
        {
            "layout": {"dim": [], "data_offset": 0},
            "data": list(current_leds),
        },
    )
    send_envelope(client_id, response)
    print(f"LED state: {list(current_leds)}", flush=True)


def enqueue_event(event):
    """Move a short Brick callback event into the App main loop."""
    try:
        events.put_nowait(event)
    except queue.Full:
        print("WebSocket event queue is full; event dropped", flush=True)


def handle_connect(client_info):
    """Queue a client connection event."""
    enqueue_event(("connect", client_info))


def handle_message(client_id, payload):
    """Queue raw text or binary payload without blocking the Brick thread."""
    enqueue_event(("message", client_id, payload))


def handle_disconnect(client_info, code, reason):
    """Queue a client disconnection event."""
    enqueue_event(("disconnect", client_info, code, reason))


def process_event(event):
    """Process one queued Brick event in the App main loop."""
    event_type = event[0]
    if event_type == "connect":
        client_info = event[1]
        print(f"WebSocket connected: {client_info['client_id']}", flush=True)
        return

    if event_type == "disconnect":
        global current_leds
        client_info, code, reason = event[1:]
        print(
            f"WebSocket disconnected: {client_info['client_id']} "
            f"code={code} reason={reason}",
            flush=True,
        )
        try:
            current_leds = set_leds_on_mcu(ALL_OFF)
        except Exception as error:
            print(f"Could not turn LEDs off: {error}", flush=True)
        return

    client_id, payload = event[1:]
    if not isinstance(payload, str):
        send_error(client_id, 0, "binary frames are not used by this ROS lesson")
        return

    sequence = 0
    try:
        decoded = json.loads(payload)
        if isinstance(decoded, dict) and isinstance(decoded.get("seq"), int):
            sequence = decoded["seq"]
        process_text_message(client_id, payload)
    except Exception as error:
        print(f"Message rejected: {error}", flush=True)
        send_error(client_id, sequence, error)


server.on_connect(handle_connect)
server.on_message(handle_message)
server.on_disconnect(handle_disconnect)


def loop():
    """Initialize the MCU, process WebSocket events, and report server state."""
    global current_leds
    global last_server_status
    global mcu_ready

    if not mcu_ready:
        try:
            current_leds = set_leds_on_mcu(ALL_OFF)
            mcu_ready = True
            print(f"MCU ready: {list(current_leds)}", flush=True)
        except Exception as error:
            print(f"MCU is not ready: {error}", flush=True)
            time.sleep(1)
            return

    for _ in range(8):
        try:
            event = events.get_nowait()
        except queue.Empty:
            break
        process_event(event)

    status = server.get_status()
    status_snapshot = (
        status["listening"],
        status["client_count"],
        status["server_error"],
    )
    if status_snapshot != last_server_status:
        print(
            f"WebSocket listening={status['listening']} "
            f"clients={status['client_count']} error={status['server_error']}",
            flush=True,
        )
        last_server_status = status_snapshot

    time.sleep(0.05)


App.run(user_loop=loop)