From ac66721de2e8b8f942f97ad62ed8b84a681d7d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:57:48 +0800 Subject: [PATCH 01/12] Add open hardware license --- LICENSE | 310 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 11 +- 2 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0f882860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,310 @@ +CERN Open Hardware Licence Version 2 - Weakly Reciprocal + +Preamble + +CERN has developed this licence to promote collaboration among +hardware designers and to provide a legal tool which supports the +freedom to use, study, modify, share and distribute hardware designs +and products based on those designs. Version 2 of the CERN Open +Hardware Licence comes in three variants: CERN-OHL-P (permissive); and +two reciprocal licences: this licence, CERN- OHL-W (weakly reciprocal) +and CERN-OHL-S (strongly reciprocal). + +The CERN-OHL-W is copyright CERN 2020. Anyone is welcome to use it, in +unmodified form only. + +Use of this Licence does not imply any endorsement by CERN of any +Licensor or their designs nor does it imply any involvement by CERN in +their development. + + +1 Definitions + + 1.1 'Licence' means this CERN-OHL-W. + + 1.2 'Compatible Licence' means + + a) any earlier version of the CERN Open Hardware licence, or + + b) any version of the CERN-OHL-S or the CERN-OHL-W, or + + c) any licence which permits You to treat the Source to which + it applies as licensed under CERN-OHL-S or CERN-OHL-W + provided that on Conveyance of any such Source, or any + associated Product You treat the Source in question as being + licensed under CERN-OHL-S or CERN-OHL-W as appropriate. + + 1.3 'Source' means information such as design materials or digital + code which can be applied to Make or test a Product or to + prepare a Product for use, Conveyance or sale, regardless of its + medium or how it is expressed. It may include Notices. + + 1.4 'Covered Source' means Source that is explicitly made available + under this Licence. + + 1.5 'Product' means any device, component, work or physical object, + whether in finished or intermediate form, arising from the use, + application or processing of Covered Source. + + 1.6 'Make' means to create or configure something, whether by + manufacture, assembly, compiling, loading or applying Covered + Source or another Product or otherwise. + + 1.7 'Available Component' means any part, sub-assembly, library or + code which: + + a) is licensed to You as Complete Source under a Compatible + Licence; or + + b) is available, at the time a Product or the Source containing + it is first Conveyed, to You and any other prospective + licensees + + i) with sufficient rights and information (including any + configuration and programming files and information + about its characteristics and interfaces) to enable it + either to be Made itself, or to be sourced and used to + Make the Product; or + ii) as part of the normal distribution of a tool used to + design or Make the Product. + + 1.8 'External Material' means anything (including Source) which: + + a) is only combined with Covered Source in such a way that it + interfaces with the Covered Source using a documented + interface which is described in the Covered Source; and + + b) is not a derivative of or contains Covered Source, or, if it + is, it is solely to the extent necessary to facilitate such + interfacing. + + 1.9 'Complete Source' means the set of all Source necessary to Make + a Product, in the preferred form for making modifications, + including necessary installation and interfacing information + both for the Product, and for any included Available Components. + If the format is proprietary, it must also be made available in + a format (if the proprietary tool can create it) which is + viewable with a tool available to potential licensees and + licensed under a licence approved by the Free Software + Foundation or the Open Source Initiative. Complete Source need + not include the Source of any Available Component, provided that + You include in the Complete Source sufficient information to + enable a recipient to Make or source and use the Available + Component to Make the Product. + + 1.10 'Source Location' means a location where a Licensor has placed + Covered Source, and which that Licensor reasonably believes will + remain easily accessible for at least three years for anyone to + obtain a digital copy. + + 1.11 'Notice' means copyright, acknowledgement and trademark notices, + Source Location references, modification notices (subsection + 3.3(b)) and all notices that refer to this Licence and to the + disclaimer of warranties that are included in the Covered + Source. + + 1.12 'Licensee' or 'You' means any person exercising rights under + this Licence. + + 1.13 'Licensor' means a natural or legal person who creates or + modifies Covered Source. A person may be a Licensee and a + Licensor at the same time. + + 1.14 'Convey' means to communicate to the public or distribute. + + +2 Applicability + + 2.1 This Licence governs the use, copying, modification, Conveying + of Covered Source and Products, and the Making of Products. By + exercising any right granted under this Licence, You irrevocably + accept these terms and conditions. + + 2.2 This Licence is granted by the Licensor directly to You, and + shall apply worldwide and without limitation in time. + + 2.3 You shall not attempt to restrict by contract or otherwise the + rights granted under this Licence to other Licensees. + + 2.4 This Licence is not intended to restrict fair use, fair dealing, + or any other similar right. + + +3 Copying, modifying and Conveying Covered Source + + 3.1 You may copy and Convey verbatim copies of Covered Source, in + any medium, provided You retain all Notices. + + 3.2 You may modify Covered Source, other than Notices, provided that + You irrevocably undertake to make that modified Covered Source + available from a Source Location should You Convey a Product in + circumstances where the recipient does not otherwise receive a + copy of the modified Covered Source. In each case subsection 3.3 + shall apply. + + You may only delete Notices if they are no longer applicable to + the corresponding Covered Source as modified by You and You may + add additional Notices applicable to Your modifications. + + 3.3 You may Convey modified Covered Source (with the effect that You + shall also become a Licensor) provided that You: + + a) retain Notices as required in subsection 3.2; + + b) add a Notice to the modified Covered Source stating that You + have modified it, with the date and brief description of how + You have modified it; + + c) add a Source Location Notice for the modified Covered Source + if You Convey in circumstances where the recipient does not + otherwise receive a copy of the modified Covered Source; and + + d) license the modified Covered Source under the terms and + conditions of this Licence (or, as set out in subsection + 8.3, a later version, if permitted by the licence of the + original Covered Source). Such modified Covered Source must + be licensed as a whole, but excluding Available Components + contained in it or External Material to which it is + interfaced, which remain licensed under their own applicable + licences. + + +4 Making and Conveying Products + + 4.1 You may Make Products, and/or Convey them, provided that You + either provide each recipient with a copy of the Complete Source + or ensure that each recipient is notified of the Source Location + of the Complete Source. That Complete Source includes Covered + Source and You must accordingly satisfy Your obligations set out + in subsection 3.3. If specified in a Notice, the Product must + visibly and securely display the Source Location on it or its + packaging or documentation in the manner specified in that + Notice. + + 4.2 Where You Convey a Product which incorporates External Material, + the Complete Source for that Product which You are required to + provide under subsection 4.1 need not include any Source for the + External Material. + + 4.3 You may license Products under terms of Your choice, provided + that such terms do not restrict or attempt to restrict any + recipients' rights under this Licence to the Covered Source. + + +5 Research and Development + +You may Convey Covered Source, modified Covered Source or Products to +a legal entity carrying out development, testing or quality assurance +work on Your behalf provided that the work is performed on terms which +prevent the entity from both using the Source or Products for its own +internal purposes and Conveying the Source or Products or any +modifications to them to any person other than You. Any modifications +made by the entity shall be deemed to be made by You pursuant to +subsection 3.2. + + +6 DISCLAIMER AND LIABILITY + + 6.1 DISCLAIMER OF WARRANTY -- The Covered Source and any Products + are provided 'as is' and any express or implied warranties, + including, but not limited to, implied warranties of + merchantability, of satisfactory quality, non-infringement of + third party rights, and fitness for a particular purpose or use + are disclaimed in respect of any Source or Product to the + maximum extent permitted by law. The Licensor makes no + representation that any Source or Product does not or will not + infringe any patent, copyright, trade secret or other + proprietary right. The entire risk as to the use, quality, and + performance of any Source or Product shall be with You and not + the Licensor. This disclaimer of warranty is an essential part + of this Licence and a condition for the grant of any rights + granted under this Licence. + + 6.2 EXCLUSION AND LIMITATION OF LIABILITY -- The Licensor shall, to + the maximum extent permitted by law, have no liability for + direct, indirect, special, incidental, consequential, exemplary, + punitive or other damages of any character including, without + limitation, procurement of substitute goods or services, loss of + use, data or profits, or business interruption, however caused + and on any theory of contract, warranty, tort (including + negligence), product liability or otherwise, arising in any way + in relation to the Covered Source, modified Covered Source + and/or the Making or Conveyance of a Product, even if advised of + the possibility of such damages, and You shall hold the + Licensor(s) free and harmless from any liability, costs, + damages, fees and expenses, including claims by third parties, + in relation to such use. + + +7 Patents + + 7.1 Subject to the terms and conditions of this Licence, each + Licensor hereby grants to You a perpetual, worldwide, + non-exclusive, no-charge, royalty-free, irrevocable (except as + stated in subsections 7.2 and 8.4) patent license to Make, have + Made, use, offer to sell, sell, import, and otherwise transfer + the Covered Source and Products, where such licence applies only + to those patent claims licensable by such Licensor that are + necessarily infringed by exercising rights under the Covered + Source as Conveyed by that Licensor. + + 7.2 If You institute patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that the + Covered Source or a Product constitutes direct or contributory + patent infringement, or You seek any declaration that a patent + licensed to You under this Licence is invalid or unenforceable + then any rights granted to You under this Licence shall + terminate as of the date such process is initiated. + + +8 General + + 8.1 If any provisions of this Licence are or subsequently become + invalid or unenforceable for any reason, the remaining + provisions shall remain effective. + + 8.2 You shall not use any of the name (including acronyms and + abbreviations), image, or logo by which the Licensor or CERN is + known, except where needed to comply with section 3, or where + the use is otherwise allowed by law. Any such permitted use + shall be factual and shall not be made so as to suggest any kind + of endorsement or implication of involvement by the Licensor or + its personnel. + + 8.3 CERN may publish updated versions and variants of this Licence + which it considers to be in the spirit of this version, but may + differ in detail to address new problems or concerns. New + versions will be published with a unique version number and a + variant identifier specifying the variant. If the Licensor has + specified that a given variant applies to the Covered Source + without specifying a version, You may treat that Covered Source + as being released under any version of the CERN-OHL with that + variant. If no variant is specified, the Covered Source shall be + treated as being released under CERN-OHL-S. The Licensor may + also specify that the Covered Source is subject to a specific + version of the CERN-OHL or any later version in which case You + may apply this or any later version of CERN-OHL with the same + variant identifier published by CERN. + + You may treat Covered Source licensed under CERN-OHL-W as + licensed under CERN-OHL-S if and only if all Available + Components referenced in the Covered Source comply with the + corresponding definition of Available Component for CERN-OHL-S. + + 8.4 This Licence shall terminate with immediate effect if You fail + to comply with any of its terms and conditions. + + 8.5 However, if You cease all breaches of this Licence, then Your + Licence from any Licensor is reinstated unless such Licensor has + terminated this Licence by giving You, while You remain in + breach, a notice specifying the breach and requiring You to cure + it within 30 days, and You have failed to come into compliance + in all material respects by the end of the 30 day period. Should + You repeat the breach after receipt of a cure notice and + subsequent reinstatement, this Licence will terminate + immediately and permanently. Section 6 shall continue to apply + after any termination. + + 8.6 This Licence shall not be enforceable except by a Licensor + acting as such, and third party beneficiary rights are + specifically excluded. diff --git a/README.md b/README.md index 07f5b9c0..5585edf3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ There are the description files of RPO, a product of Roboparty. -![RPO URDF 模型](rpo_urdf.png) +![RPO URDF 模型](rpo_urdf.png) + +## License + +Unless an individual file states otherwise, RoboParty-owned URDF, XML, STL and +related hardware-description materials are licensed under +[CERN-OHL-W-2.0](./LICENSE). The Source Location for Covered Source is +. Third-party material remains +under its original terms and is not relicensed merely because it is present in +this repository. From b6921748cf2f292e87fc89bec29ea902836c135e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:57:57 +0800 Subject: [PATCH 02/12] Clarify appearance model licensing and safety --- LICENSE | 310 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 12 ++- 2 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0f882860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,310 @@ +CERN Open Hardware Licence Version 2 - Weakly Reciprocal + +Preamble + +CERN has developed this licence to promote collaboration among +hardware designers and to provide a legal tool which supports the +freedom to use, study, modify, share and distribute hardware designs +and products based on those designs. Version 2 of the CERN Open +Hardware Licence comes in three variants: CERN-OHL-P (permissive); and +two reciprocal licences: this licence, CERN- OHL-W (weakly reciprocal) +and CERN-OHL-S (strongly reciprocal). + +The CERN-OHL-W is copyright CERN 2020. Anyone is welcome to use it, in +unmodified form only. + +Use of this Licence does not imply any endorsement by CERN of any +Licensor or their designs nor does it imply any involvement by CERN in +their development. + + +1 Definitions + + 1.1 'Licence' means this CERN-OHL-W. + + 1.2 'Compatible Licence' means + + a) any earlier version of the CERN Open Hardware licence, or + + b) any version of the CERN-OHL-S or the CERN-OHL-W, or + + c) any licence which permits You to treat the Source to which + it applies as licensed under CERN-OHL-S or CERN-OHL-W + provided that on Conveyance of any such Source, or any + associated Product You treat the Source in question as being + licensed under CERN-OHL-S or CERN-OHL-W as appropriate. + + 1.3 'Source' means information such as design materials or digital + code which can be applied to Make or test a Product or to + prepare a Product for use, Conveyance or sale, regardless of its + medium or how it is expressed. It may include Notices. + + 1.4 'Covered Source' means Source that is explicitly made available + under this Licence. + + 1.5 'Product' means any device, component, work or physical object, + whether in finished or intermediate form, arising from the use, + application or processing of Covered Source. + + 1.6 'Make' means to create or configure something, whether by + manufacture, assembly, compiling, loading or applying Covered + Source or another Product or otherwise. + + 1.7 'Available Component' means any part, sub-assembly, library or + code which: + + a) is licensed to You as Complete Source under a Compatible + Licence; or + + b) is available, at the time a Product or the Source containing + it is first Conveyed, to You and any other prospective + licensees + + i) with sufficient rights and information (including any + configuration and programming files and information + about its characteristics and interfaces) to enable it + either to be Made itself, or to be sourced and used to + Make the Product; or + ii) as part of the normal distribution of a tool used to + design or Make the Product. + + 1.8 'External Material' means anything (including Source) which: + + a) is only combined with Covered Source in such a way that it + interfaces with the Covered Source using a documented + interface which is described in the Covered Source; and + + b) is not a derivative of or contains Covered Source, or, if it + is, it is solely to the extent necessary to facilitate such + interfacing. + + 1.9 'Complete Source' means the set of all Source necessary to Make + a Product, in the preferred form for making modifications, + including necessary installation and interfacing information + both for the Product, and for any included Available Components. + If the format is proprietary, it must also be made available in + a format (if the proprietary tool can create it) which is + viewable with a tool available to potential licensees and + licensed under a licence approved by the Free Software + Foundation or the Open Source Initiative. Complete Source need + not include the Source of any Available Component, provided that + You include in the Complete Source sufficient information to + enable a recipient to Make or source and use the Available + Component to Make the Product. + + 1.10 'Source Location' means a location where a Licensor has placed + Covered Source, and which that Licensor reasonably believes will + remain easily accessible for at least three years for anyone to + obtain a digital copy. + + 1.11 'Notice' means copyright, acknowledgement and trademark notices, + Source Location references, modification notices (subsection + 3.3(b)) and all notices that refer to this Licence and to the + disclaimer of warranties that are included in the Covered + Source. + + 1.12 'Licensee' or 'You' means any person exercising rights under + this Licence. + + 1.13 'Licensor' means a natural or legal person who creates or + modifies Covered Source. A person may be a Licensee and a + Licensor at the same time. + + 1.14 'Convey' means to communicate to the public or distribute. + + +2 Applicability + + 2.1 This Licence governs the use, copying, modification, Conveying + of Covered Source and Products, and the Making of Products. By + exercising any right granted under this Licence, You irrevocably + accept these terms and conditions. + + 2.2 This Licence is granted by the Licensor directly to You, and + shall apply worldwide and without limitation in time. + + 2.3 You shall not attempt to restrict by contract or otherwise the + rights granted under this Licence to other Licensees. + + 2.4 This Licence is not intended to restrict fair use, fair dealing, + or any other similar right. + + +3 Copying, modifying and Conveying Covered Source + + 3.1 You may copy and Convey verbatim copies of Covered Source, in + any medium, provided You retain all Notices. + + 3.2 You may modify Covered Source, other than Notices, provided that + You irrevocably undertake to make that modified Covered Source + available from a Source Location should You Convey a Product in + circumstances where the recipient does not otherwise receive a + copy of the modified Covered Source. In each case subsection 3.3 + shall apply. + + You may only delete Notices if they are no longer applicable to + the corresponding Covered Source as modified by You and You may + add additional Notices applicable to Your modifications. + + 3.3 You may Convey modified Covered Source (with the effect that You + shall also become a Licensor) provided that You: + + a) retain Notices as required in subsection 3.2; + + b) add a Notice to the modified Covered Source stating that You + have modified it, with the date and brief description of how + You have modified it; + + c) add a Source Location Notice for the modified Covered Source + if You Convey in circumstances where the recipient does not + otherwise receive a copy of the modified Covered Source; and + + d) license the modified Covered Source under the terms and + conditions of this Licence (or, as set out in subsection + 8.3, a later version, if permitted by the licence of the + original Covered Source). Such modified Covered Source must + be licensed as a whole, but excluding Available Components + contained in it or External Material to which it is + interfaced, which remain licensed under their own applicable + licences. + + +4 Making and Conveying Products + + 4.1 You may Make Products, and/or Convey them, provided that You + either provide each recipient with a copy of the Complete Source + or ensure that each recipient is notified of the Source Location + of the Complete Source. That Complete Source includes Covered + Source and You must accordingly satisfy Your obligations set out + in subsection 3.3. If specified in a Notice, the Product must + visibly and securely display the Source Location on it or its + packaging or documentation in the manner specified in that + Notice. + + 4.2 Where You Convey a Product which incorporates External Material, + the Complete Source for that Product which You are required to + provide under subsection 4.1 need not include any Source for the + External Material. + + 4.3 You may license Products under terms of Your choice, provided + that such terms do not restrict or attempt to restrict any + recipients' rights under this Licence to the Covered Source. + + +5 Research and Development + +You may Convey Covered Source, modified Covered Source or Products to +a legal entity carrying out development, testing or quality assurance +work on Your behalf provided that the work is performed on terms which +prevent the entity from both using the Source or Products for its own +internal purposes and Conveying the Source or Products or any +modifications to them to any person other than You. Any modifications +made by the entity shall be deemed to be made by You pursuant to +subsection 3.2. + + +6 DISCLAIMER AND LIABILITY + + 6.1 DISCLAIMER OF WARRANTY -- The Covered Source and any Products + are provided 'as is' and any express or implied warranties, + including, but not limited to, implied warranties of + merchantability, of satisfactory quality, non-infringement of + third party rights, and fitness for a particular purpose or use + are disclaimed in respect of any Source or Product to the + maximum extent permitted by law. The Licensor makes no + representation that any Source or Product does not or will not + infringe any patent, copyright, trade secret or other + proprietary right. The entire risk as to the use, quality, and + performance of any Source or Product shall be with You and not + the Licensor. This disclaimer of warranty is an essential part + of this Licence and a condition for the grant of any rights + granted under this Licence. + + 6.2 EXCLUSION AND LIMITATION OF LIABILITY -- The Licensor shall, to + the maximum extent permitted by law, have no liability for + direct, indirect, special, incidental, consequential, exemplary, + punitive or other damages of any character including, without + limitation, procurement of substitute goods or services, loss of + use, data or profits, or business interruption, however caused + and on any theory of contract, warranty, tort (including + negligence), product liability or otherwise, arising in any way + in relation to the Covered Source, modified Covered Source + and/or the Making or Conveyance of a Product, even if advised of + the possibility of such damages, and You shall hold the + Licensor(s) free and harmless from any liability, costs, + damages, fees and expenses, including claims by third parties, + in relation to such use. + + +7 Patents + + 7.1 Subject to the terms and conditions of this Licence, each + Licensor hereby grants to You a perpetual, worldwide, + non-exclusive, no-charge, royalty-free, irrevocable (except as + stated in subsections 7.2 and 8.4) patent license to Make, have + Made, use, offer to sell, sell, import, and otherwise transfer + the Covered Source and Products, where such licence applies only + to those patent claims licensable by such Licensor that are + necessarily infringed by exercising rights under the Covered + Source as Conveyed by that Licensor. + + 7.2 If You institute patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that the + Covered Source or a Product constitutes direct or contributory + patent infringement, or You seek any declaration that a patent + licensed to You under this Licence is invalid or unenforceable + then any rights granted to You under this Licence shall + terminate as of the date such process is initiated. + + +8 General + + 8.1 If any provisions of this Licence are or subsequently become + invalid or unenforceable for any reason, the remaining + provisions shall remain effective. + + 8.2 You shall not use any of the name (including acronyms and + abbreviations), image, or logo by which the Licensor or CERN is + known, except where needed to comply with section 3, or where + the use is otherwise allowed by law. Any such permitted use + shall be factual and shall not be made so as to suggest any kind + of endorsement or implication of involvement by the Licensor or + its personnel. + + 8.3 CERN may publish updated versions and variants of this Licence + which it considers to be in the spirit of this version, but may + differ in detail to address new problems or concerns. New + versions will be published with a unique version number and a + variant identifier specifying the variant. If the Licensor has + specified that a given variant applies to the Covered Source + without specifying a version, You may treat that Covered Source + as being released under any version of the CERN-OHL with that + variant. If no variant is specified, the Covered Source shall be + treated as being released under CERN-OHL-S. The Licensor may + also specify that the Covered Source is subject to a specific + version of the CERN-OHL or any later version in which case You + may apply this or any later version of CERN-OHL with the same + variant identifier published by CERN. + + You may treat Covered Source licensed under CERN-OHL-W as + licensed under CERN-OHL-S if and only if all Available + Components referenced in the Covered Source comply with the + corresponding definition of Available Component for CERN-OHL-S. + + 8.4 This Licence shall terminate with immediate effect if You fail + to comply with any of its terms and conditions. + + 8.5 However, if You cease all breaches of this Licence, then Your + Licence from any Licensor is reinstated unless such Licensor has + terminated this Licence by giving You, while You remain in + breach, a notice specifying the breach and requiring You to cure + it within 30 days, and You have failed to come into compliance + in all material respects by the end of the 30 day period. Should + You repeat the breach after receipt of a cure notice and + subsequent reinstatement, this Licence will terminate + immediately and permanently. Section 6 shall continue to apply + after any termination. + + 8.6 This Licence shall not be enforceable except by a Licensor + acting as such, and third party beneficiary rights are + specifically excluded. diff --git a/README.md b/README.md index 4cccfaa2..ea320e77 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,13 @@ RPO 是我们这一设计理念的首次实体化呈现。作为一台孕育时 - 接缝控制:部分覆盖件之间的段差与缝隙均匀度需进一步优化。 - 走线问题:部分件存在走线问题,建议目前仅用于静态展示。 -## 免责声明 +## 许可证与安全说明 -1. 模型仅供静态展示使用。如需动态演示,请勿打印。 -2. 本仓库发布的设计文件仅供学习、研究与原型验证使用。 +除文件中另有说明外,RoboParty 自有的 STL 设计资料按 +[CERN-OHL-W-2.0](./LICENSE) 授权。Covered Source 的 Source Location 为 +。第三方资料遵循其原始条款, +不因存放在本仓库而被重新许可。 + +“建议仅用于静态展示、不建议用于动态演示”属于安全建议,不缩减许可证 +授予的权利。模型未经安全认证;制造、装配和使用前应自行完成安全评估, +相关风险由使用者承担。 From f06e69f9d6f983cd89168ded3b524f70b94ed79e Mon Sep 17 00:00:00 2001 From: Cloud <895682100@qq.com> Date: Wed, 12 Aug 2026 23:02:41 +0800 Subject: [PATCH 03/12] Correct camera model license status --- LICENSES.md | 74 ++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/LICENSES.md b/LICENSES.md index 10f38692..3baf7eae 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -1,37 +1,37 @@ -# License Scope and Inventory / 许可证范围与清单 - -This file records the license boundaries of `roboparty_deploy`. It does not replace the complete license text in this repository or its dependencies. - -本文记录 `roboparty_deploy` 的许可证边界,不替代本仓库及各依赖中的完整许可证文本。 - -## Repository License / 仓库许可证 - -Unless otherwise stated, original content authored by RoboParty in this repository is licensed under the GNU General Public License v3.0 only (`GPL-3.0-only`). See [LICENSE](LICENSE). - -除另有说明外,本仓库中由 RoboParty 创作的原创内容采用 GNU General Public License v3.0 only(`GPL-3.0-only`),完整条款见 [LICENSE](LICENSE)。 - -The root license does not relicense Git submodules, packaged binaries, or third-party material. Their copyright notices and license terms must be retained. - -根许可证不会对 Git 子模块、已打包二进制文件或第三方资料重新授权;必须保留其版权声明和许可证条款。 - -## Git Submodules / Git 子模块 - -| Path | Source | Detected license | Notes | -| --- | --- | --- | --- | -| `src/inference` | [Roboparty/roboparty_inference](https://github.com/Roboparty/roboparty_inference) | GPL-3.0 | Separate source repository and license | -| `src/motors` | [Roboparty/roboparty_motors](https://github.com/Roboparty/roboparty_motors) | GPL-3.0 | Separate source repository and license | -| `src/imu` | [Roboparty/roboparty_imu](https://github.com/Roboparty/roboparty_imu) | GPL-3.0 | Separate source repository and license | -| `tools/create_ap` | [Roboparty/create_ap](https://github.com/Roboparty/create_ap) | BSD-2-Clause | Preserve the upstream copyright and BSD license | -| `src/camera` | [Roboparty/roboparty_camera](https://github.com/Roboparty/roboparty_camera) | Not declared at repository root | Confirm and add a license in the source repository before treating it as GPLv3-covered | - -## Other Distributed Material / 其他分发内容 - -Files under `assets/`, including Linux kernel packages and configuration material, may originate from third parties or generated distributions. They are not relicensed by this repository-level GPLv3 declaration. Confirm their source, license, notices, and redistribution rights before publishing a release archive or image. - -`assets/` 下的 Linux 内核软件包、配置资料等文件可能来自第三方或生成发行物,不会因仓库级 GPLv3 声明而被重新授权。发布压缩包或镜像前,应确认其来源、许可证、通知及再分发权利。 - -## Contributions / 贡献 - -Contributions to RoboParty-authored files are accepted under `GPL-3.0-only` unless the target file or component states another license. Contributions to a Git submodule must follow that source repository's contribution and license terms. - -对 RoboParty 原创文件的贡献默认采用 `GPL-3.0-only`,目标文件或组件另有说明的除外。对子模块的贡献应遵循对应源仓库的贡献规则和许可证。 \ No newline at end of file +# License Scope and Inventory / 许可证范围与清单 + +This file records the license boundaries of `roboparty_deploy`. It does not replace the complete license text in this repository or its dependencies. + +本文记录 `roboparty_deploy` 的许可证边界,不替代本仓库及各依赖中的完整许可证文本。 + +## Repository License / 仓库许可证 + +Unless otherwise stated, original content authored by RoboParty in this repository is licensed under the GNU General Public License v3.0 only (`GPL-3.0-only`). See [LICENSE](LICENSE). + +除另有说明外,本仓库中由 RoboParty 创作的原创内容采用 GNU General Public License v3.0 only(`GPL-3.0-only`),完整条款见 [LICENSE](LICENSE)。 + +The root license does not relicense Git submodules, packaged binaries, or third-party material. Their copyright notices and license terms must be retained. + +根许可证不会对 Git 子模块、已打包二进制文件或第三方资料重新授权;必须保留其版权声明和许可证条款。 + +## Git Submodules / Git 子模块 + +| Path | Source | Detected license | Notes | +| --- | --- | --- | --- | +| `src/inference` | [Roboparty/roboparty_inference](https://github.com/Roboparty/roboparty_inference) | GPL-3.0 | Separate source repository and license | +| `src/motors` | [Roboparty/roboparty_motors](https://github.com/Roboparty/roboparty_motors) | GPL-3.0 | Separate source repository and license | +| `src/imu` | [Roboparty/roboparty_imu](https://github.com/Roboparty/roboparty_imu) | GPL-3.0 | Separate source repository and license | +| `tools/create_ap` | [Roboparty/create_ap](https://github.com/Roboparty/create_ap) | BSD-2-Clause | Preserve the upstream copyright and BSD license | +| `src/camera` | [Roboparty/roboparty_camera](https://github.com/Roboparty/roboparty_camera) | GPL-3.0-only | Separate source repository and license | + +## Other Distributed Material / 其他分发内容 + +Files under `assets/`, including Linux kernel packages and configuration material, may originate from third parties or generated distributions. They are not relicensed by this repository-level GPLv3 declaration. Confirm their source, license, notices, and redistribution rights before publishing a release archive or image. + +`assets/` 下的 Linux 内核软件包、配置资料等文件可能来自第三方或生成发行物,不会因仓库级 GPLv3 声明而被重新授权。发布压缩包或镜像前,应确认其来源、许可证、通知及再分发权利。 + +## Contributions / 贡献 + +Contributions to RoboParty-authored files are accepted under `GPL-3.0-only` unless the target file or component states another license. Contributions to a Git submodule must follow that source repository's contribution and license terms. + +对 RoboParty 原创文件的贡献默认采用 `GPL-3.0-only`,目标文件或组件另有说明的除外。对子模块的贡献应遵循对应源仓库的贡献规则和许可证。 From e3d1f0b69770196850f033d031066b080ec8f114 Mon Sep 17 00:00:00 2001 From: Cloud <895682100@qq.com> Date: Wed, 12 Aug 2026 23:02:54 +0800 Subject: [PATCH 04/12] Clarify GPLv3 and upstream Apache-2.0 licensing --- LICENSE | 704 ++++++++++++++++++++++++- LICENSES.md | 14 + LICENSES/Apache-2.0.txt | 202 +++++++ README.md | 525 +++++++++--------- THIRD_PARTY_NOTICES.md | 40 ++ teleop/utils/weighted_moving_filter.py | 195 +++---- 6 files changed, 1293 insertions(+), 387 deletions(-) mode change 100755 => 100644 LICENSE create mode 100644 LICENSES.md create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 THIRD_PARTY_NOTICES.md mode change 100755 => 100644 teleop/utils/weighted_moving_filter.py diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 index 1c019997..acc1cd34 --- a/LICENSE +++ b/LICENSE @@ -1,29 +1,675 @@ -Copyright [2025] [HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics")] - -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. - ------------------- - -This code builds upon following open-source code-bases. Please visit the URLs to see the respective LICENSES: - -1) https://github.com/OpenTeleVision/TeleVision -2) https://github.com/dexsuite/dex-retargeting -3) https://github.com/vuer-ai/vuer -4) https://github.com/stack-of-tasks/pinocchio -5) https://github.com/casadi/casadi -6) https://github.com/meshcat-dev/meshcat-python -7) https://github.com/zeromq/pyzmq -8) https://github.com/Dingry/BunnyVisionPro -9) https://github.com/unitreerobotics/unitree_sdk2_python - ------------------- +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. +Our General Public Licenses are designed to make sure that you have the +freedom to distribute copies of free software (and charge for them if +you wish), that you receive source code or can get it if you want it, +that you can change the software or use pieces of it in new free programs, +and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. + +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". + +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. + +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/LICENSES.md b/LICENSES.md new file mode 100644 index 00000000..1450917d --- /dev/null +++ b/LICENSES.md @@ -0,0 +1,14 @@ +# License scope + +This repository is distributed under `GPL-3.0-only`. See [`LICENSE`](LICENSE). + +- Source code, documentation, `assets/Atom01_urdf/**`, `image.png`, and + `image-1.png` owned by RoboParty are licensed under `GPL-3.0-only`, unless a + file carries a more specific notice. +- Parts of the code are derived from Unitree Robotics' `xr_teleoperate` + project. Unitree's copyright and Apache License 2.0 notice are retained in + [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) and + [`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt). RoboParty's + modifications and the combined work as distributed here are licensed under + `GPL-3.0-only`. +- Dependencies installed separately retain their own licenses. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 00000000..75b52484 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,202 @@ + + 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 authorized by + the copyright owner that is 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 + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (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 reproduce, prepare Derivative Works of, + publicly display, publicly 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, patent, trademark, and + attribution 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 statement 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 Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, 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 additional liability. + + 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/README.md b/README.md index 6cc6871a..ed7d8982 100644 --- a/README.md +++ b/README.md @@ -1,268 +1,271 @@ -# Roboparty XR Teleop - -基于 [Unitree xr_teleoperate](https://github.com/unitreerobotics/xr_teleoperate) 修改,面向 RPO/Roboto 机器人与 PICO VR 的双臂遥操作。 - -## 环境要求 - -- Ubuntu 20.04 / 22.04 -- Python 3.10 -- ROS2 Python 环境 -- PICO VR 设备 -- RPO/Roboto 机器人控制链路 - -## 安装 - -### 1. 创建环境 - -推荐先创建独立环境: - -```bash -conda create -n roboparty_xr python=3.10 -y -conda activate roboparty_xr -``` - -### 2. 安装 IK 依赖 - -`pinocchio` 和 `casadi` 建议通过 `conda-forge` 安装: - -```bash -conda install -c conda-forge pinocchio casadi numpy=1.26.4 -y -``` - -### 3. 安装仓库依赖 - -在仓库根目录执行: - -```bash -pip install -r requirements.txt -``` - -### 4. 准备 ROS2 - -运行本项目前,请确认当前 Python 环境与 ROS2 使用的 Python 小版本一致。 - -本项目当前推荐/测试组合为: - -- Python `3.10` -- 与 Python `3.10` 匹配的 ROS2 Python 环境 - -运行前请确认当前终端已经 `source` 过对应的 ROS2 环境,并且可以正常导入: - -- `rclpy` -- `sensor_msgs` - -可以直接用下面命令自检: - -```bash -python -c "import rclpy; from sensor_msgs.msg import JointState; print('ROS2 Python OK')" -``` - -注意: - -- 如果 ROS2 是基于 Python `3.12` 安装或构建的,而当前项目运行在 Python `3.10` 环境中,通常无法正常导入 `rclpy` -- 反过来也是一样,Python `3.12` 环境通常也不能直接使用基于 Python `3.10` 的 ROS2 Python 包 -- 不要混用不同 Python 小版本的 ROS2 和本项目运行环境 - -## 证书配置 - -PICO 通过浏览器访问遥操作页面时,需要 HTTPS 证书。 - -在仓库根目录执行: - -```bash -openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem -``` - -如果主机开了防火墙,请放行 `8012` 端口: - -```bash -sudo ufw allow 8012 -``` - -## 机器人侧平衡 Policy - -本项目负责 `VR -> 上半身遥操作` 控制链路。真机运行时,人形机器人本体仍需要底层 `policy` 负责站立与平衡控制,因此通常需要配合 [`roboparty_deploy`](https://github.com/Roboparty/roboparty_deploy) 一起使用。 - -如果需要切换机器人底层策略模型,请先修改 `roboparty_deploy` 中的 `src/inference/launch/inference.launch.py`,把加载的配置文件改成目标 policy: - -```python -configs = [ - os.path.join( - get_package_share_directory("inference"), - "config", - "inference_interrupt.yaml", - ), -] -``` - -常见可选配置文件包括: - -- `inference_amp.yaml` -- `inference_attn_enc.yaml` -- `inference_beyondmimic.yaml` -- `inference_getup.yaml` -- `inference_interrupt.yaml` - -本项目当前建议使用: - -- `inference_interrupt.yaml` - -修改完成后,重新运行 `./tools/start_robot.sh`,机器人启动时就会加载对应配置,并使用相应的底层 policy。 - -详细说明请参考 [`roboparty_deploy` 的 README_CN](https://github.com/Roboparty/roboparty_deploy/blob/main/README_CN.md)。 - -机器人侧遥控器按键操作也请参考该文档。结合本项目,主要会用到下面几个按键: - -- `X`:使能 / 失能电机 -- `A`:复位 -- `B`:进入推理模式。进入后机器人可以保持平衡并正常行走,但此时上半身控制接口还没有暴露给 VR 遥操作 -- `LB`:将上半身控制接口暴露给 VR 遥操作 - -推荐的真机联调流程如下: - -1. 在机器人侧启动 `roboparty_deploy`,并确认已经加载 `inference_interrupt.yaml` -2. 使用机器人遥控器按 `X` 使能电机,按 `A` 让机器人复位 -3. 按遥控器 `B` 进入推理模式,确认机器人已经能够稳定站立、保持平衡并正常行走 -4. 在本仓库中启动 VR 遥操作程序,并按照下文流程完成 PICO 连接,使仿真机器人开始跟随操作者动作 -5. 先不要立即把真机暴露给 VR,先观察仿真机器人与真机机器人的上半身姿态,尽量让两者接近 -6. 当仿真机器人和真机机器人姿态已经基本对齐后,按机器人遥控器的 `LB`,将上半身控制接口暴露给 VR -7. 最后在 VR 遥操作端继续执行到按下 VR controller 的 `A`,开始向真机发送上半身控制命令 - -也就是说,真机真正进入上半身遥操作,需要同时满足两边条件: - -- 机器人侧已经按下遥控器 `LB`,允许 VR 接管上半身接口 -- 遥操作侧已经按下 VR controller 的 `A`,开始发送控制命令 - -## 启动 - -在仓库根目录执行: - -```bash -python teleop/xr_control_rpo.py --xr-mode controller -``` - -常用参数: - -- `--xr-mode {hand,controller}`:选择 XR 跟踪模式,默认 `controller` -- `--frequency`:控制频率,默认 `30` -- `--headless`:关闭 IK 可视化。真机联调时建议开启,以减少 Meshcat 带来的额外开销 -- `--profile-loop`:打印控制环耗时统计,便于分析 `XR -> IK -> ROS` 各环节耗时 - -如果你想在真机前先做性能检查,推荐先使用: - -```bash -python teleop/xr_control_rpo.py --xr-mode controller --headless --profile-loop -``` - -终端会打印类似下面的统计信息: - -```text -Loop timing avg over N iters: motion=... ms, ik=... ms, send=... ms, loop=... ms, rate=... Hz, ipopt_iter_avg=..., ipopt_iter_max=..., ipopt_hit_max=.../N, ipopt_last_status=... -``` - -字段含义: - -- `motion`:XR 数据读取与坐标转换耗时 -- `ik`:`arm_ik.solve_ik()` 耗时 -- `send`:ROS 关节命令发布耗时 -- `loop`:整轮控制循环耗时 -- `rate`:实际控制频率 -- `ipopt_iter_*`:单次 IK 求解的 Ipopt 迭代统计 - -## PICO 使用流程 - -以下流程默认: - -- PICO 与运行程序的主机在同一局域网 -- 主机 IP 为 `192.168.123.2` -- 使用 `controller` 模式 - -如果你的主机 IP 不同,请把下面 URL 中的 `192.168.123.2` 替换成实际地址。 - -### 1. 启动控制程序 - -```bash -python teleop/xr_control_rpo.py --xr-mode controller -``` - -程序启动后会等待开始信号,同时浏览器会弹出 Meshcat 仿真界面。 - -

