Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support windows and add build workflow #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/workflows/python-package-action.yml
Original file line number Diff line number Diff line change
@@ -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/*

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ __pycache__

# JetBrains IDE
.idea/

build
dist
21 changes: 21 additions & 0 deletions tests/check_init.py
Original file line number Diff line number Diff line change
@@ -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)
Empty file added unitree_sdk2py/core/__init__.py
Empty file.
13 changes: 11 additions & 2 deletions unitree_sdk2py/core/channel_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
ChannelConfigHasInterface = '''<?xml version="1.0" encoding="UTF-8" ?>
import platform

# build platform compatible
if platform.system() == "Windows":
import os
tmp = os.environ['TEMP']
else:
tmp = "/tmp"

ChannelConfigHasInterface = f'''<?xml version="1.0" encoding="UTF-8" ?>
<CycloneDDS>
<Domain Id="any">
<General>
Expand All @@ -8,7 +17,7 @@
</General>
<Tracing>
<Verbosity>config</Verbosity>
<OutputFile>/tmp/cdds.LOG</OutputFile>
<OutputFile>{tmp}/cdds.LOG</OutputFile>
</Tracing>
</Domain>
</CycloneDDS>'''
Expand Down
Empty file added unitree_sdk2py/go2/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file added unitree_sdk2py/rpc/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions unitree_sdk2py/test/platform/test_timerfd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from unitree_sdk2py.utils.hz_sample import HZSample

if __name__ == "__main__":
hz=HZSample(2)
hz.Start()
while True:
hz.Sample()

Empty file.
9 changes: 3 additions & 6 deletions unitree_sdk2py/utils/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ def Wait(self, timeout: float = None):
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:
Expand All @@ -65,13 +62,13 @@ def __LoopFunc(self):
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:
Expand Down
137 changes: 104 additions & 33 deletions unitree_sdk2py/utils/timerfd.py
Original file line number Diff line number Diff line change
@@ -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_]
"""声明计时通用接口,主要为兼容windows平台和linux的文件描述符,在python3.13 os会支持但现在还没release.."""
class Timer:

"""
参数:
time: 首次运行等待时间,秒.First Wait Time, second.
period: 周期等待时间, 秒.Period Wait Time, second.
"""
def __init__(self, time :float, period :float):
pass

"""
阻塞等待,会在下方根据平台重写
"""
def blockWait(self):
pass

"""
关闭句柄
"""
def close(self):
pass

@classmethod
def from_seconds(cls, secs):
c = cls()
c.seconds = secs
return c

# 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":

@property
def seconds(self):
return self.sec + self.nsec / 1000000000
from .clib_lookup import CLIBLookup

@seconds.setter
def seconds(self, secs):
x, y = math.modf(secs)
self.sec = int(y)
self.nsec = int(x * 1000000000)
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

@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

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_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)))

# function timerfd_settime
timerfd_settime = 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)))
# 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