Twin Link CampusEMS based on oneM2M

Twin Link CampusEMS is a Environments Management System bridging Meta-Sejong and the Physical Campus through the oneM2M/Mobius IoT platform.

AdvancedShowcase (no instructions)3 days64
Twin Link CampusEMS based on oneM2M

Things used in this project

Software apps and online services

Ubuntu
Ubuntu
Robot Operating System
ROS Robot Operating System
VS Code
Microsoft VS Code

Story

Read more

Code

audio_to_mobius.py

Python
import os
import time
import base64
import configparser
from datetime import datetime, timezone

from mqtt_publish import publish_cin_only


# ========= 환경 설정 =========

WATCH_DIR = os.environ.get("AUDIO_SAVE_DIR", "/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/audios/command")
os.makedirs(WATCH_DIR, exist_ok=True)

# config.ini 경로
INI_PATH = '/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/mobius/config/config.ini'
config = configparser.ConfigParser()
if not config.read(INI_PATH):
    raise FileNotFoundError(f"config.ini를 찾을 수 없습니다: {INI_PATH}")

IOTPLATFORM_IP = config['API']['IOTPLATFORM_IP']
IOTPLATFORM_HTTP_PORT = int(config['API']['IOTPLATFORM_MQTT_PORT'])

# oneM2M 경로 설정
CSE_BASE = '/Mobius'
AE_NAME = 'CampusEMS'
CNT_CHAIN = ['ControlCenter', 'Manager', 'VoiceRawData']
AE_ID = 'CAdmin'
CIN_URI = f"{CSE_BASE}/{AE_NAME}/" + "/".join(CNT_CHAIN)


# ========= 유틸 함수 =========

def encode_audio_file(filepath: str) -> str:
    with open(filepath, "rb") as f:
        raw = f.read()
    b64 = base64.b64encode(raw).decode("ascii")
    return b64


def build_payload_from_file(filepath: str) -> dict:
    filename = os.path.basename(filepath)
    stat = os.stat(filepath)

    b64_data = encode_audio_file(filepath)

    payload = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "filename": filename,
        "size_bytes": stat.st_size,
        "mime_type": "audio/wav",
        "encoding": "base64",
        "data": b64_data,
    }
    return payload


def send_file_to_mobius(filepath: str):
    payload = build_payload_from_file(filepath)
    try:
        publish_cin_only(CIN_URI, AE_ID, payload)
        print(f"[Mobius] CIN created -> {CIN_URI}")
        print(f"          filename = {payload['filename']}, size={payload['size_bytes']} bytes")
        print(f"          base64 length = {len(payload['data'])}")
    except Exception as e:
        print(f"[Mobius] CIN 전송 실패: {e}")


def watch_directory(interval: float = 1.0):
    print(f"[Watcher] watching directory: {WATCH_DIR}")
    processed = set()

    # 시작 시점에 이미 있던 파일은 processed에 넣고 시작
    for name in os.listdir(WATCH_DIR):
        if name.lower().endswith(".wav"):
            processed.add(name)

    while True:
        try:
            current = set(
                name for name in os.listdir(WATCH_DIR)
                if name.lower().endswith(".wav")
            )

            new_files = current - processed
            for name in sorted(list(new_files)):
                filepath = os.path.join(WATCH_DIR, name)
                print(f"[Watcher] new file detected: {filepath}")
                send_file_to_mobius(filepath)
                processed.add(name)

            time.sleep(interval)
        except KeyboardInterrupt:
            print("\n[Watcher] stopped by user.")
            break
        except Exception as e:
            print(f"[Watcher] error: {e}")
            time.sleep(interval)


if __name__ == "__main__":
    watch_directory(interval=1.0)

cgcs_speech_recognition.py

Python
import logging
import threading
import pyaudio
from speechtotextmodule import speech2textmodule as s2t
from intentmakermodule_headset import IntentMaker
from speech_recognition import UnknownValueError
import time
from datetime import datetime
import os
import sys
import re

# mqtt_publish 재사용
from mqtt_publish import publish_cin_only

logging.getLogger("transformers.tokenization_utils_base").setLevel(logging.ERROR)

# ------------------------------
# 로봇 이동 명령 관련 설정
# ------------------------------

ROBOT_CODE_MAP = {
    "1001": "Robot01",
    "1002": "Robot07",
}

WAYPOINTS = {
    ("Robot01", 1): {
        "header": {
            "frame_id": "map"
        },
        "pose": {
            "position": {
                "x": 12.740621566772461,
                "y": 56.33754348754883,
                "z": 0.0
            },
            "orientation": {
                "x": 0.0,
                "y": 0.0,
                "z": 0.16512340204730916,
                "w": 0.9862729146115302
            }
        }
    },
    ("Robot07", 2): {
        "header": {
            "frame_id": "map"
        },
        "pose": {
            "position": {
                "x": 57.85000114962459,
                "y": 24.740000485032798,
                "z": 0.0
            },
            "orientation": {
                "x": 0.0,
                "y": 0.0,
                "z": 0.0,
                "w": 1.0
            }
        }
    }
}
# oneM2M Originator
AE_ID = "CAdmin"

def parse_robot_command(sentence: str):
    m = re.search(r"(\d{4})\s*지점\s*(\d+)\s*로\s*이동", sentence)
    if not m:
        return None, None
    robot_code = m.group(1)
    point_id = int(m.group(2))
    return robot_code, point_id


class SuppressStderr:
    def __enter__(self):
        self.stderr_fd = sys.stderr.fileno()
        self.saved_stderr_fd = os.dup(self.stderr_fd)
        self.devnull_fd = os.open(os.devnull, os.O_WRONLY)
        os.dup2(self.devnull_fd, self.stderr_fd)

    def __exit__(self, exc_type, exc_val, exc_tb):
        os.dup2(self.saved_stderr_fd, self.stderr_fd)
        os.close(self.devnull_fd)
        os.close(self.saved_stderr_fd)


###### 2025.11.14 수정
def list_microphones():
    with SuppressStderr():
        p = pyaudio.PyAudio()
        info = p.get_host_api_info_by_index(0)
        num_devices = info.get('deviceCount')

        print("Available microphones:")
        for i in range(num_devices):
            device_info = p.get_device_info_by_host_api_device_index(0, i)
            if device_info.get('maxInputChannels') > 0:
                print(f"Index {i}: {device_info.get('name')}")

        p.terminate()