- Meshcat 仿真界面 -

- -### 2. 在 PICO 浏览器打开页面 - -在 PICO 浏览器访问: - -```text -https://192.168.123.2:8012/?ws=wss://192.168.123.2:8012 -``` - -如果浏览器提示证书不安全,点击继续访问即可。 - -### 3. 进入 VR - -进入页面后: - -1. 点击 `Pass-Through` -2. 接受浏览器和设备弹出的权限请求 -3. 等待 XR 会话建立完成 - -终端出现连接日志后,说明 PICO 已接入成功。 - -### 4. 开始遥操作 - -推荐真机使用下面这条命令启动: - -```bash -python teleop/xr_control_rpo.py --xr-mode controller --headless -``` - -终端键位: - -- `r`:开始主循环。按下后程序开始接收 VR 传输的数据,Meshcat 中的机器人会跟随操作者动作,但此时真机不会运动。 -- `a`:允许发送机械臂命令。按下后,真机开始接收电脑发送的关节位置。 -- `q`:退出程序。 - -建议在按下 `a` 之前,先让 Meshcat 中的机器人姿态尽量接近真机复位姿态。 - -

- 开始发送命令前的姿态对齐示意 -

- -VR 控制器键位: - -- 右手 `A`:开始发送命令。效果与键盘 `a` 一致。 -- 右手 `B`:停止发送命令。停止后仍可再次按 `A` 恢复。 - -建议先让操作者手臂接近机器人真机姿态,再开始发送控制命令。 - -## 运行建议 - -- 首次使用时,先在安全空间内做小范围动作测试 -- 开始发送命令前,确认机器人周围无人靠近 -- 退出前,建议先让机械臂回到较自然的位置 - -## 目录结构 - -```text -roboparty_xr_teleop/ -├── assets/ -│ └── Atom01_urdf/ # compatibility asset path -├── teleop/ -│ ├── robot_control/ -│ │ └── robot_arm_ik.py -│ ├── utils/ -│ │ └── weighted_moving_filter.py -│ ├── xr_control_rpo.py -│ └── xr_Control_Atom.py # compatibility entrypoint +# Roboparty XR Teleop + +基于 [Unitree xr_teleoperate](https://github.com/unitreerobotics/xr_teleoperate) 修改,面向 RPO/Roboto 机器人与 PICO VR 的双臂遥操作。 + +## 环境要求 + +- Ubuntu 20.04 / 22.04 +- Python 3.10 +- ROS2 Python 环境 +- PICO VR 设备 +- RPO/Roboto 机器人控制链路 + +## 安装 + +### 1. 创建环境 + +推荐先创建独立环境: + +```bash +conda create -n roboparty_xr python=3.10 -y +conda activate roboparty_xr +``` + +### 2. 安装 IK 依赖 + +`pinocchio` 和 `casadi` 建议通过 `conda-forge` 安装: + +```bash +conda install -c conda-forge pinocchio casadi numpy=1.26.4 -y +``` + +### 3. 安装仓库依赖 + +在仓库根目录执行: + +```bash +pip install -r requirements.txt +``` + +### 4. 准备 ROS2 + +运行本项目前,请确认当前 Python 环境与 ROS2 使用的 Python 小版本一致。 + +本项目当前推荐/测试组合为: + +- Python `3.10` +- 与 Python `3.10` 匹配的 ROS2 Python 环境 + +运行前请确认当前终端已经 `source` 过对应的 ROS2 环境,并且可以正常导入: + +- `rclpy` +- `sensor_msgs` + +可以直接用下面命令自检: + +```bash +python -c "import rclpy; from sensor_msgs.msg import JointState; print('ROS2 Python OK')" +``` + +注意: + +- 如果 ROS2 是基于 Python `3.12` 安装或构建的,而当前项目运行在 Python `3.10` 环境中,通常无法正常导入 `rclpy` +- 反过来也是一样,Python `3.12` 环境通常也不能直接使用基于 Python `3.10` 的 ROS2 Python 包 +- 不要混用不同 Python 小版本的 ROS2 和本项目运行环境 + +## 证书配置 + +PICO 通过浏览器访问遥操作页面时,需要 HTTPS 证书。 + +在仓库根目录执行: + +```bash +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem +``` + +如果主机开了防火墙,请放行 `8012` 端口: + +```bash +sudo ufw allow 8012 +``` + +## 机器人侧平衡 Policy + +本项目负责 `VR -> 上半身遥操作` 控制链路。真机运行时,人形机器人本体仍需要底层 `policy` 负责站立与平衡控制,因此通常需要配合 [`roboparty_deploy`](https://github.com/Roboparty/roboparty_deploy) 一起使用。 + +如果需要切换机器人底层策略模型,请先修改 `roboparty_deploy` 中的 `src/inference/launch/inference.launch.py`,把加载的配置文件改成目标 policy: + +```python +configs = [ + os.path.join( + get_package_share_directory("inference"), + "config", + "inference_interrupt.yaml", + ), +] +``` + +常见可选配置文件包括: + +- `inference_amp.yaml` +- `inference_attn_enc.yaml` +- `inference_beyondmimic.yaml` +- `inference_getup.yaml` +- `inference_interrupt.yaml` + +本项目当前建议使用: + +- `inference_interrupt.yaml` + +修改完成后,重新运行 `./tools/start_robot.sh`,机器人启动时就会加载对应配置,并使用相应的底层 policy。 + +详细说明请参考 [`roboparty_deploy` 的 README_CN](https://github.com/Roboparty/roboparty_deploy/blob/main/README_CN.md)。 + +机器人侧遥控器按键操作也请参考该文档。结合本项目,主要会用到下面几个按键: + +- `X`:使能 / 失能电机 +- `A`:复位 +- `B`:进入推理模式。进入后机器人可以保持平衡并正常行走,但此时上半身控制接口还没有暴露给 VR 遥操作 +- `LB`:将上半身控制接口暴露给 VR 遥操作 + +推荐的真机联调流程如下: + +1. 在机器人侧启动 `roboparty_deploy`,并确认已经加载 `inference_interrupt.yaml` +2. 使用机器人遥控器按 `X` 使能电机,按 `A` 让机器人复位 +3. 按遥控器 `B` 进入推理模式,确认机器人已经能够稳定站立、保持平衡并正常行走 +4. 在本仓库中启动 VR 遥操作程序,并按照下文流程完成 PICO 连接,使仿真机器人开始跟随操作者动作 +5. 先不要立即把真机暴露给 VR,先观察仿真机器人与真机机器人的上半身姿态,尽量让两者接近 +6. 当仿真机器人和真机机器人姿态已经基本对齐后,按机器人遥控器的 `LB`,将上半身控制接口暴露给 VR +7. 最后在 VR 遥操作端继续执行到按下 VR controller 的 `A`,开始向真机发送上半身控制命令 + +也就是说,真机真正进入上半身遥操作,需要同时满足两边条件: + +- 机器人侧已经按下遥控器 `LB`,允许 VR 接管上半身接口 +- 遥操作侧已经按下 VR controller 的 `A`,开始发送控制命令 + +## 启动 + +在仓库根目录执行: + +```bash +python teleop/xr_control_rpo.py --xr-mode controller +``` + +常用参数: + +- `--xr-mode {hand,controller}`:选择 XR 跟踪模式,默认 `controller` +- `--frequency`:控制频率,默认 `30` +- `--headless`:关闭 IK 可视化。真机联调时建议开启,以减少 Meshcat 带来的额外开销 +- `--profile-loop`:打印控制环耗时统计,便于分析 `XR -> IK -> ROS` 各环节耗时 + +如果你想在真机前先做性能检查,推荐先使用: + +```bash +python teleop/xr_control_rpo.py --xr-mode controller --headless --profile-loop +``` + +终端会打印类似下面的统计信息: + +```text +Loop timing avg over N iters: motion=... ms, ik=... ms, send=... ms, loop=... ms, rate=... Hz, ipopt_iter_avg=..., ipopt_iter_max=..., ipopt_hit_max=.../N, ipopt_last_status=... +``` + +字段含义: + +- `motion`:XR 数据读取与坐标转换耗时 +- `ik`:`arm_ik.solve_ik()` 耗时 +- `send`:ROS 关节命令发布耗时 +- `loop`:整轮控制循环耗时 +- `rate`:实际控制频率 +- `ipopt_iter_*`:单次 IK 求解的 Ipopt 迭代统计 + +## PICO 使用流程 + +以下流程默认: + +- PICO 与运行程序的主机在同一局域网 +- 主机 IP 为 `192.168.123.2` +- 使用 `controller` 模式 + +如果你的主机 IP 不同,请把下面 URL 中的 `192.168.123.2` 替换成实际地址。 + +### 1. 启动控制程序 + +```bash +python teleop/xr_control_rpo.py --xr-mode controller +``` + +程序启动后会等待开始信号,同时浏览器会弹出 Meshcat 仿真界面。 + +

