From 5a4a5d30d4c0aa0c0b11303c2d38b3dbfbca1667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=AB=E5=BE=80?= Date: Tue, 4 Jun 2024 00:04:03 +0800 Subject: [PATCH] support windows and add build workflow --- .github/workflows/python-package-action.yml | 126 +++++++++++++++ .gitignore | 3 + tests/check_init.py | 21 +++ unitree_sdk2py/core/__init__.py | 0 unitree_sdk2py/core/channel_config.py | 13 +- unitree_sdk2py/go2/__init__.py | 0 .../go2/obstacles_avoid/__init__.py | 0 unitree_sdk2py/go2/robot_state/__init__.py | 0 unitree_sdk2py/go2/sport/__init__.py | 0 unitree_sdk2py/go2/video/__init__.py | 0 unitree_sdk2py/go2/vui/__init__.py | 0 unitree_sdk2py/rpc/__init__.py | 0 unitree_sdk2py/test/platform/test_timerfd.py | 8 + unitree_sdk2py/utils/__init__.py | 0 unitree_sdk2py/utils/thread.py | 9 +- unitree_sdk2py/utils/timerfd.py | 143 +++++++++++++----- 16 files changed, 279 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/python-package-action.yml create mode 100644 tests/check_init.py create mode 100644 unitree_sdk2py/core/__init__.py create mode 100644 unitree_sdk2py/go2/__init__.py create mode 100644 unitree_sdk2py/go2/obstacles_avoid/__init__.py create mode 100644 unitree_sdk2py/go2/robot_state/__init__.py create mode 100644 unitree_sdk2py/go2/sport/__init__.py create mode 100644 unitree_sdk2py/go2/video/__init__.py create mode 100644 unitree_sdk2py/go2/vui/__init__.py create mode 100644 unitree_sdk2py/rpc/__init__.py create mode 100644 unitree_sdk2py/test/platform/test_timerfd.py create mode 100644 unitree_sdk2py/utils/__init__.py diff --git a/.github/workflows/python-package-action.yml b/.github/workflows/python-package-action.yml new file mode 100644 index 0000000..c68d566 --- /dev/null +++ b/.github/workflows/python-package-action.yml @@ -0,0 +1,126 @@ +# 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 +run-name: ${{ github.actor }} package +on: + workflow_dispatch: + inputs: + c_cyclonedds_version: + description: 'clang cyclonedds version' + required: false + default: 'releases/0.10.x' + cyclonedds_python_version: + description: cyclonedds_python的版本 + required: false + default: 'releases/0.10.x' + +jobs: + build: + + # 输入参数定义部分 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.11"] + os: [macos-latest, windows-latest, ubuntu-latest] + + steps: + # 初始化 + - uses: actions/checkout@v4 + + # 配置python + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + # 初始化制品收集目录 + - name: init dist + run: | + mkdir dist + + # 准备构建python wheel包 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + + # 由于cyclonedds-python暂时没有提供python3.11 顺带也编译了 issue:https://github.com/eclipse-cyclonedds/cyclonedds-python/issues/221 + - name: build cyclonedds-python in windows + if: startsWith(runner.os, 'Windows') + run: | + git clone https://github.com/eclipse-cyclonedds/cyclonedds-python -b ${{ github.event.inputs.cyclonedds_python_version }} --depth=1 + cd cyclonedds-python + (Get-Content pyproject.toml) -replace 'https://github.com/eclipse-cyclonedds/cyclonedds.git','https://github.com/eclipse-cyclonedds/cyclonedds -b ${{ github.event.inputs.c_cyclonedds_version }} --depth=1' | Set-Content pyproject.toml + (Get-Content pyproject.toml) -replace '(?<=^skip = ")','cp3{6..10}-* ' | Set-Content pyproject.toml + (Get-Content pyproject.toml) -replace 'delvewheel==0.0.18','delvewheel==1.6.0' | Set-Content pyproject.toml + (Get-Content setup.py) -replace '(?<="Programming Language :: Python :: 3.10",.*?)', "`n `"Programming Language :: Python :: 3.11`",`n `"Programming Language :: Python :: 3.12`",`n `"Programming Language :: Python :: 3.13`"," | Set-Content setup.py + pip install --user cibuildwheel==2.18.* + python -m cibuildwheel --output-dir wheelhouse . + tar zcvf cyclonedds.tar.gz -C cyclonedds-build . + shell: pwsh + + # 兼容sed see:https://gist.github.com/andre3k1/e3a1a7133fded5de5a9ee99c87c6fa0d + - name: install GNU sed in mac + if: startsWith(runner.os, 'macOS') + run: | + brew install gnu-sed + + - name: build cyclonedds-python in unix + if: startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS') + run: | + if [ "${{ startsWith(runner.os, 'macOS') }}" ]; then + PATH="/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH" + fi + git clone https://github.com/eclipse-cyclonedds/cyclonedds-python -b ${{ github.event.inputs.c_cyclonedds_version }} --depth=1 + cd cyclonedds-python + + sed -i "s~https://github.com/eclipse-cyclonedds/cyclonedds.git~https://github.com/eclipse-cyclonedds/cyclonedds -b ${{ github.event.inputs.c_cyclonedds_version }} --depth=1~g" pyproject.toml + echo "debugprintpoint1" + sed -i 's/\(^skip = "\)/\1cp3{6..10}-* /g' pyproject.toml + echo "printpoint2" + sed -i 's/delvewheel==0\.0\.18/delvewheel==1\.6\.0/g' pyproject.toml + echo "printpoint3" + sed -i 's/(?<="Programming Language :: Python :: 3.10",.*?)/\n "Programming Language :: Python :: 3.11",\n "Programming Language :: Python :: 3.12",\n "Programming Language :: Python :: 3.13",/g' setup.py + pip install --user cibuildwheel==2.18.* + python -m cibuildwheel --output-dir wheelhouse . + shell: bash + + # 构建宇树python sdk本身 + - name: build package unix + if: startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS') + run: | + export CYCLONEDDS_HOME=${{ github.workspace }}/cyclonedds/install + python -m build --wheel + shell: bash + - name: build package windows + if: startsWith(runner.os, 'Windows') + run: | + set CYCLONEDDS_HOME=${{ github.workspace }}/cyclonedds/install + python -m build --wheel + shell: cmd + + # 收集构建产物 + - name: collect binary + if: startsWith(runner.os, 'Windows') + run: | + mv ${{ github.workspace }}/cyclonedds-python/wheelhouse/* dist/ + mv ${{ github.workspace }}/dist/* dist/ + - name: collect binary + if: startsWith(runner.os, 'Linux') || startsWith(runner.os, 'macOS') + run: | + # 对复杂的不同平台目录机制信息收集 + find . -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g' > dist/${{runner.os}}_${{runner.arch}}.txt + mv -f ${{ github.workspace }}/cyclonedds-python/wheelhouse/* dist/ + # mv -f ${{ github.workspace }}/dist/* dist/ + + # 上传到工作流制品库 + - name: upload package and dependency + uses: actions/upload-artifact@v4 + with: + name: unitree_sdk2_python${{ matrix.python-version }}_${{runner.os}}_${{runner.arch}} + path: | + dist/* + diff --git a/.gitignore b/.gitignore index 9871dce..9967852 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ __pycache__ # JetBrains IDE .idea/ + +build +dist \ No newline at end of file diff --git a/tests/check_init.py b/tests/check_init.py new file mode 100644 index 0000000..fa1886f --- /dev/null +++ b/tests/check_init.py @@ -0,0 +1,21 @@ +import os,sys + + +def check_init_files(directory): + """递归确认包存在__init__.py防止缺胳膊少腿""" + all_dirs_have_init = True + for root, dirs, files in os.walk(directory): + if "__pycache__" in dirs: + dirs.remove("__pycache__") + if "test" in dirs: + dirs.remove("test") + if "__init__.py" not in files: + print(f"Directory '{root}' is missing '__init__.py'") + all_dirs_have_init = False + return all_dirs_have_init + +directory_to_check = os.path.join(os.path.dirname(os.path.realpath(__file__)),"..","unitree_sdk2py") +if check_init_files(directory_to_check): + print("所有文件夹均含 '__init__.py'.") +else: + print("校验未通过", file=sys.stderr) \ No newline at end of file diff --git a/unitree_sdk2py/core/__init__.py b/unitree_sdk2py/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/core/channel_config.py b/unitree_sdk2py/core/channel_config.py index 19a67a4..c75fb7f 100644 --- a/unitree_sdk2py/core/channel_config.py +++ b/unitree_sdk2py/core/channel_config.py @@ -1,4 +1,13 @@ -ChannelConfigHasInterface = ''' +import platform + +# build platform compatible +if platform.system() == "Windows": + import os + tmp = os.environ['TEMP'] +else: + tmp = "/tmp" + +ChannelConfigHasInterface = f''' @@ -8,7 +17,7 @@ ChannelConfigHasInterface = ''' config - /tmp/cdds.LOG + {tmp}/cdds.LOG ''' diff --git a/unitree_sdk2py/go2/__init__.py b/unitree_sdk2py/go2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/go2/obstacles_avoid/__init__.py b/unitree_sdk2py/go2/obstacles_avoid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/go2/robot_state/__init__.py b/unitree_sdk2py/go2/robot_state/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/go2/sport/__init__.py b/unitree_sdk2py/go2/sport/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/go2/video/__init__.py b/unitree_sdk2py/go2/video/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/go2/vui/__init__.py b/unitree_sdk2py/go2/vui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/rpc/__init__.py b/unitree_sdk2py/rpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/test/platform/test_timerfd.py b/unitree_sdk2py/test/platform/test_timerfd.py new file mode 100644 index 0000000..7ffe53b --- /dev/null +++ b/unitree_sdk2py/test/platform/test_timerfd.py @@ -0,0 +1,8 @@ +from unitree_sdk2py.utils.hz_sample import HZSample + +if __name__ == "__main__": + hz=HZSample(2) + hz.Start() + while True: + hz.Sample() + diff --git a/unitree_sdk2py/utils/__init__.py b/unitree_sdk2py/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unitree_sdk2py/utils/thread.py b/unitree_sdk2py/utils/thread.py index f17cf4e..a906579 100644 --- a/unitree_sdk2py/utils/thread.py +++ b/unitree_sdk2py/utils/thread.py @@ -52,10 +52,7 @@ class RecurrentThread(Thread): super().Wait(timeout) def __LoopFunc(self): - # clock type CLOCK_MONOTONIC = 1 - tfd = timerfd_create(1, 0) - spec = itimerspec.from_seconds(self.__inter, self.__inter) - timerfd_settime(tfd, 0, ctypes.byref(spec), None) + timer = Timer(self.__inter, self.__inter) while not self.__quit: try: @@ -65,13 +62,13 @@ class RecurrentThread(Thread): print(f"[RecurrentThread] target func raise exception: name={info[0].__name__}, args={str(info[1].args)}") try: - buf = os.read(tfd, 8) + timer.blockWait() # print(struct.unpack("Q", buf)[0]) except OSError as e: if e.errno != errno.EAGAIN: raise e - os.close(tfd) + timer.close() def __LoopFunc_0(self): while not self.__quit: diff --git a/unitree_sdk2py/utils/timerfd.py b/unitree_sdk2py/utils/timerfd.py index 002ef39..83157c8 100644 --- a/unitree_sdk2py/utils/timerfd.py +++ b/unitree_sdk2py/utils/timerfd.py @@ -1,45 +1,116 @@ import math import ctypes -from .clib_lookup import CLIBLookup +import platform -class timespec(ctypes.Structure): - _fields_ = [("sec", ctypes.c_long), ("nsec", ctypes.c_long)] - __slots__ = [name for name,type in _fields_] - - @classmethod - def from_seconds(cls, secs): - c = cls() - c.seconds = secs - return c +"""声明计时通用接口,主要为兼容windows平台和linux的文件描述符,在python3.13 os会支持但现在还没release..""" +class Timer: - @property - def seconds(self): - return self.sec + self.nsec / 1000000000 - - @seconds.setter - def seconds(self, secs): - x, y = math.modf(secs) - self.sec = int(y) - self.nsec = int(x * 1000000000) - - -class itimerspec(ctypes.Structure): - _fields_ = [("interval", timespec),("value", timespec)] - __slots__ = [name for name,type in _fields_] + """ + 参数: + time: 首次运行等待时间,秒.First Wait Time, second. + period: 周期等待时间, 秒.Period Wait Time, second. + """ + def __init__(self, time :float, period :float): + pass - @classmethod - def from_seconds(cls, interval, value): - spec = cls() - spec.interval.seconds = interval - spec.value.seconds = value - return spec + """ + 阻塞等待,会在下方根据平台重写 + """ + def blockWait(self): + pass + + """ + 关闭句柄 + """ + def close(self): + pass -# function timerfd_create -timerfd_create = CLIBLookup("timerfd_create", ctypes.c_int, (ctypes.c_long, ctypes.c_int)) +# build platform compatible +if platform.system() == "Windows": + from ctypes import wintypes + kernel32 = ctypes.windll.kernel32 + INFINITE=wintypes.DWORD(-1) + PTIMERAPCROUTINE = ctypes.WINFUNCTYPE(None, wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD) + @PTIMERAPCROUTINE + def timer_callback(arg, timer_low, timer_high): + pass + # 重写Timer方法 + def Timer_init(self, time :float, period :float): + self.handle = kernel32.CreateWaitableTimerW(None, True, None) + due_time = wintypes.LARGE_INTEGER(-int(time * 10000000)) # 秒转100纳秒 + period = int(period*1000) # 秒转毫秒 + # https://learn.microsoft.com/zh-cn/windows/win32/api/synchapi/nf-synchapi-setwaitabletimer + if not kernel32.SetWaitableTimer(self.handle, ctypes.byref(due_time), period, timer_callback, 0, True): + raise OSError("Failed to set waitable timer.") + def Timer_blockWait(self): + kernel32.WaitForSingleObject(self.handle, INFINITE) + def Timer_close(self): + kernel32.CancelWaitableTimer(self.handle) + kernel32.CloseHandle(self.handle) + Timer.__init__ = Timer_init + Timer.blockWait = Timer_blockWait + Timer.close = Timer_close + +elif platform.system() == "Linux": + + from .clib_lookup import CLIBLookup -# function timerfd_settime -timerfd_settime = CLIBLookup("timerfd_settime", ctypes.c_int, (ctypes.c_int, ctypes.c_int, ctypes.POINTER(itimerspec), ctypes.POINTER(itimerspec))) + class timespec(ctypes.Structure): + _fields_ = [("sec", ctypes.c_long), ("nsec", ctypes.c_long)] + __slots__ = [name for name,type in _fields_] -# function timerfd_gettime -timerfd_gettime = CLIBLookup("timerfd_gettime", ctypes.c_int, (ctypes.c_int, ctypes.POINTER(itimerspec))) + @classmethod + def from_seconds(cls, secs): + c = cls() + c.seconds = secs + return c + + @property + def seconds(self): + return self.sec + self.nsec / 1000000000 + + @seconds.setter + def seconds(self, secs): + x, y = math.modf(secs) + self.sec = int(y) + self.nsec = int(x * 1000000000) + + + class itimerspec(ctypes.Structure): + _fields_ = [("interval", timespec),("value", timespec)] + __slots__ = [name for name,type in _fields_] + + @classmethod + def from_seconds(cls, interval, value): + spec = cls() + spec.interval.seconds = interval + spec.value.seconds = value + return spec + + + # function timerfd_create + timerfd_create = CLIBLookup("timerfd_create", ctypes.c_int, (ctypes.c_long, ctypes.c_int)) + + # function timerfd_settime + timerfd_settime_linux = CLIBLookup("timerfd_settime", ctypes.c_int, (ctypes.c_int, ctypes.c_int, ctypes.POINTER(itimerspec), ctypes.POINTER(itimerspec))) + + def timerfd_settime(handle,interval,value): + spec = itimerspec.from_seconds(interval, value) + timerfd_settime_linux(handle, 0, spec, None) + + # function timerfd_gettime + timerfd_gettime = CLIBLookup("timerfd_gettime", ctypes.c_int, (ctypes.c_int, ctypes.POINTER(itimerspec))) + # 重写Timer方法 + import os + def Timer_init(self, time :float, period :float): + self.handle = timerfd_create(1, 0) + spec = itimerspec.from_seconds(period, time) + timerfd_settime_linux(self.handle, 0, spec, None) + def Timer_blockWait(self): + os.read(self.handle,8) + def Timer_close(self): + os.close(self.handle) + Timer.__init__ = Timer_init + Timer.blockWait = Timer_blockWait + Timer.close = Timer_close \ No newline at end of file