# 음성인식 시작하는 함수
def start_recognition(s2t, stop_event):
    audio = s2t.recognize_command(stop_event)
    return audio


##### 수정함(11.13)
def transcribe(s2t, audio, previous_uuid=None):
    try:
        start_time = time.time()
        sentence = s2t.speech2text(audio)
        stop_time = time.time()
        delta_time = stop_time - start_time
        print(f'시간측정: {delta_time}')

        if sentence:
            print(f"[STT] sentence: {sentence}")
            intent_maker = IntentMaker(sentence, previous_uuid, "KETI_GCS")

            try:
                device_list, command = intent_maker.get_device_list()
                intent, date_string, new_uuid = intent_maker.intent_maker(device_list, command)

            except ValueError as ve:
                print("IntentMaker에서 ValueError 발생: ", ve)
                device_list = []
                command = " "
                intent, date_string, new_uuid = intent_maker.intent_maker(device_list, command)

            except Exception as e:
                print("IntentMaker 오류: ", e)
                date_string = "%Y-%m-%dT%H:%M:%S,%f%z"
                new_uuid = None
                intent = None

            if "확인" not in sentence and "취소" not in sentence:
                intent_maker.set_previous_uuid(new_uuid)

            return sentence, date_string, new_uuid, intent

    except UnknownValueError:
        print("Speech Recognition could not understand audio")


# 음성인식을 항상 실행하는 클래스
class RecognitionHandler:
    def __init__(self, s2t, mic_index):
        with SuppressStderr():
            self.s2t = s2t(mic_index)

        self.s2t = s2t(mic_index)
        self.stop_event = threading.Event()
        self.recognition_thread = threading.Thread(target=self.recognition_worker)
        self.recognition_thread.start()
        self.results = None
        self.previous_uuid = None
        self.intent_uuid_list = [None, None]

        # 음성 파일 저장
        self.audio_save_dir = os.environ.get(
            "AUDIO_SAVE_DIR",
            "/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/audios/command"
        )
        os.makedirs(self.audio_save_dir, exist_ok=True)

    def save_audio(self, audio):
        if audio is None:
            return None

        if hasattr(audio, "get_wav_data"):
            wav_bytes = audio.get_wav_data()
        else:
            try:
                wav_bytes = bytes(audio)
            except Exception as e:
                print(f"[save_audio] audio를 bytes로 변환 실패: {e}")
                return None

        ts = datetime.now().strftime("%Y%m%dT%H%M%S_%f")
        filename = f"{ts}.wav"
        filepath = os.path.join(self.audio_save_dir, filename)

        try:
            with open(filepath, "wb") as f:
                f.write(wav_bytes)
            print(f"[save_audio] saved: {filepath}")
            return filepath
        except Exception as e:
            print(f"[save_audio] 파일 저장 실패: {e}")
            return None

    def handle_robot_command(self, sentence: str):
        robot_code, point_id = parse_robot_command(sentence)
        if not robot_code or not point_id:
            return  # 이동 명령이 아님

        robot_name = ROBOT_CODE_MAP.get(robot_code)
        if not robot_name:
            print(f"[CMD] 알 수 없는 로봇 코드: {robot_code}")
            return

        pose_content = WAYPOINTS.get((robot_name, point_id))
        if not pose_content:
            print(f"[CMD] {robot_name}의 지점 {point_id} 좌표가 정의되어 있지 않습니다.")
            return

        # mqtt_publish.crt_cin() 안에서 con으로 들어갈 데이터
        data = {
            "pose": pose_content
        }

        # mqtt_publish.crt_cin() 에 들어갈 URI
        uri = f"/Mobius/CampusEMS/Robots/{robot_name}/Control/Nav"

        print(f"[CMD] '{sentence}' → {robot_name} 지점 {point_id} 이동 CIN publish")
        print(f"[CMD] URI={uri}, AE_ID={AE_ID}")
        try:
            publish_cin_only(uri, AE_ID, data)
        except Exception as e:
            print(f"[CMD] publish_cin_only 호출 실패: {e}")

    def recognition_worker(self):
        while not self.stop_event.is_set():
            with SuppressStderr():
                audio = start_recognition(self.s2t, self.stop_event)

            if audio:
                saved_path = self.save_audio(audio)

                self.results = transcribe(self.s2t, audio, self.previous_uuid)
                if self.results:
                    self.update_intent_list(self.results)

                    # STT 결과에서 이동 명령이 있는지 확인 후 MQTT로 Nav CIN 전송
                    try:
                        sentence, date_string, new_uuid, intent = self.results
                        self.handle_robot_command(sentence)
                    except Exception as e:
                        print(f"[RecognitionHandler] handle_robot_command 오류: {e}")

    def update_intent_list(self, results):
        if results:
            sentence, date_string, new_uuid, intent = results
            if "확인" in sentence or "취소" in sentence:
                self.intent_uuid_list = [None, None]
                self.previous_uuid = None 
            else:
                self.previous_uuid = new_uuid
                self.intent_uuid_list[1] = self.intent_uuid_list[0]
                self.intent_uuid_list[0] = {
                    "sentence": sentence,
                    "date_strinRecognitionHandlerg": date_string,
                    "new_uuid": new_uuid,
                    "intent": intent
                }

# main 함수
if __name__ == "__main__":
    list_microphones()
    mic_indices = [10]
    handlers = [RecognitionHandler(s2t, mic_index) for mic_index in mic_indices]

    for handler in handlers:
        handler.recognition_thread.join()

image_to_mobius_cctv

Python
import base64
import configparser
import os
from datetime import datetime, timezone

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy
from sensor_msgs.msg import Image

try:
    from cv_bridge import CvBridge
    import cv2
except Exception as e:
    raise RuntimeError(
        "cv_bridge와 opencv-python이 필요합니다: "
        "sudo apt install ros-$ROS_DISTRO-cv-bridge && pip install opencv-python"
    ) from e

from mqtt_publish import crt_cnt, publish_cin_only
import paho.mqtt.client as mqtt


# --------------------------
# Mobius / oneM2M 설정
# --------------------------
INI_PATH = '/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/mobius/config/config.ini'

config = configparser.ConfigParser()
if not config.read(INI_PATH):
    raise FileNotFoundError(f"config.ini를 찾을 수 없습니다: {INI_PATH}")