+ Meshcat 仿真界面 +

+ +### 2. 在 PICO 浏览器打开页面 + +在 PICO 浏览器访问: + +```text +https://192.168.123.2:8012/?ws=wss://192.168.123.2:8012 +``` + +如果浏览器提示证书不安全,点击继续访问即可。 + +### 3. 进入 VR + +进入页面后: + +1. 点击 `Pass-Through` +2. 接受浏览器和设备弹出的权限请求 +3. 等待 XR 会话建立完成 + +终端出现连接日志后,说明 PICO 已接入成功。 + +### 4. 开始遥操作 + +推荐真机使用下面这条命令启动: + +```bash +python teleop/xr_control_rpo.py --xr-mode controller --headless +``` + +终端键位: + +- `r`:开始主循环。按下后程序开始接收 VR 传输的数据,Meshcat 中的机器人会跟随操作者动作,但此时真机不会运动。 +- `a`:允许发送机械臂命令。按下后,真机开始接收电脑发送的关节位置。 +- `q`:退出程序。 + +建议在按下 `a` 之前,先让 Meshcat 中的机器人姿态尽量接近真机复位姿态。 + +

+ 开始发送命令前的姿态对齐示意 +

+ +VR 控制器键位: + +- 右手 `A`:开始发送命令。效果与键盘 `a` 一致。 +- 右手 `B`:停止发送命令。停止后仍可再次按 `A` 恢复。 + +建议先让操作者手臂接近机器人真机姿态,再开始发送控制命令。 + +## 运行建议 + +- 首次使用时,先在安全空间内做小范围动作测试 +- 开始发送命令前,确认机器人周围无人靠近 +- 退出前,建议先让机械臂回到较自然的位置 + +## 目录结构 + +```text +roboparty_xr_teleop/ +├── assets/ +│ └── Atom01_urdf/ # compatibility asset path +├── teleop/ +│ ├── robot_control/ +│ │ └── robot_arm_ik.py +│ ├── utils/ +│ │ └── weighted_moving_filter.py +│ ├── xr_control_rpo.py +│ └── xr_Control_Atom.py # compatibility entrypoint ├── televuer/ ├── LICENSE +├── LICENSES/ +├── LICENSES.md +├── THIRD_PARTY_NOTICES.md ├── README.md └── requirements.txt ``` -## 致谢 +## 许可证 -本项目基于 [Unitree xr_teleoperate](https://github.com/unitreerobotics/xr_teleoperate) 修改实现。相关许可信息见 [LICENSE](LICENSE)。 +本仓库按 `GPL-3.0-only` 发布。基于 [Unitree xr_teleoperate](https://github.com/unitreerobotics/xr_teleoperate) 的内容保留原版权和 `Apache-2.0` 声明。Atom01 的 URDF、STL 和文档图片来自 `roboto_origin`,版权属于 RoboParty。许可范围见 [LICENSES.md](LICENSES.md),第三方归属见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..d824b961 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,40 @@ +# Third-Party Notices + +This repository is distributed under `GPL-3.0-only`. It contains software +derived from third-party projects and uses third-party dependencies. Their +copyright and attribution notices remain in force. + +## Unitree xr_teleoperate + +Parts of this repository are based on and modified from Unitree Robotics' +`xr_teleoperate` project: + +- Upstream: https://github.com/unitreerobotics/xr_teleoperate +- Copyright [2025] [HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics")] +- Upstream license: Apache License 2.0; the full text is in + [`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt) + +RoboParty modified the upstream software for RPO/Roboto robot teleoperation, +PICO XR input, ROS 2 integration, and the current asset and launch layout. +RoboParty's modifications and the combined work are distributed under +`GPL-3.0-only`. Unitree's original copyright and Apache License 2.0 notice are +retained. Source files changed from the upstream project carry a modification +notice. + +## Other upstream projects and dependencies + +The upstream project identifies the following projects as code bases or +dependencies on which it builds. Their own license terms continue to apply: + +1. https://github.com/OpenTeleVision/TeleVision +2. https://github.com/dexsuite/dex-retargeting +3. https://github.com/vuer-ai/vuer +4. https://github.com/stack-of-tasks/pinocchio +5. https://github.com/casadi/casadi +6. https://github.com/meshcat-dev/meshcat-python +7. https://github.com/zeromq/pyzmq +8. https://github.com/Dingry/BunnyVisionPro +9. https://github.com/unitreerobotics/unitree_sdk2_python + +CasADi is installed separately as described in `README.md` and is not vendored +in this repository. Its license applies to the installed version. diff --git a/teleop/utils/weighted_moving_filter.py b/teleop/utils/weighted_moving_filter.py old mode 100755 new mode 100644 index 410e2bfd..f3797f37 --- a/teleop/utils/weighted_moving_filter.py +++ b/teleop/utils/weighted_moving_filter.py @@ -1,97 +1,98 @@ -import numpy as np - - -class WeightedMovingFilter: - def __init__(self, weights, data_size = 14): - self._window_size = len(weights) - self._weights = np.array(weights) - assert np.isclose(np.sum(self._weights), 1.0), "[WeightedMovingFilter] the sum of weights list must be 1.0!" - self._data_size = data_size - self._filtered_data = np.zeros(self._data_size) - self._data_queue = [] - - def _apply_filter(self): - if len(self._data_queue) < self._window_size: - return self._data_queue[-1] - - data_array = np.array(self._data_queue) - temp_filtered_data = np.zeros(self._data_size) - for i in range(self._data_size): - temp_filtered_data[i] = np.convolve(data_array[:, i], self._weights, mode='valid')[-1] - - return temp_filtered_data - - def add_data(self, new_data): - assert len(new_data) == self._data_size - - if len(self._data_queue) > 0 and np.array_equal(new_data, self._data_queue[-1]): - return # skip duplicate data - - if len(self._data_queue) >= self._window_size: - self._data_queue.pop(0) - - self._data_queue.append(new_data) - self._filtered_data = self._apply_filter() - - @property - def filtered_data(self): - return self._filtered_data - - -def visualize_filter_comparison(filter_params, steps): - import time - import matplotlib.pyplot as plt - - t = np.linspace(0, 4 * np.pi, steps) - original_data = np.array([np.sin(t + i) + np.random.normal(0, 0.2, len(t)) for i in range(35)]).T # sin wave with noise, shape is [len(t), 35] - - plt.figure(figsize=(14, 10)) - - for idx, weights in enumerate(filter_params): - filter = WeightedMovingFilter(weights, 14) - data_2b_filtered = original_data.copy() - filtered_data = [] - - time1 = time.time() - - for i in range(steps): - filter.add_data(data_2b_filtered[i][13:27]) # step i, columns 13 to 26 (total:14) - data_2b_filtered[i][13:27] = filter.filtered_data - filtered_data.append(data_2b_filtered[i]) - - time2 = time.time() - print(f"filter_params:{filter_params[idx]}, time cosume:{time2 - time1}") - - filtered_data = np.array(filtered_data) - - # col0 should not 2b filtered - plt.subplot(len(filter_params), 2, idx * 2 + 1) - plt.plot(filtered_data[:, 0], label=f'Filtered (Window {filter._window_size})') - plt.plot(original_data[:, 0], 'r--', label='Original', alpha=0.5) - plt.title(f'Joint 1 - Should not to be filtered.') - plt.xlabel('Step') - plt.ylabel('Value') - plt.legend() - - # col13 should 2b filtered - plt.subplot(len(filter_params), 2, idx * 2 + 2) - plt.plot(filtered_data[:, 13], label=f'Filtered (Window {filter._window_size})') - plt.plot(original_data[:, 13], 'r--', label='Original', alpha=0.5) - plt.title(f'Joint 13 - Window {filter._window_size}, Weights {weights}') - plt.xlabel('Step') - plt.ylabel('Value') - plt.legend() - - plt.tight_layout() - plt.show() - - -if __name__ == "__main__": - # windows_size and weights - filter_params = [ - (np.array([0.7, 0.2, 0.1])), - (np.array([0.5, 0.3, 0.2])), - (np.array([0.4, 0.3, 0.2, 0.1])), - ] - - visualize_filter_comparison(filter_params, steps = 100) +# Modified from Unitree xr_teleoperate for RPO robot teleoperation. +import numpy as np + + +class WeightedMovingFilter: + def __init__(self, weights, data_size = 14): + self._window_size = len(weights) + self._weights = np.array(weights) + assert np.isclose(np.sum(self._weights), 1.0), "[WeightedMovingFilter] the sum of weights list must be 1.0!" + self._data_size = data_size + self._filtered_data = np.zeros(self._data_size) + self._data_queue = [] + + def _apply_filter(self): + if len(self._data_queue) < self._window_size: + return self._data_queue[-1] + + data_array = np.array(self._data_queue) + temp_filtered_data = np.zeros(self._data_size) + for i in range(self._data_size): + temp_filtered_data[i] = np.convolve(data_array[:, i], self._weights, mode='valid')[-1] + + return temp_filtered_data + + def add_data(self, new_data): + assert len(new_data) == self._data_size + + if len(self._data_queue) > 0 and np.array_equal(new_data, self._data_queue[-1]): + return # skip duplicate data + + if len(self._data_queue) >= self._window_size: + self._data_queue.pop(0) + + self._data_queue.append(new_data) + self._filtered_data = self._apply_filter() + + @property + def filtered_data(self): + return self._filtered_data + + +def visualize_filter_comparison(filter_params, steps): + import time + import matplotlib.pyplot as plt + + t = np.linspace(0, 4 * np.pi, steps) + original_data = np.array([np.sin(t + i) + np.random.normal(0, 0.2, len(t)) for i in range(35)]).T # sin wave with noise, shape is [len(t), 35] + + plt.figure(figsize=(14, 10)) + + for idx, weights in enumerate(filter_params): + filter = WeightedMovingFilter(weights, 14) + data_2b_filtered = original_data.copy() + filtered_data = [] + + time1 = time.time() + + for i in range(steps): + filter.add_data(data_2b_filtered[i][13:27]) # step i, columns 13 to 26 (total:14) + data_2b_filtered[i][13:27] = filter.filtered_data + filtered_data.append(data_2b_filtered[i]) + + time2 = time.time() + print(f"filter_params:{filter_params[idx]}, time cosume:{time2 - time1}") + + filtered_data = np.array(filtered_data) + + # col0 should not 2b filtered + plt.subplot(len(filter_params), 2, idx * 2 + 1) + plt.plot(filtered_data[:, 0], label=f'Filtered (Window {filter._window_size})') + plt.plot(original_data[:, 0], 'r--', label='Original', alpha=0.5) + plt.title(f'Joint 1 - Should not to be filtered.') + plt.xlabel('Step') + plt.ylabel('Value') + plt.legend() + + # col13 should 2b filtered + plt.subplot(len(filter_params), 2, idx * 2 + 2) + plt.plot(filtered_data[:, 13], label=f'Filtered (Window {filter._window_size})') + plt.plot(original_data[:, 13], 'r--', label='Original', alpha=0.5) + plt.title(f'Joint 13 - Window {filter._window_size}, Weights {weights}') + plt.xlabel('Step') + plt.ylabel('Value') + plt.legend() + + plt.tight_layout() + plt.show() + + +if __name__ == "__main__": + # windows_size and weights + filter_params = [ + (np.array([0.7, 0.2, 0.1])), + (np.array([0.5, 0.3, 0.2])), + (np.array([0.4, 0.3, 0.2, 0.1])), + ] + + visualize_filter_comparison(filter_params, steps = 100) From a7dd94f36ccba3e48b3718eb63ea74ae3c49e8b3 Mon Sep 17 00:00:00 2001 From: Cloud <895682100@qq.com> Date: Wed, 12 Aug 2026 23:03:01 +0800 Subject: [PATCH 05/12] Document CERN-OHL-W-2.0 license --- README.md | 85 +++++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index ea320e77..9c2fd725 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,41 @@ -# RPO 机器人外观件 - -设计驱动工程 · 美学与结构的平衡 · 原型机外观件开源档案 - -> 命名说明:RPO 是 Roboto 产品系列的原型机产品名。该项目早期曾使用 Atom01 作为内部命名,历史文件名、旧链接或旧文档中仍可能出现 Atom01。 - -## 设计理念 - -本仓库收录了 RPO 人形机器人原型机的外观件(外壳)设计资料。 - -在哈尔滨的研发历程中,我们的本体设计团队对市面上普遍存在的“工程优先”或“技术优先”的传统理念提出了大胆质疑。针对外观结构,我们确立了“设计驱动工程”(Design-Driven Engineering)的核心方针: - -- 拒绝妥协的美学:外观不仅仅是覆盖件,而是机器人性格的延伸。我们追求线条的流畅与比例的和谐,力求在工业产品中注入艺术感。 -- 严谨的结构支撑:所有美学造型均建立在严谨的技术评审与合理的内部结构基础之上。 -- 高频协作的产物:这是设计团队与工程团队高频信息交流、无缝对接的成果,实现了从概念草图到工程落地的精准转化。 - -## 关于 RPO 外壳 - -RPO 是我们这一设计理念的首次实体化呈现。作为一台孕育时间短暂但意义重大的原型机,其外壳设计不仅满足了“能跑能跳”的功能需求,更是一次融合艺术与技术的工程实验。 - -虽然它已帮助我们积累了宝贵的制造与装配经验,但也暴露了大量关于材料选型、表面处理、模块化拆装及人机交互细节上的问题。这些“不完美”正是我们迭代下一代产品的核心驱动力。 - -## 文件说明 - -- `V1.0/`:RPO 原型机 V1.0 外观件 STL 文件。 -- 现有 STL 文件名暂不迁移,以避免破坏已发布资料、下载链接和社区引用。 - -## 已知问题与改进方向 - -作为原型机外壳,RPO 存在以下待改进点,欢迎社区贡献思路: - -- 接缝控制:部分覆盖件之间的段差与缝隙均匀度需进一步优化。 -- 走线问题:部分件存在走线问题,建议目前仅用于静态展示。 - -## 许可证与安全说明 - -除文件中另有说明外,RoboParty 自有的 STL 设计资料按 -[CERN-OHL-W-2.0](./LICENSE) 授权。Covered Source 的 Source Location 为 -。第三方资料遵循其原始条款, -不因存放在本仓库而被重新许可。 - -“建议仅用于静态展示、不建议用于动态演示”属于安全建议,不缩减许可证 -授予的权利。模型未经安全认证;制造、装配和使用前应自行完成安全评估, -相关风险由使用者承担。 +# RPO 机器人外观件 + +设计驱动工程 · 美学与结构的平衡 · 原型机外观件开源档案 + +> 命名说明:RPO 是 Roboto 产品系列的原型机产品名。该项目早期曾使用 Atom01 作为内部命名,历史文件名、旧链接或旧文档中仍可能出现 Atom01。 + +## 设计理念 + +本仓库收录了 RPO 人形机器人原型机的外观件(外壳)设计资料。 + +在哈尔滨的研发历程中,我们的本体设计团队对市面上普遍存在的“工程优先”或“技术优先”的传统理念提出了大胆质疑。针对外观结构,我们确立了“设计驱动工程”(Design-Driven Engineering)的核心方针: + +- 拒绝妥协的美学:外观不仅仅是覆盖件,而是机器人性格的延伸。我们追求线条的流畅与比例的和谐,力求在工业产品中注入艺术感。 +- 严谨的结构支撑:所有美学造型均建立在严谨的技术评审与合理的内部结构基础之上。 +- 高频协作的产物:这是设计团队与工程团队高频信息交流、无缝对接的成果,实现了从概念草图到工程落地的精准转化。 + +## 关于 RPO 外壳 + +RPO 是我们这一设计理念的首次实体化呈现。作为一台孕育时间短暂但意义重大的原型机,其外壳设计不仅满足了“能跑能跳”的功能需求,更是一次融合艺术与技术的工程实验。 + +虽然它已帮助我们积累了宝贵的制造与装配经验,但也暴露了大量关于材料选型、表面处理、模块化拆装及人机交互细节上的问题。这些“不完美”正是我们迭代下一代产品的核心驱动力。 + +## 文件说明 + +- `V1.0/`:RPO 原型机 V1.0 外观件 STL 文件。 +- 现有 STL 文件名暂不迁移,以避免破坏已发布资料、下载链接和社区引用。 + +## 已知问题与改进方向 + +作为原型机外壳,RPO 存在以下待改进点,欢迎社区贡献思路: + +- 接缝控制:部分覆盖件之间的段差与缝隙均匀度需进一步优化。 +- 走线问题:部分件存在走线问题,建议目前仅用于静态展示。 + +## 免责声明 + +现有外壳只完成原型验证,建议用于静态展示。用于动态演示前,请自行完成结构和安全评估。 + +## 许可证 + +设计文件的复制、修改和分发适用 [CERN Open Hardware Licence Version 2 - Weakly Reciprocal(CERN-OHL-W-2.0)](LICENSE)。静态展示建议属于安全提示,不限制许可证授予的权利。 From c09ba3a46da94310748af79da896e9bea52cc973 Mon Sep 17 00:00:00 2001 From: Cloud <895682100@qq.com> Date: Wed, 12 Aug 2026 23:03:08 +0800 Subject: [PATCH 06/12] Document CERN-OHL-W-2.0 license --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5585edf3..2e51fb49 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,7 @@ There are the description files of RPO, a product of Roboparty. -![RPO URDF 模型](rpo_urdf.png) - ## License -Unless an individual file states otherwise, RoboParty-owned URDF, XML, STL and -related hardware-description materials are licensed under -[CERN-OHL-W-2.0](./LICENSE). The Source Location for Covered Source is -. Third-party material remains -under its original terms and is not relicensed merely because it is present in -this repository. +Files in this directory are licensed under the [CERN Open Hardware Licence Version 2 - Weakly Reciprocal (CERN-OHL-W-2.0)](LICENSE). + +![RPO URDF 模型](rpo_urdf.png) From 22e42f3398bbc4bf47df864fd09d3aa42a6ee4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:21:50 +0800 Subject: [PATCH 07/12] add automated subtree snapshot workflow --- .github/scripts/report_feishu.py | 155 +++++ .github/scripts/sync_subtrees.py | 577 +++++++++++++++++++ .github/subtree-snapshots.tsv | 24 + .github/workflows/sync-subtree-snapshots.yml | 219 +++++++ 4 files changed, 975 insertions(+) create mode 100644 .github/scripts/report_feishu.py create mode 100644 .github/scripts/sync_subtrees.py create mode 100644 .github/subtree-snapshots.tsv create mode 100644 .github/workflows/sync-subtree-snapshots.yml diff --git a/.github/scripts/report_feishu.py b/.github/scripts/report_feishu.py new file mode 100644 index 00000000..36541d9e --- /dev/null +++ b/.github/scripts/report_feishu.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Append one deterministic snapshot result to a Feishu docx document.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error +import urllib.request +from datetime import datetime +from pathlib import Path +from zoneinfo import ZoneInfo + + +FEISHU_API = "https://open.feishu.cn/open-apis" + + +def request_json(url: str, method: str = "GET", payload: object | None = None, token: str | None = None) -> dict: + headers = {"Content-Type": "application/json; charset=utf-8"} + if token: + headers["Authorization"] = f"Bearer {token}" + body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8") + request = urllib.request.Request(url, data=body, headers=headers, method=method) + try: + with urllib.request.urlopen(request, timeout=30) as response: + result = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace")[:2000] + raise RuntimeError(f"Feishu HTTP {exc.code}: {detail}") from exc + if result.get("code", 0) != 0: + raise RuntimeError(f"Feishu API error {result.get('code')}: {result.get('msg')}") + return result + + +def rich_text(content: str) -> dict: + return { + "elements": [ + { + "text_run": { + "content": content[:5000], + "text_element_style": {}, + } + } + ], + "style": {}, + } + + +def text_block(content: str) -> dict: + return {"block_type": 2, "text": rich_text(content)} + + +def heading_block(content: str) -> dict: + return {"block_type": 4, "heading2": rich_text(content)} + + +def load_report(path: Path) -> dict: + if path.exists(): + return json.loads(path.read_text(encoding="utf-8")) + return { + "status": "failed", + "changed": False, + "updated": [], + "changed_files": 0, + "deleted_files": 0, + "changed_lines": 0, + "base_sha": "unknown", + "final_sha": "unknown", + "error": "GitHub Actions 未生成同步报告。", + } + + +def build_blocks(report: dict) -> list[dict]: + now = datetime.now(ZoneInfo("Asia/Shanghai")) + workflow_result = os.environ.get("WORKFLOW_RESULT", "unknown") + if workflow_result != "success": + report["status"] = "failed" + if not report.get("error"): + report["error"] = f"GitHub 同步任务结果:{workflow_result}" + + status = report.get("status", "failed") + changed = bool(report.get("changed")) + updated = list(report.get("updated") or []) + status_text = "✅ 同步成功" if status == "success" else "❌ 同步失败" + if status == "success" and not changed: + status_text = "ℹ️ 今日无改动" + + run_url = ( + f"{os.environ.get('GITHUB_SERVER_URL', 'https://github.com')}/" + f"{os.environ.get('GITHUB_REPOSITORY', 'Roboparty/roboto_origin')}/actions/runs/" + f"{os.environ.get('GITHUB_RUN_ID', '')}" + ) + pr_url = os.environ.get("PR_URL", "").strip() + base_sha = str(report.get("base_sha", "unknown"))[:12] + final_sha = str(report.get("final_sha", "unknown"))[:12] + + blocks = [ + heading_block(f"{now:%Y-%m-%d} 自动快照报告"), + text_block(f"状态:{status_text}"), + text_block(f"时间:{now:%Y-%m-%d %H:%M:%S}(Asia/Shanghai)"), + text_block( + "统计:" + f"{report.get('changed_files', 0)} 个文件," + f"{report.get('deleted_files', 0)} 个删除," + f"{report.get('changed_lines', 0)} 行增删" + ), + text_block(f"快照提交:{base_sha} → {final_sha}"), + text_block(f"更新仓库:{', '.join(updated) if updated else '无'}"), + text_block(f"GitHub Actions:{run_url}"), + ] + if pr_url: + blocks.append(text_block(f"dev → main PR:{pr_url}")) + if report.get("error"): + blocks.append(text_block(f"错误:{str(report['error'])[:1500]}")) + return blocks + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--report", required=True, type=Path) + args = parser.parse_args() + + app_id = os.environ.get("FEISHU_APP_ID", "") + app_secret = os.environ.get("FEISHU_APP_SECRET", "") + document_id = os.environ.get("FEISHU_DOCUMENT_ID", "") + if not all((app_id, app_secret, document_id)): + print("Feishu document secrets are incomplete", file=sys.stderr) + return 1 + + auth = request_json( + f"{FEISHU_API}/auth/v3/tenant_access_token/internal", + method="POST", + payload={"app_id": app_id, "app_secret": app_secret}, + ) + token = auth.get("tenant_access_token") + if not token: + raise RuntimeError("Feishu authentication returned no tenant_access_token") + + report = load_report(args.report) + blocks = build_blocks(report) + result = request_json( + f"{FEISHU_API}/docx/v1/documents/{document_id}/blocks/{document_id}/children", + method="POST", + payload={"children": blocks, "index": -1}, + token=token, + ) + created = len(result.get("data", {}).get("children", [])) + print(f"Appended {created} report blocks to the Feishu document") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/sync_subtrees.py b/.github/scripts/sync_subtrees.py new file mode 100644 index 00000000..db21c4bb --- /dev/null +++ b/.github/scripts/sync_subtrees.py @@ -0,0 +1,577 @@ +#!/usr/bin/env python3 +"""Synchronize the fixed roboto_origin subtree snapshot allowlist. + +The script deliberately fails closed: new .gitmodules entries, unknown gitlinks, +ordinary merge conflicts, out-of-scope paths, and unusually large deletions stop +the run before the workflow is allowed to push dev. +""" + +from __future__ import annotations + +import argparse +import configparser +import csv +import json +import os +import shutil +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import asdict, dataclass +from datetime import datetime, timezone +from pathlib import Path, PurePosixPath +from typing import Iterable + + +@dataclass(frozen=True) +class Entry: + level: int + name: str + prefix: str + repository: str + branch: str + mode: str + legacy_prefix: str | None + + +@dataclass +class EntryReport: + name: str + path: str + repository: str + branch: str + before: str + upstream: str + result: str = "pending" + + +class SyncError(RuntimeError): + pass + + +class SnapshotSync: + def __init__(self, repository: Path, manifest: Path, report_dir: Path) -> None: + self.repository = repository.resolve() + self.manifest = manifest.resolve() + self.report_dir = report_dir.resolve() + self.entries = self._load_manifest() + self.base_sha = self.git("rev-parse", "HEAD", quiet=True).strip() + self.remote_heads: dict[str, str] = {} + self.records: dict[str, EntryReport] = {} + + def git( + self, + *args: str, + check: bool = True, + quiet: bool = False, + ) -> str: + command = ["git", "-C", str(self.repository), *args] + completed = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + check=False, + ) + output = completed.stdout + if output and not quiet: + print(output, end="" if output.endswith("\n") else "\n") + if check and completed.returncode != 0: + raise SyncError( + f"Command failed ({completed.returncode}): {' '.join(command)}\n" + f"{output[-4000:]}" + ) + return output + + def _load_manifest(self) -> list[Entry]: + entries: list[Entry] = [] + with self.manifest.open("r", encoding="utf-8", newline="") as handle: + for row in csv.reader(handle, delimiter="\t"): + if not row or row[0].startswith("#"): + continue + if len(row) != 7: + raise SyncError(f"Invalid manifest row: {row!r}") + level, name, prefix, repository, branch, mode, legacy = row + entry = Entry( + level=int(level), + name=name, + prefix=prefix.rstrip("/"), + repository=repository, + branch=branch, + mode=mode, + legacy_prefix=None if legacy == "-" else legacy.rstrip("/"), + ) + entries.append(entry) + + if not entries: + raise SyncError("The subtree manifest is empty") + + names: set[str] = set() + prefixes: set[str] = set() + for entry in entries: + path = PurePosixPath(entry.prefix) + if path.is_absolute() or ".." in path.parts or path.parts[0] != "modules": + raise SyncError(f"Unsafe snapshot prefix: {entry.prefix}") + if entry.name in names or entry.prefix in prefixes: + raise SyncError(f"Duplicate manifest entry: {entry.name} / {entry.prefix}") + if entry.mode not in {"full", "squash"}: + raise SyncError(f"Invalid subtree mode for {entry.name}: {entry.mode}") + if not entry.repository.startswith("https://github.com/"): + raise SyncError(f"Only public GitHub HTTPS repositories are allowed: {entry.name}") + names.add(entry.name) + prefixes.add(entry.prefix) + + return sorted(entries, key=lambda item: (item.level, item.name)) + + @staticmethod + def normalize_repository(value: str) -> str: + normalized = value.strip().rstrip("/") + if normalized.endswith(".git"): + normalized = normalized[:-4] + return normalized.lower() + + def parent_for(self, entry: Entry) -> Entry | None: + candidates = [ + candidate + for candidate in self.entries + if candidate.level < entry.level + and entry.prefix.startswith(candidate.prefix + "/") + ] + return max(candidates, key=lambda item: len(item.prefix), default=None) + + def children_for(self, entry: Entry) -> list[Entry]: + return [ + candidate + for candidate in self.entries + if self.parent_for(candidate) == entry + ] + + def validate_gitmodules(self, require_all: bool) -> None: + expected: dict[str, Entry] = {} + for entry in self.entries: + if entry.level == 0: + continue + parent = self.parent_for(entry) + if parent is None: + raise SyncError(f"No manifest parent for {entry.name}") + expected[entry.prefix] = entry + + tracked = self.git("ls-files", "-z", "modules", quiet=True).split("\0") + gitmodule_files = sorted(path for path in tracked if path.endswith("/.gitmodules")) + actual: dict[str, tuple[str, str | None]] = {} + + for relative_file in gitmodule_files: + parser = configparser.RawConfigParser() + parser.read(self.repository / relative_file, encoding="utf-8") + parent_dir = PurePosixPath(relative_file).parent + for section in parser.sections(): + if not section.startswith('submodule "'): + continue + if not parser.has_option(section, "path") or not parser.has_option(section, "url"): + raise SyncError(f"Incomplete submodule declaration in {relative_file}: {section}") + child_path = (parent_dir / parser.get(section, "path")).as_posix() + branch = parser.get(section, "branch", fallback=None) + actual[child_path] = (parser.get(section, "url"), branch) + + unknown = sorted(set(actual) - set(expected)) + if unknown: + raise SyncError("Unknown submodule paths are not allowed: " + ", ".join(unknown)) + + if require_all: + missing = sorted(set(expected) - set(actual)) + if missing: + raise SyncError("Expected submodule paths are missing: " + ", ".join(missing)) + + for path, (repository, branch) in actual.items(): + entry = expected[path] + if self.normalize_repository(repository) != self.normalize_repository(entry.repository): + raise SyncError( + f"Repository mismatch for {path}: {repository} != {entry.repository}" + ) + if branch and branch != entry.branch: + raise SyncError(f"Branch mismatch for {path}: {branch} != {entry.branch}") + + def fetch_remote_heads(self) -> None: + def fetch(entry: Entry) -> tuple[str, str]: + completed = subprocess.run( + ["git", "ls-remote", entry.repository, f"refs/heads/{entry.branch}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + check=False, + ) + if completed.returncode != 0 or not completed.stdout.strip(): + raise SyncError( + f"Cannot resolve {entry.name} {entry.branch}: " + f"{completed.stderr.strip() or completed.stdout.strip()}" + ) + return entry.name, completed.stdout.split()[0] + + with ThreadPoolExecutor(max_workers=8) as executor: + futures = [executor.submit(fetch, entry) for entry in self.entries] + for future in as_completed(futures): + name, sha = future.result() + self.remote_heads[name] = sha + + def latest_splits(self) -> dict[str, str]: + history = self.git("log", "HEAD", "--format=%B%x00", quiet=True) + splits: dict[str, str] = {} + for message in history.split("\0"): + directories = [ + line.split(": ", 1)[1].rstrip("/") + for line in message.splitlines() + if line.startswith("git-subtree-dir: ") + ] + split_values = [ + line.split(": ", 1)[1] + for line in message.splitlines() + if line.startswith("git-subtree-split: ") + ] + if not split_values: + continue + for directory in directories: + splits.setdefault(directory, split_values[0]) + return splits + + def commit_exists(self, sha: str) -> bool: + completed = subprocess.run( + ["git", "-C", str(self.repository), "cat-file", "-e", f"{sha}^{{commit}}"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + return completed.returncode == 0 + + def is_ancestor(self, ancestor: str, descendant: str = "HEAD") -> bool: + completed = subprocess.run( + ["git", "-C", str(self.repository), "merge-base", "--is-ancestor", ancestor, descendant], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + return completed.returncode == 0 + + def current_reference(self, entry: Entry, splits: dict[str, str]) -> tuple[str, bool]: + remote_head = self.remote_heads[entry.name] + if entry.mode == "full" and self.commit_exists(remote_head) and self.is_ancestor(remote_head): + return remote_head, True + + current = splits.get(entry.prefix) + if current: + return current, current == remote_head + if entry.legacy_prefix: + legacy = splits.get(entry.legacy_prefix) + if legacy: + return legacy, legacy == remote_head + return "unknown", False + + def safe_remove_worktree_path(self, relative: str) -> None: + target = (self.repository / relative).resolve() + modules_root = (self.repository / "modules").resolve() + if target == modules_root or modules_root not in target.parents: + raise SyncError(f"Refusing to remove unsafe path: {target}") + if target.is_dir() and not target.is_symlink(): + shutil.rmtree(target) + elif target.exists() or target.is_symlink(): + target.unlink() + + def reseed_squashed_entry(self, entry: Entry) -> None: + print(f"Re-seeding known squash subtree metadata: {entry.name}") + self.git("rm", "-rf", "--ignore-unmatch", "--", entry.prefix) + if (self.repository / entry.prefix).exists(): + self.safe_remove_worktree_path(entry.prefix) + self.git("add", "-A", "--", entry.prefix) + staged = self.git("diff", "--cached", "--name-only", quiet=True).strip() + if staged: + self.git("commit", "-m", f"chore: prepare {entry.name} snapshot refresh") + self.subtree_add(entry) + + def subtree_add(self, entry: Entry) -> None: + args = [ + "subtree", + "add", + f"--prefix={entry.prefix}", + entry.repository, + entry.branch, + ] + if entry.mode == "squash": + args.append("--squash") + self.git(*args) + + def conflict_paths(self) -> list[str]: + output = self.git("diff", "--name-only", "--diff-filter=U", "-z", quiet=True) + return [path for path in output.split("\0") if path] + + def resolve_known_gitlink_conflict(self, entry: Entry) -> None: + merge_head = self.repository / ".git" / "MERGE_HEAD" + if not merge_head.exists(): + # Worktrees store gitdir in a file; rev-parse works in both layouts. + git_dir = Path(self.git("rev-parse", "--git-dir", quiet=True).strip()) + if not git_dir.is_absolute(): + git_dir = self.repository / git_dir + merge_head = git_dir / "MERGE_HEAD" + if not merge_head.exists(): + raise SyncError(f"Subtree pull failed for {entry.name} without a merge to resolve") + + children = self.children_for(entry) + module_file = f"{entry.prefix}/.gitmodules" + conflicts = self.conflict_paths() + + def allowed(path: str) -> bool: + if path == module_file: + return True + return any(path == child.prefix or path.startswith(child.prefix + "~") for child in children) + + unknown = [path for path in conflicts if not allowed(path)] + if not conflicts or unknown: + raise SyncError( + f"Non-gitlink conflicts in {entry.name}: " + ", ".join(unknown or conflicts) + ) + + print(f"Resolving allowlisted gitlink conflicts for {entry.name}") + if module_file in conflicts: + self.git("checkout", "--theirs", "--", module_file) + + for child in children: + self.git("rm", "-rf", "--ignore-unmatch", "--", child.prefix) + if (self.repository / child.prefix).exists(): + self.safe_remove_worktree_path(child.prefix) + + indexed = self.git("ls-files", "-s", "-z", "--", entry.prefix, quiet=True) + marker_paths: set[str] = set() + for item in indexed.split("\0"): + if "\t" not in item: + continue + path = item.split("\t", 1)[1] + if any(path.startswith(child.prefix + "~") for child in children): + marker_paths.add(path) + for marker in sorted(marker_paths): + self.git("rm", "-rf", "--ignore-unmatch", "--", marker) + if (self.repository / marker).exists(): + self.safe_remove_worktree_path(marker) + + self.git("add", "-A", "--", entry.prefix) + remaining = self.conflict_paths() + if remaining: + raise SyncError(f"Unresolved paths remain in {entry.name}: {', '.join(remaining)}") + self.git("commit", "-m", f"chore: sync {entry.name} snapshot") + self.validate_gitmodules(require_all=False) + + def subtree_pull(self, entry: Entry) -> None: + args = [ + "subtree", + "pull", + f"--prefix={entry.prefix}", + entry.repository, + entry.branch, + ] + if entry.mode == "squash": + args.append("--squash") + command = ["git", "-C", str(self.repository), *args] + completed = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + check=False, + ) + if completed.stdout: + print(completed.stdout, end="" if completed.stdout.endswith("\n") else "\n") + if completed.returncode != 0: + self.resolve_known_gitlink_conflict(entry) + + def sync_entry(self, entry: Entry, before: str) -> None: + path = self.repository / entry.prefix + if not path.exists(): + self.subtree_add(entry) + return + + if entry.mode == "squash" and before == "unknown": + self.reseed_squashed_entry(entry) + return + + self.subtree_pull(entry) + + def validate_final_state(self) -> tuple[int, int, int, str]: + self.validate_gitmodules(require_all=True) + + unmerged = self.conflict_paths() + if unmerged: + raise SyncError("Unresolved merge paths: " + ", ".join(unmerged)) + + gitlinks_output = self.git("ls-files", "-s", "-z", "modules", quiet=True) + gitlinks: list[str] = [] + for item in gitlinks_output.split("\0"): + if not item or "\t" not in item: + continue + metadata, path = item.split("\t", 1) + if metadata.split()[0] == "160000": + gitlinks.append(path) + if gitlinks: + raise SyncError("Gitlinks remain after synchronization: " + ", ".join(gitlinks)) + + porcelain = self.git("status", "--porcelain", quiet=True).strip() + if porcelain: + raise SyncError("The synchronization left an uncommitted working tree:\n" + porcelain) + + changed_paths = [ + path + for path in self.git("diff", "--name-only", f"{self.base_sha}..HEAD", quiet=True).splitlines() + if path + ] + outside = [path for path in changed_paths if not path.startswith("modules/")] + if outside: + raise SyncError("Snapshot changed paths outside modules/: " + ", ".join(outside)) + + statuses = self.git("diff", "--name-status", f"{self.base_sha}..HEAD", quiet=True).splitlines() + deleted = sum(1 for line in statuses if line.startswith("D\t")) + max_changed = int(os.environ.get("MAX_CHANGED_FILES", "5000")) + max_deleted = int(os.environ.get("MAX_DELETED_FILES", "500")) + if len(changed_paths) > max_changed or deleted > max_deleted: + raise SyncError( + f"Change-size guard failed: {len(changed_paths)} changed files, " + f"{deleted} deleted files (limits {max_changed}/{max_deleted})" + ) + + additions = 0 + deletions = 0 + for line in self.git("diff", "--numstat", f"{self.base_sha}..HEAD", quiet=True).splitlines(): + added, removed, _ = line.split("\t", 2) + if added.isdigit(): + additions += int(added) + if removed.isdigit(): + deletions += int(removed) + + diff_stat = self.git("diff", "--stat", f"{self.base_sha}..HEAD", quiet=True).strip() + return len(changed_paths), deleted, additions + deletions, diff_stat + + def write_report( + self, + status: str, + error: str | None, + metrics: tuple[int, int, int, str] | None = None, + ) -> dict[str, object]: + self.report_dir.mkdir(parents=True, exist_ok=True) + final_sha = self.git("rev-parse", "HEAD", quiet=True).strip() + changed_files, deleted_files, changed_lines, diff_stat = metrics or (0, 0, 0, "") + changed = status == "success" and final_sha != self.base_sha + updated = [record.name for record in self.records.values() if record.result == "updated"] + report: dict[str, object] = { + "status": status, + "changed": changed, + "generated_at": datetime.now(timezone.utc).isoformat(), + "base_sha": self.base_sha, + "final_sha": final_sha, + "changed_files": changed_files, + "deleted_files": deleted_files, + "changed_lines": changed_lines, + "updated": updated, + "error": error, + "diff_stat": diff_stat, + "entries": [asdict(self.records[entry.name]) for entry in self.entries], + } + (self.report_dir / "report.json").write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + status_text = {"success": "成功", "failed": "失败"}.get(status, status) + lines = [ + "# roboto_origin 每日快照报告", + "", + f"- 状态:{status_text}", + f"- 是否有改动:{'是' if changed else '否'}", + f"- 基准提交:`{self.base_sha[:12]}`", + f"- 最终提交:`{final_sha[:12]}`", + f"- 更新仓库:{', '.join(updated) if updated else '无'}", + f"- 文件统计:{changed_files} 个文件,{deleted_files} 个删除,{changed_lines} 行增删", + ] + if error: + lines.extend(["", "## 错误", "", error[:2000]]) + lines.extend(["", "## 白名单检查", "", "| 仓库 | 路径 | 上游 | 结果 |", "| --- | --- | --- | --- |"]) + for entry in self.entries: + record = self.records[entry.name] + lines.append( + f"| {record.name} | `{record.path}` | `{record.upstream[:12]}` | {record.result} |" + ) + if diff_stat: + lines.extend(["", "## Git 统计", "", "```text", diff_stat, "```"]) + (self.report_dir / "report.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + return report + + def execute(self) -> int: + error: str | None = None + metrics: tuple[int, int, int, str] | None = None + status = "failed" + try: + if self.git("status", "--porcelain", quiet=True).strip(): + raise SyncError("The snapshot worktree must be clean before synchronization") + self.validate_gitmodules(require_all=True) + self.fetch_remote_heads() + splits = self.latest_splits() + + for entry in self.entries: + remote_head = self.remote_heads[entry.name] + before, current = self.current_reference(entry, splits) + self.records[entry.name] = EntryReport( + name=entry.name, + path=entry.prefix, + repository=entry.repository, + branch=entry.branch, + before=before, + upstream=remote_head, + ) + if current: + self.records[entry.name].result = "unchanged" + print(f"[unchanged] {entry.name} {remote_head[:12]}") + continue + + print(f"[sync] {entry.name}: {before[:12]} -> {remote_head[:12]}") + self.sync_entry(entry, before) + self.records[entry.name].result = "updated" + self.validate_gitmodules(require_all=False) + splits = self.latest_splits() + + metrics = self.validate_final_state() + status = "success" + except Exception as exc: # The report must survive every fail-closed path. + error = str(exc) + print(f"ERROR: {error}", file=sys.stderr) + for entry in self.entries: + if entry.name not in self.records: + self.records[entry.name] = EntryReport( + name=entry.name, + path=entry.prefix, + repository=entry.repository, + branch=entry.branch, + before="unknown", + upstream=self.remote_heads.get(entry.name, "unknown"), + ) + elif self.records[entry.name].result == "pending": + self.records[entry.name].result = "failed" + + self.write_report(status, error, metrics) + return 0 if status == "success" else 1 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--repository", required=True, type=Path) + parser.add_argument("--manifest", required=True, type=Path) + parser.add_argument("--report-dir", required=True, type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + return SnapshotSync(args.repository, args.manifest, args.report_dir).execute() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/subtree-snapshots.tsv b/.github/subtree-snapshots.tsv new file mode 100644 index 00000000..3c7edc2d --- /dev/null +++ b/.github/subtree-snapshots.tsv @@ -0,0 +1,24 @@ +# level name prefix repository branch mode legacy_prefix +0 rpo_hardware modules/rpo_hardware https://github.com/Roboparty/rpo_hardware.git main full modules/Atom01_hardware +0 roboparty_deploy modules/roboparty_deploy https://github.com/Roboparty/roboparty_deploy.git main full modules/atom01_deploy +0 roboparty_train modules/roboparty_train https://github.com/Roboparty/roboparty_train.git main full modules/atom01_train +0 rpo_description modules/rpo_description https://github.com/Roboparty/rpo_description.git main full modules/atom01_description +0 roboparty_firmware modules/roboparty_firmware https://github.com/Roboparty/roboparty_firmware.git main full modules/atom01_firmware +0 rpo_appearance modules/rpo_appearance https://github.com/Roboparty/rpo_appearance.git main full modules/atom01_appearance +0 roboparty_navigation modules/roboparty_navigation https://github.com/Roboparty/roboparty_navigation.git master full - +0 roboparty_xr_teleop modules/roboparty_xr_teleop https://github.com/Roboparty/roboparty_xr_teleop.git main full modules/Atom_xr_teleop +1 roboparty_inference modules/roboparty_deploy/src/inference https://github.com/Roboparty/roboparty_inference.git master squash modules/atom01_deploy/src/inference +1 roboparty_motors modules/roboparty_deploy/src/motors https://github.com/Roboparty/roboparty_motors.git master squash modules/atom01_deploy/src/motors +1 roboparty_imu modules/roboparty_deploy/src/imu https://github.com/Roboparty/roboparty_imu.git master squash modules/atom01_deploy/src/imu +1 create_ap modules/roboparty_deploy/tools/create_ap https://github.com/Roboparty/create_ap.git master squash modules/atom01_deploy/tools/create_ap +1 roboparty_camera modules/roboparty_deploy/src/camera https://github.com/Roboparty/roboparty_camera.git main squash - +1 roboto_usb2can modules/roboparty_firmware/roboto_usb2can https://github.com/wentywenty/roboto_usb2can.git master squash modules/atom01_firmware/roboto_usb2can +1 orangepi-build modules/roboparty_firmware/orangepi-build https://github.com/wentywenty/orangepi-build.git next squash modules/atom01_firmware/orangepi-build +1 x5-rdk-gen modules/roboparty_firmware/x5-rdk-gen https://github.com/wentywenty/x5-rdk-gen.git main squash modules/atom01_firmware/x5-rdk-gen +1 robots_localization_ros2 modules/roboparty_navigation/robots_localization_ros2 https://github.com/Luo1imasi/robots_localization_ros2.git master squash - +1 nlink_parser_ros2 modules/roboparty_navigation/nlink_parser_ros2 https://github.com/Luo1imasi/nlink_parser_ros2.git master squash - +1 robolab modules/roboparty_train/robolab https://github.com/Roboparty/robolab.git master squash modules/atom01_train/robolab +1 rsl_rl modules/roboparty_train/rsl_rl https://github.com/Roboparty/rsl_rl.git main squash modules/atom01_train/rsl_rl +2 realsense-ros modules/roboparty_deploy/src/camera/thirdparty/realsense-ros https://github.com/realsenseai/realsense-ros.git ros2-master squash - +2 cnpy modules/roboparty_deploy/src/inference/thirdparty/cnpy https://github.com/rogersce/cnpy.git master squash - +2 robolab-rpo-description modules/roboparty_train/robolab/data/robots/roboparty/rpo https://github.com/Roboparty/rpo_description.git main squash modules/atom01_train/robolab/data/robots/roboparty/rpo diff --git a/.github/workflows/sync-subtree-snapshots.yml b/.github/workflows/sync-subtree-snapshots.yml new file mode 100644 index 00000000..407a77b3 --- /dev/null +++ b/.github/workflows/sync-subtree-snapshots.yml @@ -0,0 +1,219 @@ +name: Sync subtree snapshots + +on: + schedule: + # 09:10 Asia/Shanghai. Scheduled workflows run from the default branch. + - cron: "10 1 * * *" + workflow_dispatch: + +permissions: {} + +concurrency: + group: sync-subtree-snapshots + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + timeout-minutes: 180 + permissions: + contents: write + outputs: + changed: ${{ steps.run.outputs.changed }} + final_sha: ${{ steps.run.outputs.final_sha }} + + steps: + - name: Check out trusted automation source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.sha }} + path: automation + persist-credentials: false + + - name: Check out dev snapshot with full history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: dev + path: snapshot + fetch-depth: 0 + + - name: Verify source and dev head + id: guard + shell: bash + working-directory: snapshot + run: | + set -euo pipefail + case "$GITHUB_REF_NAME" in + main|dev) ;; + *) echo "This workflow may only run from main or dev." >&2; exit 1 ;; + esac + + local_sha="$(git rev-parse HEAD)" + git fetch --quiet origin refs/heads/dev + remote_sha="$(git rev-parse FETCH_HEAD)" + if [[ "$local_sha" != "$remote_sha" ]]; then + echo "The checked-out dev head changed before synchronization." >&2 + exit 1 + fi + echo "remote_sha=$remote_sha" >> "$GITHUB_OUTPUT" + + - name: Configure snapshot commit identity + shell: bash + working-directory: snapshot + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Synchronize fixed subtree allowlist + id: run + shell: bash + working-directory: snapshot + env: + MAX_CHANGED_FILES: "5000" + MAX_DELETED_FILES: "500" + run: | + set +e + report_dir="$RUNNER_TEMP/subtree-snapshot-report" + python3 ../automation/.github/scripts/sync_subtrees.py \ + --repository . \ + --manifest ../automation/.github/subtree-snapshots.tsv \ + --report-dir "$report_dir" + exit_code=$? + + if [[ -f "$report_dir/report.json" ]]; then + changed="$(python3 -c 'import json,sys; print(str(json.load(open(sys.argv[1]))["changed"]).lower())' "$report_dir/report.json")" + final_sha="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["final_sha"])' "$report_dir/report.json")" + else + changed=false + final_sha="$(git rev-parse HEAD)" + fi + + echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" + echo "changed=$changed" >> "$GITHUB_OUTPUT" + echo "final_sha=$final_sha" >> "$GITHUB_OUTPUT" + exit 0 + + - name: Push snapshot commits to dev only + if: steps.run.outputs.exit_code == '0' && steps.run.outputs.changed == 'true' + shell: bash + working-directory: snapshot + run: | + set -euo pipefail + git fetch --quiet origin refs/heads/dev + current_remote_sha="$(git rev-parse FETCH_HEAD)" + if [[ "$current_remote_sha" != "${{ steps.guard.outputs.remote_sha }}" ]]; then + echo "Remote dev changed during synchronization; refusing to overwrite it." >&2 + exit 1 + fi + git push origin HEAD:refs/heads/dev + + - name: Upload synchronization report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: subtree-snapshot-report-${{ github.run_id }} + path: ${{ runner.temp }}/subtree-snapshot-report + if-no-files-found: warn + retention-days: 30 + + - name: Mark failed synchronization + if: steps.run.outputs.exit_code != '0' + shell: bash + run: exit 1 + + pull_request: + needs: sync + if: needs.sync.result == 'success' && needs.sync.outputs.changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: read + pull-requests: write + outputs: + url: ${{ steps.pr.outputs.url }} + + steps: + - name: Download synchronization report + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: subtree-snapshot-report-${{ github.run_id }} + path: ${{ runner.temp }}/subtree-snapshot-report + + - name: Create or refresh dev to main pull request + id: pr + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + report="$RUNNER_TEMP/subtree-snapshot-report/report.md" + body="$RUNNER_TEMP/pr-body.md" + cp "$report" "$body" + cat >> "$body" <<'EOF' + + --- + 此 PR 由固定白名单快照工作流维护。它不会自动批准或合并;main 仍需人工审查。 + EOF + + number="$(gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --state open \ + --head dev \ + --base main \ + --json number \ + --jq '.[0].number // empty')" + + if [[ -n "$number" ]]; then + gh pr edit "$number" \ + --repo "$GITHUB_REPOSITORY" \ + --title "chore: refresh subtree snapshots" \ + --body-file "$body" + url="$(gh pr view "$number" --repo "$GITHUB_REPOSITORY" --json url --jq .url)" + else + url="$(gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --base main \ + --head dev \ + --title "chore: refresh subtree snapshots" \ + --body-file "$body")" + fi + echo "url=$url" >> "$GITHUB_OUTPUT" + + feishu_report: + needs: + - sync + - pull_request + if: always() + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: read + + steps: + - name: Check out trusted report client + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.sha }} + path: automation + persist-credentials: false + sparse-checkout: .github/scripts/report_feishu.py + + - name: Download synchronization report + continue-on-error: true + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: subtree-snapshot-report-${{ github.run_id }} + path: ${{ runner.temp }}/subtree-snapshot-report + + - name: Append result to Feishu document + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + FEISHU_DOCUMENT_ID: ${{ secrets.FEISHU_DOCUMENT_ID }} + WORKFLOW_RESULT: ${{ needs.sync.result }} + PR_URL: ${{ needs.pull_request.outputs.url }} + run: | + python3 automation/.github/scripts/report_feishu.py \ + --report "$RUNNER_TEMP/subtree-snapshot-report/report.json" From 38ebb75498d5b865adc0a87a291c3a5875a0331d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:23:01 +0800 Subject: [PATCH 08/12] Squashed 'modules/roboparty_train/robolab/data/robots/roboparty/rpo/' changes from aa332a3..37aac9c 37aac9c Merge pull request #3 from Roboparty/codex/license-audit-20260812 c09ba3a Document CERN-OHL-W-2.0 license 67573ec Merge pull request #2 from Roboparty/dev ac66721 Add open hardware license git-subtree-dir: modules/roboparty_train/robolab/data/robots/roboparty/rpo git-subtree-split: 37aac9ca665e92731444a1618320078e7ba21569 --- LICENSE | 310 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 + 2 files changed, 314 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0f882860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,310 @@ +CERN Open Hardware Licence Version 2 - Weakly Reciprocal + +Preamble + +CERN has developed this licence to promote collaboration among +hardware designers and to provide a legal tool which supports the +freedom to use, study, modify, share and distribute hardware designs +and products based on those designs. Version 2 of the CERN Open +Hardware Licence comes in three variants: CERN-OHL-P (permissive); and +two reciprocal licences: this licence, CERN- OHL-W (weakly reciprocal) +and CERN-OHL-S (strongly reciprocal). + +The CERN-OHL-W is copyright CERN 2020. Anyone is welcome to use it, in +unmodified form only. + +Use of this Licence does not imply any endorsement by CERN of any +Licensor or their designs nor does it imply any involvement by CERN in +their development. + + +1 Definitions + + 1.1 'Licence' means this CERN-OHL-W. + + 1.2 'Compatible Licence' means + + a) any earlier version of the CERN Open Hardware licence, or + + b) any version of the CERN-OHL-S or the CERN-OHL-W, or + + c) any licence which permits You to treat the Source to which + it applies as licensed under CERN-OHL-S or CERN-OHL-W + provided that on Conveyance of any such Source, or any + associated Product You treat the Source in question as being + licensed under CERN-OHL-S or CERN-OHL-W as appropriate. + + 1.3 'Source' means information such as design materials or digital + code which can be applied to Make or test a Product or to + prepare a Product for use, Conveyance or sale, regardless of its + medium or how it is expressed. It may include Notices. + + 1.4 'Covered Source' means Source that is explicitly made available + under this Licence. + + 1.5 'Product' means any device, component, work or physical object, + whether in finished or intermediate form, arising from the use, + application or processing of Covered Source. + + 1.6 'Make' means to create or configure something, whether by + manufacture, assembly, compiling, loading or applying Covered + Source or another Product or otherwise. + + 1.7 'Available Component' means any part, sub-assembly, library or + code which: + + a) is licensed to You as Complete Source under a Compatible + Licence; or + + b) is available, at the time a Product or the Source containing + it is first Conveyed, to You and any other prospective + licensees + + i) with sufficient rights and information (including any + configuration and programming files and information + about its characteristics and interfaces) to enable it + either to be Made itself, or to be sourced and used to + Make the Product; or + ii) as part of the normal distribution of a tool used to + design or Make the Product. + + 1.8 'External Material' means anything (including Source) which: + + a) is only combined with Covered Source in such a way that it + interfaces with the Covered Source using a documented + interface which is described in the Covered Source; and + + b) is not a derivative of or contains Covered Source, or, if it + is, it is solely to the extent necessary to facilitate such + interfacing. + + 1.9 'Complete Source' means the set of all Source necessary to Make + a Product, in the preferred form for making modifications, + including necessary installation and interfacing information + both for the Product, and for any included Available Components. + If the format is proprietary, it must also be made available in + a format (if the proprietary tool can create it) which is + viewable with a tool available to potential licensees and + licensed under a licence approved by the Free Software + Foundation or the Open Source Initiative. Complete Source need + not include the Source of any Available Component, provided that + You include in the Complete Source sufficient information to + enable a recipient to Make or source and use the Available + Component to Make the Product. + + 1.10 'Source Location' means a location where a Licensor has placed + Covered Source, and which that Licensor reasonably believes will + remain easily accessible for at least three years for anyone to + obtain a digital copy. + + 1.11 'Notice' means copyright, acknowledgement and trademark notices, + Source Location references, modification notices (subsection + 3.3(b)) and all notices that refer to this Licence and to the + disclaimer of warranties that are included in the Covered + Source. + + 1.12 'Licensee' or 'You' means any person exercising rights under + this Licence. + + 1.13 'Licensor' means a natural or legal person who creates or + modifies Covered Source. A person may be a Licensee and a + Licensor at the same time. + + 1.14 'Convey' means to communicate to the public or distribute. + + +2 Applicability + + 2.1 This Licence governs the use, copying, modification, Conveying + of Covered Source and Products, and the Making of Products. By + exercising any right granted under this Licence, You irrevocably + accept these terms and conditions. + + 2.2 This Licence is granted by the Licensor directly to You, and + shall apply worldwide and without limitation in time. + + 2.3 You shall not attempt to restrict by contract or otherwise the + rights granted under this Licence to other Licensees. + + 2.4 This Licence is not intended to restrict fair use, fair dealing, + or any other similar right. + + +3 Copying, modifying and Conveying Covered Source + + 3.1 You may copy and Convey verbatim copies of Covered Source, in + any medium, provided You retain all Notices. + + 3.2 You may modify Covered Source, other than Notices, provided that + You irrevocably undertake to make that modified Covered Source + available from a Source Location should You Convey a Product in + circumstances where the recipient does not otherwise receive a + copy of the modified Covered Source. In each case subsection 3.3 + shall apply. + + You may only delete Notices if they are no longer applicable to + the corresponding Covered Source as modified by You and You may + add additional Notices applicable to Your modifications. + + 3.3 You may Convey modified Covered Source (with the effect that You + shall also become a Licensor) provided that You: + + a) retain Notices as required in subsection 3.2; + + b) add a Notice to the modified Covered Source stating that You + have modified it, with the date and brief description of how + You have modified it; + + c) add a Source Location Notice for the modified Covered Source + if You Convey in circumstances where the recipient does not + otherwise receive a copy of the modified Covered Source; and + + d) license the modified Covered Source under the terms and + conditions of this Licence (or, as set out in subsection + 8.3, a later version, if permitted by the licence of the + original Covered Source). Such modified Covered Source must + be licensed as a whole, but excluding Available Components + contained in it or External Material to which it is + interfaced, which remain licensed under their own applicable + licences. + + +4 Making and Conveying Products + + 4.1 You may Make Products, and/or Convey them, provided that You + either provide each recipient with a copy of the Complete Source + or ensure that each recipient is notified of the Source Location + of the Complete Source. That Complete Source includes Covered + Source and You must accordingly satisfy Your obligations set out + in subsection 3.3. If specified in a Notice, the Product must + visibly and securely display the Source Location on it or its + packaging or documentation in the manner specified in that + Notice. + + 4.2 Where You Convey a Product which incorporates External Material, + the Complete Source for that Product which You are required to + provide under subsection 4.1 need not include any Source for the + External Material. + + 4.3 You may license Products under terms of Your choice, provided + that such terms do not restrict or attempt to restrict any + recipients' rights under this Licence to the Covered Source. + + +5 Research and Development + +You may Convey Covered Source, modified Covered Source or Products to +a legal entity carrying out development, testing or quality assurance +work on Your behalf provided that the work is performed on terms which +prevent the entity from both using the Source or Products for its own +internal purposes and Conveying the Source or Products or any +modifications to them to any person other than You. Any modifications +made by the entity shall be deemed to be made by You pursuant to +subsection 3.2. + + +6 DISCLAIMER AND LIABILITY + + 6.1 DISCLAIMER OF WARRANTY -- The Covered Source and any Products + are provided 'as is' and any express or implied warranties, + including, but not limited to, implied warranties of + merchantability, of satisfactory quality, non-infringement of + third party rights, and fitness for a particular purpose or use + are disclaimed in respect of any Source or Product to the + maximum extent permitted by law. The Licensor makes no + representation that any Source or Product does not or will not + infringe any patent, copyright, trade secret or other + proprietary right. The entire risk as to the use, quality, and + performance of any Source or Product shall be with You and not + the Licensor. This disclaimer of warranty is an essential part + of this Licence and a condition for the grant of any rights + granted under this Licence. + + 6.2 EXCLUSION AND LIMITATION OF LIABILITY -- The Licensor shall, to + the maximum extent permitted by law, have no liability for + direct, indirect, special, incidental, consequential, exemplary, + punitive or other damages of any character including, without + limitation, procurement of substitute goods or services, loss of + use, data or profits, or business interruption, however caused + and on any theory of contract, warranty, tort (including + negligence), product liability or otherwise, arising in any way + in relation to the Covered Source, modified Covered Source + and/or the Making or Conveyance of a Product, even if advised of + the possibility of such damages, and You shall hold the + Licensor(s) free and harmless from any liability, costs, + damages, fees and expenses, including claims by third parties, + in relation to such use. + + +7 Patents + + 7.1 Subject to the terms and conditions of this Licence, each + Licensor hereby grants to You a perpetual, worldwide, + non-exclusive, no-charge, royalty-free, irrevocable (except as + stated in subsections 7.2 and 8.4) patent license to Make, have + Made, use, offer to sell, sell, import, and otherwise transfer + the Covered Source and Products, where such licence applies only + to those patent claims licensable by such Licensor that are + necessarily infringed by exercising rights under the Covered + Source as Conveyed by that Licensor. + + 7.2 If You institute patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that the + Covered Source or a Product constitutes direct or contributory + patent infringement, or You seek any declaration that a patent + licensed to You under this Licence is invalid or unenforceable + then any rights granted to You under this Licence shall + terminate as of the date such process is initiated. + + +8 General + + 8.1 If any provisions of this Licence are or subsequently become + invalid or unenforceable for any reason, the remaining + provisions shall remain effective. + + 8.2 You shall not use any of the name (including acronyms and + abbreviations), image, or logo by which the Licensor or CERN is + known, except where needed to comply with section 3, or where + the use is otherwise allowed by law. Any such permitted use + shall be factual and shall not be made so as to suggest any kind + of endorsement or implication of involvement by the Licensor or + its personnel. + + 8.3 CERN may publish updated versions and variants of this Licence + which it considers to be in the spirit of this version, but may + differ in detail to address new problems or concerns. New + versions will be published with a unique version number and a + variant identifier specifying the variant. If the Licensor has + specified that a given variant applies to the Covered Source + without specifying a version, You may treat that Covered Source + as being released under any version of the CERN-OHL with that + variant. If no variant is specified, the Covered Source shall be + treated as being released under CERN-OHL-S. The Licensor may + also specify that the Covered Source is subject to a specific + version of the CERN-OHL or any later version in which case You + may apply this or any later version of CERN-OHL with the same + variant identifier published by CERN. + + You may treat Covered Source licensed under CERN-OHL-W as + licensed under CERN-OHL-S if and only if all Available + Components referenced in the Covered Source comply with the + corresponding definition of Available Component for CERN-OHL-S. + + 8.4 This Licence shall terminate with immediate effect if You fail + to comply with any of its terms and conditions. + + 8.5 However, if You cease all breaches of this Licence, then Your + Licence from any Licensor is reinstated unless such Licensor has + terminated this Licence by giving You, while You remain in + breach, a notice specifying the breach and requiring You to cure + it within 30 days, and You have failed to come into compliance + in all material respects by the end of the 30 day period. Should + You repeat the breach after receipt of a cure notice and + subsequent reinstatement, this Licence will terminate + immediately and permanently. Section 6 shall continue to apply + after any termination. + + 8.6 This Licence shall not be enforceable except by a Licensor + acting as such, and third party beneficiary rights are + specifically excluded. diff --git a/README.md b/README.md index 07f5b9c0..2e51fb49 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ There are the description files of RPO, a product of Roboparty. +## License + +Files in this directory are licensed under the [CERN Open Hardware Licence Version 2 - Weakly Reciprocal (CERN-OHL-W-2.0)](LICENSE). + ![RPO URDF 模型](rpo_urdf.png) From 64d9090721aecff302d4b054561d603d1d6d8bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:24:55 +0800 Subject: [PATCH 09/12] harden snapshot reporting --- .github/scripts/report_feishu.py | 11 ++++++++--- .github/scripts/sync_subtrees.py | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/scripts/report_feishu.py b/.github/scripts/report_feishu.py index 36541d9e..122986e0 100644 --- a/.github/scripts/report_feishu.py +++ b/.github/scripts/report_feishu.py @@ -87,10 +87,12 @@ def build_blocks(report: dict) -> list[dict]: if status == "success" and not changed: status_text = "ℹ️ 今日无改动" + run_id = os.environ.get("GITHUB_RUN_ID", "").strip() run_url = ( f"{os.environ.get('GITHUB_SERVER_URL', 'https://github.com')}/" - f"{os.environ.get('GITHUB_REPOSITORY', 'Roboparty/roboto_origin')}/actions/runs/" - f"{os.environ.get('GITHUB_RUN_ID', '')}" + f"{os.environ.get('GITHUB_REPOSITORY', 'Roboparty/roboto_origin')}/actions/runs/{run_id}" + if run_id + else "" ) pr_url = os.environ.get("PR_URL", "").strip() base_sha = str(report.get("base_sha", "unknown"))[:12] @@ -108,8 +110,11 @@ def build_blocks(report: dict) -> list[dict]: ), text_block(f"快照提交:{base_sha} → {final_sha}"), text_block(f"更新仓库:{', '.join(updated) if updated else '无'}"), - text_block(f"GitHub Actions:{run_url}"), ] + if run_url: + blocks.append(text_block(f"GitHub Actions:{run_url}")) + else: + blocks.append(text_block("执行方式:合并前本地手动快照验证")) if pr_url: blocks.append(text_block(f"dev → main PR:{pr_url}")) if report.get("error"): diff --git a/.github/scripts/sync_subtrees.py b/.github/scripts/sync_subtrees.py index db21c4bb..1ca75026 100644 --- a/.github/scripts/sync_subtrees.py +++ b/.github/scripts/sync_subtrees.py @@ -173,6 +173,8 @@ def validate_gitmodules(self, require_all: bool) -> None: raise SyncError(f"Incomplete submodule declaration in {relative_file}: {section}") child_path = (parent_dir / parser.get(section, "path")).as_posix() branch = parser.get(section, "branch", fallback=None) + if child_path in actual: + raise SyncError(f"Duplicate submodule path declaration: {child_path}") actual[child_path] = (parser.get(section, "url"), branch) unknown = sorted(set(actual) - set(expected)) From 79738a5529387b4a5328610af83caed9c53597c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:31:43 +0800 Subject: [PATCH 10/12] make snapshot reports repository-specific --- .github/scripts/report_feishu.py | 75 +++++++-- .github/scripts/sync_subtrees.py | 267 +++++++++++++++++++++++++++---- 2 files changed, 303 insertions(+), 39 deletions(-) diff --git a/.github/scripts/report_feishu.py b/.github/scripts/report_feishu.py index 122986e0..aa1eaa67 100644 --- a/.github/scripts/report_feishu.py +++ b/.github/scripts/report_feishu.py @@ -63,8 +63,11 @@ def load_report(path: Path) -> dict: "status": "failed", "changed": False, "updated": [], + "changed_repositories": [], "changed_files": 0, "deleted_files": 0, + "additions": 0, + "deletions": 0, "changed_lines": 0, "base_sha": "unknown", "final_sha": "unknown", @@ -82,7 +85,7 @@ def build_blocks(report: dict) -> list[dict]: status = report.get("status", "failed") changed = bool(report.get("changed")) - updated = list(report.get("updated") or []) + changed_repositories = list(report.get("changed_repositories") or []) status_text = "✅ 同步成功" if status == "success" else "❌ 同步失败" if status == "success" and not changed: status_text = "ℹ️ 今日无改动" @@ -106,11 +109,45 @@ def build_blocks(report: dict) -> list[dict]: "统计:" f"{report.get('changed_files', 0)} 个文件," f"{report.get('deleted_files', 0)} 个删除," - f"{report.get('changed_lines', 0)} 行增删" + f"+{report.get('additions', 0)} / -{report.get('deletions', 0)}" ), text_block(f"快照提交:{base_sha} → {final_sha}"), - text_block(f"更新仓库:{', '.join(updated) if updated else '无'}"), + text_block( + f"实际改动仓库:{len(changed_repositories)} 个(未变化仓库已省略)" + if status == "success" + else "实际改动仓库:同步失败,未完成统计" + ), ] + for repository in changed_repositories: + name = str(repository.get("name", "unknown")) + before = str(repository.get("before", "unknown"))[:12] + upstream = str(repository.get("upstream", "unknown"))[:12] + blocks.append(heading_block(f"🔔 发生改动:{name}")) + details = [ + f"仓库:{repository.get('repository', '')}", + f"快照路径:{repository.get('path', '')}", + f"上游提交:{before} → {upstream}", + ( + "统计:" + f"{repository.get('changed_files', 0)} 个文件," + f"{repository.get('deleted_files', 0)} 个删除," + f"+{repository.get('additions', 0)} / -{repository.get('deletions', 0)}" + ), + ] + compare_url = str(repository.get("compare_url") or "") + if compare_url: + details.append(f"上游对比:{compare_url}") + details.append("文件:") + for file_change in repository.get("files") or []: + path = str(file_change.get("path", "")) + prefix = str(repository.get("path", "")).rstrip("/") + "/" + if path.startswith(prefix): + path = path[len(prefix) :] + added = file_change.get("additions") + deleted = file_change.get("deletions") + line_stat = "二进制" if added is None or deleted is None else f"+{added} / -{deleted}" + details.append(f"- [{file_change.get('status', '?')}] {path}({line_stat})") + blocks.extend(text_blocks(details)) if run_url: blocks.append(text_block(f"GitHub Actions:{run_url}")) else: @@ -122,6 +159,22 @@ def build_blocks(report: dict) -> list[dict]: return blocks +def text_blocks(lines: list[str], max_chars: int = 4500) -> list[dict]: + chunks: list[str] = [] + current = "" + for line in lines: + candidate = line if not current else f"{current}\n{line}" + if len(candidate) <= max_chars: + current = candidate + continue + if current: + chunks.append(current) + current = line[:max_chars] + if current: + chunks.append(current) + return [text_block(chunk) for chunk in chunks] + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--report", required=True, type=Path) @@ -145,13 +198,15 @@ def main() -> int: report = load_report(args.report) blocks = build_blocks(report) - result = request_json( - f"{FEISHU_API}/docx/v1/documents/{document_id}/blocks/{document_id}/children", - method="POST", - payload={"children": blocks, "index": -1}, - token=token, - ) - created = len(result.get("data", {}).get("children", [])) + created = 0 + for index in range(0, len(blocks), 50): + result = request_json( + f"{FEISHU_API}/docx/v1/documents/{document_id}/blocks/{document_id}/children", + method="POST", + payload={"children": blocks[index : index + 50], "index": -1}, + token=token, + ) + created += len(result.get("data", {}).get("children", [])) print(f"Appended {created} report blocks to the Feishu document") return 0 diff --git a/.github/scripts/sync_subtrees.py b/.github/scripts/sync_subtrees.py index 1ca75026..4b3a6564 100644 --- a/.github/scripts/sync_subtrees.py +++ b/.github/scripts/sync_subtrees.py @@ -45,6 +45,42 @@ class EntryReport: result: str = "pending" +@dataclass(frozen=True) +class FileChange: + status: str + path: str + old_path: str | None + additions: int | None + deletions: int | None + + +@dataclass +class RepositoryChange: + name: str + path: str + repository: str + branch: str + before: str + upstream: str + compare_url: str | None + changed_files: int + deleted_files: int + additions: int + deletions: int + binary_files: int + files: list[FileChange] + + +@dataclass(frozen=True) +class DiffMetrics: + changed_files: int + deleted_files: int + additions: int + deletions: int + diff_stat: str + files: list[FileChange] + + class SyncError(RuntimeError): pass @@ -85,6 +121,21 @@ def git( ) return output + def git_bytes(self, *args: str) -> bytes: + command = ["git", "-C", str(self.repository), *args] + completed = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if completed.returncode != 0: + detail = completed.stderr.decode("utf-8", errors="replace")[-4000:] + raise SyncError( + f"Command failed ({completed.returncode}): {' '.join(command)}\n{detail}" + ) + return completed.stdout + def _load_manifest(self) -> list[Entry]: entries: list[Entry] = [] with self.manifest.open("r", encoding="utf-8", newline="") as handle: @@ -400,7 +451,121 @@ def sync_entry(self, entry: Entry, before: str) -> None: self.subtree_pull(entry) - def validate_final_state(self) -> tuple[int, int, int, str]: + @staticmethod + def decode_git_path(value: bytes) -> str: + return value.decode("utf-8", errors="surrogateescape") + + def collect_file_changes(self) -> list[FileChange]: + revision = f"{self.base_sha}..HEAD" + status_tokens = self.git_bytes("diff", "--name-status", "-z", revision).split(b"\0") + status_records: list[tuple[str, str, str | None]] = [] + index = 0 + while index < len(status_tokens) and status_tokens[index]: + status = status_tokens[index].decode("ascii", errors="replace") + index += 1 + if index >= len(status_tokens): + raise SyncError("Malformed git name-status output") + first_path = self.decode_git_path(status_tokens[index]) + index += 1 + if status.startswith(("R", "C")): + if index >= len(status_tokens): + raise SyncError("Malformed git rename/copy output") + new_path = self.decode_git_path(status_tokens[index]) + index += 1 + status_records.append((status, new_path, first_path)) + else: + status_records.append((status, first_path, None)) + + numstat_tokens = self.git_bytes("diff", "--numstat", "-z", revision).split(b"\0") + line_counts: dict[str, tuple[int | None, int | None]] = {} + index = 0 + while index < len(numstat_tokens) and numstat_tokens[index]: + record = numstat_tokens[index] + index += 1 + fields = record.split(b"\t", 2) + if len(fields) != 3: + raise SyncError("Malformed git numstat output") + added_raw, deleted_raw, path_raw = fields + if path_raw: + path = self.decode_git_path(path_raw) + else: + # With -z, renames/copies store old and new paths in the next two tokens. + if index + 1 >= len(numstat_tokens): + raise SyncError("Malformed git rename/copy numstat output") + index += 1 # The old path is not used for attribution. + path = self.decode_git_path(numstat_tokens[index]) + index += 1 + additions = int(added_raw) if added_raw.isdigit() else None + deletions = int(deleted_raw) if deleted_raw.isdigit() else None + line_counts[path] = (additions, deletions) + + changes: list[FileChange] = [] + for status, path, old_path in status_records: + additions, deletions = line_counts.get(path, (None, None)) + changes.append( + FileChange( + status=status, + path=path, + old_path=old_path, + additions=additions, + deletions=deletions, + ) + ) + return changes + + def entry_for_path(self, path: str) -> Entry | None: + matches = [ + entry + for entry in self.entries + if path == entry.prefix or path.startswith(entry.prefix + "/") + ] + return max(matches, key=lambda entry: len(entry.prefix), default=None) + + def repository_changes(self, files: list[FileChange]) -> list[RepositoryChange]: + grouped: dict[str, list[FileChange]] = {} + for file_change in files: + entry = self.entry_for_path(file_change.path) + if entry is None: + raise SyncError( + f"Changed path does not belong to a fixed snapshot repository: {file_change.path}" + ) + grouped.setdefault(entry.name, []).append(file_change) + + changes: list[RepositoryChange] = [] + for entry in self.entries: + entry_files = grouped.get(entry.name) + if not entry_files: + continue + record = self.records[entry.name] + before = record.before + upstream = record.upstream + compare_url = None + if before != "unknown" and upstream != "unknown" and before != upstream: + compare_url = f"{entry.repository}/compare/{before}...{upstream}" + changes.append( + RepositoryChange( + name=entry.name, + path=entry.prefix, + repository=entry.repository, + branch=entry.branch, + before=before, + upstream=upstream, + compare_url=compare_url, + changed_files=len(entry_files), + deleted_files=sum(1 for item in entry_files if item.status.startswith("D")), + additions=sum(item.additions or 0 for item in entry_files), + deletions=sum(item.deletions or 0 for item in entry_files), + binary_files=sum( + 1 + for item in entry_files + if item.additions is None or item.deletions is None + ), + files=entry_files, + ) + ) + return changes + + def validate_final_state(self) -> DiffMetrics: self.validate_gitmodules(require_all=True) unmerged = self.conflict_paths() @@ -422,17 +587,19 @@ def validate_final_state(self) -> tuple[int, int, int, str]: if porcelain: raise SyncError("The synchronization left an uncommitted working tree:\n" + porcelain) - changed_paths = [ - path - for path in self.git("diff", "--name-only", f"{self.base_sha}..HEAD", quiet=True).splitlines() - if path + file_changes = self.collect_file_changes() + changed_paths = [change.path for change in file_changes] + all_paths = changed_paths + [ + change.old_path for change in file_changes if change.old_path is not None ] - outside = [path for path in changed_paths if not path.startswith("modules/")] + outside = [path for path in all_paths if not path.startswith("modules/")] if outside: raise SyncError("Snapshot changed paths outside modules/: " + ", ".join(outside)) - statuses = self.git("diff", "--name-status", f"{self.base_sha}..HEAD", quiet=True).splitlines() - deleted = sum(1 for line in statuses if line.startswith("D\t")) + # Attribute every changed file to the deepest matching allowlisted repository. + self.repository_changes(file_changes) + + deleted = sum(1 for change in file_changes if change.status.startswith("D")) max_changed = int(os.environ.get("MAX_CHANGED_FILES", "5000")) max_deleted = int(os.environ.get("MAX_DELETED_FILES", "500")) if len(changed_paths) > max_changed or deleted > max_deleted: @@ -441,29 +608,38 @@ def validate_final_state(self) -> tuple[int, int, int, str]: f"{deleted} deleted files (limits {max_changed}/{max_deleted})" ) - additions = 0 - deletions = 0 - for line in self.git("diff", "--numstat", f"{self.base_sha}..HEAD", quiet=True).splitlines(): - added, removed, _ = line.split("\t", 2) - if added.isdigit(): - additions += int(added) - if removed.isdigit(): - deletions += int(removed) + additions = sum(change.additions or 0 for change in file_changes) + deletions = sum(change.deletions or 0 for change in file_changes) diff_stat = self.git("diff", "--stat", f"{self.base_sha}..HEAD", quiet=True).strip() - return len(changed_paths), deleted, additions + deletions, diff_stat + return DiffMetrics( + changed_files=len(changed_paths), + deleted_files=deleted, + additions=additions, + deletions=deletions, + diff_stat=diff_stat, + files=file_changes, + ) def write_report( self, status: str, error: str | None, - metrics: tuple[int, int, int, str] | None = None, + metrics: DiffMetrics | None = None, ) -> dict[str, object]: self.report_dir.mkdir(parents=True, exist_ok=True) final_sha = self.git("rev-parse", "HEAD", quiet=True).strip() - changed_files, deleted_files, changed_lines, diff_stat = metrics or (0, 0, 0, "") + changed_files = metrics.changed_files if metrics else 0 + deleted_files = metrics.deleted_files if metrics else 0 + additions = metrics.additions if metrics else 0 + deletions = metrics.deletions if metrics else 0 + changed_lines = additions + deletions + diff_stat = metrics.diff_stat if metrics else "" changed = status == "success" and final_sha != self.base_sha - updated = [record.name for record in self.records.values() if record.result == "updated"] + history_updated = [ + record.name for record in self.records.values() if record.result == "updated" + ] + repository_changes = self.repository_changes(metrics.files) if metrics else [] report: dict[str, object] = { "status": status, "changed": changed, @@ -472,8 +648,12 @@ def write_report( "final_sha": final_sha, "changed_files": changed_files, "deleted_files": deleted_files, + "additions": additions, + "deletions": deletions, "changed_lines": changed_lines, - "updated": updated, + "updated": [change.name for change in repository_changes], + "history_updated": history_updated, + "changed_repositories": [asdict(change) for change in repository_changes], "error": error, "diff_stat": diff_stat, "entries": [asdict(self.records[entry.name]) for entry in self.entries], @@ -491,17 +671,46 @@ def write_report( f"- 是否有改动:{'是' if changed else '否'}", f"- 基准提交:`{self.base_sha[:12]}`", f"- 最终提交:`{final_sha[:12]}`", - f"- 更新仓库:{', '.join(updated) if updated else '无'}", - f"- 文件统计:{changed_files} 个文件,{deleted_files} 个删除,{changed_lines} 行增删", + f"- 实际改动仓库:{len(repository_changes)} 个", + f"- 文件统计:{changed_files} 个文件,{deleted_files} 个删除,+{additions} / -{deletions}", ] if error: lines.extend(["", "## 错误", "", error[:2000]]) - lines.extend(["", "## 白名单检查", "", "| 仓库 | 路径 | 上游 | 结果 |", "| --- | --- | --- | --- |"]) - for entry in self.entries: - record = self.records[entry.name] - lines.append( - f"| {record.name} | `{record.path}` | `{record.upstream[:12]}` | {record.result} |" + lines.extend(["", f"## 实际改动仓库({len(repository_changes)})", ""]) + if status != "success": + lines.append("同步失败,未完成实际文件差异归属;请先查看上方错误信息。") + elif not repository_changes: + lines.append("本次没有仓库产生实际文件差异;未变化仓库已省略。") + for change in repository_changes: + lines.extend( + [ + f"### 🔔 [{change.name}]({change.repository})", + "", + f"- 快照路径:`{change.path}`", + f"- 跟踪分支:`{change.branch}`", + f"- 上游提交:`{change.before[:12]}` → `{change.upstream[:12]}`", + f"- 变更统计:{change.changed_files} 个文件,{change.deleted_files} 个删除,+{change.additions} / -{change.deletions}", + ] ) + if change.compare_url: + lines.append(f"- 上游对比:[查看提交差异]({change.compare_url})") + lines.extend(["", "| 状态 | 文件 | 行数 |", "| --- | --- | --- |"]) + for file_change in change.files: + relative_path = file_change.path[len(change.path) :].lstrip("/") + if file_change.old_path: + old_relative = file_change.old_path[len(change.path) :].lstrip("/") + display_path = f"{old_relative} → {relative_path}" + else: + display_path = relative_path + display_path = display_path.replace("|", "\\|").replace("`", "\\`") + if file_change.additions is None or file_change.deletions is None: + line_stat = "二进制" + else: + line_stat = f"+{file_change.additions} / -{file_change.deletions}" + lines.append(f"| {file_change.status} | `{display_path}` | {line_stat} |") + omitted = len(self.entries) - len(repository_changes) + if status == "success" and omitted: + lines.extend(["", f"> 其余 {omitted} 个白名单仓库没有实际文件差异,已省略。"]) if diff_stat: lines.extend(["", "## Git 统计", "", "```text", diff_stat, "```"]) (self.report_dir / "report.md").write_text("\n".join(lines) + "\n", encoding="utf-8") @@ -509,7 +718,7 @@ def write_report( def execute(self) -> int: error: str | None = None - metrics: tuple[int, int, int, str] | None = None + metrics: DiffMetrics | None = None status = "failed" try: if self.git("status", "--porcelain", quiet=True).strip(): From a83c50a9551a62210752c5c2017ef86c60b539c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:48:18 +0800 Subject: [PATCH 11/12] use third-level headings for changed repositories --- .github/scripts/report_feishu.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/scripts/report_feishu.py b/.github/scripts/report_feishu.py index aa1eaa67..df0cde0e 100644 --- a/.github/scripts/report_feishu.py +++ b/.github/scripts/report_feishu.py @@ -56,6 +56,10 @@ def heading_block(content: str) -> dict: return {"block_type": 4, "heading2": rich_text(content)} +def heading3_block(content: str) -> dict: + return {"block_type": 5, "heading3": rich_text(content)} + + def load_report(path: Path) -> dict: if path.exists(): return json.loads(path.read_text(encoding="utf-8")) @@ -122,7 +126,7 @@ def build_blocks(report: dict) -> list[dict]: name = str(repository.get("name", "unknown")) before = str(repository.get("before", "unknown"))[:12] upstream = str(repository.get("upstream", "unknown"))[:12] - blocks.append(heading_block(f"🔔 发生改动:{name}")) + blocks.append(heading3_block(f"🔔 发生改动:{name}")) details = [ f"仓库:{repository.get('repository', '')}", f"快照路径:{repository.get('path', '')}", From db689fb8ec9d76ba72619c1e3627ba5f2e2f6005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maple=5F=E6=9E=AB=E6=BA=AA?= <98679702+Maple127667@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:18:47 +0800 Subject: [PATCH 12/12] add signed Feishu group change notifications --- .github/scripts/report_feishu_group.py | 266 +++++++++++++++++++ .github/scripts/sync_subtrees.py | 4 +- .github/workflows/sync-subtree-snapshots.yml | 44 +++ 3 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/report_feishu_group.py diff --git a/.github/scripts/report_feishu_group.py b/.github/scripts/report_feishu_group.py new file mode 100644 index 00000000..26a8ece6 --- /dev/null +++ b/.github/scripts/report_feishu_group.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Send a signed Feishu group card for real subtree file changes only.""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import hmac +import json +import os +import time +import urllib.error +import urllib.parse +import urllib.request +from datetime import datetime +from pathlib import Path +from zoneinfo import ZoneInfo + + +MAX_FILES_PER_REPOSITORY = 8 +MAX_REPOSITORIES = 12 +MAX_PAYLOAD_BYTES = 19_000 + + +def load_report(path: Path) -> dict: + if not path.exists(): + raise RuntimeError("GitHub Actions did not produce a synchronization report") + report = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(report, dict): + raise RuntimeError("The synchronization report must be a JSON object") + return report + + +def sign(timestamp: int, secret: str) -> str: + string_to_sign = f"{timestamp}\n{secret}".encode("utf-8") + digest = hmac.new(string_to_sign, digestmod=hashlib.sha256).digest() + return base64.b64encode(digest).decode("ascii") + + +def validate_webhook(value: str) -> str: + parsed = urllib.parse.urlsplit(value) + path_parts = parsed.path.strip("/").split("/") + if ( + parsed.scheme != "https" + or parsed.hostname != "open.feishu.cn" + or parsed.port is not None + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or path_parts[:4] != ["open-apis", "bot", "v2", "hook"] + or len(path_parts) != 5 + or not path_parts[4] + ): + raise RuntimeError("FEISHU_GROUP_WEBHOOK is not an allowed Feishu v2 bot webhook") + return value + + +def github_url(value: str) -> str | None: + if not value: + return None + parsed = urllib.parse.urlsplit(value) + if ( + parsed.scheme == "https" + and parsed.hostname == "github.com" + and parsed.port is None + and parsed.username is None + and parsed.password is None + ): + return value + return None + + +def feishu_document_url(value: str) -> str: + parsed = urllib.parse.urlsplit(value) + if ( + parsed.scheme != "https" + or parsed.hostname != "roboparty.feishu.cn" + or parsed.port is not None + or parsed.username is not None + or parsed.password is not None + or not parsed.path.startswith("/wiki/") + or parsed.fragment + ): + raise RuntimeError("FEISHU_DOCUMENT_URL is not an allowed Roboparty Feishu wiki URL") + return value + + +def inline(value: object, limit: int = 180) -> str: + text = " ".join(str(value).replace("`", "ʼ").split()) + return text if len(text) <= limit else text[: limit - 1] + "…" + + +def file_line(repository: dict, file_change: dict) -> str: + path = str(file_change.get("path", "")) + prefix = str(repository.get("path", "")).rstrip("/") + "/" + if path.startswith(prefix): + path = path[len(prefix) :] + additions = file_change.get("additions") + deletions = file_change.get("deletions") + line_stat = "二进制" if additions is None or deletions is None else f"+{additions} / -{deletions}" + return f"• `{inline(file_change.get('status', '?'), 12)}` `{inline(path, 240)}`({line_stat})" + + +def build_card( + report: dict, + repository_limit: int = MAX_REPOSITORIES, + file_limit: int = MAX_FILES_PER_REPOSITORY, +) -> dict: + repositories = list(report.get("changed_repositories") or []) + now = datetime.now(ZoneInfo("Asia/Shanghai")) + document_url = feishu_document_url(os.environ.get("FEISHU_DOCUMENT_URL", "")) + + summary = ( + f"**更新时间:** {now:%Y-%m-%d %H:%M:%S}(Asia/Shanghai)\n" + f"**实际改动仓库:** {len(repositories)} 个\n" + f"**文件统计:** {report.get('changed_files', 0)} 个文件," + f"{report.get('deleted_files', 0)} 个删除," + f"+{report.get('additions', 0)} / -{report.get('deletions', 0)}\n" + "**同步方向:** 子仓库 → `dev`;`main` 仍需人工审核" + ) + elements: list[dict] = [ + {"tag": "div", "text": {"tag": "lark_md", "content": summary}}, + {"tag": "hr"}, + ] + + visible_repositories = repositories[:repository_limit] + for repository in visible_repositories: + before = inline(str(repository.get("before", "unknown"))[:12], 12) + upstream = inline(str(repository.get("upstream", "unknown"))[:12], 12) + lines = [ + f"**🔔 发生改动:{inline(repository.get('name', 'unknown'), 120)}**", + f"分支:`{inline(repository.get('branch', 'unknown'), 80)}` 提交:`{before}` → `{upstream}`", + ( + f"统计:{repository.get('changed_files', 0)} 个文件," + f"{repository.get('deleted_files', 0)} 个删除," + f"+{repository.get('additions', 0)} / -{repository.get('deletions', 0)}" + ), + ] + files = list(repository.get("files") or []) + lines.extend(file_line(repository, file_change) for file_change in files[:file_limit]) + if len(files) > file_limit: + lines.append(f"• 另有 {len(files) - file_limit} 个文件未在卡片中展开") + compare_url = github_url(str(repository.get("compare_url") or "")) + if compare_url: + lines.append(f"[查看该仓库上游差异]({compare_url})") + elements.append({"tag": "div", "text": {"tag": "lark_md", "content": "\n".join(lines)}}) + + if len(repositories) > repository_limit: + elements.append( + { + "tag": "note", + "elements": [ + { + "tag": "plain_text", + "content": f"另有 {len(repositories) - repository_limit} 个变化仓库未在卡片中展开,请查看飞书文档。", + } + ], + } + ) + elements.append( + { + "tag": "note", + "elements": [ + { + "tag": "plain_text", + "content": "未变化仓库不展示;完整详情已写入飞书文档。", + } + ], + } + ) + + elements.append( + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": {"tag": "plain_text", "content": "前往飞书文档"}, + "url": document_url, + "type": "primary", + } + ], + } + ) + + return { + "config": {"wide_screen_mode": True}, + "header": { + "template": "blue", + "title": {"tag": "plain_text", "content": "🔔 roboto_origin 子仓库快照有更新"}, + }, + "elements": elements, + } + + +def build_payload(report: dict, timestamp: int, signing_secret: str) -> dict: + limits = ( + (MAX_REPOSITORIES, MAX_FILES_PER_REPOSITORY), + (10, 6), + (8, 5), + (6, 4), + (4, 3), + (2, 2), + (1, 1), + (0, 0), + ) + for repository_limit, file_limit in limits: + payload = { + "timestamp": str(timestamp), + "sign": sign(timestamp, signing_secret), + "msg_type": "interactive", + "card": build_card(report, repository_limit, file_limit), + } + size = len(json.dumps(payload, ensure_ascii=False).encode("utf-8")) + if size <= MAX_PAYLOAD_BYTES: + return payload + raise RuntimeError("Unable to fit the Feishu group card within the payload limit") + + +def send(webhook: str, payload: dict) -> None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + request = urllib.request.Request( + webhook, + data=body, + headers={"Content-Type": "application/json; charset=utf-8"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + result = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace")[:1000] + raise RuntimeError(f"Feishu group bot HTTP {exc.code}: {detail}") from exc + + code = result.get("code", result.get("StatusCode", -1)) + if code != 0: + message = result.get("msg", result.get("StatusMessage", "unknown error")) + raise RuntimeError(f"Feishu group bot error {code}: {message}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--report", required=True, type=Path) + args = parser.parse_args() + + report = load_report(args.report) + repositories = list(report.get("changed_repositories") or []) + if report.get("status") != "success" or not report.get("changed") or not repositories: + print("No successful repository file changes; group notification skipped") + return 0 + + webhook = validate_webhook(os.environ.get("FEISHU_GROUP_WEBHOOK", "")) + signing_secret = os.environ.get("FEISHU_GROUP_SIGNING_SECRET", "") + if not signing_secret: + raise RuntimeError("FEISHU_GROUP_SIGNING_SECRET is missing") + + payload = build_payload(report, int(time.time()), signing_secret) + send(webhook, payload) + print(f"Sent one Feishu group card for {len(repositories)} changed repositories") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/sync_subtrees.py b/.github/scripts/sync_subtrees.py index 4b3a6564..2267426a 100644 --- a/.github/scripts/sync_subtrees.py +++ b/.github/scripts/sync_subtrees.py @@ -541,7 +541,9 @@ def repository_changes(self, files: list[FileChange]) -> list[RepositoryChange]: upstream = record.upstream compare_url = None if before != "unknown" and upstream != "unknown" and before != upstream: - compare_url = f"{entry.repository}/compare/{before}...{upstream}" + compare_url = ( + f"{entry.repository.removesuffix('.git')}/compare/{before}...{upstream}" + ) changes.append( RepositoryChange( name=entry.name, diff --git a/.github/workflows/sync-subtree-snapshots.yml b/.github/workflows/sync-subtree-snapshots.yml index 407a77b3..8d95dfef 100644 --- a/.github/workflows/sync-subtree-snapshots.yml +++ b/.github/workflows/sync-subtree-snapshots.yml @@ -20,6 +20,7 @@ jobs: contents: write outputs: changed: ${{ steps.run.outputs.changed }} + actual_changed: ${{ steps.run.outputs.actual_changed }} final_sha: ${{ steps.run.outputs.final_sha }} steps: @@ -82,14 +83,17 @@ jobs: if [[ -f "$report_dir/report.json" ]]; then changed="$(python3 -c 'import json,sys; print(str(json.load(open(sys.argv[1]))["changed"]).lower())' "$report_dir/report.json")" + actual_changed="$(python3 -c 'import json,sys; print(str(bool(json.load(open(sys.argv[1])).get("changed_repositories"))).lower())' "$report_dir/report.json")" final_sha="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["final_sha"])' "$report_dir/report.json")" else changed=false + actual_changed=false final_sha="$(git rev-parse HEAD)" fi echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" echo "changed=$changed" >> "$GITHUB_OUTPUT" + echo "actual_changed=$actual_changed" >> "$GITHUB_OUTPUT" echo "final_sha=$final_sha" >> "$GITHUB_OUTPUT" exit 0 @@ -217,3 +221,43 @@ jobs: run: | python3 automation/.github/scripts/report_feishu.py \ --report "$RUNNER_TEMP/subtree-snapshot-report/report.json" + + feishu_group_notification: + needs: + - sync + - pull_request + - feishu_report + if: >- + needs.sync.result == 'success' && + needs.sync.outputs.actual_changed == 'true' && + needs.pull_request.result == 'success' && + needs.feishu_report.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + contents: read + + steps: + - name: Check out trusted group notification client + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.sha }} + path: automation + persist-credentials: false + sparse-checkout: .github/scripts/report_feishu_group.py + + - name: Download synchronization report + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: subtree-snapshot-report-${{ github.run_id }} + path: ${{ runner.temp }}/subtree-snapshot-report + + - name: Send formatted Feishu group card + env: + FEISHU_GROUP_WEBHOOK: ${{ secrets.FEISHU_GROUP_WEBHOOK }} + FEISHU_GROUP_SIGNING_SECRET: ${{ secrets.FEISHU_GROUP_SIGNING_SECRET }} + FEISHU_DOCUMENT_URL: ${{ secrets.FEISHU_DOCUMENT_URL }} + run: | + python3 automation/.github/scripts/report_feishu_group.py \ + --report "$RUNNER_TEMP/subtree-snapshot-report/report.json"