commit 94f0c112e51dd573e21b922c89cc1fe0b7d9c2ad
Author: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
Date: Tue Sep 2 16:39:44 2025 +0800
init version
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
new file mode 100644
index 0000000..192a9a0
--- /dev/null
+++ b/.bumpversion.cfg
@@ -0,0 +1,10 @@
+[bumpversion]
+current_version = 0.0.2
+commit = True
+tag = True
+tag_name = v{new_version}
+message = Bump version: {current_version} → {new_version}
+
+[bumpversion:file:msgcenterpy/__init__.py]
+search = __version__ = "{current_version}"
+replace = __version__ = "{new_version}"
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a9b2a40
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,37 @@
+version: 2
+updates:
+ # Python dependencies
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "06:00"
+ open-pull-requests-limit: 10
+ reviewers:
+ - "msgcenterpy-team"
+ assignees:
+ - "msgcenterpy-team"
+ labels:
+ - "dependencies"
+ - "python"
+ commit-message:
+ prefix: "deps"
+ include: "scope"
+
+ # GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "06:00"
+ open-pull-requests-limit: 5
+ reviewers:
+ - "msgcenterpy-team"
+ labels:
+ - "dependencies"
+ - "github-actions"
+ commit-message:
+ prefix: "ci"
+ include: "scope"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ad61cb8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,224 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
+
+name: Python package
+
+on:
+ push:
+ branches: ["main", "dev"]
+ pull_request:
+ branches: ["main", "dev"]
+
+jobs:
+ # Step 1: Code formatting and pre-commit validation (fast failure)
+ code-format:
+ name: Code formatting and pre-commit validation
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10" # Use minimum version for consistency
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .[dev]
+
+ - name: Run pre-commit hooks
+ uses: pre-commit/action@v3.0.1
+ with:
+ extra_args: --all-files
+
+ # Step 2: Basic build and test with minimum Python version (3.10)
+ basic-build:
+ name: Basic build (Python 3.10, Ubuntu)
+ runs-on: ubuntu-latest
+ needs: [code-format] # Only run after code formatting passes
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Cache pip dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
+ restore-keys: |
+ ubuntu-pip-3.10-
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install flake8 pytest
+ pip install -e .[dev]
+
+ - name: Test with pytest
+ run: |
+ pytest
+
+ # Step 3: ROS2 integration test
+ test-with-ros2:
+ name: ROS2 integration test
+ runs-on: ubuntu-latest
+ needs: [basic-build] # Only run after basic build passes
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Miniconda
+ uses: conda-incubator/setup-miniconda@v3
+ with:
+ miniconda-version: "latest"
+ channels: conda-forge,robostack-staging,defaults
+ channel-priority: strict
+ activate-environment: ros2-test-env
+ python-version: "3.11.11"
+ auto-activate-base: false
+ auto-update-conda: false
+ show-channel-urls: true
+
+ - name: Install ROS2 and dependencies
+ shell: bash -l {0}
+ run: |
+ # Install ROS2 core packages
+ conda install -y \
+ ros-humble-ros-core \
+ ros-humble-std-msgs \
+ ros-humble-geometry-msgs
+
+ - name: Install package and run tests
+ shell: bash -l {0}
+ run: |
+ # Install our package with basic dependencies (not ros2 extra to avoid conflicts)
+ pip install -e .[dev]
+
+ # Run all tests with verbose output (ROS2 tests will be automatically included)
+ python -c "import rclpy, rosidl_runtime_py; print('All ROS2 dependencies available')"
+ pytest -v
+
+ # Step 4: Security scan
+ security:
+ name: Security scan
+ runs-on: ubuntu-latest
+ needs: [basic-build] # Run in parallel with ROS2 test after basic build
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10" # Use minimum version for consistency
+
+ - name: Install security tools
+ run: |
+ python -m pip install --upgrade pip
+ pip install bandit "safety>=3.0.0" "typer<0.12.0" "marshmallow<4.0.0"
+
+ - name: Run bandit security scan
+ run: bandit -r msgcenterpy/ -f json -o bandit-report.json
+
+ - name: Run safety security scan
+ run: safety check --output json > safety-report.json
+
+ - name: Upload security reports
+ uses: actions/upload-artifact@v4
+ with:
+ name: security-reports
+ path: |
+ bandit-report.json
+ safety-report.json
+ if: always()
+
+ # Step 5: Package build check
+ package-build:
+ name: Package build check
+ runs-on: ubuntu-latest
+ needs: [basic-build] # Run in parallel with other checks
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10" # Use minimum version for consistency
+
+ - name: Install build dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build twine
+
+ - name: Build package
+ run: python -m build
+
+ - name: Check package
+ run: twine check dist/*
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
+
+ # Step 6: Full matrix build (only after all basic checks pass)
+ full-matrix-build:
+ name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ needs: [test-with-ros2, security, package-build] # Wait for all prerequisite checks
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
+ exclude:
+ # Skip the combination we already tested in basic-build
+ - os: ubuntu-latest
+ python-version: "3.10"
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Cache pip dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-
+ ${{ runner.os }}-pip-
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install flake8 pytest
+ pip install -e .[dev]
+
+ - name: Lint with flake8
+ run: |
+ # stop the build if there are Python syntax errors or undefined names
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ flake8 . --count --exit-zero --max-line-length=200 --extend-ignore=E203,W503,F401,E402,E721,F841 --statistics
+
+ - name: Type checking with mypy
+ run: |
+ mypy msgcenterpy --disable-error-code=unused-ignore
+
+ - name: Test with pytest
+ run: |
+ pytest
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..f91f110
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,69 @@
+name: Build and Deploy Documentation
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ # Build documentation
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ # Install package in development mode to get version info
+ pip install -e .
+ # Install documentation dependencies
+ pip install -r docs/requirements.txt
+
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v4
+ if: github.ref == 'refs/heads/main'
+
+ - name: Build Sphinx documentation
+ run: |
+ cd docs
+ make html
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ if: github.ref == 'refs/heads/main'
+ with:
+ path: docs/_build/html
+
+ # Deploy to GitHub Pages (only on main branch)
+ deploy:
+ if: github.ref == 'refs/heads/main'
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..fd6812d
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,258 @@
+# This workflow will upload a Python Package to PyPI when a release is created
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Upload Python Package
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+ inputs:
+ test_pypi:
+ description: "Publish to Test PyPI instead of PyPI"
+ required: false
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+
+jobs:
+ code-format:
+ name: Code formatting and pre-commit validation
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .[dev]
+
+ - name: Run pre-commit hooks
+ uses: pre-commit/action@v3.0.1
+ with:
+ extra_args: --all-files
+
+ basic-build:
+ name: Basic build and test
+ runs-on: ubuntu-latest
+ needs: [code-format]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+
+ - name: Cache pip dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
+ restore-keys: |
+ ubuntu-pip-3.10-
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install flake8 pytest
+ pip install -e .[dev]
+
+ - name: Test with pytest
+ run: |
+ pytest -v
+
+ - name: Run linting
+ run: |
+ black --check --line-length=120 msgcenterpy tests
+ isort --check-only msgcenterpy tests
+ mypy msgcenterpy --disable-error-code=unused-ignore
+
+ test-with-ros2:
+ name: ROS2 integration test
+ runs-on: ubuntu-latest
+ needs: [basic-build]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Miniconda
+ uses: conda-incubator/setup-miniconda@v3
+ with:
+ miniconda-version: "latest"
+ channels: conda-forge,robostack-staging,defaults
+ channel-priority: strict
+ activate-environment: ros2-test-env
+ python-version: "3.11.11"
+ auto-activate-base: false
+ auto-update-conda: false
+ show-channel-urls: true
+
+ - name: Install ROS2 and dependencies
+ shell: bash -l {0}
+ run: |
+ conda install -y \
+ ros-humble-ros-core \
+ ros-humble-std-msgs \
+ ros-humble-geometry-msgs
+
+ - name: Install package and run tests
+ shell: bash -l {0}
+ run: |
+ pip install -e .[dev]
+ python -c "import rclpy, rosidl_runtime_py; print('All ROS2 dependencies available')"
+ pytest -v
+
+ release-build:
+ name: Build release distributions
+ runs-on: ubuntu-latest
+ needs: [test-with-ros2]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - name: Install build dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install build twine check-manifest
+
+ - name: Verify version consistency
+ if: github.event_name == 'release'
+ run: |
+ VERSION=$(python -c "import msgcenterpy; print(msgcenterpy.__version__)" 2>/dev/null || echo "unknown")
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
+ if [ "$VERSION" != "$TAG_VERSION" ]; then
+ echo "Version mismatch: package=$VERSION, tag=$TAG_VERSION"
+ exit 1
+ fi
+
+ - name: Check manifest
+ run: check-manifest
+
+ - name: Build release distributions
+ run: |
+ python -m build
+
+ - name: Check package
+ run: |
+ twine check dist/*
+
+ - name: Upload distributions
+ uses: actions/upload-artifact@v4
+ with:
+ name: release-dists
+ path: dist/
+
+ pypi-publish:
+ name: Publish to PyPI
+ runs-on: ubuntu-latest
+ needs:
+ - release-build
+ if: github.event_name == 'release' && !github.event.release.prerelease && github.event.inputs.test_pypi != 'true'
+ permissions:
+ # IMPORTANT: this permission is mandatory for trusted publishing
+ id-token: write
+
+ # Note: For enhanced security, consider configuring deployment environments
+ # in your GitHub repository settings with protection rules
+
+ steps:
+ - name: Retrieve release distributions
+ uses: actions/download-artifact@v4
+ with:
+ name: release-dists
+ path: dist/
+
+ - name: Publish release distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+
+ test-pypi-publish:
+ name: Publish to Test PyPI
+ runs-on: ubuntu-latest
+ needs:
+ - release-build
+ if: github.event.inputs.test_pypi == 'true' || (github.event_name == 'release' && github.event.release.prerelease)
+ permissions:
+ # IMPORTANT: this permission is mandatory for trusted publishing
+ id-token: write
+
+ # Note: For enhanced security, consider configuring deployment environments
+ # in your GitHub repository settings with protection rules
+
+ steps:
+ - name: Retrieve release distributions
+ uses: actions/download-artifact@v4
+ with:
+ name: release-dists
+ path: dist/
+
+ - name: Publish release distributions to Test PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ repository-url: https://test.pypi.org/legacy/
+
+ create-github-release-assets:
+ name: Add assets to GitHub release
+ runs-on: ubuntu-latest
+ needs: release-build
+ if: github.event_name == 'release'
+
+ steps:
+ - name: Retrieve release distributions
+ uses: actions/download-artifact@v4
+ with:
+ name: release-dists
+ path: dist/
+
+ - name: Upload release assets
+ uses: softprops/action-gh-release@v1
+ with:
+ files: dist/*
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ post-publish:
+ name: Post-publish tasks
+ runs-on: ubuntu-latest
+ needs: [pypi-publish, test-pypi-publish]
+ if: always() && (needs.pypi-publish.result == 'success' || needs.test-pypi-publish.result == 'success')
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Create deployment summary
+ run: |
+ echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
+ echo "| Item | Status |" >> $GITHUB_STEP_SUMMARY
+ echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
+
+ if [ "${{ needs.pypi-publish.result }}" = "success" ]; then
+ echo "| PyPI | Published |" >> $GITHUB_STEP_SUMMARY
+ elif [ "${{ needs.test-pypi-publish.result }}" = "success" ]; then
+ echo "| Test PyPI | Published |" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ echo "| GitHub Release | Assets uploaded |" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | ${{ github.event.release.tag_name || 'test' }} |" >> $GITHUB_STEP_SUMMARY
+
+ - name: Notify team
+ run: |
+ echo "Package published successfully!"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e81c88
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,57 @@
+# ================================
+# Python-related files
+# ================================
+
+# Compiled Python files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Distribution / packaging
+.Python
+build/
+dist/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Type checking
+.mypy_cache/
+
+# Documentation
+docs/_build/
+docs/_static/
+docs/_templates/
+docs/_static/
+
+# ================================
+# IDE and Editor files
+# ================================
+
+# PyCharm
+.idea/
+
+# Visual Studio Code
+.vscode/
+
+# ================================
+# Operating System files
+# ================================
+
+# macOS
+.DS_Store
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..83ead25
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,81 @@
+repos:
+ # Code formatting
+ - repo: https://github.com/psf/black
+ rev: 23.12.1
+ hooks:
+ - id: black
+ language_version: python3
+ args: ["--line-length=120"]
+
+ # Import sorting
+ - repo: https://github.com/pycqa/isort
+ rev: 5.13.2
+ hooks:
+ - id: isort
+ args: ["--profile", "black", "--multi-line", "3"]
+
+ # Linting
+ - repo: https://github.com/pycqa/flake8
+ rev: 7.0.0
+ hooks:
+ - id: flake8
+ args:
+ - "--max-line-length=200" # Allow longer lines after black formatting
+ - "--extend-ignore=E203,W503,F401,E402,E721,F841"
+ - "--exclude=build,dist,__pycache__,.mypy_cache,.pytest_cache,htmlcov,.idea,.vscode,docs/_build,msgcenterpy.egg-info"
+
+ # Type checking
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.8.0
+ hooks:
+ - id: mypy
+ additional_dependencies: [types-PyYAML, types-jsonschema, pydantic]
+ args: ["--ignore-missing-imports", "--disable-error-code=unused-ignore"]
+ files: "^(msgcenterpy/)" # Check both source code and tests
+
+ # General pre-commit hooks
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ # File checks
+ - id: trailing-whitespace
+ args: [--markdown-linebreak-ext=md]
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-json
+ - id: check-toml
+ - id: check-xml
+
+ # Security
+ - id: check-merge-conflict
+ - id: check-case-conflict
+ - id: check-symlinks
+ - id: check-added-large-files
+ args: ["--maxkb=1000"]
+
+ # Python specific
+ - id: check-ast
+ - id: debug-statements
+ - id: name-tests-test
+ args: ["--django"]
+
+ # Security scanning
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.7.5
+ hooks:
+ - id: bandit
+ args: ["-c", "pyproject.toml"]
+ additional_dependencies: ["bandit[toml]", "pbr"]
+ exclude: "^tests/"
+
+ # YAML/JSON formatting
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v4.0.0-alpha.8
+ hooks:
+ - id: prettier
+ types_or: [yaml, json, markdown]
+ exclude: "^(build/|dist/|__pycache__/|\\.mypy_cache/|\\.pytest_cache/|htmlcov/|\\.idea/|\\.vscode/|docs/_build/|msgcenterpy\\.egg-info/)"
+
+# Global settings
+default_stages: [pre-commit, pre-push]
+fail_fast: false
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f6553aa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,199 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (which shall not include communications that are merely
+ incorporated by reference without repeating their full text).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based upon (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and derivative works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control
+ systems, and issue tracking systems that are managed by, or on behalf
+ of, the Licensor for the purpose of discussing and improving the Work,
+ but excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to use, reproduce, modify, display, perform,
+ sublicense, and distribute the Work and such Derivative Works in
+ Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, trademark, patent,
+ attribution and other notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright notice to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Support. You may choose to offer and charge
+ a fee for, warranty, support, indemnity or other liability obligations
+ and/or rights consistent with this License. However, in accepting such
+ obligations, You may act only on Your own behalf and on Your sole
+ responsibility, not on behalf of any other Contributor, and only if
+ You agree to indemnify, defend, and hold each Contributor harmless for
+ any liability incurred by, or claims asserted against, such Contributor
+ by reason of your accepting any such warranty or support.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..0b0255f
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+# Include important project files
+include LICENSE
+include README.md
+
+# Include all msgcenterpy source files
+recursive-include msgcenterpy *
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e1615b1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,236 @@
+# MsgCenterPy
+
+
+
+[](https://badge.fury.io/py/msgcenterpy)
+[](https://pypi.org/project/msgcenterpy/)
+[](https://github.com/ZGCA-Forge/MsgCenterPy/actions)
+[](https://zgca-forge.github.io/MsgCenterPy/)
+
+[](https://github.com/ZGCA-Forge/MsgCenterPy)
+[](https://github.com/ZGCA-Forge/MsgCenterPy/fork)
+[](https://github.com/ZGCA-Forge/MsgCenterPy/issues)
+[](https://github.com/ZGCA-Forge/MsgCenterPy/blob/main/LICENSE)
+
+
+
+---
+
+## 🚀 概述
+
+MsgCenterPy 是一个基于统一实例管理器架构的多格式消息转换系统,支持 **ROS2**、**Pydantic**、**Dataclass**、**JSON**、**Dict**、**YAML** 和 **JSON Schema** 之间的无缝互转。
+
+### 支持的格式
+
+| 格式 | 读取 | 写入 | JSON Schema | 类型约束 |
+| ----------- | ------ | ------ | ----------- | -------- |
+| ROS2 | ✅ | ✅ | ✅ | ✅ |
+| JSON Schema | ✅ | ✅ | ✅ | ✅ |
+| Pydantic | 开发中 | 开发中 | 开发中 | 开发中 |
+| Dataclass | 开发中 | 开发中 | 开发中 | 开发中 |
+| JSON | 开发中 | 开发中 | 开发中 | 开发中 |
+| Dict | 开发中 | 开发中 | 开发中 | 开发中 |
+| YAML | 开发中 | 开发中 | 开发中 | 开发中 |
+
+## 📦 安装
+
+### 基础安装
+
+```bash
+pip install msgcenterpy
+```
+
+### 包含可选依赖
+
+```bash
+# 安装 ROS2 支持
+conda install
+
+# 安装开发工具
+pip install msgcenterpy[dev]
+
+# 安装文档工具
+pip install msgcenterpy[docs]
+
+# 安装所有依赖
+pip install msgcenterpy[all]
+```
+
+### 从源码安装
+
+```bash
+git clone https://github.com/ZGCA-Forge/MsgCenterPy.git
+cd MsgCenterPy
+pip install -e .[dev]
+```
+
+## 🔧 快速开始
+
+### 基础使用
+
+```python
+from msgcenterpy import MessageInstance, MessageType
+
+# 从字典创建消息实例
+data = {
+ "name": "sensor_001",
+ "readings": [1.0, 2.0, 3.0],
+ "active": True
+}
+dict_instance = MessageInstance.create(MessageType.DICT, data)
+
+# 生成 JSON Schema
+schema = dict_instance.get_json_schema()
+print(schema)
+```
+
+### ROS2 消息转换
+
+```python
+from std_msgs.msg import String
+from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
+
+# 创建 ROS2 消息实例
+string_msg = String()
+string_msg.data = "Hello ROS2"
+ros2_instance = ROS2MessageInstance(string_msg)
+
+# 转换为 JSON Schema
+json_schema_instance = ros2_instance.to_json_schema()
+
+# 获取生成的 Schema
+schema = json_schema_instance.json_schema
+print(schema)
+```
+
+### 字段访问和约束
+
+```python
+# 动态字段访问
+ros2_instance.data_field = "new_value"
+print(ros2_instance.fields.get_field_info("data"))
+
+# 类型约束验证
+type_info = ros2_instance.fields.get_sub_type_info("data")
+print(f"约束条件: {[c.type.value for c in type_info.constraints]}")
+```
+
+## 📖 文档
+
+### 核心概念
+
+- **MessageInstance**: 统一消息实例基类
+- **TypeInfo**: 详细的字段类型信息和约束
+- **FieldAccessor**: 统一字段访问接口
+- **MessageCenter**: 消息类型管理和转换中心
+
+### API 参考
+
+详细的 API 文档请访问:[https://zgca-forge.github.io/MsgCenterPy/](https://zgca-forge.github.io/MsgCenterPy/)
+
+### 示例代码
+
+更多示例请查看 [`examples/`](examples/) 目录:
+
+- [ROS2 消息转换示例](examples/ros2_example.py)
+- [JSON Schema 生成示例](examples/json_schema_example.py)
+- [类型约束示例](examples/type_constraints_example.py)
+
+## 🧪 测试
+
+### 运行测试
+
+```bash
+# 运行所有测试
+python -m pytest
+
+# 运行特定测试套件
+python run_all_tests.py --type json_schema
+```
+
+```bash
+# 生成覆盖率报告
+pytest --cov=msgcenterpy --cov-report=html
+```
+
+## 🛠️ 开发
+
+### 开发环境设置
+
+```bash
+git clone https://github.com/ZGCA-Forge/MsgCenterPy.git
+cd MsgCenterPy
+pip install -e .[dev]
+pre-commit install
+```
+
+### 代码质量
+
+```bash
+# 代码格式化
+black msgcenterpy tests
+isort msgcenterpy tests
+
+# 类型检查
+mypy msgcenterpy
+
+# 运行测试
+pytest
+```
+
+### 贡献指南
+
+1. Fork 项目
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交变更 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 创建 Pull Request
+
+详细贡献指南请查看 [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## 📊 开发路线图
+
+- [x] ✅ ROS2 消息支持
+- [x] ✅ JSON Schema 生成和验证
+- [x] ✅ 统一字段访问器
+- [x] ✅ 类型约束系统
+- [ ] 🔄 Pydantic 集成
+- [ ] 🔄 Dataclass 支持
+- [ ] 🔄 YAML 格式支持
+- [ ] 🔄 性能优化
+- [ ] 🔄 插件系统
+
+## 🤝 社区
+
+### 支持渠道
+
+- 💬 讨论: [GitHub Discussions](https://github.com/ZGCA-Forge/MsgCenterPy/discussions)
+- 🐛 问题: [GitHub Issues](https://github.com/ZGCA-Forge/MsgCenterPy/issues)
+- 📖 文档: [GitHub Pages](https://zgca-forge.github.io/MsgCenterPy/)
+
+### 贡献者
+
+感谢所有为 MsgCenterPy 做出贡献的开发者!
+
+[](https://github.com/ZGCA-Forge/MsgCenterPy/graphs/contributors)
+
+## 📄 许可证
+
+本项目基于 Apache-2.0 许可证开源 - 查看 [LICENSE](LICENSE) 文件了解详情。
+
+## 🙏 致谢
+
+- [ROS2](https://ros.org/) - 机器人操作系统
+- [Pydantic](https://pydantic-docs.helpmanual.io/) - 数据验证库
+- [pytest](https://pytest.org/) - 测试框架
+- [jsonschema](https://python-jsonschema.readthedocs.io/) - JSON Schema 验证
+
+---
+
+
+
+**[⭐ 给个 Star](https://github.com/ZGCA-Forge/MsgCenterPy)** • **[🍴 Fork 项目](https://github.com/ZGCA-Forge/MsgCenterPy/fork)** • **[📖 查看文档](https://zgca-forge.github.io/MsgCenterPy/)**
+
+Made with ❤️ by the MsgCenterPy Team
+
+
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..9e9215d
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,64 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+# Custom targets for common operations
+clean:
+ @echo "Cleaning build directory..."
+ rm -rf $(BUILDDIR)/*
+
+html:
+ @echo "Building HTML documentation..."
+ @$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+livehtml:
+ @echo "Starting live HTML build..."
+ sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O)
+
+linkcheck:
+ @echo "Checking external links..."
+ @$(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)/linkcheck" $(SPHINXOPTS) $(O)
+ @echo
+ @echo "Link check complete; look for any errors in the above output."
+
+doctest:
+ @echo "Running doctests..."
+ @$(SPHINXBUILD) -b doctest "$(SOURCEDIR)" "$(BUILDDIR)/doctest" $(SPHINXOPTS) $(O)
+ @echo "doctest finished; look at the results in $(BUILDDIR)/doctest."
+
+coverage:
+ @echo "Checking documentation coverage..."
+ @$(SPHINXBUILD) -b coverage "$(SOURCEDIR)" "$(BUILDDIR)/coverage" $(SPHINXOPTS) $(O)
+ @echo "Coverage finished; see $(BUILDDIR)/coverage/python.txt."
+
+latexpdf:
+ @echo "Building LaTeX files and running them through pdflatex..."
+ @$(SPHINXBUILD) -b latex "$(SOURCEDIR)" "$(BUILDDIR)/latex" $(SPHINXOPTS) $(O)
+ @echo "Running LaTeX files through pdflatex..."
+ @make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+epub:
+ @echo "Building EPUB documentation..."
+ @$(SPHINXBUILD) -b epub "$(SOURCEDIR)" "$(BUILDDIR)/epub" $(SPHINXOPTS) $(O)
+ @echo
+ @echo "Build finished. The EPUB file is in $(BUILDDIR)/epub."
diff --git a/docs/api/core.rst b/docs/api/core.rst
new file mode 100644
index 0000000..f2787aa
--- /dev/null
+++ b/docs/api/core.rst
@@ -0,0 +1,60 @@
+Core API Reference
+==================
+
+This section documents the core components of MsgCenterPy.
+
+MessageInstance
+---------------
+
+.. autoclass:: msgcenterpy.core.message_instance.MessageInstance
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+MessageType
+-----------
+
+.. autoclass:: msgcenterpy.core.types.MessageType
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+TypeInfo
+--------
+
+.. autoclass:: msgcenterpy.core.type_info.TypeInfo
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+FieldAccessor
+-------------
+
+.. autoclass:: msgcenterpy.core.field_accessor.FieldAccessor
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+MessageCenter
+-------------
+
+.. autoclass:: msgcenterpy.core.message_center.MessageCenter
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+TypeConverter
+-------------
+
+.. autoclass:: msgcenterpy.core.type_converter.TypeConverter
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+Envelopes
+---------
+
+.. automodule:: msgcenterpy.core.envelope
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..f8c1ac1
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,135 @@
+"""Configuration file for the Sphinx documentation builder.
+
+For the full list of built-in configuration values, see the documentation:
+https://www.sphinx-doc.org/en/master/usage/configuration.html
+"""
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath(".."))
+
+# Import version from package
+try:
+ from msgcenterpy import __version__
+
+ version = __version__
+except ImportError:
+ version = "0.0.1" # fallback
+
+project = "MsgCenterPy"
+copyright = "2025, MsgCenterPy Team"
+author = "MsgCenterPy Team"
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.todo",
+ "sphinx.ext.coverage",
+ "sphinx.ext.mathjax",
+ "myst_parser",
+]
+
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+language = "en"
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = "sphinx_rtd_theme"
+html_static_path = ["_static"]
+
+# -- Extension configuration -------------------------------------------------
+
+# Napoleon settings
+napoleon_google_docstring = True
+napoleon_numpy_docstring = True
+napoleon_include_init_with_doc = False
+napoleon_include_private_with_doc = False
+napoleon_include_special_with_doc = True
+napoleon_use_admonition_for_examples = False
+napoleon_use_admonition_for_notes = False
+napoleon_use_admonition_for_references = False
+napoleon_use_ivar = False
+napoleon_use_param = True
+napoleon_use_rtype = True
+
+# Autodoc settings
+autodoc_default_options = {
+ "members": True,
+ "member-order": "bysource",
+ "special-members": "__init__",
+ "undoc-members": True,
+ "exclude-members": "__weakref__",
+}
+
+# Intersphinx mapping
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3", None),
+ "numpy": ("https://numpy.org/doc/stable", None),
+ "pydantic": ("https://docs.pydantic.dev", None),
+}
+
+# MyST parser settings
+myst_enable_extensions = [
+ "deflist",
+ "tasklist",
+ "dollarmath",
+ "amsmath",
+ "colon_fence",
+ "attrs_inline",
+]
+
+# Todo settings
+todo_include_todos = True
+
+# HTML theme options
+html_theme_options = {
+ "canonical_url": "",
+ "analytics_id": "", # Provided by Google in your dashboard
+ "logo_only": False,
+ "display_version": True,
+ "prev_next_buttons_location": "bottom",
+ "style_external_links": False,
+ "vcs_pageview_mode": "",
+ "style_nav_header_background": "#2980B9",
+ # Toc options
+ "collapse_navigation": True,
+ "sticky_navigation": True,
+ "navigation_depth": 4,
+ "includehidden": True,
+ "titles_only": False,
+}
+
+# Custom sidebar
+html_sidebars = {
+ "**": [
+ "about.html",
+ "navigation.html",
+ "relations.html",
+ "searchbox.html",
+ "donate.html",
+ ]
+}
+
+# -- Custom CSS and JS -------------------------------------------------------
+html_css_files = ["custom.css"]
+
+# GitHub URL
+html_context = {
+ "display_github": True, # Integrate GitHub
+ "github_user": "ZGCA-Forge", # Username
+ "github_repo": "MsgCenterPy", # Repo name
+ "github_version": "main", # Version
+ "conf_py_path": "/docs/", # Path in the checkout to the docs root
+}
diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst
new file mode 100644
index 0000000..a18bf83
--- /dev/null
+++ b/docs/development/contributing.rst
@@ -0,0 +1,160 @@
+Contributing to MsgCenterPy
+===========================
+
+Thank you for your interest in contributing to MsgCenterPy! This document provides guidelines for contributing to the project.
+
+For detailed contribution guidelines, please see our `Contributing Guide `_ on GitHub.
+
+Quick Links
+-----------
+
+- `GitHub Repository `_
+- `Issue Tracker `_
+- `Pull Requests `_
+- `Discussions `_
+
+Development Setup
+-----------------
+
+1. Fork the repository on GitHub
+2. Clone your fork locally
+3. Install in development mode:
+
+.. code-block:: bash
+
+ git clone https://github.com/YOUR-USERNAME/MsgCenterPy.git
+ cd MsgCenterPy
+ pip install -e .[dev]
+
+4. Set up pre-commit hooks:
+
+.. code-block:: bash
+
+ pre-commit install
+
+Code Style and Quality
+----------------------
+
+We use several tools to maintain code quality:
+
+- **Black**: Code formatting
+- **isort**: Import sorting
+- **mypy**: Type checking
+- **pytest**: Testing
+
+Run quality checks:
+
+.. code-block:: bash
+
+ # Format code
+ black msgcenterpy tests
+ isort msgcenterpy tests
+
+ # Type checking
+ mypy msgcenterpy
+
+ # Run tests
+ pytest
+
+Testing
+-------
+
+Please ensure all tests pass and add tests for new features:
+
+.. code-block:: bash
+
+ # Run all tests
+ pytest
+
+ # Run with coverage
+ pytest -v
+
+ # Run specific test file
+ pytest tests/test_specific_module.py
+
+Version Management
+------------------
+
+This project uses `bump2version` for semantic version management. The tool is included in development dependencies and automatically manages version numbers across the codebase.
+
+**Setup**
+
+bump2version is automatically installed when you install development dependencies:
+
+.. code-block:: bash
+
+ pip install -e .[dev]
+
+**Configuration**
+
+Version configuration is stored in `.bumpversion.cfg`:
+
+- **Single source of truth**: `msgcenterpy/__init__.py`
+- **Auto-commit**: Creates commit with version bump
+- **Auto-tag**: Creates git tag for new version
+- **Semantic versioning**: Follows MAJOR.MINOR.PATCH format
+
+**Usage**
+
+.. code-block:: bash
+
+ # Bug fixes (0.0.1 → 0.0.2)
+ bump2version patch
+
+ # New features (0.0.2 → 0.1.0)
+ bump2version minor
+
+ # Breaking changes (0.1.0 → 1.0.0)
+ bump2version major
+
+**Release Workflow**
+
+1. Make your changes and commit them
+2. Choose appropriate version bump type
+3. Run bump2version command
+4. Push changes and tags:
+
+.. code-block:: bash
+
+ git push && git push --tags
+
+**Version Bump Guidelines**
+
+- **patch**: Bug fixes, documentation updates, internal refactoring
+- **minor**: New features, backward-compatible API additions
+- **major**: Breaking changes, API removals or modifications
+
+**Notes**
+
+- Only developers need bump2version (it's in dev dependencies only)
+- Version numbers are automatically synchronized across all files
+- Git working directory must be clean before version bump
+- Each version bump creates a commit and git tag automatically
+
+Submitting Changes
+------------------
+
+1. Create a new branch for your feature/fix
+2. Make your changes
+3. Add tests for new functionality
+4. Ensure all tests pass
+5. Update documentation if needed
+6. Submit a pull request
+
+Pull Request Guidelines
+-----------------------
+
+- Use descriptive titles and descriptions
+- Reference related issues
+- Include tests for new features
+- Update documentation as needed
+- Follow the existing code style
+
+Getting Help
+------------
+
+If you need help:
+
+- Check existing `Issues `_
+- Start a `Discussion `_
+- Contact the maintainers
diff --git a/docs/examples/basic_usage.rst b/docs/examples/basic_usage.rst
new file mode 100644
index 0000000..20f9300
--- /dev/null
+++ b/docs/examples/basic_usage.rst
@@ -0,0 +1,225 @@
+Basic Usage Examples
+====================
+
+This page contains basic usage examples to help you get started with MsgCenterPy.
+
+Creating Message Instances
+---------------------------
+
+Dictionary Messages
+~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ from msgcenterpy import MessageInstance, MessageType
+
+ # Simple dictionary
+ simple_data = {"name": "sensor_01", "active": True}
+ instance = MessageInstance.create(MessageType.DICT, simple_data)
+
+ # Access fields
+ name = instance.get_field("name")
+ print(f"Sensor name: {name}")
+
+ # Nested dictionary
+ nested_data = {
+ "device": {
+ "id": "dev_001",
+ "sensors": [
+ {"type": "temperature", "value": 23.5},
+ {"type": "humidity", "value": 65.2}
+ ]
+ }
+ }
+ nested_instance = MessageInstance.create(MessageType.DICT, nested_data)
+
+JSON Schema Generation
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ # Generate JSON Schema from dictionary
+ schema = instance.get_json_schema()
+ print("Generated Schema:")
+ print(schema)
+
+ # Schema includes type information
+ assert schema["type"] == "object"
+ assert "name" in schema["properties"]
+ assert schema["properties"]["name"]["type"] == "string"
+
+Field Access and Manipulation
+------------------------------
+
+Getting Field Values
+~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ # Simple field access
+ name = instance.get_field("name")
+
+ # Nested field access using dot notation
+ device_id = nested_instance.get_field("device.id")
+ temp_value = nested_instance.get_field("device.sensors.0.value")
+
+Setting Field Values
+~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ # Set simple field
+ instance.set_field("name", "sensor_02")
+
+ # Set nested field
+ nested_instance.set_field("device.id", "dev_002")
+ nested_instance.set_field("device.sensors.0.value", 24.1)
+
+Working with Field Information
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ # Get field type information
+ field_info = instance.fields.get_field_info("name")
+ print(f"Field type: {field_info.type}")
+ print(f"Field constraints: {field_info.constraints}")
+
+ # Check if field exists
+ if instance.fields.has_field("name"):
+ print("Field 'name' exists")
+
+Type Constraints and Validation
+-------------------------------
+
+.. code-block:: python
+
+ from msgcenterpy.core.types import TypeConstraintError
+
+ try:
+ # This will raise an error if type doesn't match
+ instance.set_field("active", "not_a_boolean")
+ except TypeConstraintError as e:
+ print(f"Type constraint violation: {e}")
+
+ # Type conversion when possible
+ instance.set_field("name", 123) # Converts to string if allowed
+
+Message Conversion
+------------------
+
+Converting Between Formats
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ # Create from dictionary
+ dict_instance = MessageInstance.create(MessageType.DICT, {"key": "value"})
+
+ # Convert to JSON Schema instance
+ schema_instance = dict_instance.to_json_schema()
+
+ # Get the actual schema
+ schema = schema_instance.json_schema
+ print(schema)
+
+Error Handling
+--------------
+
+Common Error Scenarios
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ from msgcenterpy.core.exceptions import FieldAccessError, TypeConstraintError
+
+ # Field access errors
+ try:
+ value = instance.get_field("nonexistent_field")
+ except FieldAccessError as e:
+ print(f"Field not found: {e}")
+
+ # Type constraint errors
+ try:
+ instance.set_field("active", "invalid_boolean")
+ except TypeConstraintError as e:
+ print(f"Invalid type: {e}")
+
+ # Graceful handling
+ def safe_get_field(instance, field_name, default=None):
+ try:
+ return instance.get_field(field_name)
+ except FieldAccessError:
+ return default
+
+ # Usage
+ value = safe_get_field(instance, "optional_field", "default_value")
+
+Best Practices
+--------------
+
+1. **Always handle exceptions** when accessing fields that might not exist
+2. **Use type hints** in your code for better development experience
+3. **Validate data** before creating instances when working with external data
+4. **Use dot notation** for nested field access instead of manual dictionary traversal
+5. **Check field existence** before accessing optional fields
+
+Complete Example
+----------------
+
+Here's a complete example that demonstrates multiple features:
+
+.. code-block:: python
+
+ from msgcenterpy import MessageInstance, MessageType
+ from msgcenterpy.core.exceptions import FieldAccessError, TypeConstraintError
+ import json
+
+ def process_sensor_data(sensor_data):
+ """Process sensor data with proper error handling."""
+ try:
+ # Create instance
+ instance = MessageInstance.create(MessageType.DICT, sensor_data)
+
+ # Validate required fields
+ required_fields = ["id", "type", "readings"]
+ for field in required_fields:
+ if not instance.fields.has_field(field):
+ raise ValueError(f"Missing required field: {field}")
+
+ # Process readings
+ readings = instance.get_field("readings")
+ if isinstance(readings, list) and len(readings) > 0:
+ avg_reading = sum(readings) / len(readings)
+ instance.set_field("average", avg_reading)
+
+ # Generate schema for validation
+ schema = instance.get_json_schema()
+
+ # Return processed data and schema
+ return {
+ "processed_data": instance.to_dict(),
+ "schema": schema,
+ "success": True
+ }
+
+ except (FieldAccessError, TypeConstraintError, ValueError) as e:
+ return {
+ "error": str(e),
+ "success": False
+ }
+
+ # Usage
+ sensor_data = {
+ "id": "temp_001",
+ "type": "temperature",
+ "readings": [23.1, 23.5, 24.0, 23.8],
+ "unit": "celsius"
+ }
+
+ result = process_sensor_data(sensor_data)
+ if result["success"]:
+ print("Processing successful!")
+ print(f"Average reading: {result['processed_data']['average']}")
+ else:
+ print(f"Processing failed: {result['error']}")
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..c0a4a12
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,176 @@
+Welcome to MsgCenterPy's Documentation!
+========================================
+
+.. image:: https://img.shields.io/badge/license-Apache--2.0-blue.svg
+ :target: https://github.com/ZGCA-Forge/MsgCenterPy/blob/main/LICENSE
+ :alt: License
+
+.. image:: https://img.shields.io/pypi/v/msgcenterpy.svg
+ :target: https://pypi.org/project/msgcenterpy/
+ :alt: PyPI version
+
+.. image:: https://img.shields.io/pypi/pyversions/msgcenterpy.svg
+ :target: https://pypi.org/project/msgcenterpy/
+ :alt: Python versions
+
+MsgCenterPy is a unified message conversion system based on unified instance manager architecture,
+supporting seamless conversion between **ROS2**, **Pydantic**, **Dataclass**, **JSON**, **Dict**,
+**YAML** and **JSON Schema**.
+
+✨ Key Features
+---------------
+
+🔄 **Unified Conversion**: Supports bidirectional conversion between multiple message formats
+
+🤖 **ROS2 Integration**: Complete support for ROS2 message types and constraints
+
+📊 **JSON Schema**: Automatic generation and validation of JSON Schema
+
+🏗️ **Type Safety**: Strong type constraint system based on TypeInfo
+
+🔍 **Field Access**: Unified field accessor interface
+
+⚡ **High Performance**: Optimized conversion algorithms and caching mechanism
+
+🧪 **Complete Testing**: 47+ test cases with >90% coverage
+
+📦 Quick Start
+--------------
+
+Installation
+~~~~~~~~~~~~
+
+.. code-block:: bash
+
+ pip install msgcenterpy
+
+Basic Usage
+~~~~~~~~~~~
+
+.. code-block:: python
+
+ from msgcenterpy import MessageInstance, MessageType
+
+ # Create message instance from dictionary
+ data = {
+ "name": "sensor_001",
+ "readings": [1.0, 2.0, 3.0],
+ "active": True
+ }
+ dict_instance = MessageInstance.create(MessageType.DICT, data)
+
+ # Generate JSON Schema
+ schema = dict_instance.get_json_schema()
+ print(schema)
+
+🎯 Supported Formats
+--------------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 20 20 20 20
+
+ * - Format
+ - Read
+ - Write
+ - JSON Schema
+ - Type Constraints
+ * - ROS2
+ - ✅
+ - ✅
+ - ✅
+ - ✅
+ * - JSON Schema
+ - ✅
+ - ✅
+ - ✅
+ - ✅
+ * - Pydantic
+ - 开发中
+ - 开发中
+ - 开发中
+ - 开发中
+ * - Dataclass
+ - 开发中
+ - 开发中
+ - 开发中
+ - 开发中
+ * - JSON
+ - 开发中
+ - 开发中
+ - ✅
+ - ⚡
+ * - Dict
+ - 开发中
+ - 开发中
+ - ✅
+ - ⚡
+ * - YAML
+ - 开发中
+ - 开发中
+ - ✅
+ - ⚡
+
+.. note::
+ ✅ Fully Supported | 开发中 In Development | ⚡ Basic Support
+
+📚 Documentation Contents
+-------------------------
+
+.. toctree::
+ :maxdepth: 2
+ :caption: User Guide
+
+ installation
+ quickstart
+ user_guide/index
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Examples
+
+ examples/basic_usage
+ examples/ros2_examples
+ examples/json_schema_examples
+
+.. toctree::
+ :maxdepth: 2
+ :caption: API Reference
+
+ api/core
+ api/instances
+ api/utils
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Development
+
+ development/contributing
+ development/testing
+ development/changelog
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Community
+
+ community/support
+ community/faq
+
+Indices and Tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+🤝 Community & Support
+======================
+
+- 📖 **Documentation**: https://zgca-forge.github.io/MsgCenterPy/
+- 🐛 **Issues**: https://github.com/ZGCA-Forge/MsgCenterPy/issues
+- 💬 **Discussions**: https://github.com/ZGCA-Forge/MsgCenterPy/discussions
+
+📄 License
+==========
+
+This project is licensed under the Apache-2.0 License - see the `LICENSE `_ file for details.
diff --git a/docs/installation.rst b/docs/installation.rst
new file mode 100644
index 0000000..c3f8fd0
--- /dev/null
+++ b/docs/installation.rst
@@ -0,0 +1,122 @@
+Installation Guide
+==================
+
+This guide will help you install MsgCenterPy in different environments.
+
+Quick Installation
+------------------
+
+The easiest way to install MsgCenterPy is using pip:
+
+.. code-block:: bash
+
+ pip install msgcenterpy
+
+Requirements
+------------
+
+- Python 3.10 or higher (Python 3.11+ recommended for optimal ROS2 compatibility)
+- Operating System: Linux, macOS, or Windows
+
+Optional Dependencies
+---------------------
+
+ROS2 Support
+~~~~~~~~~~~~
+
+To use ROS2 message conversion features:
+
+.. code-block:: bash
+
+ pip install msgcenterpy[ros2]
+
+This will install:
+- ``rosidl-runtime-py>=0.10.0``
+- ``rclpy>=3.0.0``
+
+.. warning::
+ **ROS2 Python Version Compatibility Notice**
+
+ While Python 3.10+ is supported by this package, ROS2 official binary distributions
+ may have varying support across Python versions. You might need to:
+
+ - Build ROS2 from source for newer Python versions
+ - Use ROS2 distributions that officially support your Python version
+ - Consider using conda-forge ROS2 packages if available
+
+ For production environments, verify ROS2 compatibility in your specific setup.
+
+Development Tools
+~~~~~~~~~~~~~~~~~
+
+For development and testing:
+
+.. code-block:: bash
+
+ pip install msgcenterpy[dev]
+
+This includes:
+- ``pytest>=7.0.0``
+- ``black>=22.0.0``
+- ``isort>=5.0.0``
+- ``mypy>=1.0.0``
+- ``pre-commit>=2.20.0``
+
+Documentation Tools
+~~~~~~~~~~~~~~~~~~~
+
+For building documentation:
+
+.. code-block:: bash
+
+ pip install msgcenterpy[docs]
+
+All Dependencies
+~~~~~~~~~~~~~~~~
+
+To install all optional dependencies:
+
+.. code-block:: bash
+
+ pip install msgcenterpy[all]
+
+From Source
+-----------
+
+Development Installation
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To install from source for development:
+
+.. code-block:: bash
+
+ git clone https://github.com/ZGCA-Forge/MsgCenterPy.git
+ cd MsgCenterPy
+ pip install -e .[dev]
+
+This will install the package in development mode, allowing you to make changes to the source code.
+
+Verification
+------------
+
+To verify your installation:
+
+.. code-block:: python
+
+ import msgcenterpy
+ print(msgcenterpy.get_version())
+ print(msgcenterpy.check_dependencies())
+
+The output should show the version number and available dependencies.
+
+Troubleshooting
+---------------
+
+Common Issues
+~~~~~~~~~~~~~
+
+1. **Python Version**: Ensure you're using Python 3.10 or higher (3.11+ recommended for optimal ROS2 compatibility)
+2. **ROS2 Dependencies**: ROS2 support requires a proper ROS2 installation
+3. **Virtual Environments**: Consider using virtual environments for isolation
+
+If you encounter any issues, please check the `GitHub Issues `_ page.
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..0358981
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1,138 @@
+Quick Start Guide
+=================
+
+This guide will get you up and running with MsgCenterPy in just a few minutes.
+
+First Steps
+-----------
+
+After installation, you can start using MsgCenterPy immediately:
+
+.. code-block:: python
+
+ from msgcenterpy import MessageInstance, MessageType
+
+ # Create a simple message from dictionary
+ data = {"name": "test", "value": 42}
+ instance = MessageInstance.create(MessageType.DICT, data)
+
+ # Get JSON Schema
+ schema = instance.get_json_schema()
+ print(schema)
+
+Basic Concepts
+--------------
+
+MessageInstance
+~~~~~~~~~~~~~~~
+
+The core concept in MsgCenterPy is the ``MessageInstance``, which provides a unified interface for different message formats.
+
+.. code-block:: python
+
+ from msgcenterpy import MessageInstance, MessageType
+
+ # Different ways to create instances
+ dict_instance = MessageInstance.create(MessageType.DICT, {"key": "value"})
+
+ # Access fields
+ print(dict_instance.get_field("key"))
+
+MessageType
+~~~~~~~~~~~
+
+MsgCenterPy supports various message types:
+
+- ``MessageType.DICT``: Python dictionaries
+- ``MessageType.JSON_SCHEMA``: JSON Schema objects
+- ``MessageType.ROS2``: ROS2 messages (with optional dependency)
+
+Working with ROS2 Messages
+---------------------------
+
+If you have ROS2 installed, you can work with ROS2 messages:
+
+.. code-block:: python
+
+ from std_msgs.msg import String
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
+
+ # Create ROS2 message
+ msg = String()
+ msg.data = "Hello ROS2"
+
+ # Create instance
+ ros2_instance = ROS2MessageInstance(msg)
+
+ # Convert to JSON Schema
+ json_schema_instance = ros2_instance.to_json_schema()
+ print(json_schema_instance.json_schema)
+
+JSON Schema Generation
+----------------------
+
+One of the key features is automatic JSON Schema generation:
+
+.. code-block:: python
+
+ from msgcenterpy import MessageInstance, MessageType
+
+ # Complex nested structure
+ data = {
+ "sensor": {
+ "name": "temperature_01",
+ "readings": [23.5, 24.1, 23.8],
+ "metadata": {
+ "unit": "celsius",
+ "precision": 0.1
+ }
+ }
+ }
+
+ instance = MessageInstance.create(MessageType.DICT, data)
+ schema = instance.get_json_schema()
+
+ # The schema will include type information for all nested structures
+
+Field Access and Type Information
+----------------------------------
+
+MsgCenterPy provides detailed type information and field access:
+
+.. code-block:: python
+
+ # Access field information
+ field_info = instance.fields.get_field_info("sensor.name")
+ print(f"Field type: {field_info.type}")
+ print(f"Field constraints: {field_info.constraints}")
+
+ # Dynamic field access
+ instance.set_field("sensor.name", "temperature_02")
+ value = instance.get_field("sensor.name")
+
+Error Handling
+--------------
+
+MsgCenterPy includes comprehensive error handling:
+
+.. code-block:: python
+
+ try:
+ # Invalid field access
+ value = instance.get_field("nonexistent.field")
+ except FieldAccessError as e:
+ print(f"Field access error: {e}")
+
+ try:
+ # Type constraint violation
+ instance.set_field("sensor.readings", "invalid")
+ except TypeConstraintError as e:
+ print(f"Type error: {e}")
+
+Next Steps
+----------
+
+- Read the :doc:`user_guide/index` for detailed usage
+- Check out :doc:`examples/basic_usage` for more examples
+- Explore the :doc:`api/core` documentation
+- Learn about :doc:`development/contributing` if you want to contribute
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..a13df35
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,14 @@
+# Documentation build requirements
+sphinx>=5.0.0
+sphinx_rtd_theme>=1.0.0
+myst-parser>=0.18.0
+
+# For autodoc generation
+sphinx-autodoc-typehints>=1.19.0
+
+# Additional Sphinx extensions
+sphinx-copybutton>=0.5.0
+sphinx-tabs>=3.4.0
+
+# For parsing project dependencies
+packaging>=21.0
diff --git a/msgcenterpy/__init__.py b/msgcenterpy/__init__.py
new file mode 100644
index 0000000..a092062
--- /dev/null
+++ b/msgcenterpy/__init__.py
@@ -0,0 +1,97 @@
+"""
+MsgCenterPy - Unified Message Conversion System
+
+A multi-format message conversion system supporting seamless conversion
+between ROS2, Pydantic, Dataclass, JSON, Dict, YAML and JSON Schema.
+"""
+
+__version__ = "0.0.2"
+__license__ = "Apache-2.0"
+
+from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
+from msgcenterpy.core.field_accessor import FieldAccessor
+from msgcenterpy.core.message_center import MessageCenter
+
+# Core imports
+from msgcenterpy.core.message_instance import MessageInstance
+from msgcenterpy.core.type_converter import StandardType, TypeConverter
+from msgcenterpy.core.type_info import ConstraintType, TypeInfo
+from msgcenterpy.core.types import ConversionError, MessageType, ValidationError
+
+# Always available instance
+from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
+
+# Optional ROS2 instance (with graceful fallback)
+try:
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
+
+ _HAS_ROS2 = True
+except ImportError:
+ _HAS_ROS2 = False
+
+
+# Convenience function
+def get_message_center() -> MessageCenter:
+ """Get the MessageCenter singleton instance."""
+ return MessageCenter.get_instance()
+
+
+# Main exports
+__all__ = [
+ # Version info
+ "__version__",
+ "__license__",
+]
+
+
+def get_version() -> str:
+ """Get the current version of MsgCenterPy."""
+ return __version__
+
+
+def get_package_info() -> dict:
+ """Get package information."""
+ return {
+ "name": "msgcenterpy",
+ "version": __version__,
+ "description": "Unified message conversion system supporting ROS2, Pydantic, Dataclass, JSON, YAML, Dict, and JSON Schema inter-conversion",
+ "license": __license__,
+ "url": "https://github.com/ZGCA-Forge/MsgCenterPy",
+ "keywords": [
+ "message",
+ "conversion",
+ "ros2",
+ "pydantic",
+ "dataclass",
+ "json",
+ "yaml",
+ "mcp",
+ ],
+ }
+
+
+def check_dependencies() -> dict:
+ """Check which optional dependencies are available."""
+ dependencies = {
+ "ros2": False,
+ "jsonschema": False,
+ }
+
+ # Check ROS2
+ try:
+ import rclpy # type: ignore
+ import rosidl_runtime_py # type: ignore
+
+ dependencies["ros2"] = True
+ except ImportError:
+ pass
+
+ # Check jsonschema
+ try:
+ import jsonschema # type: ignore
+
+ dependencies["jsonschema"] = True
+ except ImportError:
+ pass
+
+ return dependencies
diff --git a/msgcenterpy/core/__init__.py b/msgcenterpy/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/msgcenterpy/core/envelope.py b/msgcenterpy/core/envelope.py
new file mode 100644
index 0000000..39d77c7
--- /dev/null
+++ b/msgcenterpy/core/envelope.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+from typing import Any, Dict, TypedDict
+
+ENVELOPE_VERSION: str = "1"
+
+
+class Properties(TypedDict, total=False):
+ ros_msg_cls_path: str
+ ros_msg_cls_namespace: str
+ json_schema: Dict[str, Any]
+
+
+class FormatMetadata(TypedDict, total=False):
+ """Additional metadata for source format, optional.
+
+ Examples: field statistics, original type descriptions, field type mappings, etc.
+ """
+
+ current_format: str
+ source_cls_name: str
+ source_cls_module: str
+ properties: Properties
+
+
+class MessageEnvelope(TypedDict, total=True):
+ """Unified message envelope format.
+
+ - version: Protocol version
+ - format: Source format (MessageType.value)
+ - type_info: Type information (applicable for ROS2, Pydantic, etc.)
+ - content: Normalized message content (dictionary)
+ - metadata: Additional metadata
+ """
+
+ version: str
+ format: str
+ content: Dict[str, Any]
+ metadata: FormatMetadata
+
+
+def create_envelope(
+ *,
+ format_name: str,
+ content: Dict[str, Any],
+ metadata: FormatMetadata,
+) -> MessageEnvelope:
+ env: MessageEnvelope = {
+ "version": ENVELOPE_VERSION,
+ "format": format_name,
+ "content": content,
+ "metadata": metadata,
+ }
+ return env
diff --git a/msgcenterpy/core/field_accessor.py b/msgcenterpy/core/field_accessor.py
new file mode 100644
index 0000000..e40ce41
--- /dev/null
+++ b/msgcenterpy/core/field_accessor.py
@@ -0,0 +1,406 @@
+from abc import ABC, abstractmethod
+from typing import Any, Dict, Optional, cast
+
+from msgcenterpy.core.type_converter import StandardType
+from msgcenterpy.core.type_info import (
+ ConstraintType,
+ Consts,
+ TypeInfo,
+ TypeInfoPostProcessor,
+)
+from msgcenterpy.utils.decorator import experimental
+
+TEST_MODE = True
+
+
+class FieldAccessor:
+ """
+ 字段访问器,支持类型转换和约束验证的统一字段访问接口
+ 只需要getitem和setitem,外部必须通过字典的方式来赋值
+ """
+
+ @property
+ def parent_msg_center(self) -> Optional["FieldAccessor"]:
+ return self._parent
+
+ @property
+ def full_path_from_root(self) -> str:
+ if self._parent is None:
+ return self._field_name or "unknown"
+ else:
+ parent_path = self._parent.full_path_from_root
+ return f"{parent_path}.{self._field_name or 'unknown'}"
+
+ @property
+ def root_accessor_msg_center(self) -> "FieldAccessor":
+ """获取根访问器"""
+ current = self
+ while current._parent is not None:
+ current = current._parent
+ return current
+
+ @property
+ def value(self) -> Any:
+ return self._data
+
+ @value.setter
+ def value(self, data: Any) -> None:
+ if self._parent is not None and self._field_name is not None:
+ self._parent[self._field_name] = data
+
+ @property
+ def type_info(self) -> Optional[TypeInfo]:
+ if self._type_info is not None:
+ return self._type_info
+
+ # 如果是根accessor或者没有字段名,无法获取TypeInfo
+ if self._parent is None or self._field_name is None:
+ return None
+
+ # 调用类型信息提供者获取类型信息,调用是耗时的
+ if self._type_info_provider is None:
+ return None
+ type_info = self._type_info_provider.get_field_type_info(self._field_name, self._data, self._parent)
+
+ # 对TypeInfo进行后处理,添加默认约束
+ if type_info:
+ TypeInfoPostProcessor.post_process_type_info(type_info)
+ self._type_info = type_info
+
+ return type_info
+
+ """标记方便排除getitem/setitem,不要删除"""
+ _data: Any = None
+ _type_info_provider: "TypeInfoProvider" = None # type: ignore[assignment]
+ _parent: Optional["FieldAccessor"] = None
+ _field_name: str = None # type: ignore[assignment]
+ _cache: Dict[str, "FieldAccessor"] = None # type: ignore[assignment]
+ _type_info: Optional[TypeInfo] = None
+
+ def __init__(
+ self,
+ data: Any,
+ type_info_provider: "TypeInfoProvider",
+ parent: Optional["FieldAccessor"],
+ field_name: str,
+ ):
+ """
+ 初始化字段访问器
+
+ Args:
+ data: 要访问的数据对象
+ type_info_provider: 类型信息提供者
+ parent: 父字段访问器,用于嵌套访问
+ field_name: 当前访问器对应的字段名(用于构建路径)
+ """
+ self._data = data
+ self._type_info_provider = type_info_provider
+ self._parent = parent
+ self._field_name = field_name
+ self._cache: Dict[str, "FieldAccessor"] = {} # 缓存FieldAccessor而不是TypeInfo
+ self._type_info: Optional[TypeInfo] = None # 当前accessor的TypeInfo
+
+ def get_sub_type_info(self, field_name: str) -> Optional[TypeInfo]:
+ """获取字段的类型信息,通过获取字段的accessor"""
+ field_accessor = self[field_name]
+ return field_accessor.type_info
+
+ def __getitem__(self, field_name: str) -> "FieldAccessor":
+ """获取字段访问器,支持嵌套访问"""
+ # 检查缓存中是否有对应的 accessor
+ if self._cache is None:
+ self._cache = {}
+ if field_name in self._cache:
+ cached_accessor = self._cache[field_name]
+ # 更新 accessor 的数据源,以防数据已更改
+ if TEST_MODE:
+ raw_value = self._get_raw_value(field_name)
+ if cached_accessor.value != raw_value:
+ raise ValueError(
+ f"Cached accessor value mismatch for field '{field_name}': expected {raw_value}, got {cached_accessor.value}"
+ )
+ return cached_accessor
+ # 获取原始值并创建新的 accessor
+ raw_value = self._get_raw_value(field_name)
+ if self._type_info_provider is None:
+ raise RuntimeError("TypeInfoProvider not initialized")
+ accessor = FieldAccessorFactory.create_accessor(
+ data=raw_value,
+ type_info_provider=self._type_info_provider,
+ parent=self,
+ field_name=field_name,
+ )
+
+ self._cache[field_name] = accessor
+ return accessor
+
+ def __setitem__(self, field_name: str, value: Any) -> None:
+ """设置字段值,支持类型转换和验证"""
+ # 获取类型信息
+ if field_name in self._get_field_names():
+ type_info = self.get_sub_type_info(field_name)
+ if type_info is not None:
+ # 进行类型转换
+ converted_value = type_info.convert_value(value) # 这步自带validate
+ value = converted_value
+ # 对子field设置value,依然会上溯走set_raw_value,确保一致性
+ # 设置值
+ sub_accessor = self[field_name]
+ self._set_raw_value(field_name, value)
+ sub_accessor._data = self._get_raw_value(field_name) # 有可能内部还有赋值的处理
+ # 清除相关缓存
+ self.clear_cache(field_name)
+
+ def __contains__(self, field_name: str) -> bool:
+ return self._has_field(field_name)
+
+ def __getattr__(self, field_name: str) -> "FieldAccessor | Any":
+ """支持通过属性访问字段,用于嵌套访问如 accessor.pose.position.x"""
+ for cls in self.__class__.__mro__:
+ if field_name in cls.__dict__:
+ return cast(Any, super().__getattribute__(field_name))
+ return self[field_name]
+
+ def __setattr__(self, field_name: str, value: Any) -> None:
+ """支持通过属性设置字段值,用于嵌套赋值如 accessor.pose.position.x = 1.0"""
+ for cls in self.__class__.__mro__:
+ if field_name in cls.__dict__:
+ return super().__setattr__(field_name, value)
+ self[field_name] = value
+ return None
+
+ def clear_cache(self, field_name: Optional[str] = None) -> None:
+ """失效字段相关的缓存"""
+ if self._cache is not None and field_name is not None and field_name in self._cache:
+ self._cache[field_name].clear_type_info()
+
+ def clear_type_info(self) -> None:
+ """清空所有缓存"""
+ if self._type_info is not None:
+ self._type_info._outdated = True
+ self._type_info = None
+
+ def get_nested_field_accessor(self, path: str, separator: str = ".") -> "FieldAccessor":
+ parts = path.split(separator)
+ current = self
+ for part in parts:
+ current = self[part]
+ return current
+
+ def set_nested_value(self, path: str, value: Any, separator: str = ".") -> None:
+ current = self.get_nested_field_accessor(path, separator)
+ current.value = value
+
+ def _get_raw_value(self, field_name: str) -> Any:
+ """获取原始字段值(子类实现)"""
+ if hasattr(self._data, "__getitem__"):
+ return self._data[field_name]
+ elif hasattr(self._data, field_name):
+ return getattr(self._data, field_name)
+ else:
+ raise KeyError(f"Field {field_name} not found")
+
+ def _set_raw_value(self, field_name: str, value: Any) -> None:
+ """设置原始字段值(子类实现)"""
+ if hasattr(self._data, "__setitem__"):
+ self._data[field_name] = value
+ elif hasattr(self._data, field_name):
+ setattr(self._data, field_name, value)
+ else:
+ raise KeyError(f"Field {field_name} not found")
+
+ def _has_field(self, field_name: str) -> bool:
+ """检查字段是否存在(子类实现)"""
+ if hasattr(self._data, "__contains__"):
+ return field_name in self._data
+ else:
+ return field_name in self._get_field_names()
+
+ def _get_field_names(self) -> list[str]:
+ """获取所有字段名(子类实现)"""
+ if callable(getattr(self._data, "keys", None)):
+ # noinspection PyCallingNonCallable
+ return list(self._data.keys())
+ elif hasattr(self._data, "__dict__"):
+ return list(self._data.__dict__.keys())
+ elif hasattr(self._data, "__slots__"):
+ # noinspection PyTypeChecker
+ return list(self._data.__slots__)
+ else:
+ # 回退方案:使用dir()但过滤掉特殊方法
+ return [name for name in dir(self._data) if not name.startswith("_")]
+
+ def get_json_schema(self) -> Dict[str, Any]:
+ """原有的递归生成 JSON Schema 逻辑"""
+ # 获取当前访问器的类型信息
+ current_type_info = self.type_info
+
+ # 如果当前层级有类型信息,使用它生成基本schema
+ if current_type_info is not None:
+ schema = current_type_info.to_json_schema_property()
+ else:
+ # 如果没有类型信息,创建基本的object schema
+ schema = {"type": "object", "additionalProperties": True}
+
+ # 如果这是一个对象类型,需要递归处理其字段
+ if schema.get("type") == "object":
+ properties = {}
+ required_fields = []
+
+ # 获取所有字段名
+ field_names = self._get_field_names()
+
+ for field_name in field_names:
+ try:
+ # 获取字段的访问器
+ field_accessor = self[field_name]
+ field_type_info = field_accessor.type_info
+
+ if field_type_info is not None:
+ # 根据字段类型决定如何生成schema
+ if field_type_info.standard_type == StandardType.OBJECT:
+ # 对于嵌套对象,递归调用
+ field_schema = field_accessor.get_json_schema()
+ else:
+ # 对于基本类型,直接使用type_info转换
+ field_schema = field_type_info.to_json_schema_property()
+
+ properties[field_name] = field_schema
+
+ # 检查是否为必需字段
+ if field_type_info.has_constraint(ConstraintType.REQUIRED):
+ required_fields.append(field_name)
+
+ except Exception as e:
+ # 如果字段处理失败,记录警告但继续处理其他字段
+ print(f"Warning: Failed to generate schema for field '{field_name}': {e}")
+ continue
+
+ # 更新schema中的properties
+ if properties:
+ schema["properties"] = properties
+
+ # 设置必需字段
+ if required_fields:
+ schema["required"] = required_fields
+
+ # 如果没有字段信息,允许额外属性
+ if not properties:
+ schema["additionalProperties"] = True
+ else:
+ schema["additionalProperties"] = False
+
+ return schema
+
+ @experimental("Feature under development")
+ def update_from_dict(self, source_data: Dict[str, Any]) -> None:
+ """递归更新嵌套字典
+
+ Args:
+ source_data: 源数据字典
+ """
+ for key, new_value in source_data.items():
+ field_exists = self._has_field(key)
+ could_add = self._could_allow_new_field(key, new_value)
+ if field_exists:
+ current_field_accessor = self[key]
+ current_type_info = current_field_accessor.type_info
+ # 当前key: Object,交给子dict去迭代
+ if (
+ current_type_info
+ and current_type_info.standard_type == StandardType.OBJECT
+ and isinstance(new_value, dict)
+ ):
+ current_field_accessor.update_from_dict(new_value)
+ # 当前key: Array,每个值要交给子array去迭代
+ elif (
+ current_type_info
+ and hasattr(current_type_info.standard_type, "IS_ARRAY")
+ and current_type_info.standard_type.IS_ARRAY
+ and isinstance(new_value, list)
+ ):
+ # 存在情况Array嵌套,这里后续支持逐个赋值,可能需要利用iter进行赋值
+ self[key] = new_value
+ else:
+ # 不限制类型 或 python类型包含
+ if could_add or (current_type_info and issubclass(type(new_value), current_type_info.python_type)):
+ self[key] = new_value
+ else:
+ raise ValueError(f"{key}")
+ elif could_add:
+ self[key] = new_value
+
+ def _could_allow_new_field(self, field_name: str, field_value: Any) -> bool:
+ """检查是否应该允许添加新字段
+
+ 通过检查当前type_info中的TYPE_KEEP约束来判断:
+ - 如果有TYPE_KEEP且为True,说明类型结构固定,不允许添加新字段
+ - 如果没有TYPE_KEEP约束或为False,则允许添加新字段
+
+ Args:
+ field_name: 字段名
+ field_value: 字段值
+
+ Returns:
+ 是否允许添加该字段
+ """
+ parent_type_info = self.type_info
+ if parent_type_info is None:
+ return True # DEBUGGER NEEDED
+ type_keep_constraint = parent_type_info.get_constraint(ConstraintType.TYPE_KEEP)
+ if type_keep_constraint is not None and type_keep_constraint.value:
+ return False
+ return True
+
+
+class TypeInfoProvider(ABC):
+ """Require All Message Instances Extends This get_field_typ_info"""
+
+ @abstractmethod
+ def get_field_type_info(
+ self, field_name: str, field_value: Any, field_accessor: "FieldAccessor"
+ ) -> Optional[TypeInfo]:
+ """获取指定字段的类型信息
+
+ Args:
+ field_name: 字段名,简单字段名如 'field'
+ field_value: 字段的当前值,用于动态类型推断,不能为None
+ field_accessor: 字段访问器,提供额外的上下文信息,不能为None
+
+ Returns:
+ 字段的TypeInfo,如果字段不存在则返回None
+ """
+ pass
+
+
+class ROS2FieldAccessor(FieldAccessor):
+ def _get_raw_value(self, field_name: str) -> Any:
+ return getattr(self._data, field_name)
+
+ def _set_raw_value(self, field_name: str, value: Any) -> None:
+ return setattr(self._data, field_name, value)
+
+ def _has_field(self, field_name: str) -> bool:
+ return hasattr(self._data, field_name)
+
+ def _get_field_names(self) -> list[str]:
+ if hasattr(self._data, "_fields_and_field_types"):
+ # noinspection PyProtectedMember
+ fields_and_types: Dict[str, str] = cast(Dict[str, str], self._data._fields_and_field_types)
+ return list(fields_and_types.keys())
+ else:
+ return []
+
+
+class FieldAccessorFactory:
+ @staticmethod
+ def create_accessor(
+ data: Any,
+ type_info_provider: TypeInfoProvider,
+ parent: Optional[FieldAccessor] = None,
+ field_name: str = Consts.ACCESSOR_ROOT_NODE,
+ ) -> FieldAccessor:
+ if hasattr(data, "_fields_and_field_types"):
+ return ROS2FieldAccessor(data, type_info_provider, parent, field_name)
+ else:
+ return FieldAccessor(data, type_info_provider, parent, field_name)
diff --git a/msgcenterpy/core/message_center.py b/msgcenterpy/core/message_center.py
new file mode 100644
index 0000000..397c268
--- /dev/null
+++ b/msgcenterpy/core/message_center.py
@@ -0,0 +1,69 @@
+from typing import Any, Dict, Optional, Type
+
+from msgcenterpy.core.envelope import MessageEnvelope, Properties
+from msgcenterpy.core.message_instance import MessageInstance
+from msgcenterpy.core.types import MessageType
+
+
+class MessageCenter:
+ """Message Center singleton class that manages all message types and instances"""
+
+ _instance: Optional["MessageCenter"] = None
+
+ @classmethod
+ def get_instance(cls) -> "MessageCenter":
+ """Get MessageCenter singleton instance"""
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ def __init__(self) -> None:
+ """Private constructor, use get_instance() to get singleton"""
+ self._type_registry: Dict[MessageType, Type[MessageInstance]] = {}
+ self._register_builtin_types()
+
+ def _register_builtin_types(self) -> None:
+ """Register built-in message types with lazy import to avoid circular dependencies"""
+ try:
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
+
+ self._type_registry[MessageType.ROS2] = ROS2MessageInstance
+ except ImportError:
+ pass
+ try:
+ from msgcenterpy.instances.json_schema_instance import (
+ JSONSchemaMessageInstance,
+ )
+
+ self._type_registry[MessageType.JSON_SCHEMA] = JSONSchemaMessageInstance
+ except ImportError:
+ pass
+
+ def get_instance_class(self, message_type: MessageType) -> Type[MessageInstance]:
+ """Get instance class for the specified message type"""
+ instance_class = self._type_registry.get(message_type)
+ if not instance_class:
+ raise ValueError(f"Unsupported message type: {message_type}")
+ return instance_class
+
+ def convert(
+ self,
+ source: MessageInstance,
+ target_type: MessageType,
+ override_properties: Dict[str, Any],
+ **kwargs: Any,
+ ) -> MessageInstance:
+ """Convert message types"""
+ target_class = self.get_instance_class(target_type)
+ dict_data: MessageEnvelope = source.export_to_envelope()
+ if "properties" not in dict_data["metadata"]:
+ dict_data["metadata"]["properties"] = Properties()
+ dict_data["metadata"]["properties"].update(override_properties) # type: ignore[typeddict-item]
+ target_instance = target_class.import_from_envelope(dict_data)
+ return target_instance
+
+
+# Module-level convenience function using singleton
+def get_message_center() -> MessageCenter:
+ """Get message center singleton"""
+ return MessageCenter.get_instance()
diff --git a/msgcenterpy/core/message_instance.py b/msgcenterpy/core/message_instance.py
new file mode 100644
index 0000000..8ecda1b
--- /dev/null
+++ b/msgcenterpy/core/message_instance.py
@@ -0,0 +1,193 @@
+import uuid
+from abc import ABC, abstractmethod
+from datetime import datetime, timezone
+from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar, cast
+
+from msgcenterpy.core.envelope import FormatMetadata, MessageEnvelope, Properties
+from msgcenterpy.core.field_accessor import (
+ FieldAccessor,
+ FieldAccessorFactory,
+ TypeInfoProvider,
+)
+from msgcenterpy.core.types import MessageType
+
+if TYPE_CHECKING:
+ # 仅用于类型检查的导入,避免运行时循环依赖
+ from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
+
+T = TypeVar("T")
+
+
+class MessageInstance(TypeInfoProvider, ABC, Generic[T]):
+ """统一消息实例基类"""
+
+ _init_ok: bool = False
+
+ # 字段访问器相关方法
+ @property
+ def fields(self) -> FieldAccessor:
+ if self._field_accessor is None:
+ raise RuntimeError("FieldAccessor not initialized")
+ return self._field_accessor
+
+ def __setattr__(self, field_name: str, value: Any) -> None:
+ if not self._init_ok:
+ return super().__setattr__(field_name, value)
+ for cls in self.__class__.__mro__:
+ if field_name in cls.__dict__:
+ return super().__setattr__(field_name, value)
+ self.fields[field_name] = value
+ return None
+
+ def __getattr__(self, field_name: str) -> Any:
+ if not self._init_ok:
+ return super().__getattribute__(field_name)
+ for cls in self.__class__.__mro__:
+ if field_name in cls.__dict__:
+ return super().__getattribute__(field_name)
+ return self.fields[field_name]
+
+ def __getitem__(self, field_name: str) -> Any:
+ """支持通过下标访问字段"""
+ return self.fields[field_name]
+
+ def __setitem__(self, field_name: str, value: Any) -> None:
+ """支持通过下标设置字段"""
+ self.fields[field_name] = value
+
+ def __contains__(self, field_name: str) -> bool:
+ """支持in操作符检查字段是否存在"""
+ return field_name in self.fields
+
+ def __init__(
+ self,
+ inner_data: T,
+ message_type: MessageType,
+ metadata: Optional[FormatMetadata] = None,
+ ):
+ # 初始化标记和基础属性
+ self._field_accessor: Optional[FieldAccessor] = None
+
+ self._instance_id: str = str(uuid.uuid4())
+ self.inner_data: T = inner_data # 原始类型数据
+ self.message_type: MessageType = message_type
+ self._metadata: FormatMetadata = metadata or FormatMetadata()
+ self._created_at = datetime.now(timezone.utc)
+ self._collect_public_properties_to_metadata()
+ self._field_accessor = FieldAccessorFactory.create_accessor(self.inner_data, self)
+ self._init_ok = True
+
+ def _collect_public_properties_to_metadata(self) -> None:
+ """将所有非下划线开头的 @property 的当前值放入 metadata.properties 中。
+
+ 仅收集只读属性,忽略访问抛出异常的属性。
+ """
+ properties_bucket = self._metadata.setdefault("properties", Properties())
+ for cls in self.__class__.__mro__:
+ for attribute_name, attribute_value in cls.__dict__.items():
+ if attribute_name.startswith("_"):
+ continue
+ if isinstance(attribute_value, property):
+ try:
+ # 避免重复收集已存在的属性
+ if attribute_name not in properties_bucket:
+ properties_bucket[attribute_name] = getattr(self, attribute_name) # type: ignore[literal-required]
+ except (AttributeError, TypeError, RuntimeError):
+ # Skip attributes that can't be accessed or have incompatible types
+ # This includes attributes that require initialization to complete (like 'fields')
+ pass
+
+ def to(self, target_type: MessageType, **kwargs: Any) -> "MessageInstance[Any]":
+ """直接转换到目标类型"""
+ if target_type == MessageType.ROS2:
+ return cast("MessageInstance[Any]", self.to_ros2(**kwargs))
+ elif target_type == MessageType.DICT:
+ return cast("MessageInstance[Any]", self.to_dict(**kwargs))
+ elif target_type == MessageType.JSON:
+ return cast("MessageInstance[Any]", self.to_json(**kwargs))
+ elif target_type == MessageType.JSON_SCHEMA:
+ return cast("MessageInstance[Any]", self.to_json_schema(**kwargs))
+ elif target_type == MessageType.YAML:
+ return cast("MessageInstance[Any]", self.to_yaml(**kwargs))
+ elif target_type == MessageType.PYDANTIC:
+ return cast("MessageInstance[Any]", self.to_pydantic(**kwargs))
+ elif target_type == MessageType.DATACLASS:
+ return cast("MessageInstance[Any]", self.to_dataclass(**kwargs))
+ else:
+ raise ValueError(f"Unsupported target message type: {target_type}")
+
+ @classmethod
+ @abstractmethod
+ def import_from_envelope(cls, data: MessageEnvelope, **kwargs: Any) -> "MessageInstance[Any]":
+ """从统一信封字典创建实例(仅接受 data 一个参数)。"""
+ # metadata会被重置
+ pass
+
+ @abstractmethod
+ def export_to_envelope(self, **kwargs: Any) -> MessageEnvelope:
+ """导出为字典格式"""
+ pass
+
+ @abstractmethod
+ def get_python_dict(self) -> Dict[str, Any]:
+ """获取当前所有的字段名和对应的python可读值"""
+ pass
+
+ @abstractmethod
+ def set_python_dict(self, value: Dict[str, Any], **kwargs: Any) -> bool:
+ """设置所有字段的值"""
+ pass
+
+ def get_json_schema(self) -> Dict[str, Any]:
+ """生成当前消息实例的JSON Schema,委托给FieldAccessor递归处理"""
+ # 直接调用FieldAccessor的get_json_schema方法
+ schema = self.fields.get_json_schema()
+
+ # 添加schema元信息(对于JSONSchemaMessageInstance,如果已有title则保持,否则添加默认title)
+ from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
+
+ if isinstance(self, JSONSchemaMessageInstance):
+ # 对于JSON Schema实例,如果schema中没有title,则添加一个
+ if "title" not in schema:
+ schema["title"] = f"{self.__class__.__name__} Schema" # type: ignore
+ if "description" not in schema:
+ schema["description"] = f"JSON Schema generated from {self.message_type.value} message instance" # type: ignore
+ else:
+ # 对于其他类型的实例,总是添加schema元信息
+ schema["title"] = f"{self.__class__.__name__} Schema" # type: ignore
+ schema["description"] = f"JSON Schema generated from {self.message_type.value} message instance" # type: ignore
+
+ return schema
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(type={self.message_type.value}, id={self._instance_id[:8]})"
+
+ # 便捷转换方法,使用MessageCenter单例
+ def to_ros2(self, type_hint: str | Type[Any], **kwargs: Any) -> "ROS2MessageInstance":
+ """转换到ROS2实例。传入必备的类型提示,"""
+ override_properties = {}
+ from msgcenterpy.core.message_center import get_message_center
+
+ ros2_message_instance = cast(
+ ROS2MessageInstance,
+ get_message_center().get_instance_class(MessageType.ROS2),
+ )
+ ros_type = ros2_message_instance.obtain_ros_cls_from_str(type_hint)
+ override_properties["ros_msg_cls_path"] = ROS2MessageInstance.get_ros_msg_cls_path(ros_type)
+ override_properties["ros_msg_cls_namespace"] = ROS2MessageInstance.get_ros_msg_cls_namespace(ros_type)
+ return cast(
+ ROS2MessageInstance,
+ get_message_center().convert(self, MessageType.ROS2, override_properties, **kwargs),
+ )
+
+ def to_json_schema(self, **kwargs: Any) -> "JSONSchemaMessageInstance":
+ """转换到JSON Schema实例"""
+ override_properties = {"json_schema": self.get_json_schema()}
+ from msgcenterpy.core.message_center import get_message_center
+ from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
+
+ return cast(
+ JSONSchemaMessageInstance,
+ get_message_center().convert(self, MessageType.JSON_SCHEMA, override_properties, **kwargs),
+ )
diff --git a/msgcenterpy/core/type_converter.py b/msgcenterpy/core/type_converter.py
new file mode 100644
index 0000000..6d847b5
--- /dev/null
+++ b/msgcenterpy/core/type_converter.py
@@ -0,0 +1,411 @@
+from datetime import datetime
+from decimal import Decimal
+from enum import Enum
+from typing import Any, Dict, Type, Union, get_args, get_origin
+
+
+class StandardType(Enum):
+ """标准化的数据类型,用于不同数据源之间的转换
+
+ 增强版本,提供更细粒度的类型保留以更好地保存原始类型信息
+ """
+
+ # 基础类型
+ STRING = "string" # 字符串类型
+ WSTRING = "wstring" # 宽字符串类型
+ CHAR = "char" # 字符类型
+ WCHAR = "wchar" # 宽字符类型
+
+ # 整数类型(细分以保留精度信息)
+ INT8 = "int8" # 8位有符号整数
+ UINT8 = "uint8" # 8位无符号整数
+ INT16 = "int16" # 16位有符号整数
+ UINT16 = "uint16" # 16位无符号整数
+ INT32 = "int32" # 32位有符号整数
+ UINT32 = "uint32" # 32位无符号整数
+ INT64 = "int64" # 64位有符号整数
+ UINT64 = "uint64" # 64位无符号整数
+ INTEGER = "integer" # 通用整数类型(向后兼容)
+ BYTE = "byte" # 字节类型
+ OCTET = "octet" # 八位字节类型
+
+ # 浮点类型(细分以保留精度信息)
+ FLOAT32 = "float32" # 32位浮点数
+ FLOAT64 = "float64" # 64位浮点数(双精度)
+ DOUBLE = "double" # 双精度浮点数
+ FLOAT = "float" # 通用浮点类型(向后兼容)
+
+ # 布尔类型
+ BOOLEAN = "boolean" # 布尔类型
+ BOOL = "bool" # 布尔类型(ROS2风格)
+
+ # 空值类型
+ NULL = "null" # 空值类型
+
+ # 容器类型
+ ARRAY = "array" # 数组/序列类型
+ BOUNDED_ARRAY = "bounded_array" # 有界数组类型
+ UNBOUNDED_ARRAY = "unbounded_array" # 无界数组类型
+ SEQUENCE = "sequence" # 序列类型
+ BOUNDED_SEQUENCE = "bounded_sequence" # 有界序列类型
+ UNBOUNDED_SEQUENCE = "unbounded_sequence" # 无界序列类型
+ OBJECT = "object" # 对象/映射类型
+
+ # 扩展类型
+ DATETIME = "datetime" # 日期时间类型
+ TIME = "time" # 时间类型
+ DURATION = "duration" # 持续时间类型
+ BYTES = "bytes" # 字节数据类型
+ DECIMAL = "decimal" # 精确小数类型
+
+ # 特殊类型
+ UNKNOWN = "unknown" # 未知类型
+ ANY = "any" # 任意类型
+
+ @property
+ def IS_ARRAY(self) -> bool:
+ """判断该类型是否为数组/序列类型"""
+ array_like = {
+ StandardType.ARRAY,
+ StandardType.BOUNDED_ARRAY,
+ StandardType.UNBOUNDED_ARRAY,
+ StandardType.SEQUENCE,
+ StandardType.BOUNDED_SEQUENCE,
+ StandardType.UNBOUNDED_SEQUENCE,
+ }
+ return self in array_like
+
+
+class TypeConverter:
+ """类型转换器,负责不同数据源类型之间的转换和标准化"""
+
+ # Python基础类型到标准类型的映射
+ PYTHON_TO_STANDARD = {
+ str: StandardType.STRING,
+ int: StandardType.INTEGER, # 保持向后兼容的通用整数
+ float: StandardType.FLOAT, # 保持向后兼容的通用浮点
+ bool: StandardType.BOOLEAN,
+ type(None): StandardType.NULL,
+ list: StandardType.ARRAY,
+ tuple: StandardType.ARRAY,
+ dict: StandardType.OBJECT,
+ datetime: StandardType.DATETIME,
+ bytes: StandardType.BYTES,
+ bytearray: StandardType.BYTES,
+ Decimal: StandardType.DECIMAL,
+ }
+
+ # 标准类型到Python类型的映射
+ STANDARD_TO_PYTHON = {
+ # 字符串类型
+ StandardType.STRING: str,
+ StandardType.WSTRING: str,
+ StandardType.CHAR: str,
+ StandardType.WCHAR: str,
+ # 整数类型(都映射到int,Python会自动处理范围)
+ StandardType.INT8: int,
+ StandardType.UINT8: int,
+ StandardType.INT16: int,
+ StandardType.UINT16: int,
+ StandardType.INT32: int,
+ StandardType.UINT32: int,
+ StandardType.INT64: int,
+ StandardType.UINT64: int,
+ StandardType.INTEGER: int,
+ StandardType.BYTE: int,
+ StandardType.OCTET: int,
+ # 浮点类型
+ StandardType.FLOAT32: float,
+ StandardType.FLOAT64: float,
+ StandardType.DOUBLE: float,
+ StandardType.FLOAT: float,
+ # 布尔类型
+ StandardType.BOOLEAN: bool,
+ StandardType.BOOL: bool,
+ # 空值类型
+ StandardType.NULL: type(None),
+ # 容器类型
+ StandardType.ARRAY: list,
+ StandardType.BOUNDED_ARRAY: list,
+ StandardType.UNBOUNDED_ARRAY: list,
+ StandardType.SEQUENCE: list,
+ StandardType.BOUNDED_SEQUENCE: list,
+ StandardType.UNBOUNDED_SEQUENCE: list,
+ StandardType.OBJECT: dict,
+ # 扩展类型
+ StandardType.DATETIME: datetime,
+ StandardType.TIME: datetime,
+ StandardType.DURATION: float, # 持续时间用秒表示
+ StandardType.BYTES: bytes,
+ StandardType.DECIMAL: Decimal,
+ # 特殊类型
+ StandardType.UNKNOWN: object,
+ StandardType.ANY: object,
+ }
+
+ # ROS2类型到标准类型的映射(保留原始类型精度)
+ ROS2_TO_STANDARD = {
+ # 字符串类型(保留具体类型)
+ "string": StandardType.STRING,
+ "wstring": StandardType.WSTRING,
+ "char": StandardType.CHAR,
+ "wchar": StandardType.WCHAR,
+ # 整数类型(保留精度信息)
+ "int8": StandardType.INT8,
+ "uint8": StandardType.UINT8,
+ "int16": StandardType.INT16,
+ "short": StandardType.INT16, # to check
+ "uint16": StandardType.UINT16,
+ "unsigned short": StandardType.UINT16, # to check
+ "int32": StandardType.INT32,
+ "uint32": StandardType.UINT32,
+ "int64": StandardType.INT64,
+ "long": StandardType.INT64, # to check
+ "long long": StandardType.INT64, # to check
+ "uint64": StandardType.UINT64,
+ "unsigned long": StandardType.UINT64, # to check
+ "unsigned long long": StandardType.UINT64, # to check
+ "byte": StandardType.BYTE,
+ "octet": StandardType.OCTET,
+ # 浮点类型(保留精度信息)
+ "float32": StandardType.FLOAT32,
+ "float64": StandardType.FLOAT64,
+ "double": StandardType.DOUBLE,
+ "long double": StandardType.DOUBLE,
+ "float": StandardType.FLOAT32, # 默认为32位
+ # 布尔类型
+ "bool": StandardType.BOOL,
+ "boolean": StandardType.BOOLEAN,
+ # 时间和持续时间(更精确的类型映射)
+ "time": StandardType.TIME,
+ "duration": StandardType.DURATION,
+ # 向后兼容的通用映射(当需要时可以回退到这些)
+ "generic_int": StandardType.INTEGER,
+ "generic_float": StandardType.FLOAT,
+ "generic_bool": StandardType.BOOLEAN,
+ "generic_string": StandardType.STRING,
+ }
+
+ # JSON Schema类型到标准类型的映射
+ JSON_SCHEMA_TO_STANDARD = {
+ "string": StandardType.STRING,
+ "integer": StandardType.INTEGER,
+ "number": StandardType.FLOAT,
+ "boolean": StandardType.BOOLEAN,
+ "null": StandardType.NULL,
+ "array": StandardType.ARRAY,
+ "object": StandardType.OBJECT,
+ }
+
+ # 标准类型到Python类型的映射
+ STANDARD_TO_JSON_SCHEMA = {
+ # 字符串类型
+ StandardType.STRING: "string",
+ StandardType.WSTRING: "string",
+ StandardType.CHAR: "string",
+ StandardType.WCHAR: "string",
+ # 整数类型(都映射到int,Python会自动处理范围)
+ StandardType.INT8: "integer",
+ StandardType.UINT8: "integer",
+ StandardType.INT16: "integer",
+ StandardType.UINT16: "integer",
+ StandardType.INT32: "integer",
+ StandardType.UINT32: "integer",
+ StandardType.INT64: "integer",
+ StandardType.UINT64: "integer",
+ StandardType.INTEGER: "integer",
+ StandardType.BYTE: "integer",
+ StandardType.OCTET: "integer",
+ # 浮点类型
+ StandardType.FLOAT32: "number",
+ StandardType.FLOAT64: "number",
+ StandardType.DOUBLE: "number",
+ StandardType.FLOAT: "number",
+ # 布尔类型
+ StandardType.BOOLEAN: "boolean",
+ StandardType.BOOL: "boolean",
+ # 空值类型
+ StandardType.NULL: "null",
+ # 容器类型
+ StandardType.ARRAY: "array",
+ StandardType.BOUNDED_ARRAY: "array",
+ StandardType.UNBOUNDED_ARRAY: "array",
+ StandardType.SEQUENCE: "array",
+ StandardType.BOUNDED_SEQUENCE: "array",
+ StandardType.UNBOUNDED_SEQUENCE: "array",
+ StandardType.OBJECT: "object",
+ # 扩展类型
+ StandardType.DATETIME: "string", # 在JSON Schema中日期时间通常表示为字符串
+ StandardType.TIME: "string",
+ StandardType.DURATION: "number",
+ StandardType.BYTES: "string", # 字节数据在JSON Schema中通常表示为base64字符串
+ StandardType.DECIMAL: "number",
+ # 特殊类型
+ StandardType.UNKNOWN: "string",
+ StandardType.ANY: "string",
+ }
+
+ # Array typecode到标准类型的映射(更精确的类型保留)
+ ARRAY_TYPECODE_TO_STANDARD = {
+ "b": StandardType.INT8, # signed char
+ "B": StandardType.UINT8, # unsigned char
+ "h": StandardType.INT16, # signed short
+ "H": StandardType.UINT16, # unsigned short
+ "i": StandardType.INT32, # signed int
+ "I": StandardType.UINT32, # unsigned int
+ "l": StandardType.INT64, # signed long
+ "L": StandardType.UINT64, # unsigned long
+ "f": StandardType.FLOAT32, # float
+ "d": StandardType.FLOAT64, # double
+ }
+
+ # Array typecode到Python类型的映射
+ ARRAY_TYPECODE_TO_PYTHON = {
+ "b": int, # signed char
+ "B": int, # unsigned char
+ "h": int, # signed short
+ "H": int, # unsigned short
+ "i": int, # signed int
+ "I": int, # unsigned int
+ "l": int, # signed long
+ "L": int, # unsigned long
+ "f": float, # float
+ "d": float, # double
+ }
+
+ """Python Type"""
+
+ @classmethod
+ def python_type_to_standard(cls, python_type: Type) -> StandardType:
+ """将Python类型转换为标准类型"""
+ # 处理泛型类型
+ origin = get_origin(python_type)
+ if origin is not None:
+ if origin in (list, tuple):
+ return StandardType.ARRAY
+ elif origin is dict:
+ return StandardType.OBJECT
+ elif origin in (Union, type(Union[int, None])):
+ # 处理Optional类型和Union类型
+ args = get_args(python_type)
+ non_none_types = [arg for arg in args if arg != type(None)]
+ if len(non_none_types) == 1:
+ return cls.python_type_to_standard(non_none_types[0])
+ return StandardType.ANY
+
+ # 处理基础类型
+ return cls.PYTHON_TO_STANDARD.get(python_type, StandardType.UNKNOWN)
+
+ @classmethod
+ def standard_to_python_type(cls, standard_type: StandardType) -> Type:
+ """将标准类型转换为Python类型"""
+ return cls.STANDARD_TO_PYTHON.get(standard_type, object)
+
+ """ROS2"""
+
+ @classmethod
+ def ros2_type_str_to_standard(cls, ros2_type_str: str) -> StandardType:
+ """将ROS2类型字符串转换为标准类型"""
+ if "[" in ros2_type_str and "]" in ros2_type_str:
+ return StandardType.ARRAY
+ base_type = ros2_type_str.split("/")[-1].lower()
+ return cls.ROS2_TO_STANDARD.get(base_type, StandardType.UNKNOWN)
+
+ @classmethod
+ def rosidl_definition_to_standard(cls, definition_type: Any) -> StandardType:
+ from rosidl_parser.definition import ( # type: ignore
+ Array,
+ BasicType,
+ BoundedSequence,
+ BoundedString,
+ BoundedWString,
+ NamedType,
+ NamespacedType,
+ UnboundedSequence,
+ UnboundedString,
+ UnboundedWString,
+ )
+
+ # 基础类型转换(保留原始类型精度)
+ if isinstance(definition_type, BasicType):
+ type_name = definition_type.typename.lower()
+ return cls.ros2_type_str_to_standard(type_name)
+ # 字符串类型(区分普通字符串和宽字符串)
+ elif isinstance(definition_type, (UnboundedString, BoundedString)):
+ return StandardType.STRING
+ elif isinstance(definition_type, (UnboundedWString, BoundedWString)):
+ return StandardType.WSTRING
+ # 数组和序列类型(更精确的类型区分)
+ elif isinstance(definition_type, Array):
+ return StandardType.BOUNDED_ARRAY
+ elif isinstance(definition_type, UnboundedSequence):
+ return StandardType.UNBOUNDED_SEQUENCE
+ elif isinstance(definition_type, BoundedSequence):
+ return StandardType.BOUNDED_SEQUENCE
+ # 命名类型和命名空间类型统一为OBJECT
+ elif isinstance(definition_type, (NamedType, NamespacedType)):
+ return StandardType.OBJECT
+ # 未知类型
+ else:
+ return StandardType.UNKNOWN
+
+ @classmethod
+ def array_typecode_to_standard(cls, typecode: str) -> StandardType:
+ """将array.array的typecode转换为标准类型"""
+ return cls.ARRAY_TYPECODE_TO_STANDARD.get(typecode, StandardType.UNKNOWN)
+
+ """JSON Schema"""
+
+ @classmethod
+ def json_schema_type_to_standard(cls, json_type: str) -> StandardType:
+ """将JSON Schema类型转换为标准类型"""
+ return cls.JSON_SCHEMA_TO_STANDARD.get(json_type, StandardType.UNKNOWN)
+
+ @classmethod
+ def standard_type_to_json_schema_type(cls, standard_type: StandardType) -> str:
+ """将StandardType转换为JSON Schema类型字符串"""
+ return cls.STANDARD_TO_JSON_SCHEMA.get(standard_type, "string")
+
+ """值转换"""
+
+ @classmethod
+ def convert_to_python_value_with_standard_type(cls, value: Any, target_standard_type: StandardType) -> Any:
+ """将值转换为指定的标准类型对应的Python值"""
+ if value is None:
+ return None if target_standard_type == StandardType.NULL else None
+ target_python_type = cls.standard_to_python_type(target_standard_type)
+ if target_python_type != object and type(value) == target_python_type:
+ # object交由target_standard_type为OBJECT的分支处理,同样返回原值
+ return value
+ if target_standard_type == StandardType.ARRAY:
+ if isinstance(value, (list, tuple)):
+ return list(value)
+ elif hasattr(value, "typecode"): # array.array
+ return list(value)
+ elif isinstance(value, str):
+ return list(value) # 字符串转为字符数组
+ else:
+ return [value] # 单个值包装为数组
+ elif target_standard_type == StandardType.OBJECT:
+ return value
+ elif target_standard_type == StandardType.DATETIME:
+ if isinstance(value, datetime):
+ return value
+ elif isinstance(value, (int, float)):
+ return datetime.fromtimestamp(value)
+ elif isinstance(value, str):
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
+ else:
+ return datetime.now()
+ elif target_standard_type == StandardType.BYTES:
+ if isinstance(value, bytes):
+ return value
+ elif isinstance(value, str):
+ return value.encode("utf-8")
+ elif isinstance(value, (list, tuple)):
+ return bytes(value)
+ else:
+ return str(value).encode("utf-8")
+ else:
+ # 基础类型转换
+ return target_python_type(value)
diff --git a/msgcenterpy/core/type_info.py b/msgcenterpy/core/type_info.py
new file mode 100644
index 0000000..c97d1b3
--- /dev/null
+++ b/msgcenterpy/core/type_info.py
@@ -0,0 +1,400 @@
+import copy
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, Dict, List, Optional, Type
+
+from msgcenterpy.core.type_converter import StandardType, TypeConverter
+
+
+class Consts:
+ ELEMENT_TYPE_INFO_SYMBOL = "ELEMENT_TYPE_INFO"
+ ACCESSOR_ROOT_NODE = "MSG_CENTER_ROOT"
+
+
+class ConstraintType(Enum):
+ """Constraint type enumeration"""
+
+ MIN_VALUE = "min_value"
+ MAX_VALUE = "max_value"
+ MIN_LENGTH = "min_length"
+ MAX_LENGTH = "max_length"
+ MIN_ITEMS = "min_items"
+ MAX_ITEMS = "max_items"
+ PATTERN = "pattern"
+ ENUM_VALUES = "enum_values"
+ MULTIPLE_OF = "multiple_of"
+ TYPE_KEEP = "type_keep"
+ EXCLUSIVE_MIN = "exclusive_min"
+ EXCLUSIVE_MAX = "exclusive_max"
+ UNIQUE_ITEMS = "unique_items"
+ DEFAULT_VALUE = "default_value"
+ REQUIRED = "required"
+ FORMAT = "format"
+
+
+@dataclass
+class TypeConstraint:
+ """Type constraint definition"""
+
+ type: ConstraintType
+ value: Any
+ description: Optional[str] = None
+
+ def to_json_schema_property(self) -> Dict[str, Any]:
+ """Convert to JSON Schema property"""
+ mapping = {
+ ConstraintType.MIN_VALUE: "minimum",
+ ConstraintType.MAX_VALUE: "maximum",
+ ConstraintType.MIN_LENGTH: "minLength",
+ ConstraintType.MAX_LENGTH: "maxLength",
+ ConstraintType.MIN_ITEMS: "minItems",
+ ConstraintType.MAX_ITEMS: "maxItems",
+ ConstraintType.PATTERN: "pattern",
+ ConstraintType.ENUM_VALUES: "enum",
+ ConstraintType.MULTIPLE_OF: "multipleOf",
+ ConstraintType.EXCLUSIVE_MIN: "exclusiveMinimum",
+ ConstraintType.EXCLUSIVE_MAX: "exclusiveMaximum",
+ ConstraintType.UNIQUE_ITEMS: "uniqueItems",
+ ConstraintType.DEFAULT_VALUE: "default",
+ ConstraintType.FORMAT: "format",
+ }
+
+ property_name = mapping.get(self.type)
+ if property_name:
+ result = {property_name: self.value}
+ if self.description:
+ result["description"] = self.description
+ return result
+ return {}
+
+
+@dataclass
+class TypeInfo:
+ """Detailed type information including standard type, Python type and constraints"""
+
+ # Basic type information
+ field_name: str
+ field_path: str
+ standard_type: StandardType
+ python_type: Type
+ original_type: Any # Original type (e.g., ROS2 type instance)
+ _outdated: bool = False
+
+ @property
+ def outdated(self) -> bool:
+ return self._outdated
+
+ # Value information
+ current_value: Any = None
+ default_value: Any = None
+
+ # Constraints
+ constraints: List[TypeConstraint] = field(default_factory=list)
+
+ # Array/sequence related information
+ is_array: bool = False
+ array_size: Optional[int] = None # Fixed size array
+ _element_type_info: Optional["TypeInfo"] = None # Array element type
+
+ @property
+ def element_type_info(self) -> Optional["TypeInfo"]:
+ return self._element_type_info
+
+ @element_type_info.setter
+ def element_type_info(self, value: Optional["TypeInfo"]) -> None:
+ if self.outdated:
+ raise ValueError("Should not change an outdated type")
+ if value is not None:
+ value.field_name = Consts.ELEMENT_TYPE_INFO_SYMBOL
+ value.field_path = Consts.ELEMENT_TYPE_INFO_SYMBOL
+ self._element_type_info = value
+
+ # Object related information
+ is_object: bool = False
+ object_fields: Dict[str, "TypeInfo"] = field(default_factory=dict)
+
+ # Additional metadata
+ metadata: Dict[str, Any] = field(default_factory=dict)
+
+ @property
+ def python_value_from_standard_type(self) -> Any:
+ return TypeConverter.convert_to_python_value_with_standard_type(self.current_value, self.standard_type)
+
+ def add_constraint(
+ self,
+ constraint_type: ConstraintType,
+ value: Any,
+ description: Optional[str] = None,
+ ) -> None:
+ """Add constraint"""
+ constraint = TypeConstraint(constraint_type, value, description)
+ # Avoid duplicate constraints of the same type
+ self.constraints = [c for c in self.constraints if c.type != constraint_type]
+ self.constraints.append(constraint)
+
+ def get_constraint(self, constraint_type: ConstraintType) -> Optional[TypeConstraint]:
+ """Get constraint of specified type"""
+ for constraint in self.constraints:
+ if constraint.type == constraint_type:
+ return constraint
+ return None
+
+ def has_constraint(self, constraint_type: ConstraintType) -> bool:
+ """Check if constraint of specified type exists"""
+ return self.get_constraint(constraint_type) is not None
+
+ def get_constraint_value(self, constraint_type: ConstraintType) -> Any:
+ """Get value of specified constraint"""
+ constraint = self.get_constraint(constraint_type)
+ return constraint.value if constraint else None
+
+ def validate_value(self, value: Any) -> bool:
+ """Validate value according to constraints"""
+ try:
+ if self.get_constraint(ConstraintType.TYPE_KEEP):
+ # ROS includes TYPE_KEEP
+ if type(self.current_value) != type(value):
+ return False
+ # Basic type check
+ if not self._validate_basic_type(value):
+ return False
+
+ # Numeric constraint check
+ if not self._validate_numeric_constraints(value):
+ return False
+
+ # String constraint check
+ if not self._validate_string_constraints(value):
+ return False
+
+ # Array constraint check
+ if not self._validate_array_constraints(value):
+ return False
+
+ return True
+
+ except Exception:
+ return False
+
+ def _validate_basic_type(self, value: Any) -> bool:
+ """Validate basic type"""
+ if value is None:
+ return not self.has_constraint(ConstraintType.REQUIRED)
+ return True
+
+ def _validate_numeric_constraints(self, value: Any) -> bool:
+ """Validate numeric constraints"""
+ if not isinstance(value, (int, float)):
+ return True
+
+ min_val = self.get_constraint_value(ConstraintType.MIN_VALUE)
+ if min_val is not None and value < min_val:
+ return False
+
+ max_val = self.get_constraint_value(ConstraintType.MAX_VALUE)
+ if max_val is not None and value > max_val:
+ return False
+
+ exclusive_min = self.get_constraint_value(ConstraintType.EXCLUSIVE_MIN)
+ if exclusive_min is not None and value <= exclusive_min:
+ return False
+
+ exclusive_max = self.get_constraint_value(ConstraintType.EXCLUSIVE_MAX)
+ if exclusive_max is not None and value >= exclusive_max:
+ return False
+
+ multiple_of = self.get_constraint_value(ConstraintType.MULTIPLE_OF)
+ if multiple_of is not None and value % multiple_of != 0:
+ return False
+
+ return True
+
+ def _validate_string_constraints(self, value: Any) -> bool:
+ """Validate string constraints"""
+ if not isinstance(value, str):
+ return True
+
+ min_len = self.get_constraint_value(ConstraintType.MIN_LENGTH)
+ if min_len is not None and len(value) < min_len:
+ return False
+
+ max_len = self.get_constraint_value(ConstraintType.MAX_LENGTH)
+ if max_len is not None and len(value) > max_len:
+ return False
+
+ pattern = self.get_constraint_value(ConstraintType.PATTERN)
+ if pattern is not None:
+ import re
+
+ if not re.match(pattern, value):
+ return False
+
+ enum_values = self.get_constraint_value(ConstraintType.ENUM_VALUES)
+ if enum_values is not None and value not in enum_values:
+ return False
+
+ return True
+
+ def _validate_array_constraints(self, value: Any) -> bool:
+ """Validate array constraints"""
+ if not isinstance(value, (list, tuple)):
+ return True
+
+ min_items = self.get_constraint_value(ConstraintType.MIN_ITEMS)
+ if min_items is not None and len(value) < min_items:
+ return False
+
+ max_items = self.get_constraint_value(ConstraintType.MAX_ITEMS)
+ if max_items is not None and len(value) > max_items:
+ return False
+
+ if self.array_size is not None and len(value) != self.array_size:
+ return False
+
+ unique_items = self.get_constraint_value(ConstraintType.UNIQUE_ITEMS)
+ if unique_items and len(set(value)) != len(value):
+ return False
+
+ return True
+
+ def to_json_schema_property(self, include_constraints: bool = True) -> Dict[str, Any]:
+ """Convert to JSON Schema property definition"""
+ from msgcenterpy.core.type_converter import TypeConverter
+
+ # Basic properties
+ property_schema: Dict[str, Any] = {"type": TypeConverter.standard_type_to_json_schema_type(self.standard_type)}
+
+ # Add constraints
+ if include_constraints:
+ for constraint in self.constraints:
+ constraint_props = constraint.to_json_schema_property()
+ property_schema.update(constraint_props)
+
+ # Special handling for array types
+ if self.is_array and self.element_type_info:
+ property_schema["items"] = self.element_type_info.to_json_schema_property(include_constraints)
+
+ # Special handling for object types
+ if self.is_object and self.object_fields:
+ properties = {}
+ for field_name, field_info in self.object_fields.items():
+ properties[field_name] = field_info.to_json_schema_property(include_constraints)
+ property_schema["properties"] = properties
+
+ # Add description
+ if self.original_type:
+ property_schema["description"] = f"Field of type {self.original_type}"
+
+ return property_schema
+
+ def convert_value(self, value: Any, target_standard_type: Optional[StandardType] = None) -> Any:
+ """Convert value to current type or specified target type"""
+ target_type = target_standard_type or self.standard_type
+ converted_value = TypeConverter.convert_to_python_value_with_standard_type(value, target_type)
+ # Validate converted value
+ if target_standard_type is None and not self.validate_value(converted_value):
+ # Format constraint information
+ constraints_info = []
+ for c in self.constraints:
+ constraint_desc = f"{c.type.value}: {c.value}"
+ if c.description:
+ constraint_desc += f" ({c.description})"
+ constraints_info.append(constraint_desc)
+
+ constraints_str = ", ".join(constraints_info) if constraints_info else "No constraints"
+ raise ValueError(
+ f"Value {value} does not meet constraints for field {self.field_name}. "
+ f"Constraints: [{constraints_str}]"
+ )
+ return converted_value
+
+ def get_value_info(self) -> Dict[str, Any]:
+ """Get detailed information about current value"""
+ return {
+ "field_name": self.field_name,
+ "current_value": self.current_value,
+ "standard_type": self.standard_type.value,
+ "python_type": self.python_type.__name__,
+ "original_type": self.original_type,
+ "is_valid": self.validate_value(self.current_value),
+ "constraints": [
+ {"type": c.type.value, "value": c.value, "description": c.description} for c in self.constraints
+ ],
+ "is_array": self.is_array,
+ "array_size": self.array_size,
+ "is_object": self.is_object,
+ "metadata": self.metadata,
+ }
+
+ def clone(self) -> "TypeInfo":
+ """Create deep copy of TypeInfo"""
+ return copy.deepcopy(self)
+
+ def __repr__(self) -> str:
+ constraints_str = f", {len(self.constraints)} constraints" if self.constraints else ""
+ return f"TypeInfo({self.field_name}: {self.standard_type.value}{constraints_str})"
+
+
+class TypeInfoPostProcessor:
+ """TypeInfo post-processor that adds default constraints to TypeInfo"""
+
+ @staticmethod
+ def add_basic_type_constraints(type_info: TypeInfo) -> None:
+ """Add range constraints for basic types"""
+ if not type_info.standard_type:
+ return
+
+ standard_type = type_info.standard_type
+
+ # Integer type range constraints
+ if standard_type == StandardType.INT8:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, -128)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 127)
+ elif standard_type in (
+ StandardType.UINT8,
+ StandardType.BYTE,
+ StandardType.OCTET,
+ ):
+ type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 255)
+ elif standard_type == StandardType.INT16:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, -32768)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 32767)
+ elif standard_type == StandardType.UINT16:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 65535)
+ elif standard_type == StandardType.INT32:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, -2147483648)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 2147483647)
+ elif standard_type == StandardType.UINT32:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 4294967295)
+ elif standard_type == StandardType.INT64:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, -9223372036854775808)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 9223372036854775807)
+ elif standard_type == StandardType.UINT64:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 18446744073709551615)
+ # Floating point type range constraints
+ elif standard_type in (StandardType.FLOAT, StandardType.FLOAT32):
+ type_info.add_constraint(ConstraintType.MIN_VALUE, -3.4028235e38)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 3.4028235e38)
+ elif standard_type in (StandardType.DOUBLE, StandardType.FLOAT64):
+ type_info.add_constraint(ConstraintType.MIN_VALUE, -1.7976931348623157e308)
+ type_info.add_constraint(ConstraintType.MAX_VALUE, 1.7976931348623157e308)
+
+ @staticmethod
+ def add_default_constraints(type_info: TypeInfo) -> None:
+ """Add default constraints"""
+ field_value = type_info.current_value
+
+ # Add constraints for array types
+ if isinstance(field_value, (list, tuple)):
+ type_info.is_array = True
+ # 不再添加冗余的 MIN_ITEMS: 0
+
+ @staticmethod
+ def post_process_type_info(type_info: TypeInfo) -> None:
+ """Post-process TypeInfo, adding various default constraints"""
+ TypeInfoPostProcessor.add_basic_type_constraints(type_info)
+ TypeInfoPostProcessor.add_default_constraints(type_info)
diff --git a/msgcenterpy/core/types.py b/msgcenterpy/core/types.py
new file mode 100644
index 0000000..6487e2f
--- /dev/null
+++ b/msgcenterpy/core/types.py
@@ -0,0 +1,25 @@
+from enum import Enum
+
+
+class MessageType(Enum):
+ """Supported message types"""
+
+ ROS2 = "ros2"
+ PYDANTIC = "pydantic"
+ DATACLASS = "dataclass"
+ JSON = "json"
+ JSON_SCHEMA = "json_schema"
+ DICT = "dict"
+ YAML = "yaml"
+
+
+class ConversionError(Exception):
+ """Conversion error exception"""
+
+ pass
+
+
+class ValidationError(Exception):
+ """Validation error exception"""
+
+ pass
diff --git a/msgcenterpy/instances/__init__.py b/msgcenterpy/instances/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/msgcenterpy/instances/json_schema_instance.py b/msgcenterpy/instances/json_schema_instance.py
new file mode 100644
index 0000000..5b4c4cd
--- /dev/null
+++ b/msgcenterpy/instances/json_schema_instance.py
@@ -0,0 +1,303 @@
+from typing import TYPE_CHECKING, Any, Dict, Optional
+
+import jsonschema
+
+from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
+from msgcenterpy.core.message_instance import MessageInstance
+from msgcenterpy.core.type_converter import TypeConverter
+from msgcenterpy.core.type_info import ConstraintType, Consts, TypeInfo
+from msgcenterpy.core.types import MessageType
+
+if TYPE_CHECKING:
+ from msgcenterpy.core.field_accessor import FieldAccessor
+
+
+class JSONSchemaMessageInstance(MessageInstance[Dict[str, Any]]):
+ """JSON Schema消息实例,支持类型信息提取和字段访问器"""
+
+ _validation_errors: list[str] = []
+ _json_schema: Dict[str, Any] = dict()
+ _json_data: Dict[str, Any] = dict()
+
+ def __init__(self, inner_data: Dict[str, Any], schema: Dict[str, Any], **kwargs: Any) -> None:
+ """
+ 初始化JSON Schema消息实例
+
+ Args:
+ inner_data: JSON数据字典
+ schema: JSON Schema定义(必需)
+ """
+ # 直接存储schema和data
+ self._json_schema = schema
+ self._json_data = inner_data
+ self._validation_errors = []
+ # 验证数据
+ self._validate_data()
+ super().__init__(inner_data, MessageType.JSON_SCHEMA)
+
+ @property
+ def json_schema(self) -> Dict[str, Any]:
+ """获取JSON Schema"""
+ return self._json_schema
+
+ def _validate_data(self) -> None:
+ """根据schema验证数据"""
+ try:
+ jsonschema.validate(self._json_data, self._json_schema)
+ except jsonschema.ValidationError as e:
+ # 不抛出异常,只记录验证错误
+ self._validation_errors = [str(e)]
+ except Exception:
+ self._validation_errors = ["Schema validation failed"]
+ else:
+ self._validation_errors = []
+
+ def export_to_envelope(self, **kwargs: Any) -> MessageEnvelope:
+ """导出为统一信封字典"""
+ base_dict = self.get_python_dict()
+
+ envelope = create_envelope(
+ format_name=self.message_type.value,
+ content=base_dict,
+ metadata={
+ "current_format": self.message_type.value,
+ "source_cls_name": self.__class__.__name__,
+ "source_cls_module": self.__class__.__module__,
+ **self._metadata,
+ },
+ )
+ return envelope
+
+ @classmethod
+ def import_from_envelope(cls, data: MessageEnvelope, **kwargs: Any) -> "JSONSchemaMessageInstance":
+ """从规范信封创建JSON Schema实例"""
+ content = data["content"]
+ properties = data["metadata"]["properties"]
+ json_schema = properties["json_schema"]
+ instance = cls(content, json_schema)
+ return instance
+
+ def get_python_dict(self) -> Dict[str, Any]:
+ """获取当前所有的字段名和对应的原始值"""
+ return self._json_data.copy()
+
+ def set_python_dict(self, value: Dict[str, Any], **kwargs: Any) -> bool:
+ """设置所有字段的值,只做已有字段的更新"""
+ # 获取根访问器
+ root_accessor = self._field_accessor
+ if root_accessor is not None:
+ root_accessor.update_from_dict(source_data=value)
+ # 重新验证数据
+ self._validate_data()
+ return True
+
+ def _get_schema_from_path(self, path: str) -> Dict[str, Any]:
+ """根据访问器路径获取对应的JSON Schema定义
+
+ Args:
+ path: 字段访问器的完整路径,如 "MSG_CENTER_ROOT.user.address"
+
+ Returns:
+ 对应路径的JSON Schema定义
+ """
+ # 移除根路径前缀
+ if path.startswith(Consts.ACCESSOR_ROOT_NODE):
+ if path == Consts.ACCESSOR_ROOT_NODE:
+ return self._json_schema
+ path = path[len(Consts.ACCESSOR_ROOT_NODE) + 1 :]
+
+ # 如果路径为空,返回根schema
+ if not path:
+ return self._json_schema
+
+ # 分割路径并逐级导航
+ path_parts = path.split(".")
+ current_schema = self._json_schema
+
+ for part in path_parts:
+ # 检查当前schema是否有properties
+ if "properties" not in current_schema:
+ return {}
+
+ properties = current_schema["properties"]
+ if part not in properties:
+ return {}
+
+ current_schema = properties[part]
+
+ # 如果当前schema是数组,需要获取items的schema
+ if current_schema.get("type") == "array" and "items" in current_schema:
+ current_schema = current_schema["items"]
+
+ return current_schema
+
+ def _get_property_schema_for_field(self, field_name: str, parent_field_accessor: "FieldAccessor") -> Dict[str, Any]:
+ """获取字段的JSON Schema属性定义
+
+ Args:
+ field_name: 字段名
+ parent_field_accessor: 父级字段访问器
+
+ Returns:
+ 字段的JSON Schema属性定义
+ """
+ # 获取父级的schema定义
+ parent_schema = self._get_schema_from_path(parent_field_accessor.full_path_from_root)
+
+ # 从父级schema的properties中获取字段定义
+ if "properties" in parent_schema:
+ return parent_schema["properties"].get(field_name, {}) # type: ignore[no-any-return]
+ elif parent_schema.get("type") == "array" and "items" in parent_schema:
+ # 如果父级是数组,获取items的属性
+ items_schema = parent_schema["items"]
+ if "properties" in items_schema:
+ return items_schema["properties"].get(field_name, {}) # type: ignore[no-any-return]
+
+ return {}
+
+ # TypeInfoProvider 接口实现
+ def get_field_type_info(
+ self, field_name: str, field_value: Any, parent_field_accessor: "FieldAccessor"
+ ) -> Optional[TypeInfo]:
+ """从JSON Schema定义中提取字段类型信息"""
+ # 构建完整路径
+ full_path = f"{parent_field_accessor.full_path_from_root}.{field_name}"
+
+ # 获取字段的JSON Schema定义
+ property_schema = self._get_property_schema_for_field(field_name, parent_field_accessor)
+
+ # 确定类型信息
+ python_type = type(field_value)
+ if "type" in property_schema:
+ json_type = property_schema["type"]
+ standard_type = TypeConverter.json_schema_type_to_standard(json_type)
+ else:
+ # 如果schema中没有类型定义,从Python类型推断
+ standard_type = TypeConverter.python_type_to_standard(python_type)
+ json_type = TypeConverter.standard_type_to_json_schema_type(standard_type)
+
+ # 创建基础TypeInfo
+ type_info = TypeInfo(
+ field_name=field_name,
+ field_path=full_path,
+ standard_type=standard_type,
+ python_type=python_type,
+ original_type=json_type,
+ current_value=field_value,
+ )
+
+ # 提取约束信息
+ self._extract_constraints_from_schema(type_info, property_schema)
+
+ # 检查字段是否在父级的required列表中
+ parent_schema = self._get_schema_from_path(parent_field_accessor.full_path_from_root)
+ required_fields = parent_schema.get("required", [])
+ if field_name in required_fields:
+ type_info.add_constraint(ConstraintType.REQUIRED, True, "Field is required by JSON Schema")
+
+ # 处理数组类型
+ if json_type == "array":
+ type_info.is_array = True
+ self._extract_array_constraints(type_info, property_schema)
+
+ # 处理对象类型
+ elif json_type == "object":
+ type_info.is_object = True
+ self._extract_object_constraints(type_info, property_schema)
+
+ # 设置默认值
+ if "default" in property_schema:
+ type_info.default_value = property_schema["default"]
+
+ return type_info
+
+ @classmethod
+ def _extract_constraints_from_schema(cls, type_info: TypeInfo, property_schema: Dict[str, Any]) -> None:
+ """从JSON Schema属性中提取约束条件"""
+ # 数值约束
+ if "minimum" in property_schema:
+ type_info.add_constraint(ConstraintType.MIN_VALUE, property_schema["minimum"])
+ if "maximum" in property_schema:
+ type_info.add_constraint(ConstraintType.MAX_VALUE, property_schema["maximum"])
+ if "exclusiveMinimum" in property_schema:
+ type_info.add_constraint(ConstraintType.EXCLUSIVE_MIN, property_schema["exclusiveMinimum"])
+ if "exclusiveMaximum" in property_schema:
+ type_info.add_constraint(ConstraintType.EXCLUSIVE_MAX, property_schema["exclusiveMaximum"])
+ if "multipleOf" in property_schema:
+ type_info.add_constraint(ConstraintType.MULTIPLE_OF, property_schema["multipleOf"])
+
+ # 字符串约束
+ if "minLength" in property_schema:
+ type_info.add_constraint(ConstraintType.MIN_LENGTH, property_schema["minLength"])
+ if "maxLength" in property_schema:
+ type_info.add_constraint(ConstraintType.MAX_LENGTH, property_schema["maxLength"])
+ if "pattern" in property_schema:
+ type_info.add_constraint(ConstraintType.PATTERN, property_schema["pattern"])
+
+ # 枚举约束
+ if "enum" in property_schema:
+ type_info.add_constraint(ConstraintType.ENUM_VALUES, property_schema["enum"])
+
+ # 格式约束
+ if "format" in property_schema:
+ type_info.add_constraint(ConstraintType.FORMAT, property_schema["format"])
+
+ # 默认值
+ if "default" in property_schema:
+ type_info.add_constraint(ConstraintType.DEFAULT_VALUE, property_schema["default"])
+
+ @classmethod
+ def _extract_array_constraints(cls, type_info: TypeInfo, property_schema: Dict[str, Any]) -> None:
+ """提取数组类型的约束"""
+ if "minItems" in property_schema:
+ type_info.add_constraint(ConstraintType.MIN_ITEMS, property_schema["minItems"])
+ if "maxItems" in property_schema:
+ type_info.add_constraint(ConstraintType.MAX_ITEMS, property_schema["maxItems"])
+ if "uniqueItems" in property_schema:
+ type_info.add_constraint(ConstraintType.UNIQUE_ITEMS, property_schema["uniqueItems"])
+
+ # 提取数组元素类型信息
+ items_schema = property_schema.get("items")
+ if isinstance(items_schema, dict) and "type" in items_schema:
+ element_type = TypeConverter.json_schema_type_to_standard(items_schema["type"])
+ type_info.element_type_info = TypeInfo(
+ field_name=f"{type_info.field_name}_item",
+ field_path=f"{type_info.field_path}_item",
+ standard_type=element_type,
+ python_type=TypeConverter.standard_to_python_type(element_type),
+ original_type=items_schema["type"],
+ current_value=None,
+ )
+ # 递归提取元素约束
+ cls._extract_constraints_from_schema(type_info.element_type_info, items_schema)
+
+ @classmethod
+ def _extract_object_constraints(cls, type_info: TypeInfo, property_schema: Dict[str, Any]) -> None:
+ """提取对象类型的约束"""
+ # 对象类型的属性定义
+ properties = property_schema.get("properties", {})
+ required_fields = property_schema.get("required", [])
+
+ for prop_name, prop_schema in properties.items():
+ if isinstance(prop_schema, dict) and "type" in prop_schema:
+ prop_type = TypeConverter.json_schema_type_to_standard(prop_schema["type"])
+ prop_type_info = TypeInfo(
+ field_name=prop_name,
+ field_path=f"{type_info.field_path}.{prop_name}",
+ standard_type=prop_type,
+ python_type=TypeConverter.standard_to_python_type(prop_type),
+ original_type=prop_schema["type"],
+ current_value=None,
+ )
+ # 递归提取属性约束
+ cls._extract_constraints_from_schema(prop_type_info, prop_schema)
+
+ # 如果字段在required列表中,添加REQUIRED约束
+ if prop_name in required_fields:
+ prop_type_info.add_constraint(
+ ConstraintType.REQUIRED,
+ True,
+ "Field is required by JSON Schema",
+ )
+
+ type_info.object_fields[prop_name] = prop_type_info
diff --git a/msgcenterpy/instances/ros2_instance.py b/msgcenterpy/instances/ros2_instance.py
new file mode 100644
index 0000000..2804b07
--- /dev/null
+++ b/msgcenterpy/instances/ros2_instance.py
@@ -0,0 +1,242 @@
+import array
+import importlib
+from collections import OrderedDict
+from typing import TYPE_CHECKING, Any, Dict, Optional, Type
+
+from rosidl_parser.definition import NamespacedType # type: ignore
+from rosidl_runtime_py import ( # type: ignore
+ import_message_from_namespaced_type,
+ message_to_ordereddict,
+ set_message_fields,
+)
+
+from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
+from msgcenterpy.core.message_instance import MessageInstance
+from msgcenterpy.core.type_converter import TypeConverter
+from msgcenterpy.core.type_info import ConstraintType, Consts, TypeInfo
+from msgcenterpy.core.types import MessageType
+
+if TYPE_CHECKING:
+ from msgcenterpy.core.field_accessor import FieldAccessor
+
+
+class ROS2MessageInstance(MessageInstance[Any]):
+ """ROS2消息实例,支持类型信息提取和字段访问器"""
+
+ ros_msg_cls: Type[Any] = None # type: ignore
+
+ @classmethod
+ def get_ros_msg_cls_path(cls, ros_msg_cls: Type[Any]) -> str:
+ return ros_msg_cls.__module__ + "." + ros_msg_cls.__name__
+
+ @property
+ def ros_msg_cls_path(self) -> str:
+ return self.get_ros_msg_cls_path(self.ros_msg_cls)
+
+ @classmethod
+ def get_ros_msg_cls_namespace(cls, ros_msg_cls: Type[Any]) -> str:
+ class_name, module_name = ros_msg_cls.__name__, ros_msg_cls.__module__
+ package = module_name.split(".")[0] if module_name else ""
+ interface = (
+ "msg"
+ if ".msg" in module_name
+ else "srv"
+ if ".srv" in module_name
+ else "action"
+ if ".action" in module_name
+ else "msg"
+ )
+ return f"{package}/{interface}/{class_name}" if package and class_name else f"{module_name}.{class_name}"
+
+ @property
+ def ros_msg_cls_namespace(self) -> str:
+ return self.get_ros_msg_cls_namespace(self.ros_msg_cls)
+
+ @classmethod
+ def obtain_ros_cls_from_str(cls, message_type: str | Type[Any]) -> Type[Any]:
+ # 需要先解析出正确的消息类
+ if isinstance(message_type, str):
+ if "/" in message_type:
+ namespace, name = message_type.rsplit("/", 1)
+ message_type = import_message_from_namespaced_type(NamespacedType(namespace.split("/"), name))
+ elif "." in message_type:
+ module_path, class_name = message_type.rsplit(".", 1)
+ mod = importlib.import_module(module_path)
+ message_type = getattr(mod, class_name)
+ return message_type # type: ignore
+
+ def __init__(self, inner_data: Any, **kwargs: Any) -> None:
+ self.ros_msg_cls = inner_data.__class__
+ if not isinstance(self.ros_msg_cls, type):
+ raise TypeError(f"Expected ROS message class to be a type, got {type(self.ros_msg_cls)}")
+ super().__init__(inner_data, MessageType.ROS2)
+
+ def export_to_envelope(self, **kwargs: Any) -> MessageEnvelope:
+ """导出为统一信封字典
+
+ 用户可从 metadata.properties 中读取:
+ - properties.ros_msg_cls_namespace
+ - properties.ros_msg_cls_path
+ """
+ base_dict = self.get_python_dict()
+ export_envelope = create_envelope(
+ format_name=self.message_type.value,
+ content=base_dict,
+ metadata={
+ "current_format": self.message_type.value,
+ "source_cls_name": self.inner_data.__class__.__name__,
+ "source_cls_module": self.inner_data.__class__.__module__,
+ **self._metadata,
+ },
+ )
+ return export_envelope
+
+ @classmethod
+ def _ordered_to_dict(cls, obj: Any) -> Any:
+ if isinstance(obj, OrderedDict):
+ return {k: cls._ordered_to_dict(v) for k, v in obj.items()}
+ elif isinstance(obj, tuple):
+ return tuple(cls._ordered_to_dict(v) for v in obj)
+ elif isinstance(obj, (list, array.array)):
+ return [cls._ordered_to_dict(v) for v in obj]
+ else:
+ return obj
+
+ @classmethod
+ def import_from_envelope(cls, data: MessageEnvelope, **kwargs: Any) -> "ROS2MessageInstance":
+ """从规范信封创建ROS2实例(仅 data 一个参数)。
+
+ 类型信息从 data.metadata.properties 读取
+ """
+ content = data["content"]
+ properties = data["metadata"]["properties"]
+ ros_msg_cls = cls.obtain_ros_cls_from_str(properties["ros_msg_cls_namespace"]) or cls.obtain_ros_cls_from_str(
+ properties["ros_msg_cls_path"]
+ )
+ if ros_msg_cls is None:
+ raise ValueError(
+ "ros2 type must be provided via metadata.properties.ros_msg_cls_namespace or legacy type_info.ros_namespaced"
+ )
+ ros_msg = ros_msg_cls()
+ set_message_fields(ros_msg, content)
+ instance = ROS2MessageInstance(ros_msg)
+ return instance
+
+ def get_python_dict(self) -> Dict[str, Any]:
+ """获取当前所有的字段名和对应的原始值,使用 SLOT_TYPES 进行类型推断和嵌套导入"""
+ base_obj = message_to_ordereddict(self.inner_data)
+ base_dict = self._ordered_to_dict(base_obj)
+ return base_dict # type: ignore[no-any-return]
+
+ def set_python_dict(self, value: Dict[str, Any], **kwargs: Any) -> bool:
+ """获取当前所有的字段名和对应的原始值,使用 SLOT_TYPES 进行类型推断和嵌套导入"""
+ timestamp_fields = set_message_fields(self.inner_data, value, **kwargs)
+ # todo: 因为ROS自身机制,字段并不会增减,所以需要更新cache中所有accessor的值(通过parent获取)
+ return True
+
+ # TypeInfoProvider 接口实现
+ def get_field_type_info(
+ self, field_name: str, field_value: Any, parent_field_accessor: "FieldAccessor"
+ ) -> Optional[TypeInfo]:
+ """从ROS2消息定义中提取字段类型信息
+
+ 使用 ROS 消息的 SLOT_TYPES 获取精确的类型信息,并通过 TypeConverter 转换为标准类型
+ """
+ # 通过 parent_field_accessor 获取 ROS 消息实例
+ ros_msg_instance = parent_field_accessor.value
+
+ # 构建完整路径用于TypeInfo
+ full_path = f"{parent_field_accessor.full_path_from_root}.{field_name}"
+
+ # noinspection PyProtectedMember
+ slots = ros_msg_instance._fields_and_field_types
+ slot_types = ros_msg_instance.SLOT_TYPES
+
+ # 通过 zip 找到 field_name 对应的类型定义
+ ros_definition_type = None
+ for slot_name, slot_type in zip(slots, slot_types):
+ if slot_name == field_name:
+ ros_definition_type = slot_type
+ break
+
+ if ros_definition_type is None:
+ raise ValueError(f"Field '{field_name}' not found in ROS message slots")
+
+ # 使用 TypeConverter 转换为标准类型
+ standard_type = TypeConverter.rosidl_definition_to_standard(ros_definition_type)
+
+ # 创建 TypeInfo
+ type_info = TypeInfo(
+ field_name=field_name,
+ field_path=full_path,
+ standard_type=standard_type,
+ python_type=type(field_value),
+ original_type=ros_definition_type,
+ )
+ type_info.current_value = field_value
+
+ # 从 rosidl 定义中提取详细类型信息(约束、数组信息等)
+ self._extract_from_rosidl_definition(type_info)
+
+ return type_info
+
+ def _extract_from_rosidl_definition(self, type_info: TypeInfo) -> None:
+ """从rosidl_parser定义中提取详细类型信息
+
+ Args:
+ type_info: 要填充的TypeInfo对象
+ """
+ from rosidl_parser.definition import (
+ AbstractNestedType,
+ Array,
+ BasicType,
+ BoundedSequence,
+ BoundedString,
+ BoundedWString,
+ NamespacedType,
+ UnboundedSequence,
+ )
+
+ # 从type_info获取所需信息
+ definition_type = type_info.original_type
+
+ get_element_type = False
+ # 提取约束信息
+ if isinstance(definition_type, (BoundedString, BoundedWString)):
+ type_info.add_constraint(ConstraintType.MAX_LENGTH, definition_type.maximum_size)
+ elif isinstance(definition_type, Array):
+ type_info.is_array = True
+ type_info.array_size = definition_type.size
+ type_info.add_constraint(ConstraintType.MIN_ITEMS, definition_type.size)
+ type_info.add_constraint(ConstraintType.MAX_ITEMS, definition_type.size)
+ get_element_type = True
+ elif isinstance(definition_type, BoundedSequence):
+ type_info.is_array = True
+ type_info.add_constraint(ConstraintType.MAX_ITEMS, definition_type.maximum_size)
+ get_element_type = True
+ elif isinstance(definition_type, UnboundedSequence):
+ type_info.is_array = True
+ get_element_type = True
+ elif isinstance(definition_type, BasicType):
+ # 基础类型的约束将在 field_accessor 中自动添加
+ pass
+ elif isinstance(definition_type, NamespacedType):
+ # 对象类型,标记为对象并提取字段信息
+ type_info.is_object = True
+ type_info.add_constraint(ConstraintType.TYPE_KEEP, True)
+ # 这里可以进一步扩展来提取对象字段信息
+ # 提取元素类型信息
+ if get_element_type:
+ if not isinstance(definition_type, AbstractNestedType):
+ raise TypeError(f"Expected AbstractNestedType for element type extraction, got {type(definition_type)}")
+ # 创建元素类型的TypeInfo并递归填充
+ std_type = TypeConverter.rosidl_definition_to_standard(definition_type.value_type)
+ python_type = TypeConverter.standard_to_python_type(std_type)
+ type_info.element_type_info = TypeInfo(
+ field_name=Consts.ELEMENT_TYPE_INFO_SYMBOL,
+ field_path=Consts.ELEMENT_TYPE_INFO_SYMBOL,
+ standard_type=std_type,
+ python_type=python_type,
+ original_type=definition_type.value_type,
+ )
+ self._extract_from_rosidl_definition(type_info.element_type_info)
diff --git a/msgcenterpy/utils/__init__.py b/msgcenterpy/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/msgcenterpy/utils/decorator.py b/msgcenterpy/utils/decorator.py
new file mode 100644
index 0000000..18542e7
--- /dev/null
+++ b/msgcenterpy/utils/decorator.py
@@ -0,0 +1,29 @@
+import functools
+import warnings
+from typing import Any, Callable
+
+
+def experimental(
+ reason: str = "This API is experimental and may change or be removed in future.",
+) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
+ """
+ 装饰器:标记函数为实验性。
+ 调用时会发出 RuntimeWarning。
+
+ :param reason: 警告信息,可以自定义说明原因
+ """
+
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
+ @functools.wraps(func)
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
+ warnings.warn(
+ f"Call to experimental function '{func.__name__}': {reason}",
+ category=RuntimeWarning,
+ stacklevel=2,
+ )
+ return func(*args, **kwargs)
+
+ wrapper.__experimental__ = True # type: ignore[attr-defined] # 给函数打个标记,方便外部检测
+ return wrapper
+
+ return decorator
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d2750ca
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,106 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "msgcenterpy"
+dynamic = ["version"]
+description = "Unified message conversion system supporting ROS2, Pydantic, Dataclass, JSON, YAML, Dict, and MCP schema inter-conversion"
+readme = "README.md"
+requires-python = ">=3.10"
+license = {file = "LICENSE"}
+authors = [
+ {name = "MsgCenterPy Team", email = "zgca@zgca.com"}
+]
+keywords = ["message", "conversion", "ros2", "pydantic", "dataclass", "json", "yaml", "mcp"]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: System :: Distributed Computing",
+ "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator"
+]
+
+dependencies = [
+ "pydantic>=2.0.0",
+ "jsonschema>=4.25",
+ "PyYAML>=6.0",
+ "typing-extensions>=4.0.0; python_version<'3.11'"
+]
+
+[project.optional-dependencies]
+ros2 = [
+ "rosidl_runtime_py>=0.9.0",
+ "rclpy>=3.0.0"
+]
+dev = [
+ "pytest>=7.0.0",
+ "black>=22.0.0",
+ "isort>=5.0.0",
+ "mypy>=1.0.0",
+ "types-jsonschema>=4.0.0",
+ "types-PyYAML>=6.0.0",
+ "pre-commit>=2.20.0",
+ "bump2version>=1.0.0"
+]
+docs = [
+ "sphinx>=5.0.0",
+ "sphinx-rtd-theme>=1.0.0",
+ "myst-parser>=0.18.0"
+]
+all = [
+ "msgcenterpy[ros2,dev,docs]"
+]
+
+[project.urls]
+Homepage = "https://github.com/ZGCA-Forge/MsgCenterPy"
+Documentation = "https://zgca-forge.github.io/MsgCenterPy/"
+Repository = "https://github.com/ZGCA-Forge/MsgCenterPy"
+Issues = "https://github.com/ZGCA-Forge/MsgCenterPy/issues"
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["msgcenterpy*"]
+
+[tool.setuptools.dynamic]
+version = {attr = "msgcenterpy.__version__"}
+
+[tool.black]
+line-length = 120
+target-version = ['py310']
+
+[tool.isort]
+profile = "black"
+multi_line_output = 3
+
+[tool.mypy]
+python_version = "3.10"
+plugins = ["pydantic.mypy"]
+warn_return_any = true
+warn_unused_configs = true
+disallow_untyped_defs = true
+no_implicit_optional = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+strict_equality = true
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = "test_*.py"
+python_classes = "Test*"
+python_functions = "test_*"
+addopts = "-v --tb=short --strict-markers --strict-config -ra --color=yes"
+
+filterwarnings = [
+ "ignore::DeprecationWarning",
+ "ignore::PendingDeprecationWarning"
+]
+
+[tool.bandit]
+exclude_dirs = ["tests", "build", "dist"]
+skips = ["B101", "B601"]
diff --git a/scripts/setup-dev.ps1 b/scripts/setup-dev.ps1
new file mode 100644
index 0000000..45b819c
--- /dev/null
+++ b/scripts/setup-dev.ps1
@@ -0,0 +1,59 @@
+Write-Host "[INFO] Setting up MsgCenterPy development environment..." -ForegroundColor Green
+
+# Check if Python is available
+try {
+ $pythonVersion = python --version
+ Write-Host "[SUCCESS] Found: $pythonVersion" -ForegroundColor Green
+} catch {
+ Write-Host "[ERROR] Python 3 is required but not found in PATH." -ForegroundColor Red
+ Write-Host "Please install Python 3.10+ and add it to your PATH." -ForegroundColor Yellow
+ exit 1
+}
+
+# Install the package in development mode
+Write-Host "[INFO] Installing package in development mode..." -ForegroundColor Blue
+pip install -e .[dev]
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "[ERROR] Failed to install package in development mode" -ForegroundColor Red
+ exit 1
+}
+
+# Install pre-commit hooks
+Write-Host "[INFO] Installing pre-commit hooks..." -ForegroundColor Blue
+pre-commit install
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "[ERROR] Failed to install pre-commit hooks" -ForegroundColor Red
+ exit 1
+}
+
+# Install pre-commit hooks for commit-msg (optional)
+Write-Host "[INFO] Installing commit-msg hooks..." -ForegroundColor Blue
+pre-commit install --hook-type commit-msg
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "[WARNING] commit-msg hooks installation failed (optional)" -ForegroundColor Yellow
+}
+
+# Run pre-commit on all files to check setup
+Write-Host "[INFO] Running pre-commit on all files to verify setup..." -ForegroundColor Blue
+pre-commit run --all-files
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "[SUCCESS] Pre-commit setup completed successfully!" -ForegroundColor Green
+ Write-Host ""
+ Write-Host "[SUCCESS] You're all set! Pre-commit will now run automatically on every commit." -ForegroundColor Green
+ Write-Host ""
+ Write-Host "[INFO] Quick commands:" -ForegroundColor Cyan
+ Write-Host " • Run all hooks manually: pre-commit run --all-files" -ForegroundColor White
+ Write-Host " • Update hook versions: pre-commit autoupdate" -ForegroundColor White
+ Write-Host " • Skip hooks for one commit: git commit --no-verify" -ForegroundColor White
+ Write-Host " • Run tests: pytest" -ForegroundColor White
+ Write-Host " • Type checking: mypy msgcenterpy" -ForegroundColor White
+} else {
+ Write-Host "[WARNING] Pre-commit found some issues. Please fix them and run 'pre-commit run --all-files' again." -ForegroundColor Yellow
+ Write-Host "[TIP] Or use 'pre-commit run --all-files --show-diff-on-failure' to see what needs to be fixed." -ForegroundColor Yellow
+}
+
+Write-Host ""
+Write-Host "[INFO] Integration with CI:" -ForegroundColor Cyan
+Write-Host " • CI will run the same pre-commit hooks" -ForegroundColor White
+Write-Host " • If you skip pre-commit locally, CI will catch the issues" -ForegroundColor White
+Write-Host " • Best practice: Always let pre-commit fix issues before committing" -ForegroundColor White
diff --git a/scripts/setup-dev.sh b/scripts/setup-dev.sh
new file mode 100755
index 0000000..7d93ca2
--- /dev/null
+++ b/scripts/setup-dev.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+set -e # Exit on any error
+
+echo "[INFO] Setting up MsgCenterPy development environment..."
+
+# Check if Python is available
+if ! command -v python3 &> /dev/null; then
+ echo "[ERROR] Python 3 is required but not installed."
+ exit 1
+fi
+
+# Install the package in development mode
+echo "[INFO] Installing package in development mode..."
+pip install -e .[dev]
+
+# Install pre-commit hooks
+echo "[INFO] Installing pre-commit hooks..."
+pre-commit install
+
+# Install pre-commit hooks for commit-msg (optional)
+echo "[INFO] Installing commit-msg hooks..."
+pre-commit install --hook-type commit-msg || echo "[WARNING] commit-msg hooks installation failed (optional)"
+
+# Run pre-commit on all files to check setup
+echo "[INFO] Running pre-commit on all files to verify setup..."
+if pre-commit run --all-files; then
+ echo "[SUCCESS] Pre-commit setup completed successfully!"
+ echo ""
+ echo "[SUCCESS] You're all set! Pre-commit will now run automatically on every commit."
+ echo ""
+ echo "[INFO] Quick commands:"
+ echo " • Run all hooks manually: pre-commit run --all-files"
+ echo " • Update hook versions: pre-commit autoupdate"
+ echo " • Skip hooks for one commit: git commit --no-verify"
+ echo " • Run tests: pytest"
+ echo " • Type checking: mypy msgcenterpy"
+else
+ echo "[WARNING] Pre-commit found some issues. Please fix them and run 'pre-commit run --all-files' again."
+ echo "[TIP] Or use 'pre-commit run --all-files --show-diff-on-failure' to see what needs to be fixed."
+fi
+
+echo ""
+echo "[INFO] Integration with CI:"
+echo " • CI will run the same pre-commit hooks"
+echo " • If you skip pre-commit locally, CI will catch the issues"
+echo " • Best practice: Always let pre-commit fix issues before committing"
diff --git a/setup-dev.ps1 b/setup-dev.ps1
new file mode 100644
index 0000000..dac7bdd
--- /dev/null
+++ b/setup-dev.ps1
@@ -0,0 +1,69 @@
+# Development environment setup script for MsgCenterPy (Windows PowerShell)
+
+Write-Host "🚀 Setting up MsgCenterPy development environment..." -ForegroundColor Green
+
+# Check if Python is available
+try {
+ $pythonVersion = python --version
+ Write-Host "✅ Found: $pythonVersion" -ForegroundColor Green
+} catch {
+ Write-Host "❌ Error: Python 3 is required but not found in PATH." -ForegroundColor Red
+ Write-Host "Please install Python 3.10+ and add it to your PATH." -ForegroundColor Yellow
+ exit 1
+}
+
+# Install the package in development mode
+Write-Host "📦 Installing package in development mode..." -ForegroundColor Blue
+pip install -e .[dev]
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "❌ Failed to install package in development mode" -ForegroundColor Red
+ exit 1
+}
+
+# Install pre-commit
+Write-Host "🔧 Installing pre-commit..." -ForegroundColor Blue
+pip install pre-commit
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "❌ Failed to install pre-commit" -ForegroundColor Red
+ exit 1
+}
+
+# Install pre-commit hooks
+Write-Host "🪝 Installing pre-commit hooks..." -ForegroundColor Blue
+pre-commit install
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "❌ Failed to install pre-commit hooks" -ForegroundColor Red
+ exit 1
+}
+
+# Install pre-commit hooks for commit-msg (optional)
+Write-Host "📝 Installing commit-msg hooks..." -ForegroundColor Blue
+pre-commit install --hook-type commit-msg
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "⚠️ commit-msg hooks installation failed (optional)" -ForegroundColor Yellow
+}
+
+# Run pre-commit on all files to check setup
+Write-Host "🔍 Running pre-commit on all files to verify setup..." -ForegroundColor Blue
+pre-commit run --all-files
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "✅ Pre-commit setup completed successfully!" -ForegroundColor Green
+ Write-Host ""
+ Write-Host "🎉 You're all set! Pre-commit will now run automatically on every commit." -ForegroundColor Green
+ Write-Host ""
+ Write-Host "📋 Quick commands:" -ForegroundColor Cyan
+ Write-Host " • Run all hooks manually: pre-commit run --all-files" -ForegroundColor White
+ Write-Host " • Update hook versions: pre-commit autoupdate" -ForegroundColor White
+ Write-Host " • Skip hooks for one commit: git commit --no-verify" -ForegroundColor White
+ Write-Host " • Run tests: pytest" -ForegroundColor White
+ Write-Host " • Type checking: mypy msgcenterpy" -ForegroundColor White
+} else {
+ Write-Host "⚠️ Pre-commit found some issues. Please fix them and run 'pre-commit run --all-files' again." -ForegroundColor Yellow
+ Write-Host "💡 Or use 'pre-commit run --all-files --show-diff-on-failure' to see what needs to be fixed." -ForegroundColor Yellow
+}
+
+Write-Host ""
+Write-Host "🔗 Integration with CI:" -ForegroundColor Cyan
+Write-Host " • CI will run the same pre-commit hooks" -ForegroundColor White
+Write-Host " • If you skip pre-commit locally, CI will catch the issues" -ForegroundColor White
+Write-Host " • Best practice: Always let pre-commit fix issues before committing" -ForegroundColor White
diff --git a/setup-dev.sh b/setup-dev.sh
new file mode 100644
index 0000000..18717e8
--- /dev/null
+++ b/setup-dev.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+# Development environment setup script for MsgCenterPy
+
+set -e # Exit on any error
+
+echo "🚀 Setting up MsgCenterPy development environment..."
+
+# Check if Python is available
+if ! command -v python3 &> /dev/null; then
+ echo "❌ Error: Python 3 is required but not installed."
+ exit 1
+fi
+
+# Install the package in development mode
+echo "📦 Installing package in development mode..."
+pip install -e .[dev]
+
+# Install pre-commit
+echo "🔧 Installing pre-commit..."
+pip install pre-commit
+
+# Install pre-commit hooks
+echo "🪝 Installing pre-commit hooks..."
+pre-commit install
+
+# Install pre-commit hooks for commit-msg (optional)
+echo "📝 Installing commit-msg hooks..."
+pre-commit install --hook-type commit-msg || echo "⚠️ commit-msg hooks installation failed (optional)"
+
+# Run pre-commit on all files to check setup
+echo "🔍 Running pre-commit on all files to verify setup..."
+if pre-commit run --all-files; then
+ echo "✅ Pre-commit setup completed successfully!"
+ echo ""
+ echo "🎉 You're all set! Pre-commit will now run automatically on every commit."
+ echo ""
+ echo "📋 Quick commands:"
+ echo " • Run all hooks manually: pre-commit run --all-files"
+ echo " • Update hook versions: pre-commit autoupdate"
+ echo " • Skip hooks for one commit: git commit --no-verify"
+ echo " • Run tests: pytest"
+ echo " • Type checking: mypy msgcenterpy"
+else
+ echo "⚠️ Pre-commit found some issues. Please fix them and run 'pre-commit run --all-files' again."
+ echo "💡 Or use 'pre-commit run --all-files --show-diff-on-failure' to see what needs to be fixed."
+fi
+
+echo ""
+echo "🔗 Integration with CI:"
+echo " • CI will run the same pre-commit hooks"
+echo " • If you skip pre-commit locally, CI will catch the issues"
+echo " • Best practice: Always let pre-commit fix issues before committing"
diff --git a/tests/test_json_schema_instance.py b/tests/test_json_schema_instance.py
new file mode 100644
index 0000000..5bad6ae
--- /dev/null
+++ b/tests/test_json_schema_instance.py
@@ -0,0 +1,463 @@
+import os
+import sys
+
+import pytest
+
+# 添加项目路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+# JSON Schema 依赖检查
+
+from msgcenterpy.core.type_info import ConstraintType
+from msgcenterpy.core.types import MessageType
+from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
+
+
+class TestJSONSchemaMessageInstance:
+ """JSONSchemaMessageInstance 基本功能测试"""
+
+ @pytest.fixture
+ def simple_schema(self):
+ """简单的 JSON Schema 示例"""
+ return {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "age": {"type": "integer", "minimum": 0, "maximum": 150},
+ "active": {"type": "boolean"},
+ },
+ "required": ["name"],
+ }
+
+ @pytest.fixture
+ def complex_schema(self):
+ """复杂的 JSON Schema 示例"""
+ return {
+ "type": "object",
+ "properties": {
+ "user": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "string"},
+ "profile": {
+ "type": "object",
+ "properties": {
+ "email": {"type": "string", "format": "email"},
+ "phone": {
+ "type": "string",
+ "pattern": "^\\+?[1-9]\\d{1,14}$",
+ },
+ },
+ },
+ },
+ "required": ["id"],
+ },
+ "tags": {
+ "type": "array",
+ "items": {"type": "string"},
+ "minItems": 1,
+ "maxItems": 10,
+ },
+ "scores": {
+ "type": "array",
+ "items": {"type": "number", "minimum": 0, "maximum": 100},
+ },
+ "metadata": {"type": "object", "additionalProperties": True},
+ },
+ "required": ["user", "tags"],
+ }
+
+ @pytest.fixture
+ def simple_data(self):
+ """匹配简单 schema 的数据"""
+ return {"name": "John Doe", "age": 30, "active": True}
+
+ @pytest.fixture
+ def complex_data(self):
+ """匹配复杂 schema 的数据"""
+ return {
+ "user": {
+ "id": "user_123",
+ "profile": {"email": "john@example.com", "phone": "+1234567890"},
+ },
+ "tags": ["developer", "python", "testing"],
+ "scores": [85.5, 92.0, 78.3],
+ "metadata": {"created_at": "2024-01-01", "version": 1},
+ }
+
+ def test_basic_creation(self, simple_schema, simple_data):
+ """测试基本创建功能"""
+ instance = JSONSchemaMessageInstance(simple_data, simple_schema)
+
+ assert instance.message_type == MessageType.JSON_SCHEMA
+ assert instance.inner_data == simple_data
+ assert instance.json_schema == simple_schema
+ assert len(instance._validation_errors) == 0
+
+ def test_data_validation_success(self, simple_schema, simple_data):
+ """测试数据验证成功"""
+ instance = JSONSchemaMessageInstance(simple_data, simple_schema)
+
+ # 验证成功,没有错误
+ assert len(instance._validation_errors) == 0
+
+ def test_data_validation_failure(self, simple_schema):
+ """测试数据验证失败"""
+ invalid_data = {
+ "age": -5, # 违反 minimum 约束
+ "active": "not_boolean", # 类型错误
+ # 缺少必需的 "name" 字段
+ }
+
+ instance = JSONSchemaMessageInstance(invalid_data, simple_schema)
+
+ # 应该有验证错误
+ assert len(instance._validation_errors) > 0
+
+ def test_get_python_dict(self, simple_schema, simple_data):
+ """测试获取 Python 字典"""
+ instance = JSONSchemaMessageInstance(simple_data, simple_schema)
+
+ result = instance.get_python_dict()
+
+ assert isinstance(result, dict)
+ assert result == simple_data
+ assert result is not simple_data # 应该是副本
+
+ def test_set_python_dict(self, simple_schema, simple_data):
+ """测试设置 Python 字典"""
+ instance = JSONSchemaMessageInstance(simple_data, simple_schema)
+
+ new_data = {"name": "Jane Smith", "age": 25}
+
+ result = instance.set_python_dict(new_data)
+
+ assert result is True
+ assert instance.get_python_dict()["name"] == "Jane Smith"
+ assert instance.get_python_dict()["age"] == 25
+
+ def test_export_to_envelope(self, simple_schema, simple_data):
+ """测试导出信封"""
+ instance = JSONSchemaMessageInstance(simple_data, simple_schema)
+
+ envelope = instance.export_to_envelope()
+
+ assert "content" in envelope
+ assert "metadata" in envelope
+ assert envelope["content"] == simple_data
+
+ metadata = envelope["metadata"]
+ assert metadata["current_format"] == "json_schema"
+ assert "properties" in metadata
+
+ def test_import_from_envelope(self, simple_schema, simple_data):
+ """测试从信封导入"""
+ # 创建原始实例
+ original = JSONSchemaMessageInstance(simple_data, simple_schema)
+ envelope = original.export_to_envelope()
+
+ # 从信封导入新实例
+ new_instance = JSONSchemaMessageInstance.import_from_envelope(envelope)
+
+ assert isinstance(new_instance, JSONSchemaMessageInstance)
+ assert new_instance.get_python_dict() == simple_data
+ assert new_instance.json_schema == simple_schema
+
+
+class TestJSONSchemaFieldTypeInfo:
+ """JSON Schema 字段类型信息测试"""
+
+ @pytest.fixture
+ def typed_schema(self):
+ """包含各种类型的 schema"""
+ return {
+ "type": "object",
+ "properties": {
+ "string_field": {"type": "string", "minLength": 3, "maxLength": 50},
+ "integer_field": {"type": "integer", "minimum": 0, "maximum": 100},
+ "number_field": {"type": "number", "multipleOf": 0.5},
+ "boolean_field": {"type": "boolean"},
+ "array_field": {
+ "type": "array",
+ "items": {"type": "string"},
+ "minItems": 1,
+ "maxItems": 5,
+ },
+ "object_field": {
+ "type": "object",
+ "properties": {"nested_string": {"type": "string"}},
+ },
+ "enum_field": {
+ "type": "string",
+ "enum": ["option1", "option2", "option3"],
+ },
+ "format_field": {"type": "string", "format": "email"},
+ },
+ "required": ["string_field", "integer_field"],
+ }
+
+ @pytest.fixture
+ def typed_data(self):
+ """匹配类型化 schema 的数据"""
+ return {
+ "string_field": "hello",
+ "integer_field": 42,
+ "number_field": 3.5,
+ "boolean_field": True,
+ "array_field": ["item1", "item2"],
+ "object_field": {"nested_string": "nested_value"},
+ "enum_field": "option1",
+ "format_field": "test@example.com",
+ }
+
+ def test_string_field_type_info(self, typed_schema, typed_data):
+ """测试字符串字段类型信息"""
+ instance = JSONSchemaMessageInstance(typed_data, typed_schema)
+ type_info = instance.fields.get_sub_type_info("string_field")
+
+ assert type_info is not None
+ assert type_info.field_name == "string_field"
+ assert type_info.standard_type.value == "string"
+ assert type_info.current_value == "hello"
+
+ # 检查约束
+ assert type_info.has_constraint(ConstraintType.MIN_LENGTH)
+ assert type_info.get_constraint_value(ConstraintType.MIN_LENGTH) == 3
+ assert type_info.has_constraint(ConstraintType.MAX_LENGTH)
+ assert type_info.get_constraint_value(ConstraintType.MAX_LENGTH) == 50
+ assert type_info.has_constraint(ConstraintType.REQUIRED)
+
+ def test_integer_field_type_info(self, typed_schema, typed_data):
+ """测试整数字段类型信息"""
+ instance = JSONSchemaMessageInstance(typed_data, typed_schema)
+ type_info = instance.fields.get_sub_type_info("integer_field")
+
+ assert type_info is not None
+ assert type_info.standard_type.value == "integer"
+ assert type_info.current_value == 42
+
+ # 检查数值约束
+ assert type_info.has_constraint(ConstraintType.MIN_VALUE)
+ assert type_info.get_constraint_value(ConstraintType.MIN_VALUE) == 0
+ assert type_info.has_constraint(ConstraintType.MAX_VALUE)
+ assert type_info.get_constraint_value(ConstraintType.MAX_VALUE) == 100
+ assert type_info.has_constraint(ConstraintType.REQUIRED)
+
+ def test_array_field_type_info(self, typed_schema, typed_data):
+ """测试数组字段类型信息"""
+ instance = JSONSchemaMessageInstance(typed_data, typed_schema)
+ type_info = instance.fields.get_sub_type_info("array_field")
+
+ assert type_info is not None
+ assert type_info.is_array is True
+ assert type_info.current_value == ["item1", "item2"]
+
+ # 检查数组约束
+ assert type_info.has_constraint(ConstraintType.MIN_ITEMS)
+ assert type_info.get_constraint_value(ConstraintType.MIN_ITEMS) == 1
+ assert type_info.has_constraint(ConstraintType.MAX_ITEMS)
+ assert type_info.get_constraint_value(ConstraintType.MAX_ITEMS) == 5
+
+ # 检查元素类型信息
+ assert type_info.element_type_info is not None
+ assert type_info.element_type_info.standard_type.value == "string"
+
+ def test_object_field_type_info(self, typed_schema, typed_data):
+ """测试对象字段类型信息"""
+ instance = JSONSchemaMessageInstance(typed_data, typed_schema)
+ type_info = instance.fields.get_sub_type_info("object_field")
+
+ assert type_info is not None
+ assert type_info.is_object is True
+ assert type_info.current_value == {"nested_string": "nested_value"}
+
+ # 检查对象字段定义
+ assert len(type_info.object_fields) > 0
+ assert "nested_string" in type_info.object_fields
+
+ nested_field_info = type_info.object_fields["nested_string"]
+ assert nested_field_info.standard_type.value == "string"
+
+ def test_enum_field_type_info(self, typed_schema, typed_data):
+ """测试枚举字段类型信息"""
+ instance = JSONSchemaMessageInstance(typed_data, typed_schema)
+ type_info = instance.fields.get_sub_type_info("enum_field")
+
+ assert type_info is not None
+ assert type_info.has_constraint(ConstraintType.ENUM_VALUES)
+ enum_values = type_info.get_constraint_value(ConstraintType.ENUM_VALUES)
+ assert enum_values == ["option1", "option2", "option3"]
+
+ def test_format_field_type_info(self, typed_schema, typed_data):
+ """测试格式字段类型信息"""
+ instance = JSONSchemaMessageInstance(typed_data, typed_schema)
+ type_info = instance.fields.get_sub_type_info("format_field")
+
+ assert type_info is not None
+ assert type_info.has_constraint(ConstraintType.FORMAT)
+ format_value = type_info.get_constraint_value(ConstraintType.FORMAT)
+ assert format_value == "email"
+
+
+class TestJSONSchemaInstanceJSONSchema:
+ """JSONSchemaMessageInstance 自身的 JSON Schema 生成测试"""
+
+ def test_get_json_schema_simple(self):
+ """测试简单数据的 JSON Schema 生成"""
+ schema = {
+ "type": "object",
+ "properties": {"name": {"type": "string"}, "count": {"type": "integer"}},
+ }
+ data = {"name": "test", "count": 5}
+
+ instance = JSONSchemaMessageInstance(data, schema)
+ generated_schema = instance.get_json_schema()
+
+ assert generated_schema["type"] == "object"
+ assert "properties" in generated_schema
+ assert "name" in generated_schema["properties"]
+ assert "count" in generated_schema["properties"]
+ assert generated_schema["title"] == "JSONSchemaMessageInstance Schema"
+
+ def test_get_json_schema_with_constraints(self):
+ """测试包含约束的 JSON Schema 生成"""
+ schema = {
+ "type": "object",
+ "properties": {
+ "email": {"type": "string", "format": "email", "minLength": 5},
+ "age": {"type": "integer", "minimum": 0, "maximum": 120},
+ "tags": {"type": "array", "items": {"type": "string"}, "minItems": 1},
+ },
+ "required": ["email"],
+ }
+ data = {"email": "test@example.com", "age": 25, "tags": ["tag1", "tag2"]}
+
+ instance = JSONSchemaMessageInstance(data, schema)
+ generated_schema = instance.get_json_schema()
+
+ # 检查约束是否保留在生成的 schema 中
+ properties = generated_schema["properties"]
+
+ # email 字段约束
+ email_prop = properties["email"]
+ assert email_prop["type"] == "string"
+
+ # age 字段约束
+ age_prop = properties["age"]
+ assert age_prop["type"] == "integer"
+
+ # tags 数组约束
+ tags_prop = properties["tags"]
+ assert tags_prop["type"] == "array"
+
+ def test_get_json_schema_nested_objects(self):
+ """测试嵌套对象的 JSON Schema 生成"""
+ schema = {
+ "type": "object",
+ "properties": {
+ "user": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "string"},
+ "settings": {
+ "type": "object",
+ "properties": {"theme": {"type": "string"}},
+ },
+ },
+ }
+ },
+ }
+ data = {"user": {"id": "user123", "settings": {"theme": "dark"}}}
+
+ instance = JSONSchemaMessageInstance(data, schema)
+ generated_schema = instance.get_json_schema()
+
+ assert "user" in generated_schema["properties"]
+ user_prop = generated_schema["properties"]["user"]
+ assert user_prop["type"] == "object"
+
+
+class TestJSONSchemaValidation:
+ """JSON Schema 验证功能测试"""
+
+ def test_constraint_validation(self):
+ """测试约束验证"""
+ schema = {
+ "type": "object",
+ "properties": {
+ "age": {"type": "integer", "minimum": 0, "maximum": 150},
+ "name": {"type": "string", "minLength": 2, "maxLength": 50},
+ },
+ "required": ["name"],
+ }
+
+ # 有效数据
+ valid_data = {"name": "John", "age": 30}
+ valid_instance = JSONSchemaMessageInstance(valid_data, schema)
+ assert len(valid_instance._validation_errors) == 0
+
+ # 无效数据 - 年龄超出范围
+ invalid_data1 = {"name": "John", "age": 200}
+ invalid_instance1 = JSONSchemaMessageInstance(invalid_data1, schema)
+ assert len(invalid_instance1._validation_errors) > 0
+
+ # 无效数据 - 缺少必需字段
+ invalid_data2 = {"age": 30}
+ invalid_instance2 = JSONSchemaMessageInstance(invalid_data2, schema)
+ assert len(invalid_instance2._validation_errors) > 0
+
+ def test_type_validation(self):
+ """测试类型验证"""
+ schema = {
+ "type": "object",
+ "properties": {
+ "count": {"type": "integer"},
+ "active": {"type": "boolean"},
+ "items": {"type": "array", "items": {"type": "string"}},
+ },
+ }
+
+ # 类型正确的数据
+ valid_data = {"count": 42, "active": True, "items": ["a", "b", "c"]}
+ valid_instance = JSONSchemaMessageInstance(valid_data, schema)
+ assert len(valid_instance._validation_errors) == 0
+
+ # 类型错误的数据
+ invalid_data = {
+ "count": "not_integer",
+ "active": "not_boolean",
+ "items": "not_array",
+ }
+ invalid_instance = JSONSchemaMessageInstance(invalid_data, schema)
+ assert len(invalid_instance._validation_errors) > 0
+
+
+# 运行测试的便捷函数
+def run_json_schema_tests():
+ """运行 JSON Schema 相关测试"""
+
+ import subprocess
+
+ result = subprocess.run(
+ [
+ "python",
+ "-m",
+ "pytest",
+ "tests/test_json_schema_instance.py",
+ "-v",
+ "--tb=short",
+ ],
+ capture_output=True,
+ text=True,
+ )
+
+ print(result.stdout)
+ if result.stderr:
+ print("STDERR:", result.stderr)
+
+ return result.returncode == 0
+
+
+if __name__ == "__main__":
+ # 直接运行测试
+ run_json_schema_tests()
diff --git a/tests/test_ros2_instance.py b/tests/test_ros2_instance.py
new file mode 100644
index 0000000..7da33ad
--- /dev/null
+++ b/tests/test_ros2_instance.py
@@ -0,0 +1,325 @@
+import array
+import os
+import sys
+
+import pytest
+
+from msgcenterpy import TypeConverter
+
+# Add project path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+# ROS2 dependency check
+try:
+ from geometry_msgs.msg import Point, Pose
+ from std_msgs.msg import Float64MultiArray, String
+
+ # Only import ROS2MessageInstance when ROS2 message packages are available
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
+
+ HAS_ROS2 = True
+except ImportError:
+ HAS_ROS2 = False
+
+
+from msgcenterpy.core.types import MessageType
+
+
+class TestROS2MessageInstance:
+ """ROS2MessageInstance test class"""
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_basic_creation_string_message(self):
+ """Test basic String message creation"""
+ # Create String message
+ string_msg = String()
+ string_msg.data = "Hello ROS2"
+
+ # Create ROS2MessageInstance
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ assert ros2_inst.message_type == MessageType.ROS2
+ assert ros2_inst.inner_data is string_msg
+ assert ros2_inst.ros_msg_cls == String
+ assert ros2_inst.ros_msg_cls_namespace == "std_msgs/msg/String"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_basic_creation_float_array(self):
+ """Test Float64MultiArray message creation"""
+ # Create Float64MultiArray message
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.1, 2.2, 3.3, 4.4, 5.5]
+
+ # Create ROS2MessageInstance
+ ros2_inst = ROS2MessageInstance(array_msg)
+
+ assert ros2_inst.message_type == MessageType.ROS2
+ assert ros2_inst.inner_data is array_msg
+ assert ros2_inst.ros_msg_cls == Float64MultiArray
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_simple_field_assignment(self):
+ """Test simple field assignment - based on __main__ test1"""
+ # Create String message
+ string_msg = String()
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ # Initial state check
+ assert string_msg.data == ""
+
+ # Test field assignment
+ ros2_inst.data = "test_value" # Assignment through field accessor
+ assert string_msg.data == "test_value"
+ assert ros2_inst.inner_data.data == "test_value"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_nested_field_assignment(self):
+ """测试嵌套字段赋值 - 基于 __main__ 测试2,3"""
+ # Create Pose message
+ pose_msg = Pose()
+ ros2_inst = ROS2MessageInstance(pose_msg)
+
+ # 测试嵌套字段赋值
+ ros2_inst.position.x = 1.5
+ ros2_inst.position.y = 2.5
+ ros2_inst.position.z = 3.5
+
+ assert pose_msg.position.x == 1.5
+ assert pose_msg.position.y == 2.5
+ assert pose_msg.position.z == 3.5
+
+ # 测试整个对象赋值
+ new_position = Point(x=10.0, y=20.0, z=30.0)
+ ros2_inst.position = new_position
+
+ assert pose_msg.position.x == 10.0
+ assert pose_msg.position.y == 20.0
+ assert pose_msg.position.z == 30.0
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_export_to_envelope(self):
+ """测试导出信封功能 - 基于 __main__ 测试6"""
+ # Create and setup String message
+ string_msg = String()
+ string_msg.data = "test_envelope_data"
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ # 导出信封
+ envelope = ros2_inst.export_to_envelope()
+
+ # 验证信封结构
+ assert "content" in envelope
+ assert "metadata" in envelope
+ assert envelope["content"]["data"] == "test_envelope_data"
+
+ # 验证元数据
+ metadata = envelope["metadata"]
+ assert metadata["current_format"] == "ros2"
+ assert "properties" in metadata
+
+ properties = metadata["properties"]
+ assert "ros_msg_cls_namespace" in properties
+ assert "ros_msg_cls_path" in properties
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_get_python_dict(self):
+ """测试获取 Python 字典"""
+ # Create Float64MultiArray message
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.0, 2.0, 3.0]
+ ros2_inst = ROS2MessageInstance(array_msg)
+
+ # 获取 Python 字典
+ python_dict = ros2_inst.get_python_dict()
+
+ assert isinstance(python_dict, dict)
+ assert "data" in python_dict
+ assert python_dict["data"] == [1.0, 2.0, 3.0]
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_set_python_dict(self):
+ """测试设置 Python 字典"""
+ # Create String message
+ string_msg = String()
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ # 设置字典数据
+ new_data = {"data": "updated_value"}
+ result = ros2_inst.set_python_dict(new_data)
+
+ assert result is True
+ assert string_msg.data == "updated_value"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_field_type_info_extraction(self):
+ """测试字段类型信息提取"""
+ # Create Float64MultiArray message
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.0, 2.0, 3.0]
+ ros2_inst = ROS2MessageInstance(array_msg)
+
+ # 获取字段类型信息
+ type_info = ros2_inst.fields.get_sub_type_info("data")
+
+ assert type_info is not None
+ assert type_info.field_name == "data"
+ assert type_info.is_array is True
+ assert type_info.python_type == array.array
+ assert type_info.python_value_from_standard_type == [1.0, 2.0, 3.0]
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_obtain_ros_cls_from_string(self):
+ """测试从字符串获取 ROS 类"""
+ # 测试 namespace 格式
+ ros_cls_ns = ROS2MessageInstance.obtain_ros_cls_from_str("std_msgs/msg/String")
+ assert ros_cls_ns == String
+
+ # 测试模块路径格式
+ ros_cls_path = ROS2MessageInstance.obtain_ros_cls_from_str("std_msgs.msg._string.String")
+ assert ros_cls_path == String
+
+ # 测试直接传入类
+ ros_cls_direct = ROS2MessageInstance.obtain_ros_cls_from_str(String)
+ assert ros_cls_direct == String
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_ros_msg_cls_properties(self):
+ """测试 ROS 消息类属性"""
+ string_msg = String()
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ # 测试类路径属性
+ cls_path = ros2_inst.ros_msg_cls_path
+ assert "std_msgs.msg" in cls_path
+ assert "String" in cls_path
+
+ # 测试命名空间属性
+ namespace = ros2_inst.ros_msg_cls_namespace
+ assert namespace == "std_msgs/msg/String"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_import_from_envelope(self):
+ """测试从信封导入"""
+ # Create original message
+ original_msg = String()
+ original_msg.data = "envelope_test"
+ original_inst = ROS2MessageInstance(original_msg)
+
+ # 导出信封
+ envelope = original_inst.export_to_envelope()
+
+ # 从信封导入新实例
+ new_inst = ROS2MessageInstance.import_from_envelope(envelope)
+
+ assert isinstance(new_inst, ROS2MessageInstance)
+ assert new_inst.inner_data.data == "envelope_test"
+ assert new_inst.ros_msg_cls == String
+
+
+class TestROS2MessageInstanceJSONSchema:
+ """ROS2MessageInstance JSON Schema 生成测试"""
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_get_json_schema_string_message(self):
+ """测试 String 消息的 JSON Schema 生成"""
+ string_msg = String()
+ string_msg.data = "test_schema"
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ # 生成 JSON Schema
+ schema = ros2_inst.get_json_schema()
+
+ assert schema["type"] == "object"
+ assert "properties" in schema
+ assert "data" in schema["properties"]
+ assert schema["properties"]["data"]["type"] == "string"
+ assert schema["title"] == "ROS2MessageInstance Schema"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_get_json_schema_float_array(self):
+ """测试 Float64MultiArray 的 JSON Schema 生成"""
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.1, 2.2, 3.3]
+ ros2_inst = ROS2MessageInstance(array_msg)
+
+ # 生成 JSON Schema
+ schema = ros2_inst.get_json_schema()
+
+ assert schema["type"] == "object"
+ assert "properties" in schema
+ assert "data" in schema["properties"]
+
+ # 检查数组类型
+ data_prop = schema["properties"]["data"]
+ assert data_prop["type"] == "array"
+ assert "items" in data_prop
+ assert data_prop["items"]["type"] == "number"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_get_json_schema_pose_message(self):
+ """测试复杂 Pose 消息的 JSON Schema 生成"""
+ pose_msg = Pose()
+ pose_msg.position.x = 1.0
+ pose_msg.position.y = 2.0
+ pose_msg.position.z = 3.0
+ ros2_inst = ROS2MessageInstance(pose_msg)
+
+ # 生成 JSON Schema
+ schema = ros2_inst.get_json_schema()
+
+ assert schema["type"] == "object"
+ assert "properties" in schema
+
+ # 检查嵌套对象
+ properties = schema["properties"]
+ assert "position" in properties
+ assert "orientation" in properties
+
+ # 验证对象类型
+ position_prop = properties["position"]
+ assert position_prop["type"] == "object"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
+ def test_json_schema_constraint_extraction(self):
+ """测试约束条件提取"""
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.0, 2.0, 3.0, 4.0, 5.0]
+ ros2_inst = ROS2MessageInstance(array_msg)
+
+ # 获取字段类型信息检查约束
+ type_info = ros2_inst.fields.get_sub_type_info("data")
+
+ assert type_info is not None
+ assert type_info.is_array is True
+
+ # 生成 Schema 并检查约束是否转换
+ schema = ros2_inst.get_json_schema()
+ data_prop = schema["properties"]["data"]
+ assert data_prop["type"] == "array"
+
+
+# 运行测试的便捷函数
+def run_ros2_tests():
+ """运行 ROS2 相关测试"""
+ if not HAS_ROS2:
+ print("❌ ROS2 dependencies not available, skipping tests")
+ return False
+
+ import subprocess
+
+ result = subprocess.run(
+ ["python", "-m", "pytest", "tests/test_ros2_instance.py", "-v", "--tb=short"],
+ capture_output=True,
+ text=True,
+ )
+
+ print(result.stdout)
+ if result.stderr:
+ print("STDERR:", result.stderr)
+
+ return result.returncode == 0
+
+
+if __name__ == "__main__":
+ # 直接运行测试
+ run_ros2_tests()
diff --git a/tests/test_ros_to_json_schema_conversion.py b/tests/test_ros_to_json_schema_conversion.py
new file mode 100644
index 0000000..57aa14f
--- /dev/null
+++ b/tests/test_ros_to_json_schema_conversion.py
@@ -0,0 +1,384 @@
+import os
+import sys
+
+import pytest
+
+# 添加项目路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+# 依赖检查
+try:
+ from geometry_msgs.msg import Point, Pose, Vector3
+ from std_msgs.msg import Bool, Float64MultiArray, Int32, String
+
+ # 只有在 ROS2 消息包可用时才导入 ROS2MessageInstance
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
+
+ HAS_ROS2 = True
+except ImportError:
+ HAS_ROS2 = False
+
+
+import jsonschema
+
+from msgcenterpy.core.types import MessageType
+from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
+
+
+class TestROS2ToJSONSchemaConversion:
+ """ROS2 转 JSON Schema 转换测试"""
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_string_message_to_json_schema(self):
+ """测试 String 消息转 JSON Schema"""
+ # 创建 ROS2 String 消息
+ string_msg = String()
+ string_msg.data = "Hello JSON Schema"
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ # 生成 JSON Schema
+ schema = ros2_inst.get_json_schema()
+
+ # 验证 Schema 结构
+ assert schema["type"] == "object"
+ assert "properties" in schema
+ assert "data" in schema["properties"]
+ assert schema["properties"]["data"]["type"] == "string"
+ assert schema["title"] == "ROS2MessageInstance Schema"
+ assert "ros2" in schema["description"]
+
+ # 验证与原始数据的一致性
+ ros2_dict = ros2_inst.get_python_dict()
+ assert "data" in ros2_dict
+ assert ros2_dict["data"] == "Hello JSON Schema"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_float_array_to_json_schema(self):
+ """测试 Float64MultiArray 转 JSON Schema"""
+ # 创建 Float64MultiArray 消息
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.1, 2.2, 3.3, 4.4, 5.5]
+ ros2_inst = ROS2MessageInstance(array_msg)
+
+ # 生成 JSON Schema
+ schema = ros2_inst.get_json_schema()
+
+ # 验证数组字段的 Schema
+ assert "data" in schema["properties"]
+ data_prop = schema["properties"]["data"]
+ assert data_prop["type"] == "array"
+ assert "items" in data_prop
+ assert data_prop["items"]["type"] == "number"
+
+ # 验证约束条件
+ assert "minItems" in data_prop or "maxItems" in data_prop or data_prop["type"] == "array"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_pose_message_to_json_schema(self):
+ """测试复杂 Pose 消息转 JSON Schema"""
+ # 创建 Pose 消息
+ pose_msg = Pose()
+ pose_msg.position.x = 1.0
+ pose_msg.position.y = 2.0
+ pose_msg.position.z = 3.0
+ pose_msg.orientation.w = 1.0
+ ros2_inst = ROS2MessageInstance(pose_msg)
+
+ # 生成 JSON Schema
+ schema = ros2_inst.get_json_schema()
+
+ # 验证嵌套对象结构
+ properties = schema["properties"]
+ assert "position" in properties
+ assert "orientation" in properties
+
+ # 验证对象类型
+ position_prop = properties["position"]
+ assert position_prop["type"] == "object"
+
+ orientation_prop = properties["orientation"]
+ assert orientation_prop["type"] == "object"
+
+
+class TestROS2ToJSONSchemaInstanceConversion:
+ """ROS2 转 JSONSchemaMessageInstance 转换测试"""
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_ros2_to_json_schema_instance_string(self):
+ """测试 ROS2 String 转 JSONSchemaMessageInstance"""
+ # 创建 ROS2 实例
+ string_msg = String()
+ string_msg.data = "Test conversion"
+ ros2_inst = ROS2MessageInstance(string_msg)
+
+ # 转换为 JSONSchemaMessageInstance
+ json_schema_inst = ros2_inst.to_json_schema()
+
+ # 验证转换结果
+ assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
+ assert json_schema_inst.message_type == MessageType.JSON_SCHEMA
+
+ # 验证数据一致性
+ original_data = ros2_inst.get_python_dict()
+ converted_data = json_schema_inst.get_python_dict()
+ assert original_data == converted_data
+
+ # 验证 Schema 存在
+ schema = json_schema_inst.json_schema
+ assert schema is not None
+ assert schema["type"] == "object"
+ assert "properties" in schema
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_ros2_to_json_schema_instance_array(self):
+ """测试 ROS2 数组转 JSONSchemaMessageInstance"""
+ # 创建 ROS2 数组实例
+ array_msg = Float64MultiArray()
+ array_msg.data = [10.5, 20.3, 30.7]
+ ros2_inst = ROS2MessageInstance(array_msg)
+
+ # 转换为 JSONSchemaMessageInstance
+ json_schema_inst = ros2_inst.to_json_schema()
+
+ # 验证转换结果
+ assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
+
+ # 验证数据一致性
+ original_data = ros2_inst.get_python_dict()
+ converted_data = json_schema_inst.get_python_dict()
+ assert original_data == converted_data
+ assert converted_data["data"] == [10.5, 20.3, 30.7]
+
+ # 验证 Schema 的数组类型
+ schema = json_schema_inst.json_schema
+ if "data" in schema["properties"]:
+ data_prop = schema["properties"]["data"]
+ assert data_prop["type"] == "array"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_ros2_to_json_schema_instance_pose(self):
+ """测试 ROS2 Pose 转 JSONSchemaMessageInstance"""
+ # 创建复杂的 Pose 消息
+ pose_msg = Pose()
+ pose_msg.position.x = 5.0
+ pose_msg.position.y = 10.0
+ pose_msg.position.z = 15.0
+ pose_msg.orientation.x = 0.0
+ pose_msg.orientation.y = 0.0
+ pose_msg.orientation.z = 0.0
+ pose_msg.orientation.w = 1.0
+ ros2_inst = ROS2MessageInstance(pose_msg)
+
+ # 转换为 JSONSchemaMessageInstance
+ json_schema_inst = ros2_inst.to_json_schema()
+
+ # 验证转换结果
+ assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
+
+ # 验证嵌套数据一致性
+ original_data = ros2_inst.get_python_dict()
+ converted_data = json_schema_inst.get_python_dict()
+ assert original_data == converted_data
+
+ # 验证嵌套结构
+ assert "position" in converted_data
+ assert "orientation" in converted_data
+ assert converted_data["position"]["x"] == 5.0
+ assert converted_data["position"]["y"] == 10.0
+ assert converted_data["position"]["z"] == 15.0
+
+
+class TestConversionConsistency:
+ """转换一致性测试"""
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_schema_data_consistency(self):
+ """测试 Schema 与数据的一致性"""
+ # 创建多种类型的数据
+ test_cases = []
+
+ # String 消息
+ string_msg = String()
+ string_msg.data = "consistency_test"
+ test_cases.append(("String", ROS2MessageInstance(string_msg)))
+
+ # Float64MultiArray 消息
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.0, 2.0, 3.0]
+ test_cases.append(("Float64MultiArray", ROS2MessageInstance(array_msg)))
+
+ for test_name, ros2_inst in test_cases:
+ # 生成 Schema
+ schema = ros2_inst.get_json_schema()
+ original_data = ros2_inst.get_python_dict()
+
+ # 验证 Schema 属性与数据字段一致
+ schema_props = set(schema["properties"].keys())
+ data_keys = set(original_data.keys())
+
+ assert schema_props == data_keys, f"{test_name}: Schema properties and data keys don't match"
+
+ # 验证每个字段的类型一致性
+ for field_name, field_value in original_data.items():
+ if field_name in schema["properties"]:
+ prop_schema = schema["properties"][field_name]
+
+ # 基本类型检查
+ if isinstance(field_value, str):
+ assert prop_schema["type"] == "string", f"{test_name}.{field_name}: Type mismatch"
+ elif isinstance(field_value, bool):
+ assert prop_schema["type"] == "boolean", f"{test_name}.{field_name}: Type mismatch"
+ elif isinstance(field_value, int):
+ assert prop_schema["type"] == "integer", f"{test_name}.{field_name}: Type mismatch"
+ elif isinstance(field_value, float):
+ assert prop_schema["type"] == "number", f"{test_name}.{field_name}: Type mismatch"
+ elif isinstance(field_value, list):
+ assert prop_schema["type"] == "array", f"{test_name}.{field_name}: Type mismatch"
+ elif isinstance(field_value, dict):
+ assert prop_schema["type"] == "object", f"{test_name}.{field_name}: Type mismatch"
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_conversion_roundtrip_data(self):
+ """测试转换往返的数据一致性"""
+ # 创建原始 ROS2 消息
+ string_msg = String()
+ string_msg.data = "roundtrip_test"
+ original_ros2_inst = ROS2MessageInstance(string_msg)
+ original_data = original_ros2_inst.get_python_dict()
+
+ # ROS2 -> JSONSchema
+ json_schema_inst = original_ros2_inst.to_json_schema()
+ converted_data = json_schema_inst.get_python_dict()
+
+ # 验证数据在转换过程中保持一致
+ assert original_data == converted_data
+
+ # 验证 Schema 的有效性
+ schema = json_schema_inst.json_schema
+ assert schema is not None
+
+ # 使用 jsonschema 库验证数据符合生成的 Schema
+ try:
+ jsonschema.validate(converted_data, schema)
+ except jsonschema.ValidationError as e:
+ pytest.fail(f"Generated data doesn't match generated schema: {e}")
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_multiple_message_types_conversion(self):
+ """测试多种消息类型的转换"""
+ test_messages = []
+
+ # String
+ string_msg = String()
+ string_msg.data = "multi_test_string"
+ test_messages.append(("String", string_msg))
+
+ # Float64MultiArray
+ array_msg = Float64MultiArray()
+ array_msg.data = [1.5, 2.5, 3.5]
+ test_messages.append(("Float64MultiArray", array_msg))
+
+ # Point
+ point_msg = Point()
+ point_msg.x = 1.0
+ point_msg.y = 2.0
+ point_msg.z = 3.0
+ test_messages.append(("Point", point_msg))
+
+ for msg_type_name, msg in test_messages:
+ # 创建 ROS2 实例
+ ros2_inst = ROS2MessageInstance(msg)
+
+ # 转换为 JSONSchema 实例
+ json_schema_inst = ros2_inst.to_json_schema()
+
+ # 验证转换成功
+ assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
+ assert json_schema_inst.message_type == MessageType.JSON_SCHEMA
+
+ # 验证数据一致性
+ original_data = ros2_inst.get_python_dict()
+ converted_data = json_schema_inst.get_python_dict()
+ assert original_data == converted_data, f"{msg_type_name}: Data inconsistency after conversion"
+
+ # 验证 Schema 生成
+ schema = json_schema_inst.json_schema
+ assert schema is not None
+ assert schema["type"] == "object"
+ assert len(schema["properties"]) > 0, f"{msg_type_name}: No properties in generated schema"
+
+
+class TestConversionErrorHandling:
+ """转换错误处理测试"""
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_empty_message_conversion(self):
+ """测试空消息的转换"""
+ # 创建空的 String 消息
+ empty_string = String()
+ ros2_inst = ROS2MessageInstance(empty_string)
+
+ # 转换应该成功,即使数据为空
+ json_schema_inst = ros2_inst.to_json_schema()
+
+ assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
+
+ # 验证 Schema 仍然有效
+ schema = json_schema_inst.json_schema
+ assert schema["type"] == "object"
+ assert "properties" in schema
+
+ @pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
+ def test_large_data_conversion(self):
+ """测试大数据量的转换"""
+ # 创建包含大量数据的数组消息
+ large_array = Float64MultiArray()
+ large_array.data = [float(i) for i in range(1000)] # 1000 个浮点数
+ ros2_inst = ROS2MessageInstance(large_array)
+
+ # 转换应该能处理大数据量
+ json_schema_inst = ros2_inst.to_json_schema()
+
+ assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
+
+ # 验证数据完整性
+ original_data = ros2_inst.get_python_dict()
+ converted_data = json_schema_inst.get_python_dict()
+ assert len(original_data["data"]) == 1000
+ assert len(converted_data["data"]) == 1000
+ assert original_data == converted_data
+
+
+# 运行测试的便捷函数
+def run_conversion_tests():
+ """运行转换测试"""
+ if not HAS_ROS2:
+ print("❌ Required dependencies not available, skipping tests")
+ print(f" ROS2: {HAS_ROS2}")
+ return False
+
+ import subprocess
+
+ result = subprocess.run(
+ [
+ "python",
+ "-m",
+ "pytest",
+ "tests/test_ros_to_json_schema_conversion.py",
+ "-v",
+ "--tb=short",
+ ],
+ capture_output=True,
+ text=True,
+ )
+
+ print(result.stdout)
+ if result.stderr:
+ print("STDERR:", result.stderr)
+
+ return result.returncode == 0
+
+
+if __name__ == "__main__":
+ # 直接运行测试
+ run_conversion_tests()