Files
Uni-Lab-OS/unilabos/web/templates/status.html
Junhan Chang a62a695812 Ready for open source (#47)
* Create app/main API

* create example device

* create ROS backend and example device SDK Wrapper

* Add ROS host and host starting from app.py

* Add gripper device and mock implementation

* add "status_types" & "action_types" to ROS device decorator

* add ActionServer debug example

* [bugfix] complete mock gripper example

* ROS Backend Host for Device action calling and Resource management

* add conda/mamba ENV file

* add host_node communication with app/main.py

* add action message value mappings and converters

* Update ilabos.yaml

* Update issue templates

* example devices.json and resources.json

* Fix Device wrapper to use async property and actions (#7)

* Fix Device wrapper to use async property and actions

* Resolve #1 : support async get methods and actions. Give new examples.

* add both sync/async GRBL controller SDK

* 2 call device actions from appmainpy api to ros hostpy (#8)

* feature: add job

* fix:node start

* feature:add get job status

* fix:get device

* clean

* Resolve #5 device connection diagram and workflow compilation support (#9)

* add syringe pump device and its compilation using device connection diagram

* add RunzeSyringePump real device with ROS2 example

* Prototype machine with 1 pump and 1 CNC

* add ROS2ProtocolNode and related functions

* add ilabos_msgs (to use PumpTransfer action)

* add example device connection graph

* refactor protocol_node code into separate file

* add ROS2SerialNode

* add SerialCommand srv in ilabos_msgs

* add pump_protocol example, and fix bugs

* [fix] serial service: avoid async service deadlock by directly call serial `send_command`

* use SendCmd instead of SingleJointPosition for valve control

* initialize device connection graph when server starts

* Fix #5: async workflow execution (#16)

* add rclpyx and protocol example for async-native workflow

* use async in ROS2ProtocolNode, and host initialization

* add examples of "ros-async" protocol implementation, and `run_in_event_loop` for using native async functions

* use "ros-async" in protocols and device nodes

* fix pump_protocol: push to 0.0 μL

* Envs, docs, and conda recipes (#19)

* update ENV: use python 3.11 and deprecate ros-humble-gazebo-ros

* add ilabos-msgs conda recipe

* Update ilabos.yaml

* fix recipe and env yaml

* Add sphinx docs

* add aichemeco

* add bioyong

* add bioyong

* Support XDL devices & protocols (#20)

* [Feature] support multiple protocols in a single ProtocolNode

* add Junjie's code

* Support "Clean" protocol

* Update Grignard_flow_batchreact_single_pumpvalve.json

* test_grignard_add

* add stir device node and example

* Update device_node.py

add print_publish flag to control the node's log output

* NH4Cl_add

* add "HeaterStirrer" device and "HeatChill" action

* add wait time after each pump action for equilibration

* fix stir

* add Separate protocol

* Refactor Separator device and Stir action

* add rotavap_node

* fix stir

* add chiller node

* Move rinsings into PumpTransfer

* Fix SeparateProtocol under refactored Separator device and Stir action

* Supports automatically add new protocol action_types

* fix PumpTransfer protocol because of rinsing

* Add Rotavap and Chiller devices

* fix SeparateProtocol

* add EvaporateProtocol

* add rotavap devices config

* fix HeaterStirrer and SeparatorController IO

* Fix automatically add new protocol action_types

* Add HeaterStirrer and SeparatorController device config

* fix pump protocols

* Fix Evaporate action

* Update evaporate_protocol.py

* add temp_sensor node and add function remap

* update docs

---------

Co-authored-by: 王俊杰 <1800011822@pku.edu.cn>
Co-authored-by: q434343 <554662886@qq.com>

* fix aichemeco

* [Bugfix] fix Windows conda packaging

* add file upload api

* update dependencies: force to use 3.11 and remove conflict in WIN64 and OSX64

* update dependencies: force to use 3.11 and remove conflict in WIN64 and OSX64

* Create aichemeco_simple.py

* fix

* update

* add aichemeco file

* MQTT [1/2]: action start (#25)

* add mq

* fix

* clean

* add class

* fix excel

* update bioyong

* add api

* fix

---------

Co-authored-by: caok@dp.tech <xiaoyeqiannian@163.com>

* motor & grasp

* Add Grasp motor support and enhance EleGripper class

- Introduced a new motor configuration for Grasp in sjtu.json.
- Updated EleGripper class to inherit from UniversalDriver and added status property.
- Implemented move_and_rotate method for coordinated movement.
- Adjusted threading logic in EleGripper initialization.
- Registered Grasp motor in ROS2 device node configuration.

This commit enhances the motor control capabilities by integrating the Grasp motor and improving the existing EleGripper functionality.

* fix read data lenth

* Update Grasp.py

* MQTT (2/2): publish Device Status, Action Feedback & Results (#27)

* Add bridges in HostNode for device_status publishing

* Add "bridges" selection (fastapi & mqtt) when app start

* add MQ feedback & result publisher, and fix message converter

* fix UUID converting between ROS and MQ

* lint api model.py

* Continuous controllers: PID, MPC, custom controllers etc. (#23)

* add controller config & wrapper

* add controller setup at app.main

* control loop example

* fix com port

* add agv , ur_arm and raman

* MQTT (3/4): Unified Resources and Sync when starting the server (#28)

* update http upload api

* generate uuid when init device

* example resource json

* fix

* add new example full-content json (device, resource, graph)

* fix full-content json and related reading code

* fix

* add json_schema when initialize resources

* fix

* update schema

* refactor heaterstirrer.dalong

* fix

* fix refactor heaterstirrer.dalong

* refactor syringepump.runze: use ml instead of μL

* Update ilabos/ros/host.py

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: 王俊杰 <1800011822@pku.edu.cn>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* Distributed initialization with self-organizing network (#29)

* add distributed launching option "--without_host"

* fix

---------

Co-authored-by: 王俊杰 <1800011822@pku.edu.cn>

* Refactor Workstation: Add resource service and tracking (#30)

* move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices

* add Resource srv and message_converter

* move graphio to ilabos/resources

* refactor resources type conversion

* add resource clients in device_node

* add mock resources service

* pass Gripper1 resource test

* update http resource services

* add AGV compile function

* add AGV transfer protocol

* update full mock_gripper edit_id example

* update full mock_gripper edit_id example

* get and update resource also in protocol_node

* mock resource update in AichemecoHiwo

* Create HT_hiwo.json

* add children in resources

* bugfixes

* fix rpc

* add Revvity winprep

---------

Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com>
Co-authored-by: 3218923350 <105201755+3218923350@users.noreply.github.com>

* Distributed launch (2/2): distributed resource create (#32)

* add resource_add request to host for slave mode

* add AGV

* fix protocol resources

* optimize host callbacks

* bugfixes

* add revvity registry

---------

Co-authored-by: 王俊杰 <1800011822@pku.edu.cn>
Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com>

* Refactor Driver Files Structure (#33)

* Integration with pywinauto & recorder
Added execute run and initialize procdure

* 酶标仪状态检测、使用示例,整体流程待测试

* nivo ready version

* Add HPLC driver and example script

- Introduced HPLCDriver class for managing HPLC device status and operations.
- Implemented device status monitoring and command execution via ROS2 actions.
- Added example script (hplc.py) demonstrating how to run commands on the HPLC device.
- Created PlayerUtil and UniversalDriver classes for shared functionality across devices.
- Refactored NivoDriver to utilize the new UniversalDriver structure.
- Enhanced error handling and process management in the NivoDriver.

* 修复start的错误定位

* hplc tested ok

* relative path to build msgs

* template_driver & jiageng devices

* fetch correct status type and action type

* fix mtype fetch

* gpc bus integration

* ilab build

* remove chs

* recipe rename

* modbus update 1

* json available

* hplc & modbus rewrite

* Update AgilentHPLC.py

hplc datafile reader

* move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices

* modbus分设备

* gpc

* gpc 2

* fix address

* default register node

* fix MainScreenGPC

* add Resource srv and message_converter

* move graphio to ilabos/resources

* refactor resources type conversion

* add resource clients in device_node

* add mock resources service

* pass Gripper1 resource test

* update http resource services

* add AGV compile function

* add AGV transfer protocol

* update recipe.yaml

* update full mock_gripper edit_id example

* update full mock_gripper edit_id example

* get and update resource also in protocol_node

* mock resource update in AichemecoHiwo

* feat: add other jiageng PLC device code

* ilabos compile

* correct format

* correct recipe format

* correct setup.py format

* remove unnecessary files

* remove unnecessary files

* Create HT_hiwo.json

* add children in resources

* hplc support sample_id

* correct hplc sample_id

* correct hplc sample_id

* hplc upload

* fix type hint

* oss upload tested ver

* recipe yaml fix for linux

* update installation yaml

* refactor: moved all driver files according to its feat

* merge main to dev

---------

Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
Co-authored-by: jiawei <miaojiawei@dp.tech>

* add: NMR LH and RU device control (#34)

* Add Registry for device drivers and Support GraphML (#35)

* read chemputer graphml

* read graphml in app/main

* add devices in ros/devices

* add schema for devices

* read registry directory and initialize when entry from main

* Delete devices.py

* Update add_protocol.md

* delete unecessary files

* feat: 2278 devices registry yaml (#36)

* read chemputer graphml

* read graphml in app/main

* add devices in ros/devices

* add schema for devices

* read registry directory and initialize when entry from main

* Delete devices.py

* add: NMR LH and RU device control

* fix: modify jiageng devices registry

---------

Co-authored-by: Junhan Chang <changjh@pku.edu.cn>

* Device/Resource Registry and GraphML support (#37)

* add resource type conversion to PLR

* add resource registry and test

* add docs

* fix registry

* add solenoid_valve_mock, its registry and test

* fix registry for directly using examples

* add EvacuateAndRefillProtocol and testcases

* allow function sequence call in ACTION

* add read & write & extra_info for hardware_interface

* Update device_node.py

* add solenoid valve

* add doc developer guide yaml

* fixes for starting IK station

* add graphml grouping parser

* fix graphml grouping parser

* add communication edge parser

* fix io solenoid valve

* Update .gitignore

* Update plates.yaml

---------

Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn>

* Uni-Lab Doc v0.2 (#39)

* add Uni-Lab docs

* change doc name

* Dev (#41)

* Integration with pywinauto & recorder
Added execute run and initialize procdure

* 酶标仪状态检测、使用示例,整体流程待测试

* nivo ready version

* Add HPLC driver and example script

- Introduced HPLCDriver class for managing HPLC device status and operations.
- Implemented device status monitoring and command execution via ROS2 actions.
- Added example script (hplc.py) demonstrating how to run commands on the HPLC device.
- Created PlayerUtil and UniversalDriver classes for shared functionality across devices.
- Refactored NivoDriver to utilize the new UniversalDriver structure.
- Enhanced error handling and process management in the NivoDriver.

* 修复start的错误定位

* hplc tested ok

* relative path to build msgs

* template_driver & jiageng devices

* fetch correct status type and action type

* fix mtype fetch

* gpc bus integration

* ilab build

* remove chs

* recipe rename

* modbus update 1

* json available

* hplc & modbus rewrite

* Update AgilentHPLC.py

hplc datafile reader

* move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices

* modbus分设备

* gpc

* gpc 2

* fix address

* default register node

* fix MainScreenGPC

* add Resource srv and message_converter

* move graphio to ilabos/resources

* refactor resources type conversion

* add resource clients in device_node

* add mock resources service

* pass Gripper1 resource test

* update http resource services

* add AGV compile function

* add AGV transfer protocol

* update recipe.yaml

* update full mock_gripper edit_id example

* update full mock_gripper edit_id example

* get and update resource also in protocol_node

* mock resource update in AichemecoHiwo

* feat: add other jiageng PLC device code

* ilabos compile

* correct format

* correct recipe format

* correct setup.py format

* remove unnecessary files

* remove unnecessary files

* Create HT_hiwo.json

* add children in resources

* hplc support sample_id

* correct hplc sample_id

* correct hplc sample_id

* hplc upload

* fix type hint

* oss upload tested ver

* recipe yaml fix for linux

* update installation yaml

* refactor: moved all driver files according to its feat

* merge main to dev

* add HPLC registry and json

* 升级 ros2-distro-mutex 依赖版本至 0.6

* 修改 ros2-distro-mutex 依赖版本为通配符匹配

* 更新 ros-humble-ilabos-msgs 依赖为 robostack-humble 命名空间

* add resource type conversion to PLR

* add resource registry and test

* feat: 更新oss上传

* fix device id

* add docs

* fix registry

* add solenoid_valve_mock, its registry and test

* fix registry for directly using examples

* add EvacuateAndRefillProtocol and testcases

* allow function sequence call in ACTION

* add read & write & extra_info for hardware_interface

* Update device_node.py

* add solenoid valve

* add doc developer guide yaml

* use robostack-staging

* rclpy version test

* lower rclpy

* ensure 0.6* env

* fixes for starting IK station

* add graphml grouping parser

* fix graphml grouping parser

* add communication edge parser

* fix io solenoid valve

* Update .gitignore

* Update plates.yaml

* Feature/device node later init (#40)

* 修改config路径,方便后续打包
增加device_node打印

* 支持plr序列化/init创建

* 统一命名

* import mgr
logger optimize
banner print

* 日志OK

* fix unicorn frame

* banner print

* correct import format

* file path changes

* 取消后补全,在加载设备的时候直接替换

* converter update

* web page update

* 在线device更新,node继承替换

* 修复动作、状态的类型缺失 和 命令提示

* web功能实现结束

* host节点更改完成
新增status时间戳管理
新增每10s动态发现其他node

* ros2类型的节点也应该被包一次

* 修复类型提示

* websocket 动态显示状态

* add workflow & book theme for docs

* add workflow & book theme for docs

* fix workflow build

* fix workflow build

* 理清启动关系

* stm32 example

* mac . name

* device_instance device_cls

* 新增config添加方式
更新mqtt提示

* plr test

* 移动is_host_mode
新增slave_no_host

* 确保config优先修改生效

* fix graph io

* 支持带参数传入

* 支持物料解析

* 支持物料解析

* device为空的时候不进行绑定或初始化

* protocol node new

* protocol node runnable

* protocol node runnable

---------

Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
Co-authored-by: jiawei <miaojiawei@dp.tech>
Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn>

* Dev (#45)

* Integration with pywinauto & recorder
Added execute run and initialize procdure

* 酶标仪状态检测、使用示例,整体流程待测试

* nivo ready version

* Add HPLC driver and example script

- Introduced HPLCDriver class for managing HPLC device status and operations.
- Implemented device status monitoring and command execution via ROS2 actions.
- Added example script (hplc.py) demonstrating how to run commands on the HPLC device.
- Created PlayerUtil and UniversalDriver classes for shared functionality across devices.
- Refactored NivoDriver to utilize the new UniversalDriver structure.
- Enhanced error handling and process management in the NivoDriver.

* 修复start的错误定位

* hplc tested ok

* relative path to build msgs

* template_driver & jiageng devices

* fetch correct status type and action type

* fix mtype fetch

* gpc bus integration

* ilab build

* remove chs

* recipe rename

* modbus update 1

* json available

* hplc & modbus rewrite

* Update AgilentHPLC.py

hplc datafile reader

* move ilabos/ros/rpc to ilabos/device_comms/rpc, and merge bioyond/aichemeco files under /devices

* modbus分设备

* gpc

* gpc 2

* fix address

* default register node

* fix MainScreenGPC

* add Resource srv and message_converter

* move graphio to ilabos/resources

* refactor resources type conversion

* add resource clients in device_node

* add mock resources service

* pass Gripper1 resource test

* update http resource services

* add AGV compile function

* add AGV transfer protocol

* update recipe.yaml

* update full mock_gripper edit_id example

* update full mock_gripper edit_id example

* get and update resource also in protocol_node

* mock resource update in AichemecoHiwo

* feat: add other jiageng PLC device code

* ilabos compile

* correct format

* correct recipe format

* correct setup.py format

* remove unnecessary files

* remove unnecessary files

* Create HT_hiwo.json

* add children in resources

* hplc support sample_id

* correct hplc sample_id

* correct hplc sample_id

* hplc upload

* fix type hint

* oss upload tested ver

* recipe yaml fix for linux

* update installation yaml

* refactor: moved all driver files according to its feat

* merge main to dev

* add HPLC registry and json

* 升级 ros2-distro-mutex 依赖版本至 0.6

* 修改 ros2-distro-mutex 依赖版本为通配符匹配

* 更新 ros-humble-ilabos-msgs 依赖为 robostack-humble 命名空间

* add resource type conversion to PLR

* add resource registry and test

* feat: 更新oss上传

* fix device id

* add docs

* fix registry

* add solenoid_valve_mock, its registry and test

* fix registry for directly using examples

* add EvacuateAndRefillProtocol and testcases

* allow function sequence call in ACTION

* add read & write & extra_info for hardware_interface

* Update device_node.py

* add solenoid valve

* add doc developer guide yaml

* use robostack-staging

* rclpy version test

* lower rclpy

* ensure 0.6* env

* fixes for starting IK station

* add graphml grouping parser

* fix graphml grouping parser

* add communication edge parser

* fix io solenoid valve

* Update .gitignore

* Update plates.yaml

* Feature/device node later init (#40)

* 修改config路径,方便后续打包
增加device_node打印

* 支持plr序列化/init创建

* 统一命名

* import mgr
logger optimize
banner print

* 日志OK

* fix unicorn frame

* banner print

* correct import format

* file path changes

* 取消后补全,在加载设备的时候直接替换

* converter update

* web page update

* 在线device更新,node继承替换

* 修复动作、状态的类型缺失 和 命令提示

* web功能实现结束

* host节点更改完成
新增status时间戳管理
新增每10s动态发现其他node

* ros2类型的节点也应该被包一次

* 修复类型提示

* websocket 动态显示状态

* add workflow & book theme for docs

* add workflow & book theme for docs

* fix workflow build

* fix workflow build

* 理清启动关系

* stm32 example

* mac . name

* device_instance device_cls

* 新增config添加方式
更新mqtt提示

* plr test

* 移动is_host_mode
新增slave_no_host

* 确保config优先修改生效

* fix graph io

* 支持带参数传入

* 支持物料解析

* 支持物料解析

* device为空的时候不进行绑定或初始化

* protocol node new

* protocol node runnable

* protocol node runnable

* Feature/device node later init (#42)

* 修改config路径,方便后续打包
增加device_node打印

* 支持plr序列化/init创建

* 统一命名

* import mgr
logger optimize
banner print

* 日志OK

* fix unicorn frame

* banner print

* correct import format

* file path changes

* 取消后补全,在加载设备的时候直接替换

* converter update

* web page update

* 在线device更新,node继承替换

* 修复动作、状态的类型缺失 和 命令提示

* web功能实现结束

* host节点更改完成
新增status时间戳管理
新增每10s动态发现其他node

* ros2类型的节点也应该被包一次

* 修复类型提示

* websocket 动态显示状态

* add workflow & book theme for docs

* add workflow & book theme for docs

* fix workflow build

* fix workflow build

* 理清启动关系

* stm32 example

* mac . name

* device_instance device_cls

* 新增config添加方式
更新mqtt提示

* plr test

* 移动is_host_mode
新增slave_no_host

* 确保config优先修改生效

* fix graph io

* 支持带参数传入

* 支持物料解析

* 支持物料解析

* device为空的时候不进行绑定或初始化

* protocol node new

* protocol node runnable

* protocol node runnable

* action

* plr suc

* plr suc!!

* plr suc!!

* plr suc!!

* plr msgs

* Feature/device node later init (#43)

* 修改config路径,方便后续打包
增加device_node打印

* 支持plr序列化/init创建

* 统一命名

* import mgr
logger optimize
banner print

* 日志OK

* fix unicorn frame

* banner print

* correct import format

* file path changes

* 取消后补全,在加载设备的时候直接替换

* converter update

* web page update

* 在线device更新,node继承替换

* 修复动作、状态的类型缺失 和 命令提示

* web功能实现结束

* host节点更改完成
新增status时间戳管理
新增每10s动态发现其他node

* ros2类型的节点也应该被包一次

* 修复类型提示

* websocket 动态显示状态

* add workflow & book theme for docs

* add workflow & book theme for docs

* fix workflow build

* fix workflow build

* 理清启动关系

* stm32 example

* mac . name

* device_instance device_cls

* 新增config添加方式
更新mqtt提示

* plr test

* 移动is_host_mode
新增slave_no_host

* 确保config优先修改生效

* fix graph io

* 支持带参数传入

* 支持物料解析

* 支持物料解析

* device为空的时候不进行绑定或初始化

* protocol node new

* protocol node runnable

* protocol node runnable

* action

* plr suc

* plr suc!!

* plr suc!!

* plr suc!!

* plr msgs

* plr

* action

* plr reg fix

* Feature/device node later init (#44)

* 修改config路径,方便后续打包
增加device_node打印

* 支持plr序列化/init创建

* 统一命名

* import mgr
logger optimize
banner print

* 日志OK

* fix unicorn frame

* banner print

* correct import format

* file path changes

* 取消后补全,在加载设备的时候直接替换

* converter update

* web page update

* 在线device更新,node继承替换

* 修复动作、状态的类型缺失 和 命令提示

* web功能实现结束

* host节点更改完成
新增status时间戳管理
新增每10s动态发现其他node

* ros2类型的节点也应该被包一次

* 修复类型提示

* websocket 动态显示状态

* add workflow & book theme for docs

* add workflow & book theme for docs

* fix workflow build

* fix workflow build

* 理清启动关系

* stm32 example

* mac . name

* device_instance device_cls

* 新增config添加方式
更新mqtt提示

* plr test

* 移动is_host_mode
新增slave_no_host

* 确保config优先修改生效

* fix graph io

* 支持带参数传入

* 支持物料解析

* 支持物料解析

* device为空的时候不进行绑定或初始化

* protocol node new

* protocol node runnable

* protocol node runnable

* action

* plr suc

* plr suc!!

* plr suc!!

* plr suc!!

* plr msgs

* plr

* fix convert error
fix async logic error
added async error print

* new device test

* test resource add

* test resource add

* test resource add

* test resource add

* local env setup

* node type fix
temp fix root_node error
fix convert res from type error

* resource tracker

* fix bug from qhh

* fix bug from qhh

* fix bug from qhh

* fix bug from qhh

* refactor MQTT client logging and connection handling; update group ID in config

* driver_params allow empty

* allow other init param

* fix driver param and enhance type hint

* refactor MQConfig to use double quotes for string literals

* fix wrong function calling

* fix wrong function calling

* fix log for mac

* fix networkx compatibility

* add mqtt loggers

* add action to jsonschema converter

* random client id

* type converter & registry

* correct conversion

* fix action publish only from discovered devices

* add "Bio" tag for action doc generation

* 改进module提示

* Fix doc

* mqtt不连接也可用
性价样例提示

* add docs

* 更新plr test案例

* Update intro.md

* 更新有机案例

* skip

---------

Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: Junhan Chang <1700011741@pku.edu.cn>

---------

Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
Co-authored-by: jiawei <miaojiawei@dp.tech>
Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn>
Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: Junhan Chang <1700011741@pku.edu.cn>

* Canonicalize before Open Source (#46)

* big big refactor try01

* refactor 02

---------

Co-authored-by: ck <xiaoyeqiannian@163.com>
Co-authored-by: 王俊杰 <1800011822@pku.edu.cn>
Co-authored-by: q434343 <554662886@qq.com>
Co-authored-by: Xuwznln <xuwznln@gmail.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: wjjxxx <43375851+wjjxxx@users.noreply.github.com>
Co-authored-by: 3218923350 <105201755+3218923350@users.noreply.github.com>
Co-authored-by: Xuwznln <1023025701@qq.com>
Co-authored-by: 王俊杰 <2201110460@stu.pku.edu.cn>
Co-authored-by: jiawei <miaojiawei@dp.tech>
Co-authored-by: Jiawei <91898272+jiawei723@users.noreply.github.com>
Co-authored-by: ColumbiaCC <2100011801@stu.pku.edu.cn>
Co-authored-by: Harvey Que <Q-Query@outlook.com>
2025-04-17 15:09:58 +08:00

1241 lines
41 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}UniLab System Status{% endblock %}
{% block header %}UniLab System Status{% endblock %}
{% block top_info %}
<!-- 系统模式显示 -->
<div class="system-mode-banner">
<div class="mode-indicator {% if is_host_mode %}host-mode{% else %}slave-mode{% endif %}">
系统模式: <strong>{{ "主机模式 (HOST)" if is_host_mode else "从机模式 (SLAVE)" }}</strong>
</div>
</div>
{% if registry_info %}
<div class="registry-info">
{% if registry_info.paths %}
<div class="registry-path">
<strong>注册表路径:</strong>
<ul class="path-list">
{% for path in registry_info.paths %}
<li>
<span class="path">{{ path }}</span>
<span class="folder-link" onclick="openFolder('{{ path }}')" title="打开文件夹">📁</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if registry_info.devices_paths %}
<div class="registry-path">
<strong>设备目录:</strong>
<ul class="path-list">
{% for path in registry_info.devices_paths %}
<li>
<span class="path">{{ path }}</span>
<span class="folder-link" onclick="openFolder('{{ path }}')" title="打开文件夹">📁</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if registry_info.device_comms_paths %}
<div class="registry-path">
<strong>设备通信目录:</strong>
<ul class="path-list">
{% for path in registry_info.device_comms_paths %}
<li>
<span class="path">{{ path }}</span>
<span class="folder-link" onclick="openFolder('{{ path }}')" title="打开文件夹">📁</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if registry_info.resources_paths %}
<div class="registry-path">
<strong>资源目录:</strong>
<ul class="path-list">
{% for path in registry_info.resources_paths %}
<li>
<span class="path">{{ path }}</span>
<span class="folder-link" onclick="openFolder('{{ path }}')" title="打开文件夹">📁</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
<div class="nav-tabs">
{% if is_host_mode and host_node_info.available %}
<a href="#host-node-section" class="nav-tab">主机节点</a>
{% endif %}
<a href="#online-devices-section" class="nav-tab">本地设备</a>
<a href="#devices-section" class="nav-tab">设备类型</a>
<a href="#resources-section" class="nav-tab">资源类型</a>
<a href="#modules-section" class="nav-tab">转换器模块</a>
</div>
{% endblock %}
{% block content %}
<!-- 主机节点信息部分 -->
{% if is_host_mode and host_node_info.available %}
<div class="card" id="host-node-section">
<h2>主机节点信息</h2>
<!-- 主机控制的设备 -->
<div class="host-section">
<h3>已管理设备 <span class="count-badge">{{ host_node_info.devices|length }}</span></h3>
<table class="responsive-table">
<tr>
<th>设备ID</th>
<th>命名空间</th>
<th>状态</th>
</tr>
{% for device_id, device_info in host_node_info.devices.items() %}
<tr>
<td>{{ device_id }}</td>
<td>{{ device_info.namespace }}</td>
<td><span class="status-badge online">{{ "在线" if device_info.is_online else "离线" }}</span></td>
</tr>
{% else %}
<tr>
<td colspan="3" class="empty-state">没有发现已管理的设备</td>
</tr>
{% endfor %}
</table>
</div>
<!-- 主机的动作客户端 -->
<div class="host-section">
<h3>动作客户端 <span class="count-badge">{{ host_node_info.action_clients|length }}</span></h3>
<table class="responsive-table collapsible-table">
<h4>已接纳动作:</h4>
<table class="inner-table">
<tr>
<th>话题</th>
<th>类型</th>
<th></th>
</tr>
{% for action_name, action_info in host_node_info.action_clients.items() %}
<tr class="action-row collapsible-sub-row" data-target="action-cmd-{{ loop.index }}-{{ device_loop_index }}">
<td>{{ action_name }}</td>
<td>{{ action_info.type_name }}</td>
<td><span class="toggle-sub-indicator"></span></td>
</tr>
<tr id="action-cmd-{{ loop.index }}-{{ device_loop_index }}" class="cmd-row" style="display: none;">
<td colspan="5">
<div class="cmd-block">
<strong>发送命令:</strong>
<div class="cmd-line">
<pre>ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_convert }} "{{ action_info.goal_info }}"</pre>
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
</div>
<p class="goal-tip">提示: 根据目标结构修改命令参数</p>
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
</div>
<!-- 主机已订阅的主题 -->
<div class="host-section">
<h3>已订阅主题 <span class="count-badge">{{ host_node_info.subscribed_topics|length }}</span></h3>
<div class="topics-container">
{% if host_node_info.subscribed_topics %}
<div class="topics-list">
{% for topic in host_node_info.subscribed_topics %}
<div class="topic-item">
<span class="topic-name">{{ topic }}</span>
<button class="copy-btn small" onclick="copyToClipboard('{{ topic }}', event)" title="复制主题名">复制</button>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">没有发现已订阅的主题</div>
{% endif %}
</div>
</div>
<!-- 设备状态 -->
{% if host_node_info.device_status %}
<div class="host-section">
<h3>设备状态</h3>
<table class="responsive-table">
<tr>
<th>设备ID</th>
<th>属性</th>
<th></th>
<th>最后更新</th>
</tr>
{% for device_id, properties in host_node_info.device_status.items() %}
{% for prop_name, prop_value in properties.items() %}
<tr>
{% if loop.first %}
<td rowspan="{{ properties|length }}">{{ device_id }}</td>
{% endif %}
<td>{{ prop_name }}</td>
<td>{{ prop_value }}</td>
<td>
{% if device_id in host_node_info.device_status_timestamps and prop_name in host_node_info.device_status_timestamps[device_id] %}
{% set ts_info = host_node_info.device_status_timestamps[device_id][prop_name] %}
{% if ts_info.elapsed >= 0 %}
<span class="timestamp" title="{{ ts_info.timestamp }}">{{ ts_info.elapsed }} 秒前</span>
{% else %}
<span class="timestamp not-updated">未更新</span>
{% endif %}
{% else %}
<span class="timestamp not-updated">无数据</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" class="empty-state">没有设备状态数据</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
{% endif %}
<!-- 当前中断设备部分 -->
<div class="card" id="online-devices-section">
<h2>Local Devices</h2>
<table class="responsive-table">
<tr>
<th>Device ID</th>
<th>节点名称</th>
<th>命名空间</th>
<th>状态项</th>
<th>动作数</th>
</tr>
{% for device_id, device_info in ros_node_info.registered_devices.items() %}
{% set device_loop_index = loop.index %}
<tr class="collapsible-row device-row" data-target="device-detail-{{ loop.index }}">
<td>{{ device_id }}</td>
<td>{{ device_info.node_name }}</td>
<td>{{ device_info.namespace }}</td>
<td>{{ ros_node_info.device_topics.get(device_id, {})|length }}</td>
<td>{{ ros_node_info.device_actions.get(device_id, {})|length }} <span class="toggle-indicator"></span></td>
</tr>
<tr id="device-detail-{{ loop.index }}" class="detail-row" style="display: none;">
<td colspan="5">
<div class="content-full">
UUID: {{ device_info.uuid }}
{% if device_id in ros_node_info.device_topics %}
<h4>已发布状态:</h4>
<table class="inner-table">
<tr>
<th>名称</th>
<th>类型</th>
<th>话题</th>
<th>间隔</th>
<th></th>
</tr>
{% for status_name, status_info in ros_node_info.device_topics[device_id].items() %}
<tr class="topic-row collapsible-sub-row" data-target="topic-cmd-{{ loop.index }}-{{ device_loop_index }}">
<td>{{ status_name }}</td>
<td>{{ status_info.type_name }}</td>
<td>{{ status_info.topic_path }}</td>
<td>{{ status_info.timer_period }}</td>
<td><span class="toggle-sub-indicator"></span></td>
</tr>
<tr id="topic-cmd-{{ loop.index }}-{{ device_loop_index }}" class="cmd-row" style="display: none;">
<td colspan="5">
<div class="cmd-block">
<strong>订阅命令:</strong>
<div class="cmd-line">
<pre>ros2 topic echo {{ status_info.topic_path }}</pre>
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
</div>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% if device_id in ros_node_info.device_actions %}
<h4>已发布动作:</h4>
<table class="inner-table">
<tr>
<th>名称</th>
<th>类型</th>
<th>话题</th>
<th></th>
</tr>
{% for action_name, action_info in ros_node_info.device_actions[device_id].items() %}
<tr class="action-row collapsible-sub-row" data-target="action-cmd-{{ loop.index }}-{{ device_loop_index }}">
<td>{{ action_name }}</td>
<td>{{ action_info.type_name }}</td>
<td>{{ action_info.action_path }}</td>
<td><span class="toggle-sub-indicator"></span></td>
</tr>
<tr id="action-cmd-{{ loop.index }}-{{ device_loop_index }}" class="cmd-row" style="display: none;">
<td colspan="5">
<div class="cmd-block">
<strong>发送命令:</strong>
<div class="cmd-line">
<pre>ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_convert }} "{{ action_info.goal_info }}"</pre>
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
</div>
<p class="goal-tip">提示: 根据目标结构修改命令参数</p>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
<!-- 设备部分 -->
<div class="card" id="devices-section">
<h2>Device Types</h2>
<table class="responsive-table">
<tr>
<th>ID</th>
<th>Name</th>
<th>File Path</th>
<th></th>
</tr>
{% for device in devices %}
<tr class="collapsible-row" data-target="device-info-{{ loop.index }}">
<td>{{ device.id }}</td>
<td>{{ device.name }}</td>
<td class="file-path">
{{ device.file_path }}
<span class="folder-link" onclick="openFolder('{{ device.file_path }}'); event.stopPropagation();" title="打开文件夹">📁</span>
</td>
<td><span class="toggle-indicator"></span></td>
</tr>
<tr id="device-info-{{ loop.index }}" class="detail-row" style="display: none;">
<td colspan="5">
<div class="content-full">
<pre>{{ device.class_json }}</pre>
{% if device.is_online %}
<div class="status-badge"><span class="online-status">在线</span></div>
{% endif %}
{% if device.is_online and device.status_publishers %}
<h4>状态发布者:</h4>
<ul class="detail-list">
{% for status_name, status_info in device.status_publishers.items() %}
<li>
<strong>{{ status_name }}</strong> - 类型: {{ status_info.type }}
<br>话题: {{ status_info.topic }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if device.is_online and device.actions %}
<h4>可用动作:</h4>
<ul class="detail-list">
{% for action_name, action_info in device.actions.items() %}
<li>
<strong>{{ action_name }}</strong> - 类型: {{ action_info.type }}
<br>话题: {{ action_info.topic }}
<br>
<div class="cmd-block">
<strong>发送命令:</strong>
<div class="cmd-line">
<pre>{{ action_info.command }}</pre>
<button class="copy-btn" onclick="copyToClipboard(this.previousElementSibling.textContent, event)">复制</button>
<button class="debug-btn" onclick="toggleDebugInfo(this, event)">调试</button>
<div class="debug-info" style="display:none;">
<pre>{{ action_info|tojson(indent=2) }}</pre>
</div>
</div>
<p class="goal-tip">提示: 根据目标结构修改命令参数</p>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
<!-- 资源部分 -->
<div class="card" id="resources-section">
<h2>Resource Types</h2>
<table class="responsive-table">
<tr>
<th>ID</th>
<th>Name</th>
<th>File Path</th>
</tr>
{% for resource in resources %}
<tr>
<td>{{ resource.id }}</td>
<td>{{ resource.name }}</td>
<td class="file-path">
{{ resource.file_path }}
<span class="folder-link" onclick="openFolder('{{ resource.file_path }}')" title="打开文件夹">📁</span>
</td>
</tr>
{% endfor %}
</table>
</div>
<!-- 模块部分 -->
<div class="card" id="modules-section">
<h2>Converter Modules</h2>
<h3>Loaded Modules</h3>
<table class="responsive-table">
<tr>
<th>Module Path</th>
</tr>
{% for module in modules.names %}
<tr>
<td>{{ module }}</td>
</tr>
{% endfor %}
</table>
<h3>Available Classes
<span class="classes-count">({{ modules.total_count }})</span>
</h3>
<table class="responsive-table">
<tr>
<th>Class Name</th>
</tr>
{% for class_name in modules.classes %}
<tr>
<td>{{ class_name }}</td>
</tr>
{% endfor %}
</table>
</div>
<!-- 返回顶部按钮 -->
<button id="back-to-top" title="返回顶部"></button>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 在页面加载完成后执行初始化
document.addEventListener('DOMContentLoaded', function() {
initFolderOpener();
initCollapsibleRows();
initScrollToSections();
initBackToTop();
initWebSocket();
});
// WebSocket连接
let ws = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectDelay = 3000; // 3秒
let connectionStatusElement = null;
function showConnectionStatus(status, message) {
// 如果状态元素不存在,创建一个
if (!connectionStatusElement) {
connectionStatusElement = document.createElement('div');
connectionStatusElement.className = 'connection-status';
document.body.appendChild(connectionStatusElement);
}
connectionStatusElement.className = 'connection-status ' + status;
connectionStatusElement.innerHTML = `<span class="status-dot"></span><span class="status-text">${message}</span>`;
// 3秒后自动隐藏
if (status === 'connected') {
setTimeout(() => {
connectionStatusElement.style.opacity = '0';
}, 3000);
} else {
connectionStatusElement.style.opacity = '1';
}
}
function initWebSocket() {
// 获取WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/v1/ws/device_status`;
showConnectionStatus('connecting', '正在连接服务器...');
ws = new WebSocket(wsUrl);
ws.onopen = function() {
console.log('WebSocket连接已建立');
showConnectionStatus('connected', '已连接到服务器');
reconnectAttempts = 0;
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'device_status') {
updateDeviceStatus(data.data);
}
} catch (error) {
console.error('处理WebSocket消息时出错:', error);
}
};
ws.onclose = function(event) {
console.log(`WebSocket连接已关闭代码: ${event.code}, 原因: ${event.reason}`);
if (event.wasClean) {
showConnectionStatus('disconnected', '连接已正常关闭');
} else {
showConnectionStatus('error', '连接意外断开');
}
if (reconnectAttempts < maxReconnectAttempts) {
showConnectionStatus('reconnecting', `正在尝试重新连接 (${reconnectAttempts + 1}/${maxReconnectAttempts})...`);
setTimeout(initWebSocket, reconnectDelay);
reconnectAttempts++;
} else {
showConnectionStatus('error', '重连失败,请刷新页面重试');
}
};
ws.onerror = function(error) {
console.error('WebSocket错误:', error);
showConnectionStatus('error', 'WebSocket连接错误');
};
}
function updateDeviceStatus(data) {
const { device_status, device_status_timestamps } = data;
// 查找设备状态表格 - 在host-node-section下寻找具有"设备状态"标题的host-section
const deviceStatusSections = Array.from(document.querySelectorAll('#host-node-section .host-section h3'))
.filter(h3 => h3.textContent.trim() === '设备状态')
.map(h3 => h3.parentElement);
if (deviceStatusSections.length === 0) {
console.log('未找到设备状态部分');
return;
}
const statusSection = deviceStatusSections[0];
const table = statusSection.querySelector('table.responsive-table');
if (!table) {
console.log('未找到设备状态表格');
return;
}
// 保存表头
const header = table.rows[0].cloneNode(true);
// 清空表格(保留表头)
while (table.rows.length > 1) {
table.deleteRow(1);
}
// 没有数据时显示空状态行
if (Object.keys(device_status).length === 0) {
const emptyRow = table.insertRow();
const emptyCell = emptyRow.insertCell();
emptyCell.colSpan = 4;
emptyCell.className = 'empty-state';
emptyCell.textContent = '没有设备状态数据';
return;
}
// 添加数据行
for (const [device_id, properties] of Object.entries(device_status)) {
const propNames = Object.keys(properties);
for (let i = 0; i < propNames.length; i++) {
const prop_name = propNames[i];
const prop_value = properties[prop_name];
const row = table.insertRow();
// 如果是设备的第一个属性添加设备ID单元格
if (i === 0) {
const deviceCell = row.insertCell();
deviceCell.rowSpan = propNames.length;
deviceCell.textContent = device_id;
}
// 添加属性名称单元格
const propCell = row.insertCell();
propCell.textContent = prop_name;
// 添加属性值单元格
const valueCell = row.insertCell();
valueCell.textContent = prop_value;
// 添加时间戳单元格
const timestampCell = row.insertCell();
const tsInfo = device_status_timestamps[device_id]?.[prop_name];
if (tsInfo && tsInfo.elapsed >= 0) {
const timestampSpan = document.createElement('span');
timestampSpan.className = 'timestamp';
timestampSpan.setAttribute('title', tsInfo.timestamp);
timestampSpan.textContent = `${Math.round(tsInfo.elapsed)} 秒前`;
timestampCell.appendChild(timestampSpan);
} else {
const notUpdatedSpan = document.createElement('span');
notUpdatedSpan.className = 'timestamp not-updated';
notUpdatedSpan.textContent = '未更新';
timestampCell.appendChild(notUpdatedSpan);
}
}
}
}
// 文件夹打开功能
function initFolderOpener() {
function openFolder(path) {
if (!path) {
alert("路径为空");
return;
}
// 对于Windows路径确保格式正确
const formattedPath = path.includes(':\\') ? path : path.replace(':', ':\\');
fetch(`/open-folder?path=${encodeURIComponent(formattedPath)}`)
.then(response => response.json())
.then(data => {
if (data.status === 'error') {
alert('错误: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('请求失败: ' + error);
});
}
// 确保openFolder函数在全局范围内可用
window.openFolder = openFolder;
}
// 折叠行功能
function initCollapsibleRows() {
// 设备行点击事件
const deviceRows = document.querySelectorAll('.collapsible-row');
deviceRows.forEach(row => {
row.addEventListener("click", function() {
const targetId = this.getAttribute('data-target');
if (!targetId) return;
const content = document.getElementById(targetId);
if (!content) return;
this.classList.toggle("active");
if (content.style.display === "table-row") {
content.style.display = "none";
const indicator = this.querySelector('.toggle-indicator');
if (indicator) indicator.textContent = "▼";
} else {
content.style.display = "table-row";
const indicator = this.querySelector('.toggle-indicator');
if (indicator) indicator.textContent = "▲";
}
});
});
// 子行点击事件(状态和动作行)
const subRows = document.querySelectorAll('.collapsible-sub-row');
subRows.forEach(row => {
row.addEventListener("click", function(e) {
const targetId = this.getAttribute('data-target');
if (!targetId) return;
const content = document.getElementById(targetId);
if (!content) return;
if (content.style.display === "table-row" || content.style.display === "") {
content.style.display = "none";
const indicator = this.querySelector('.toggle-sub-indicator');
if (indicator) indicator.textContent = "▼";
} else {
content.style.display = "table-row";
const indicator = this.querySelector('.toggle-sub-indicator');
if (indicator) indicator.textContent = "▲";
}
// 阻止事件冒泡
e.stopPropagation();
});
});
}
// 初始化导航标签跳转
function initScrollToSections() {
const navTabs = document.querySelectorAll('.nav-tab');
navTabs.forEach(tab => {
tab.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId && targetId.startsWith('#')) {
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth'
});
}
}
});
});
}
// 初始化返回顶部按钮
function initBackToTop() {
const backToTopBtn = document.getElementById("back-to-top");
if (!backToTopBtn) return;
// 显示/隐藏按钮
window.addEventListener('scroll', function() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
backToTopBtn.style.display = "block";
} else {
backToTopBtn.style.display = "none";
}
});
// 点击返回顶部
backToTopBtn.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
// 复制功能
function copyToClipboard(text, event) {
if (event) event.stopPropagation();
const textToCopy = text.trim();
// 使用现代API复制
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(textToCopy)
.then(() => {
showCopySuccess(event.target);
})
.catch(err => {
console.error('复制失败: ', err);
fallbackCopy(textToCopy, event.target);
});
} else {
fallbackCopy(textToCopy, event.target);
}
}
// 复制功能的后备方案
function fallbackCopy(text, button) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showCopySuccess(button);
} else {
alert("复制失败,请手动复制");
}
} catch (err) {
console.error('复制失败: ', err);
alert("复制失败,请手动复制");
}
document.body.removeChild(textarea);
}
// 显示复制成功
function showCopySuccess(button) {
const originalText = button.textContent;
button.textContent = "已复制!";
button.classList.add("copy-success");
setTimeout(() => {
button.textContent = originalText;
button.classList.remove("copy-success");
}, 1500);
}
// 调试信息切换
function toggleDebugInfo(btn, event) {
event.stopPropagation();
const debugInfo = btn.nextElementSibling;
if (debugInfo.style.display === "none" || debugInfo.style.display === "") {
debugInfo.style.display = "block";
btn.textContent = "隐藏调试";
} else {
debugInfo.style.display = "none";
btn.textContent = "调试";
}
}
</script>
<style>
/* 基础样式改进 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.card {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
border-radius: 8px;
margin-bottom: 30px;
padding: 20px;
background-color: #fff;
transition: all 0.3s ease;
scroll-margin-top: 20px; /* 为锚点添加滚动边距 */
}
.card h2 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
/* 导航标签美化 */
.nav-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
flex-wrap: wrap;
}
.nav-tab {
padding: 8px 16px;
margin-right: 5px;
background-color: #f5f5f5;
border-radius: 5px 5px 0 0;
text-decoration: none;
color: #555;
transition: all 0.2s ease;
font-weight: 500;
}
.nav-tab:hover {
background-color: #e0e0e0;
color: #333;
}
/* 表格样式美化 */
.responsive-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
font-size: 14px;
}
.responsive-table th {
background-color: #f5f5f5;
color: #333;
font-weight: 600;
text-align: left;
padding: 12px 15px;
border-bottom: 2px solid #ddd;
}
.responsive-table td {
padding: 10px 15px;
border-bottom: 1px solid #eee;
vertical-align: middle;
}
/* 可折叠行美化 */
.collapsible-row {
transition: background-color 0.2s ease;
cursor: pointer;
}
.collapsible-row:hover {
background-color: #f9f9f9;
}
.collapsible-row.active {
background-color: #f0f7ff;
border-left: 3px solid #4285f4;
}
.toggle-indicator, .toggle-sub-indicator {
font-size: 12px;
color: #888;
float: right;
transition: transform 0.2s ease;
}
.collapsible-row.active .toggle-indicator {
transform: rotate(180deg);
color: #4285f4;
}
/* 详情行样式 */
.detail-row td {
padding: 0;
}
.content-full {
padding: 15px;
background-color: #fafafa;
border-top: 1px solid #eee;
}
/* 子表格(内部表格)样式 */
.inner-table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
font-size: 13px;
}
.inner-table th {
background-color: #eef2f7;
padding: 8px 10px;
font-weight: 500;
}
.inner-table td {
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
/* 状态和动作行样式 */
.topic-row, .action-row {
background-color: #f9f9f9;
cursor: pointer;
transition: background-color 0.2s ease;
}
.topic-row:hover, .action-row:hover {
background-color: #f0f0f0;
}
/* 复制按钮美化 */
.toggle-cmd-btn {
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.toggle-cmd-btn:hover {
background: #e0e0e0;
}
.copy-btn {
background: #eef;
border: 1px solid #ccf;
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
margin-left: 10px;
transition: all 0.2s ease;
}
.copy-btn:hover {
background: #ddf;
}
.copy-success {
background: #dffddf !important;
color: #2c7c2c !important;
}
.debug-btn {
background: #fee;
border: 1px solid #fcc;
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
margin-left: 5px;
font-size: 12px;
transition: all 0.2s ease;
}
.debug-btn:hover {
background: #fdd;
}
/* 命令块样式优化 */
.cmd-block {
background: #f8f8f8;
border: 1px solid #eee;
border-radius: 4px;
padding: 12px;
margin: 8px 0;
}
.cmd-line {
display: flex;
align-items: center;
margin: 8px 0;
background: #fff;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
overflow-x: auto;
font-family: 'Consolas', 'Monaco', monospace;
}
.cmd-line pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
flex: 1;
}
/* 路径和文件夹链接样式 */
.folder-link {
cursor: pointer;
margin-left: 5px;
color: #4285f4;
transition: transform 0.2s ease;
}
.folder-link:hover {
transform: scale(1.1);
}
.path-list {
list-style-type: none;
padding-left: 10px;
}
.path-list li {
margin-bottom: 5px;
}
/* 返回顶部按钮 */
#back-to-top {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99;
font-size: 18px;
border: none;
outline: none;
background-color: #4285f4;
color: white;
cursor: pointer;
padding: 10px 15px;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease;
}
#back-to-top:hover {
background-color: #3367d6;
transform: translateY(-2px);
}
/* 在线状态标签 */
.online-status {
display: inline-block;
background-color: #4caf50;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
/* 详情列表样式 */
.detail-list {
padding-left: 15px;
}
.detail-list li {
margin-bottom: 15px;
padding-bottom: 12px;
border-bottom: 1px dashed #eee;
}
/* 提示文本样式 */
.goal-tip {
font-size: 12px;
color: #666;
margin-top: 8px;
font-style: italic;
background: #fffde7;
padding: 5px 8px;
border-radius: 3px;
border-left: 3px solid #ffd54f;
}
/* 调试信息样式 */
.debug-info {
margin-top: 8px;
background: #fffde7;
border: 1px solid #fff9c4;
border-radius: 4px;
padding: 10px;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
/* 时间戳样式 */
.timestamp {
font-size: 13px;
background-color: #f5f5f5;
padding: 3px 6px;
border-radius: 3px;
color: #555;
transition: background-color 0.3s ease;
}
.timestamp.not-updated {
background-color: #fff3e0;
color: #e65100;
}
.timestamp[title]:hover::after {
content: '时间戳: ' attr(title);
position: absolute;
background: #333;
color: #fff;
padding: 5px 10px;
border-radius: 4px;
z-index: 10;
font-size: 12px;
margin-top: 5px;
margin-left: -10px;
}
/* 响应式样式优化 */
@media screen and (max-width: 768px) {
.responsive-table {
width: 100%;
overflow-x: auto;
display: block;
}
.nav-tabs {
overflow-x: auto;
white-space: nowrap;
display: block;
padding-bottom: 10px;
}
.nav-tab {
display: inline-block;
margin-bottom: 5px;
}
.file-path {
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cmd-block {
padding: 10px;
overflow-x: auto;
}
.cmd-block pre {
white-space: pre-wrap;
word-break: break-word;
font-size: 12px;
}
.device-row td:nth-child(3) {
display: none;
}
}
/* 动画效果 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.detail-row {
animation: fadeIn 0.3s ease;
}
/* 连接状态样式 */
.connection-status {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
background-color: #f5f5f5;
z-index: 1000;
display: flex;
align-items: center;
font-size: 14px;
transition: opacity 0.3s ease;
}
.connection-status .status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.connection-status.connecting .status-dot {
background-color: #ffc107;
animation: pulse 1.5s infinite;
}
.connection-status.connected .status-dot {
background-color: #4caf50;
}
.connection-status.disconnected .status-dot {
background-color: #9e9e9e;
}
.connection-status.reconnecting .status-dot {
background-color: #ff9800;
animation: pulse 1.5s infinite;
}
.connection-status.error .status-dot {
background-color: #f44336;
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
</style>
{% endblock %}