IOTPLATFORM_IP = config['API']['IOTPLATFORM_IP']
IOTPLATFORM_MQTT_PORT = int(config['API']['IOTPLATFORM_MQTT_PORT'])

CSE_BASE = '/Mobius'
AE_NAME = 'CampusEMS'
AE_ID   = 'CAdmin'

# 카메라별 컨테이너 URI
CIN_URI_CAM1 = f"{CSE_BASE}/{AE_NAME}/CCTV/ChungmuHall/CCTV01/ImageRawData"
CIN_URI_CAM2 = f"{CSE_BASE}/{AE_NAME}/CCTV/ChungmuHall/CCTV02/ImageRawData"


# --------------------------
# ROS2 노드
# --------------------------
class ImageToMobiusNode(Node):
    def __init__(self):
        super().__init__('image_to_mobius_multi_cam')

        qos = QoSProfile(
            reliability=ReliabilityPolicy.BEST_EFFORT,
            durability=DurabilityPolicy.VOLATILE,
            history=HistoryPolicy.KEEP_LAST,
            depth=1
        )

        self.bridge = CvBridge()

        # 전송 주기 제한(Hz). 0이면 모든 프레임 전송
        self.publish_hz = float(os.environ.get('PUB_HZ', '2.0'))
        self.min_interval = 0.0 if self.publish_hz <= 0 else 1.0 / self.publish_hz

        # 카메라별 마지막 전송 시각 (초 단위)
        self.last_pub_ts = {
            "cam1": 0.0,
            "cam2": 0.0,
        }

        # 토픽 이름
        self.topic_cam1 = '/s1/cam1'
        self.topic_cam2 = '/s1/cam2'

        # 구독 등록
        self.sub_cam1 = self.create_subscription(
            Image, self.topic_cam1, self.cb_image_cam1, qos
        )
        self.sub_cam2 = self.create_subscription(
            Image, self.topic_cam2, self.cb_image_cam2, qos
        )

        self.get_logger().info(f"Subscribed: {self.topic_cam1} (CCTV01, BEST_EFFORT)")
        self.get_logger().info(f"Subscribed: {self.topic_cam2} (CCTV02, BEST_EFFORT)")
        self.get_logger().info(f"CIN_URI_CAM1 = {CIN_URI_CAM1}")
        self.get_logger().info(f"CIN_URI_CAM2 = {CIN_URI_CAM2}")

    # -------- 카메라별 콜백 -------- #

    def cb_image_cam1(self, msg: Image):
        self._process_image(msg, "cam1", CIN_URI_CAM1, cam_name="CCTV01")

    def cb_image_cam2(self, msg: Image):
        self._process_image(msg, "cam2", CIN_URI_CAM2, cam_name="CCTV02")

    # -------- 공통 처리 함수 -------- #

    def _process_image(self, msg: Image, cam_key: str, cin_uri: str, cam_name: str):
        # 전송 간격 제한 (카메라별로 독립)
        now_msg = self.get_clock().now()
        now = now_msg.seconds_nanoseconds()[0] + now_msg.seconds_nanoseconds()[1] * 1e-9

        if self.min_interval and (now - self.last_pub_ts[cam_key]) < self.min_interval:
            return
        self.last_pub_ts[cam_key] = now

        # ROS Image -> OpenCV BGR
        try:
            cv_img = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
        except Exception as e:
            self.get_logger().error(f"[{cam_name}] cv_bridge 변환 실패: {e}")
            return

        # JPEG 인코딩
        ok, enc = cv2.imencode('.jpg', cv_img, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
        if not ok:
            self.get_logger().error(f"[{cam_name}] JPEG 인코딩 실패")
            return

        b64 = base64.b64encode(enc.tobytes()).decode('ascii')

        # oneM2M cin con에 넣을 JSON
        payload = {
            "ts": datetime.now(timezone.utc).isoformat(),
            "frame_id": msg.header.frame_id,
            "height": msg.height,
            "width": msg.width,
            "encoding": "jpeg",
            "data": b64,
            "camera": cam_name,
        }

        try:
            publish_cin_only(cin_uri, AE_ID, payload)
            self.get_logger().info(
                f"[{cam_name}] cin -> {cin_uri} "
                f"(base64 length={len(b64)})"
            )
        except Exception as e:
            self.get_logger().error(f"[{cam_name}] Mobius cin publish 실패: {e}")


def main():
    rclpy.init()
    node = ImageToMobiusNode()
    try:
        rclpy.spin(node)
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == "__main__":
    main()

image_to_mobius_robot.py

Python
import base64
import configparser
import json
import os
from datetime import datetime, timezone

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy
from sensor_msgs.msg import Image

try:
    from cv_bridge import CvBridge
    import cv2
except Exception as e:
    raise RuntimeError("cv_bridge와 opencv-python이 필요합니다: sudo apt install ros-$ROS_DISTRO-cv-bridge && pip install opencv-python") from e

from mqtt_publish import crt_cnt, publish_cin_only
import paho.mqtt.client as mqtt


# --------------------------
# Mobius / oneM2M 설정
# --------------------------
INI_PATH = '/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/mobius/config/config.ini'

config = configparser.ConfigParser()
if not config.read(INI_PATH):
    raise FileNotFoundError(f"config.ini를 찾을 수 없습니다: {INI_PATH}")

IOTPLATFORM_IP = config['API']['IOTPLATFORM_IP']
IOTPLATFORM_MQTT_PORT = int(config['API']['IOTPLATFORM_MQTT_PORT'])

# CSE/AE/Container 경로
CSE_BASE = '/Mobius'
AE_NAME = 'CampusEMS'
CNT_CHAIN = ['Robots', 'Robot01', 'ImageRawData']
AE_ID = 'CAdmin'
CIN_URI = f"{CSE_BASE}/{AE_NAME}/" + "/".join(CNT_CHAIN)


# --------------------------
# ROS2 노드
# --------------------------
class ImageToMobiusNode(Node):
    def __init__(self):
        super().__init__('image_to_mobius')

        # QoS: Sensor Data (Best Effort, volatile, depth=1)
        qos = QoSProfile(
            reliability=ReliabilityPolicy.BEST_EFFORT,
            durability=DurabilityPolicy.VOLATILE,
            history=HistoryPolicy.KEEP_LAST,
            depth=1
        )

        topic_name = '/s1/rgb'
        self.bridge = CvBridge()

        # 전송 주기 제한(Hz). 0이면 모든 프레임 전송
        self.publish_hz = float(os.environ.get('PUB_HZ', '2.0'))
        self.min_interval = 0.0 if self.publish_hz <= 0 else 1.0 / self.publish_hz
        self.last_pub_ts = 0.0

        self.sub = self.create_subscription(Image, topic_name, self.cb_image, qos)
        self.get_logger().info(f"Subscribed: {topic_name} (BEST_EFFORT)")

    def cb_image(self, msg: Image):
        now = self.get_clock().now().seconds_nanoseconds()[0] + \
              self.get_clock().now().seconds_nanoseconds()[1] * 1e-9
        if self.min_interval and (now - self.last_pub_ts) < self.min_interval:
            return
        self.last_pub_ts = now

        # ROS Image -> OpenCV BGR
        try:
            cv_img = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
        except Exception as e:
            self.get_logger().error(f"cv_bridge 변환 실패: {e}")
            return

        # JPEG 인코딩
        ok, enc = cv2.imencode('.jpg', cv_img, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
        if not ok:
            self.get_logger().error("JPEG 인코딩 실패")
            return

        b64 = base64.b64encode(enc.tobytes()).decode('ascii')

        # oneM2M cin con에 넣을 JSON
        payload = {
            "ts": datetime.now(timezone.utc).isoformat(),
            "frame_id": msg.header.frame_id,
            "height": msg.height,
            "width": msg.width,
            "encoding": "jpeg",
            "data": b64
        }

        try:
            publish_cin_only(CIN_URI, AE_ID, payload)
            self.get_logger().info(f"cin -> {CIN_URI} (size={len(b64)} base64 chars)")
        except Exception as e:
            self.get_logger().error(f"Mobius cin publish 실패: {e}")


def main():
    rclpy.init()
    node = ImageToMobiusNode()
    try:
        rclpy.spin(node)
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == "__main__":
    main()

mobius_bootstrap.py

Python
import os
import uuid
import requests
import configparser
from pathlib import Path
from typing import Dict, List, Any, Optional

# ---------- Config loader ----------
def load_api_config() -> Dict[str, Optional[str]]:
    cfg_path = Path(__file__).resolve().parent / "config" / "config.ini"
    api = {}
    if cfg_path.exists():
        parser = configparser.ConfigParser(interpolation=None)
        parser.read(cfg_path)
        if parser.has_section("API"):
            sec = parser["API"]
            api = {
                "ip": sec.get("IOTPLATFORM_IP", fallback=None),
                "http_port": sec.get("IOTPLATFORM_HTTP_PORT", fallback=None),
                "mqtt_port": sec.get("IOTPLATFORM_MQTT_PORT", fallback=None),
                "http_tpl": sec.get("IOTPLATFORM_URL_HTTP", fallback="http://{}:{}"),
                "mqtt_tpl": sec.get("IOTPLATFORM_URL_MQTT", fallback="mqtt://{}/{}?ct=json"),
            }
    return api or {
        "ip": None,
        "http_port": None,
        "mqtt_port": None,
        "http_tpl": "http://{}:{}",
        "mqtt_tpl": "mqtt://{}/{}?ct=json",
    }

_API = load_api_config()

def _compose_http_base(api: Dict[str, Optional[str]]) -> Optional[str]:
    ip = api.get("ip")
    port = api.get("http_port")
    tpl = api.get("http_tpl") or "http://{}:{}"
    if ip and port:
        return tpl.format(ip, port)
    return None

_HTTP_BASE = _compose_http_base(_API)

# ---------- Effective settings (ENV > config.ini > defaults) ----------
MOBIUS_BASE_URL = os.getenv("MOBIUS_BASE_URL")
MOBIUS_ORIGIN   = os.getenv("MOBIUS_ORIGIN", "CAdmin")
FD_AE_NAME      = os.getenv("FD_AE_NAME", "CampusEMS")

def mqtt_nu(topic: str) -> str:
    ip = _API.get("ip") or "127.0.0.1"
    mport = _API.get("mqtt_port") or "1883"
    tpl = "mqtt://{}:{}/{}?ct=json"
    return tpl.format(ip, mport, topic)

# ---------- HTTP helpers ----------
HEADERS_BASE = {
    "X-M2M-Origin": MOBIUS_ORIGIN,
    "Accept": "application/json",
    "X-M2M-RVI": "3",
}

def _headers_with_type(ty: int) -> Dict[str, str]:
    h = dict(HEADERS_BASE)
    h["X-M2M-RI"] = f"ri-{uuid.uuid4().hex}"
    h["Content-Type"] = f"application/json;ty={ty}"
    return h

def _post(url: str, ty: int, payload: Dict[str, Any]) -> requests.Response:
    return requests.post(url, json=payload, headers=_headers_with_type(ty), timeout=10)

# ---------- oneM2M creators ----------
def _create_ae(cse_base_url: str, rn: str, api: str, rr: bool, poa: List[str]) -> None:
    body = {"m2m:ae": {"rn": rn, "api": api, "rr": rr, "poa": poa}}
    resp = _post(cse_base_url, ty=2, payload=body)
    if resp.status_code in (200, 201):
        print(f"[AE] created: {rn}")
    elif resp.status_code == 409:
        print(f"[AE] already exists: {rn}")
    else:
        raise RuntimeError(f"AE create failed {rn}: {resp.status_code} {resp.text}")

def _create_container(parent_url: str, rn: str, lbl: List[str] = None, mni: int = None) -> None:
    cnt: Dict[str, Any] = {"rn": rn}
    if lbl:
        cnt["lbl"] = lbl
    if mni is not None:
        cnt["mni"] = mni
    body = {"m2m:cnt": cnt}
    resp = _post(parent_url, ty=3, payload=body)
    if resp.status_code in (200, 201):
        print(f"[CNT] created: {parent_url}/{rn}")
    elif resp.status_code == 409:
        print(f"[CNT] already exists: {parent_url}/{rn}")
    else:
        raise RuntimeError(f"CNT create failed {parent_url}/{rn}: {resp.status_code} {resp.text}")
    
def _create_cin(parent_url: str, con: Any) -> None:
    body = {
        "m2m:cin": {
            "con": con,
            "cnf": "application/json"
        }
    }
    resp = _post(parent_url, ty=4, payload=body)
    if resp.status_code in (200, 201):
        print(f"[CIN] created under: {parent_url}")
    elif resp.status_code == 409:
        print(f"[CIN] already exists? {parent_url}: {resp.status_code} {resp.text}")
    else:
        raise RuntimeError(f"CIN create failed {parent_url}: {resp.status_code} {resp.text}")

def _create_subscription(parent_url: str, rn: str, enc_net: List[int], nct: int, nu: List[str]) -> None:
    body = {"m2m:sub": {"rn": rn, "enc": {"net": enc_net}, "nct": nct, "nu": nu}}
    resp = _post(parent_url, ty=23, payload=body)
    if resp.status_code in (200, 201):
        print(f"[SUB] created: {parent_url}/{rn}")
    elif resp.status_code == 409:
        print(f"[SUB] already exists: {parent_url}/{rn}")
    else:
        raise RuntimeError(f"SUB create failed {parent_url}/{rn}: {resp.status_code} {resp.text}")

def ensure_data_and_subs(parent_url: str, node: Dict[str, Any]) -> None:
    # 1) find data spec if present
    data_spec = None
    for c in node.get("cnt", []):
        if isinstance(c, dict) and c.get("rn") == "data":
            data_spec = c
            break

    if data_spec is None:
        _create_container(parent_url, "data", mni=3600)
        data_url = f"{parent_url}/data"
        subs_list = node.get("subs", [])
        for s in subs_list:
            _create_subscription(
                data_url,
                s["rn"],
                s["enc"]["net"],
                s["nct"],
                [mqtt_nu(_extract_topic(s["nu"][0]))],
            )
        return
    

    _create_container(parent_url, "data", mni=data_spec.get("mni"))
    data_url = f"{parent_url}/data"

    subs_at_parent = node.get("subs", [])
    subs_at_data = data_spec.get("subs", [])
    for s in subs_at_parent + subs_at_data:
        _create_subscription(
            data_url,
            s["rn"],
            s["enc"]["net"],
            s["nct"],
            [mqtt_nu(_extract_topic(s["nu"][0]))],
        )

def _create_subscriptions_here(parent_url: str, subs: List[Dict[str, Any]]) -> None:
    if not subs:
        return
    for s in subs:
        rn   = s["rn"]
        enc  = s.get("enc", {"net": [3]})
        nct  = s.get("nct", 2)
        nu   = s.get("nu", [])
        _create_subscription(parent_url, rn, enc["net"], nct, nu)

def build_tree_recursive(parent_url: str, nodes: List[Dict[str, Any]]) -> None:
    for node in nodes:
        rn  = node["rn"]
        lbl = node.get("lbl")
        # 1) 컨테이너 생성
        _create_container(parent_url, rn, lbl=lbl)
        here_url = f"{parent_url}/{rn}"

        # 2) 이 노드에 subs가 있으면 "현재 URL"에 SUB 생성
        if "subs" in node:
            _create_subscriptions_here(here_url, node["subs"])

        # 3) 이 노드에 cin 정의가 있으면, 여기 URL 아래에 CIN 생성
        cin_list = node.get("cin", [])
        for ci in cin_list:
            # 트리 정의에서 ci를 {"con": {...}} 형태로 넣었으니까 그걸 받아서 사용
            if isinstance(ci, dict) and "con" in ci:
                _create_cin(here_url, ci["con"])
            else:
                # 혹시 그냥 con만 넘겼으면 그대로 쓰기
                _create_cin(here_url, ci)

        # 4) 하위 cnt 재귀
        if "cnt" in node and isinstance(node["cnt"], list):
            build_tree_recursive(here_url, node["cnt"])

def _extract_topic(nu_url: str) -> str:
    try:
        if "mqtt://" in nu_url:
            # strip scheme
            rest = nu_url.split("mqtt://", 1)[1]
            # drop host
            after_host = rest.split("/", 1)[1] if "/" in rest else rest
            # drop query
            topic = after_host.split("?", 1)[0]
            return topic
        return nu_url
    except Exception:
        return nu_url

# ---------- Tree builder ----------
def build_tree(ae_url: str, tree: List[Dict[str, Any]]) -> None:
    for top in tree:
        top_rn = top["rn"]
        _create_container(ae_url, top_rn, lbl=top.get("lbl"))
        top_url = f"{ae_url}/{top_rn}"

        for child in top.get("cnt", []):
            child_rn = child["rn"]
            _create_container(top_url, child_rn, lbl=child.get("lbl"))
            child_url = f"{top_url}/{child_rn}"
            # ensure_data_and_subs(child_url, child)

# ---------- Main ----------
def main():
    _create_ae(
        cse_base_url=MOBIUS_BASE_URL,
        rn=FD_AE_NAME,
        api="N.Campus.EMS",
        rr=True,
        poa=[],
    )

    ae_url = f"{MOBIUS_BASE_URL}/{FD_AE_NAME}"

    tree = [
        {
            "rn": "CCTV",
            "lbl": ["type=camera", "cid=C"],
            "cnt": [
                {
                    "rn": "ChungmuHall",
                    "lbl": ["type=region", "rid=CH"],
                    "cnt": [
                        {
                            "rn": "CCTV01",
                            "lbl": ["type=camera", "rid=C-CH-01", "x=10.0", "y=10.0", "z=0.0", "adjx=10.0", "adjy=10.5", "adjz=0.0"],
                            "cnt": [
                                {
                                    "rn": "ImageRawData",
                                    "lbl": ["type=data", "did=C-CH-01-D"]
                                }
                            ],
                        },
                        {
                            "rn": "CCTV02",
                            "lbl": ["type=camera", "rid=C-CH-02", "x=10.0", "y=10.0", "z=0.0", "adjx=10.0", "adjy=10.5", "adjz=0.0"],
                            "cnt": [
                                {
                                    "rn": "ImageRawData",
                                    "lbl": ["type=data", "did=C-CH-02-D"]
                                }
                            ],
                        },
                    ]
                },
                {
                    "rn": "YongdukHall",
                    "lbl": ["type=region", "rid=YH"],
                    "cnt": [
                        {
                            "rn": "CCTV01",
                            "lbl": ["type=camera", "rid=C-YH-01", "x=10.0", "y=10.0", "z=0.0", "adjx=10.0", "adjy=10.5", "adjz=0.0"],
                            "cnt": [
                                {
                                    "rn": "ImageRawData",
                                    "lbl": ["type=data", "did=C-YH-01-D"]
                                }
                            ],
                        },
                        {
                            "rn": "CCTV02",
                            "lbl": ["type=camera", "rid=C-YH-02", "x=10.0", "y=10.0", "z=0.0", "adjx=10.0", "adjy=10.5", "adjz=0.0"],
                            "cnt": [
                                {
                                    "rn": "ImageRawData",
                                    "lbl": ["type=data", "did=C-YH-02-D"]
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "GwanggaetoHall",
                    "lbl": ["type=region", "rid=GH"],
                    "cnt": [
                        {
                            "rn": "CCTV01",
                            "lbl": ["type=camera", "rid=C-GH-01", "x=10.0", "y=10.0", "z=0.0", "adjx=10.0", "adjy=10.5", "adjz=0.0"],
                            "cnt": [
                                {
                                    "rn": "ImageRawData",
                                    "lbl": ["type=data", "did=C-GH-01-D"]
                                }
                            ],
                        },
                        {
                            "rn": "CCTV02",
                            "lbl": ["type=camera", "rid=C-GH-02", "x=10.0", "y=10.0", "z=0.0", "adjx=10.0", "adjy=10.5", "adjz=0.0"],
                            "cnt": [
                                {
                                    "rn": "ImageRawData",
                                    "lbl": ["type=data", "did=C-GH-02-D"]
                                }
                            ],
                        },
                    ],
                },
            ],
        },
        {
            "rn": "Robots",
            "lbl": ["type=robots", "rid=R"],
            "cnt": [
                {
                    "rn": "Robot01",
                    "lbl": ["type=robot", "region=ChungmuHall", "sid=R-CH-01"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-CH-01-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-CH-01-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot01-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-CH-01-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-CH-01-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-01-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-CH-01-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot01",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot01/camera",
                                                        "--result-topic", "/robot01/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-01-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-01-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot02",
                    "lbl": ["type=robot", "region=ChungmuHall", "sid=R-CH-02"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-CH-02-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-CH-02-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot02-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-CH-02-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-CH-02-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-02-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-CH-02-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot02",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot02/camera",
                                                        "--result-topic", "/robot02/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-02-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-02-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot03",
                    "lbl": ["type=robot", "region=ChungmuHall", "sid=R-CH-03"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-CH-03-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-CH-03-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot03-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-CH-03-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-CH-03-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-03-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-CH-03-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot03",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot03/camera",
                                                        "--result-topic", "/robot03/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-03-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-CH-03-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot04",
                    "lbl": ["type=robot", "region=YongdukHall", "sid=R-YH-04"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-YH-04-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-YH-04-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot04-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-YH-04-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-YH-04-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-04-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-YH-04-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot04",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot04/camera",
                                                        "--result-topic", "/robot04/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-04-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-04-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot05",
                    "lbl": ["type=robot", "region=YongdukHall", "sid=R-YH-05"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-YH-05-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-YH-05-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot05-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-YH-05-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-YH-05-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-05-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-YH-05-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot05",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot05/camera",
                                                        "--result-topic", "/robot05/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-05-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-05-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot06",
                    "lbl": ["type=robot", "region=YongdukHall", "sid=R-YH-06"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-YH-06-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-YH-06-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot06-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-YH-06-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-YH-06-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-06-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-YH-06-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot06",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot06/camera",
                                                        "--result-topic", "/robot06/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-06-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-YH-06-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot07",
                    "lbl": ["type=robot", "region=GwanggaetoHall", "sid=R-GH-07"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-GH-07-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-GH-07-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot07-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-GH-07-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-GH-07-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-07-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-GH-07-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot07",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot07/camera",
                                                        "--result-topic", "/robot07/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-07-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-07-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot08",
                    "lbl": ["type=robot", "region=GwanggaetoHall", "sid=R-GH-08"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-GH-08-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-GH-08-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot08-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-GH-08-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-GH-08-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-08-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-GH-08-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot08",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot08/camera",
                                                        "--result-topic", "/robot08/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-08-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-08-VLM"],
                                }
                            ],
                        },
                    ],
                },
                {
                    "rn": "Robot09",
                    "lbl": ["type=robot", "region=GwanggaetoHall", "sid=R-GH-09"],
                    "cnt": [
                        {
                            "rn": "ImageRawData",
                            "lbl": ["type=data", "did=R-GH-09-D"]
                        },
                        {
                            "rn": "Control",
                            "lbl": ["type=command", "cid=R-GH-09-C"],
                            "cnt": [
                                {
                                    "rn": "Nav",
                                    "lbl": ["type=command"],
                                    "subs": [
                                        {
                                            "rn": "Nav_sub",
                                            "enc": {"net": [3]},
                                            "nct": 2,
                                            "nu": [mqtt_nu("CampusEMS-Robots-Robot09-Control-Nav")]
                                        }
                                    ]
                                },
                            ],
                        },
                        {
                            "rn": "Report",
                            "lbl": ["type=report", "rid=R-GH-09-R"],
                        },
                        {
                            "rn": "ModelDeploymentList",
                            "lbl": ["type=modelDeploymentList", "mid=R-GH-09-MDL"],
                            "cnt": [
                                {
                                    "rn": "ObjectDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-09-ODM", "modelRef=ODM"],
                                    "cin": [
                                        {
                                            "con": {
                                                "kind": "deployment",
                                                "deploymentId": "R-GH-09-ODM",
                                                "modelRef": "ODM",
                                                "modelPath": "/CampusEMS/ModelRepo/ObjectDetectionModel",
                                                "status": "active",
                                                "version": "yolov8s-2024-11-15",
                                                "runtime": {
                                                    "host": "robot09",
                                                    "device": "cuda:0",
                                                    "num_workers": 2
                                                },
                                                "infer": {
                                                    "type": "python",
                                                    "entry": "python -m robot.perception.detector",
                                                    "args": [
                                                        "--weights", "/models/yolov8s.pt",
                                                        "--source-topic", "/robot09/camera",
                                                        "--result-topic", "/robot09/detections"
                                                    ]
                                                }
                                            }
                                        }
                                    ]
                                },
                                {
                                    "rn": "HumanDetectionModel",
                                    "lbl": ["type=modelDeployment", "mid=R-GH-09-HDM"],
                                },
                                {
                                    "rn": "VisualLocalizationModel",
...

This file has been truncated, please download it to see its full contents.

mqtt_publish.py

Python
import paho.mqtt.client as mqtt
import json
import random
import configparser

# Config 파일 로드
config = configparser.ConfigParser()
config.read('/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/mobius/config/config.ini')

# API 섹션에서 기본 변수 로드
IOTPLATFORM_IP = config['API']['IOTPLATFORM_IP']
IOTPLATFORM_MQTT_PORT = config['API']['IOTPLATFORM_MQTT_PORT']
IOTPLATFORM_URL_MQTT = config['API']['IOTPLATFORM_URL_MQTT']

def _normalize_to_for_mqtt(uri: str) -> str:
    # MQTT 경로는 'Mobius/...' 형태로
    if isinstance(uri, str) and uri.startswith('/'):
        return uri[1:]
    return uri

# cnt(container) 생성
def crt_cnt(URI, AE_ID, resourceName):
    rand = str(int(random.random()*100000)) 
    
    crt_cnt = {
                "to": URI,
                "fr": AE_ID,
                "op":1,
                "ty":3,
                "rqi": rand,
                "pc":{
                    "m2m:cnt": {
                        "rn": resourceName
                        }
                    }
            }
    return crt_cnt


# sub(subscription) 생성
def crt_sub(URI, AE_ID, resourceName):
    rand = str(int(random.random()*100000))
    crt_sub = {
                    "to": URI,
                    "fr": AE_ID,
                    "op":1,
                    "ty":23,
                    "rqi": rand,
                    "pc":{
                        "m2m:sub": {
                            "rn": resourceName,
                            "enc":{"net":[3]},
                            "nu":[IOTPLATFORM_URL_MQTT.format(IOTPLATFORM_IP, resourceName)]
                            }
                        }
                }
    return crt_sub


# cin(content instance) 생성
def crt_cin(URI, AE_ID, data):
    if isinstance(data, (dict, list)):
        data = json.dumps(data, ensure_ascii=False)
    rand = str(int(__import__('random').random()*100000))
    return {
        "to": _normalize_to_for_mqtt(URI),
        "fr": AE_ID,
        "op": 1,
        "ty": 4,
        "rqi": rand,
        "pc": {"m2m:cin": {"con": data, "cnf": "application/json"}}
    }


# 기본 MQTT 콜백 함수
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("connected OK")
    else:
        print("Bad connection Returned code=", rc)

def on_disconnect(client, userdata, flags, rc=0):
    print("Disconnected")

def on_publish(client, userdata, mid):
    print("In on_pub callback mid= ", mid)


def publishing(URI, AE_ID, resourceName = None, data = None):
    # 새로운 클라이언트 생성
    client = mqtt.Client()

    # 콜백 함수 설정 on_connect(브로커에 접속), on_disconnect(브로커에 접속중료), on_publish(메세지 발행)
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_publish = on_publish

    # ip_address : IOTPLATFORM_IP, port: IOTPLATFORM_MQTT_PORT 에 연결
    client.connect(IOTPLATFORM_IP, int(IOTPLATFORM_MQTT_PORT))

    # crt_cnt, crt_sub, crt_cin 함수를 통해 생성한 json 형식의 데이터를 string 형태로 변환
    create_cnt = json.dumps(crt_cnt(URI, AE_ID, resourceName))
    create_sub = json.dumps(crt_sub(URI, AE_ID, resourceName))
    create_cin = json.dumps(crt_cin(URI, AE_ID, data))

    # common topic 으로 메세지 발행
    client.publish('/oneM2M/req/' + AE_ID + '/Mobius2/json', create_cnt)
    client.publish('/oneM2M/req/' + AE_ID + '/Mobius2/json', create_sub)
    client.publish('/oneM2M/req/' + AE_ID + '/Mobius2/json', create_cin)

    # 연결 종료
    client.disconnect()

def publish_cin_only(URI, AE_ID, data):
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_publish = on_publish

    client.connect(IOTPLATFORM_IP, int(IOTPLATFORM_MQTT_PORT))

    # cin payload만 생성
    create_cin = json.dumps(crt_cin(URI, AE_ID, data), ensure_ascii=False)

    req_topic = f'/oneM2M/req/{AE_ID}/Mobius2/json'
    client.loop_start()
    client.publish(req_topic, create_cin)
    client.loop_stop()
    client.disconnect()


if __name__ == "__main__":
    publishing('Mobius/Meta-Sejong/Chungmu-hall/Sensor1/data', 'CAdmin', data = 'hi')

mqtt_subscribe.py

Python
import paho.mqtt.client as mqtt
import json
import configparser

# Config 파일 로드
config = configparser.ConfigParser()
config.read('/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/mobius/config/config.ini')

# API 섹션에서 기본 변수 로드
IOTPLATFORM_IP = config['API']['IOTPLATFORM_IP']
IOTPLATFORM_MQTT_PORT = config['API']['IOTPLATFORM_MQTT_PORT']
IOTPLATFORM_URL_MQTT = config['API']['IOTPLATFORM_URL_MQTT']

# 기본 MQTT 콜백 함수
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("connected OK")
    else:
        print("Bad connection Returned code=", rc)

def on_disconnect(client, userdata, flags, rc=0):
    print(str(rc))

def on_subscribe(client, userdata, mid, granted_qos):
    print("subscribed: " + str(mid) + " " + str(granted_qos))


# 서버에게서 PUBLISH 메시지를 받을 때 호출되는 콜백
def on_message(client, userdata, msg): 
    msg = msg.payload.decode("utf-8")
    msg = json.loads(msg)
    print(msg)

       
def subscribing(topic):
    # 새로운 클라이언트 생성
    client = mqtt.Client()
    
    # 콜백 함수 설정 on_connect(브로커에 접속), on_disconnect(브로커에 접속중료), on_subscribe(topic 구독), on_message(발행된 메세지가 들어왔을 때)
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_subscribe = on_subscribe
    client.on_message = on_message
    
    # ip_address : IOTPLATFORM_IP, port: IOTPLATFORM_MQTT_PORT 에 연결
    client.connect(IOTPLATFORM_IP, int(IOTPLATFORM_MQTT_PORT))
    
    # common topic 으로 메세지 발행
    client.subscribe('/oneM2M/req/+/' + topic + '/#')
    client.loop_forever() # 네트웍 트래픽을 처리, 콜백 디스패치, 재접속 등을 수행하는 블러킹 함수
                          # 멀티스레드 인터페이스나 수동 인터페이스를 위한 다른 loop*() 함수도 있음


if __name__=="__main__":
    subscribing('sub_data')

nav_robot

Python
import json
import threading
import configparser
import paho.mqtt.client as mqtt

import rclpy
from rclpy.node import Node
from rclpy.action import ActionClient

from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped, Quaternion

INI_PATH = '/home/taeram/Desktop/Digital-Twin-IoT-Anomaly-Detection/mobius/config/config.ini'

config = configparser.ConfigParser()
if not config.read(INI_PATH):
    raise FileNotFoundError(f"config.ini를 찾을 수 없습니다: {INI_PATH}")

IOTPLATFORM_IP = config['API']['IOTPLATFORM_IP']
IOTPLATFORM_MQTT_PORT = int(config['API']['IOTPLATFORM_MQTT_PORT'])
MQTT_TOPIC = "/oneM2M/req/Mobius2/CampusEMS-Robot-Robot01-Control-Nav/json"


# ============ ROS2 Nav2 브릿지 노드 ============

class NavToChungBridge(Node):
    def __init__(self):
        super().__init__("nav_to_chung_bridge")
        self._nav_client = ActionClient(self, NavigateToPose, "/navigate_to_pose")
        self.get_logger().info("NavToChungBridge node started.")

    def _parse_pose_from_con(self, con: dict) -> PoseStamped:
        pose_dict = con.get("pose", {})

        header = pose_dict.get("header", {})
        pose_inner = pose_dict.get("pose", {})

        pos = pose_inner.get("position", {})
        ori = pose_inner.get("orientation", {})

        msg = PoseStamped()
        msg.header.frame_id = header.get("frame_id", "map")
        msg.header.stamp = self.get_clock().now().to_msg()

        msg.pose.position.x = float(pos.get("x", 0.0))
        msg.pose.position.y = float(pos.get("y", 0.0))
        msg.pose.position.z = float(pos.get("z", 0.0))

        q = Quaternion()
        q.x = float(ori.get("x", 0.0))
        q.y = float(ori.get("y", 0.0))
        q.z = float(ori.get("z", 0.0))
        q.w = float(ori.get("w", 1.0))
        msg.pose.orientation = q

        return msg

    def send_nav_goal_from_con(self, con: dict):
        try:
            pose_stamped = self._parse_pose_from_con(con)
        except Exception as e:
            self.get_logger().error(f"Failed to parse pose from con: {e}")
            return

        if not self._nav_client.wait_for_server(timeout_sec=5.0):
            self.get_logger().error("navigate_to_pose action server not available.")
            return

        goal_msg = NavigateToPose.Goal()
        goal_msg.pose = pose_stamped

        self.get_logger().info(
            f"Sending Nav goal: "
            f"x={pose_stamped.pose.position.x:.3f}, "
            f"y={pose_stamped.pose.position.y:.3f}, "
            f"frame={pose_stamped.header.frame_id}"
        )

        send_future = self._nav_client.send_goal_async(goal_msg)

        def _goal_response_cb(fut):
            goal_handle = fut.result()
            if not goal_handle.accepted:
                self.get_logger().warn("Nav goal rejected.")
                return
            self.get_logger().info("Nav goal accepted.")

            result_future = goal_handle.get_result_async()

            def _result_cb(rfut):
                result = rfut.result().result
                self.get_logger().info(f"Nav result received: {result}")
            result_future.add_done_callback(_result_cb)

        send_future.add_done_callback(_goal_response_cb)


_ros_node: NavToChungBridge = None


# ============ MQTT 콜백 ============

def on_connect(client, userdata, flags, rc, properties=None):
    print(f"[MQTT] Connected rc={rc}")
    client.subscribe(MQTT_TOPIC)
    print(f"[MQTT] Subscribed: {MQTT_TOPIC}")


def on_message(client, userdata, msg):
    global _ros_node
    print(f"[MQTT] msg from {msg.topic}: {msg.payload[:200]!r}")

    try:
        payload_str = msg.payload.decode("utf-8")
        payload = json.loads(payload_str)

        # oneM2M Notify 구조에서 cin/con 뽑기
        cin = (
            payload.get("pc", {})
                   .get("m2m:sgn", {})
                   .get("nev", {})
                   .get("rep", {})
                   .get("m2m:cin", {})
        )
        con = cin.get("con")

        print("\n[DEBUG] ==== con ====")
        print(json.dumps(con, ensure_ascii=False, indent=2))
        print("====================================\n")

        if _ros_node is None:
            print("[Bridge] ROS node not ready yet.")
            return

        # 여기서 con 을 기반으로 로봇에게 네비게이션 명령 전송
        _ros_node.send_nav_goal_from_con(con)

    except Exception as e:
        print("[Bridge] on_message error:", e)


# ============ main ============

def main():
    global _ros_node

    # 1) ROS2 초기화 및 노드 시작
    rclpy.init()
    _ros_node = NavToChungBridge()

    # ROS2 스핀을 별도 스레드에서 돌림
    ros_thread = threading.Thread(target=rclpy.spin, args=(_ros_node,), daemon=True)
    ros_thread.start()

    # 2) MQTT 클라이언트 설정
    client = mqtt.Client(client_id="Robot01-NavBridge")
    client.on_connect = on_connect
    client.on_message = on_message

    client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
    print("[Bridge] MQTT loop start")

    try:
        client.loop_forever()
    except KeyboardInterrupt:
        print("Shutting down...")
    finally:
        client.disconnect()
        _ros_node.destroy_node()
        rclpy.shutdown()


if __name__ == "__main__":
    main()

TwinLinkCampusEMS

Credits

유태람
1 project • 0 followers
승연
0 projects • 0 followers
신성한
0 projects • 0 followers
Andreas Kraft
45 projects • 12 followers
IoT & connected home architect and developer. Ask me about oneM2M.
Bob Flynn
13 projects • 2 followers
SeungMyeong Jeong
45 projects • 12 followers
Miguel Angel Reina Ortega
46 projects • 7 followers
Poornima Shandilya
20 projects • 3 followers
JaeSeung Song
11 projects • 0 followers

Comments