Linux设备模型4

Linux设备模型4(基于Linux6.6)---attribute介绍

 


一、前情回顾

sysfs是一个基于RAM的文件系统,它和kobject一起,可以将kernel的数据结构导出到用户空间,以文件目录结构的形式,提供对这些数据结构(以及数据结构的属性)的访问支持。

sysfs具备文件系统的所有属性,而本文主要侧重其设备模型的特性,因此不会涉及过多的文件系统实现细节,而只介绍sysfs在Linux设备模型中的作用和使用方法。具体包括:

  • sysfs和kobject的关系
  • attribute的概念
  • sysfs的文件系统操作接口

二、sysfs和Kobject的关系

sysfskobject 是 Linux 内核中两个密切相关的概念,它们共同提供了内核与用户空间之间的交互接口。kobject 是内核对象的基础数据结构,而 sysfs 则是一个文件系统,它提供了一种通过文件接口访问内核对象的方式。

2.1 kobject(内核对象)

kobject 是 Linux 内核中的一个数据结构,用于表示内核中的对象(例如,设备、驱动程序、内核模块等)。它是内核对象系统的一部分,用来管理内核对象的生命周期、属性和关系。

  • 作用kobject 代表一个内核对象,并且提供了一个通用的接口,允许内核对象在内核中进行管理和交互。
  • 属性管理kobject 允许内核对象定义和管理自己的属性,这些属性通常以键值对的形式存储,可以通过 sysfs 进行访问和修改。
  • 通知机制kobject 可以通过 kobject_uevent() 函数发送事件,通知用户空间的程序(如 udev)有关对象状态的变化。

2.2 sysfs(系统文件系统)

sysfs 是一个虚拟文件系统,用于在用户空间和内核空间之间进行交互。它暴露了内核对象的属性,并允许用户空间程序通过文件操作(如读取、写入)来访问和修改这些属性。

  • 作用sysfs 提供了一个统一的接口,使得用户空间程序能够访问内核对象的属性、状态及配置信息。它为内核对象的属性提供了一个文件系统接口,可以通过简单的文件操作(如 catecho 等)来获取和设置属性值。

  • 目录结构sysfs 中的目录结构通常与内核对象的层级结构相对应。例如,设备驱动程序、网络接口、PCI 设备等对象都会在 /sys 下有对应的目录,用户可以通过路径访问它们。

    典型路径示例:

    • /sys/class/net/eth0/:表示一个网络设备(例如 eth0)的相关信息。
    • /sys/devices/pci0000:00/0000:00:1f.2/:表示一个 PCI 设备的信息。

2.3 kobject 与 sysfs 的关系

  • 创建和管理:当内核创建一个新的 kobject 时,通常会在 sysfs 中为该对象创建一个对应的目录,并通过该目录暴露 kobject 的属性。这个过程通过 kobject_add()sysfs_create_group() 等函数实现。

  • 属性的表示kobject 本身可以包含多个属性,每个属性对应一个文件。这些文件会出现在 sysfs 中的相应目录下。当用户通过 sysfs 文件访问属性时,内核会调用 kobject 中定义的回调函数来处理这些操作。

  • 属性与文件系统的交互sysfs 文件系统通过将 kobject 中的属性与文件系统中的文件进行映射,允许用户空间通过读取和写入文件来操作内核对象。例如,当用户通过 cat /sys/class/net/eth0/operstate 命令读取网络接口状态时,内核会访问对应的 kobject,获取该属性的值并返回。

  • 事件通知:当 kobject 的状态发生变化时,内核可以通过 kobject_uevent() 向用户空间发送 uevent 事件,通知相关的用户空间程序(如 udev)。sysfs 则可以通过文件系统接口提供有关事件的进一步信息。

2.4 示例

假设有一个内核对象,比如一个网络设备 eth0。当内核为这个设备创建一个 kobject 时,内核会在 sysfs 中为 eth0 创建一个目录,类似于 /sys/class/net/eth0/,并将该设备的相关属性(如状态、MAC 地址等)映射到该目录下的文件。用户可以通过访问这些文件来查询或修改设备的属性。

例如:

cat /sys/class/net/eth0/address 会显示该网络接口的 MAC 地址。
echo "up" > /sys/class/net/eth0/operstate 会将网络接口的状态更改为 "up"

2.5 代码说明

在设备模型kobject文章中,有提到过,每一个kobject,都会对应sysfs中的一个目录。因此在将Kobject添加到Kernel时,create_dir接口会调用sysfs文件系统的创建目录接口,创建和kobject对应的目录,相关的代码如下:

 lib/kobject.c

static int create_dir(struct kobject *kobj)
{
	const struct kobj_type *ktype = get_ktype(kobj);
	const struct kobj_ns_type_operations *ops;
	int error;

	error = sysfs_create_dir_ns(kobj, kobject_namespace(kobj));
	if (error)
		return error;

	if (ktype) {
		error = sysfs_create_groups(kobj, ktype->default_groups);
		if (error) {
			sysfs_remove_dir(kobj);
			return error;
		}
	}

	/*
	 * @kobj->sd may be deleted by an ancestor going away.  Hold an
	 * extra reference so that it stays until @kobj is gone.
	 */
	sysfs_get(kobj->sd);

	/*
	 * If @kobj has ns_ops, its children need to be filtered based on
	 * their namespace tags.  Enable namespace support on @kobj->sd.
	 */
	ops = kobj_child_ns_ops(kobj);
	if (ops) {
		BUG_ON(!kobj_ns_type_is_valid(ops->type));
		BUG_ON(!kobj_ns_type_registered(ops->type));

		sysfs_enable_ns(kobj->sd);
	}

	return 0;
}

 fs/sysfs/dir.c

 

/**
 * sysfs_create_dir_ns - create a directory for an object with a namespace tag
 * @kobj: object we're creating directory for
 * @ns: the namespace tag to use
 */
int sysfs_create_dir_ns(struct kobject *kobj, const void *ns)
{
	struct kernfs_node *parent, *kn;
	kuid_t uid;
	kgid_t gid;

	if (WARN_ON(!kobj))
		return -EINVAL;

	if (kobj->parent)
		parent = kobj->parent->sd;
	else
		parent = sysfs_root_kn;

	if (!parent)
		return -ENOENT;

	kobject_get_ownership(kobj, &uid, &gid);

	kn = kernfs_create_dir_ns(parent, kobject_name(kobj), 0755, uid, gid,
				  kobj, ns);
	if (IS_ERR(kn)) {
		if (PTR_ERR(kn) == -EEXIST)
			sysfs_warn_dup(parent, kobject_name(kobj));
		return PTR_ERR(kn);
	}

	kobj->sd = kn;
	return 0;
}

三、 attribute分析

在 Linux 内核中,attribute(属性) 是与内核对象相关的值,它们可以被用来表示内核对象的状态、配置信息或其他重要数据。每个属性通常会对应一个文件,这些文件会暴露在 sysfs 文件系统中,允许用户空间通过读取或写入这些文件来获取或修改内核对象的状态。属性的功能和作用可以从以下几个方面进行概述:

1. 定义和功能

  • 属性的定义:在 Linux 内核中,属性是与 kobject(内核对象)关联的数据项。每个 kobject 可以拥有多个属性,每个属性通常表示一个关键的配置或状态值。属性值可以是数字、字符串、布尔值等数据类型。

  • 功能:属性用于描述内核对象的状态或配置,通常通过文件接口暴露在 sysfs 中。用户空间程序可以通过对这些文件的操作(如读取、写入)来查看或修改属性的值。例如,网络设备的状态、磁盘设备的读写权限、温度传感器的温度值等,都可以通过属性来管理。

2. 与 sysfs 的关系

  • sysfs 文件系统sysfs 是一个虚拟文件系统,它通过将内核对象的属性暴露为文件来与用户空间进行交互。每个属性通常都会映射到 sysfs 中的一个文件,用户可以通过文件系统接口访问这些属性。通过 sysfs,属性变得容易访问、读取和修改,用户可以像操作普通文件一样操作内核对象的属性。

  • 创建属性:内核通过 sysfs_create_file() 等函数创建属性文件,并将其与对应的 kobject 关联。属性文件会出现在与该 kobject 相关的目录下。例如,对于一个网络设备 eth0,可以通过 /sys/class/net/eth0/ 目录访问该设备的属性,如 address(MAC 地址)、operstate(接口状态)等。

3. 属性类型和操作

  • 只读属性:某些属性可能仅允许读取,而不允许写入。读取这些属性通常会返回当前状态或配置信息。例如,/sys/class/net/eth0/address 文件会返回网络接口 eth0 的 MAC 地址。用户可以读取该文件,但不能直接修改它。

  • 可写属性:其他属性可以被修改,通常用于配置或控制设备行为。比如,通过写入 /sys/class/net/eth0/operstate 来改变网络接口的状态。例如,可以将 eth0 的状态设置为 "up" 或 "down"。

  • 回调函数:当属性的值被读取或修改时,内核会通过回调函数(例如 show()store() 函数)来处理相应的操作。回调函数实现了对属性的访问控制和数据转换。

4. 属性的应用场景

  • 设备管理:属性广泛应用于设备驱动程序中,表示设备的各种状态和配置。例如,网络设备的速率、硬件错误状态、I/O 控制权限等都可以通过属性来管理。

  • 系统监控:系统中的许多监控工具和应用程序(如 topiotop)通过读取 sysfs 中的属性来获取系统资源的使用情况。例如,CPU 温度、内存使用情况、硬盘健康状态等都可以通过属性获取。

  • 动态配置:用户空间应用程序可以通过写入属性文件来动态调整内核对象的行为或配置。例如,可以通过写入 /sys/class/net/eth0/operstate 文件来启用或禁用网络接口,或者通过修改 /sys/class/power_supply/BAT0/charging_enabled 来控制电池的充电状态。

3.1、 attribute的功能概述

在sysfs中,为什么会有attribute的概念呢?其实它是对应kobject而言的,指的是kobject的“属性”。我们知道,

sysfs中的目录描述了kobject,而kobject是特定数据类型变量(如struct device)的体现。因此kobject的属性,就是这些变量的属性。它可以是任何东西,名称、一个内部变量、一个字符串等等。而attribute,在sysfs文件系统中是以文件的形式提供的,即:kobject的所有属性,都在它对应的sysfs目录下以文件的形式呈现。这些文件一般是可读、写的,而kernel中定义了这些属性的模块,会根据用户空间的读写操作,记录和返回这些attribute的值。

总结一下:所谓的attibute,就是内核空间和用户空间进行信息交互的一种方法。例如某个driver定义了一个变量,却希望用户空间程序可以修改该变量,以控制driver的运行行为,那么就可以将该变量以sysfs attribute的形式开放出来。

Linux内核中,attribute分为普通的attribute和二进制attribute,如下:

 include/linux/sysfs.h

 struct attribute {
	const char		*name;
	umode_t			mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	bool			ignore_lockdep:1;
	struct lock_class_key	*key;
	struct lock_class_key	skey;
#endif
};
 
struct bin_attribute {
	struct attribute	attr;
	size_t			size;
	void			*private;
	ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *,
			char *, loff_t, size_t);
	ssize_t (*write)(struct file *, struct kobject *, struct bin_attribute *,
			 char *, loff_t, size_t);
	int (*mmap)(struct file *, struct kobject *, struct bin_attribute *attr,
		    struct vm_area_struct *vma);
};

struct attribute为普通的attribute,使用该attribute生成的sysfs文件,只能用字符串的形式读写(后面会说为什么)。而struct bin_attribute在struct attribute的基础上,增加了read、write等函数,因此它所生成的sysfs文件可以用任何方式读写。

说完基本概念,两个问题:

kernel怎么把attribute变成sysfs中的文件呢?

用户空间对sysfs的文件进行的读写操作,怎么传递给kernel呢?

3.2、 attibute文件的创建

在Linux内核中,attibute文件的创建是由fs/sysfs/file.c中sysfs_create_file接口完成的,该接口的实现没有什么特殊之处,大多是文件系统相关的操作,和设备模型没有太多的关系,这里先略过不提。

include/linux/sysfs.h

static inline int __must_check sysfs_create_file(struct kobject *kobj,
						 const struct attribute *attr)
{
	return sysfs_create_file_ns(kobj, attr, NULL);
}

3.3、 attibute文件的read和write

 

以下是详细的源码分析,重点介绍 readwrite 的实现。

1. 核心概念:struct attributesysfs

sysfs 是一个虚拟文件系统,用于暴露内核对象及其属性给用户空间。每个 attribute 文件通过 struct attribute 结构体定义,文件的读取和写入操作通过 sysfs_ops 结构体中的 showstore 函数来实现。

struct attribute 结构体

struct attribute 是一个基础结构体,表示单个 sysfs 属性,它包含属性的名称和文件的访问权限。

struct attribute {
    const char *name;  // 属性名称(文件名)
    umode_t mode;      // 文件权限,定义文件的读写权限
};
  • name:属性的名称,文件系统中显示的文件名。
  • mode:文件的权限,通常使用 Linux 标准权限位,如 S_IRUGO(可读),S_IWUSR(可写)等。

sysfs_ops 结构体

sysfs_ops 结构体定义了如何处理 sysfs 文件的读取和写入操作:

struct sysfs_ops {
    ssize_t (*show)(struct kobject *kobj, struct attribute *attr, char *buf);
    ssize_t (*store)(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count);
};
  • show:当读取该 sysfs 文件时,会调用 show 函数。该函数将内核对象的数据格式化后写入 buf 中,返回值为实际写入的字节数。
  • store:当向该 sysfs 文件写入数据时,会调用 store 函数。该函数将用户提供的数据(通过 buf)传递给内核处理,并返回处理的字节数。

2. attribute 文件的 readwrite 流程

  • read 操作:当用户空间读取 sysfs 文件时,内核会调用该文件对应的 show 函数,将内核对象的值写入到用户提供的缓冲区,并返回数据的大小。
  • write 操作:当用户空间写入 sysfs 文件时,内核会调用该文件对应的 store 函数,并将用户提供的数据传递给内核进行处理。

read 的实现流程:

  1. 用户通过 cat 或其他工具读取 /sys 下的某个属性文件。
  2. sysfs 内部会通过 vfs_read 调用文件的 f_op->read 操作(即 sysfsread 操作)。
  3. read 调用会触发 sysfs_ops.show 函数。
  4. show 函数将属性数据格式化并写入缓冲区。
  5. 数据从缓冲区返回给用户空间。

write 的实现流程:

  1. 用户通过 echo 或其他工具写入数据到 /sys 下的某个属性文件。
  2. sysfs 内部会通过 vfs_write 调用文件的 f_op->write 操作(即 sysfswrite 操作)。
  3. write 调用会触发 sysfs_ops.store 函数。
  4. store 函数将用户提供的数据处理后,更新内核对象的状态。
  5. 返回处理的字节数。

3. 内核源码详细分析:Linux 6.1 内核

在 Linux 6.1 内核中,sysfs 的核心代码位于 fs/sysfs/ 目录下。特别是,关于 readwrite 操作的实现可以在以下源文件中找到:

  • fs/sysfs/file.c:这个文件负责 sysfs 文件的处理,包括读取和写入操作。

关键函数

  • sysfs_read_file

    sysfs_read_file 是处理 sysfs 文件读取请求的函数,它会调用 show 函数来获取属性的值。

  • ssize_t sysfs_read_file(struct file *filp, char __user *buf,
                            size_t count, loff_t *ppos)
    {
        struct kobject *kobj = filp->f_path.dentry->d_parent->d_inode->i_private;
        struct attribute *attr = filp->f_path.dentry->d_inode->i_private;
        ssize_t retval = 0;
    
        if (*ppos > 0)
            return 0;  // EOF
    
        retval = attr->show(kobj, attr, buf);
        if (retval >= 0)
            *ppos = retval;
    
        return retval;
    }
    
    • 这个函数通过 filp->f_path.dentry->d_inode->i_private 获取到与该文件相关的 kobjectattribute
    • 然后,它调用 attribute->show 来读取数据,将内核数据写入到用户提供的缓冲区 buf 中。
    • 最后,返回实际读取的字节数。
  • sysfs_write_file

    sysfs_write_file 是处理 sysfs 文件写入请求的函数,它会调用 store 函数来更新内核对象的属性值。

  • ssize_t sysfs_write_file(struct file *filp, const char __user *buf,
                             size_t count, loff_t *ppos)
    {
        struct kobject *kobj = filp->f_path.dentry->d_parent->d_inode->i_private;
        struct attribute *attr = filp->f_path.dentry->d_inode->i_private;
        ssize_t retval;
    
        if (*ppos > 0)
            return -EINVAL;
    
        retval = attr->store(kobj, attr, buf, count);
        if (retval >= 0)
            *ppos = retval;
    
        return retval;
    }
    
    • 这个函数通过 filp->f_path.dentry->d_inode->i_private 获取到与该文件相关的 kobjectattribute
    • 然后,它调用 attribute->store 函数,将用户空间的输入数据存储到内核中。
    • 最后,返回处理的字节数。

4. showstore 函数实现

在实际的设备驱动程序中,showstore 函数通常会根据设备的实际状态来定义。这些函数的返回值表示操作的字节数,并且必须遵循一些规范:

  • show 函数:通常会将内核中的数据格式化为字符串,并将其写入到用户提供的缓冲区(buf)中,返回值是写入的字节数。

    示例:

  • static ssize_t my_device_show(struct kobject *kobj, struct attribute *attr, char *buf)
    {
        return sprintf(buf, "%d\n", my_device_state);
    }
    
  • store 函数:通常会解析用户空间传入的数据,并更新内核中的相应状态,返回值是处理的字节数。

    示例:

  • static ssize_t my_device_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count)
    {
        if (buf[0] == '1') {
            my_device_state = 1;
        } else {
            my_device_state = 0;
        }
        return count;
    }
    

小结

  • sysfs 文件的 readwrite 操作通过 sysfs_read_filesysfs_write_file 函数实现,它们分别调用 showstore 函数来读取和写入内核对象的属性。
  • show 函数负责读取内核数据并将其格式化后写入用户缓冲区,而 store 函数则负责从用户空间获取数据并更新内核状态。
  • sysfs 提供了一个灵活的机制,使得内核可以通过简单的文件操作与用户空间进行交互。

四、sysfs在设备模型中的应用总结

让我们通过设备模型class.c中有关sysfs的实现,来总结一下sysfs的应用方式。

首先,在class.c中,定义了Class所需的ktype以及sysfs_ops类型的变量,如下:

 drivers/base/class.c

 static const struct sysfs_ops class_sysfs_ops = {
	.show	= class_attr_show,        
	.store	= class_attr_store,
};
 
/*  */
static struct kobj_type class_ktype = {
	.sysfs_ops	= &class_sysfs_ops,
	.release	= class_release,
	.child_ns_type	= class_child_ns_type,
};

由前面章节的描述可知,所有class_type的Kobject下面的attribute文件的读写操作,都会交给class_attr_show和class_attr_store两个接口处理。以class_attr_show为例:

 drivers/base/class.c

 #define to_class_attr(_attr) container_of(_attr, struct class_attribute, attr)
 
 
static ssize_t class_attr_show(struct kobject *kobj, struct attribute *attr,
			       char *buf)
{
	struct class_attribute *class_attr = to_class_attr(attr);
	struct class_private *cp = to_class(kobj);
	ssize_t ret = -EIO;
 
	if (class_attr->show)
		ret = class_attr->show(cp->class, class_attr, buf);
	return ret;
}

该接口使用container_of从struct attribute类型的指针中取得一个class模块的自定义指针:struct class_attribute,该指针中包含了class模块自身的show和store接口。下面是struct class_attribute的声明:

include/linux/sysfs.h

struct sysfs_ops {
	ssize_t	(*show)(struct kobject *, struct attribute *, char *);
	ssize_t	(*store)(struct kobject *, struct attribute *, const char *, size_t);
};
  • show 函数:当用户通过 cat 命令或类似方式读取 sysfs 文件时,内核会调用 show 函数。该函数负责将数据格式化并返回给用户空间。例如,返回一个字符串表示设备的当前状态。

  • store 函数:当用户通过 echo 命令或类似方式向 sysfs 文件写入数据时,内核会调用 store 函数。该函数负责解析用户写入的数据,并更新内核中的状态或设置。

led-class 驱动为例,讲解 attribute 如何使用。

1. 背景:led-class 驱动

led-class 驱动是一个简单的示例,演示如何通过 sysfs 文件操作 LED 设备的状态。它通过 sysfs 提供两个基本操作:读取 LED 的状态(例如,是否开启)和设置 LED 的状态(例如,开启或关闭)。这些操作通常通过 attribute 结构体来实现。

2. attribute 结构体

led-class 驱动中,我们使用 attribute 结构体定义每个设备的属性。这些属性会映射到 sysfs 文件系统中的文件。

struct attribute {
    const char *name;   // 属性名称
    umode_t mode;       // 属性的访问权限
};

每个 attribute 结构体代表一个单一的属性,例如 LED 的状态、亮度等。

3. 创建 sysfs 属性

以下是一个简单的 led-class 驱动中如何使用 attribute 的示例:

(1) 定义 LED 状态的 attribute

首先,我们定义一个 attribute 来表示 LED 的状态,通常这个属性可以是一个读取操作(如显示当前 LED 是否开启)和一个写入操作(如开启或关闭 LED)。

static struct attribute led_attribute = {
    .name = "led_state",   // 属性名称,表示 LED 状态
    .mode = S_IRUGO | S_IWUSR, // 允许读取和写入
};
  • name 是该属性的名称,在 sysfs 中对应的文件名。
  • mode 表示权限,S_IRUGO 表示所有用户可读,S_IWUSR 表示只有用户(写入者)可写。

(2) 创建对应的 showstore 函数

为了处理对 sysfs 文件的读写,我们需要定义 showstore 函数。show 函数用于返回 LED 当前状态,store 函数用于设置 LED 的状态。

static ssize_t led_state_show(struct kobject *kobj, struct attribute *attr, char *buf)
{
    // 假设我们有一个名为 "led_on" 的标志来表示 LED 是否开启
    bool led_on = true;  // 假设 LED 当前是开启状态
    
    // 将 LED 状态输出到用户空间,"1" 表示开启,"0" 表示关闭
    return sprintf(buf, "%d\n", led_on ? 1 : 0);
}

static ssize_t led_state_store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count)
{
    // 假设 LED 的状态是通过一个变量控制
    bool led_on = (buf[0] == '1');  // 如果用户输入 "1",则开启 LED
    
    // 更新 LED 状态(此处仅为示例,实际操作可能涉及硬件控制)
    if (led_on)
        printk(KERN_INFO "LED turned on\n");
    else
        printk(KERN_INFO "LED turned off\n");
    
    return count;  // 返回写入的字节数
}
  • led_state_show 用于将 LED 当前状态以字符串形式返回给用户空间。
  • led_state_store 用于将用户空间传入的数据解析并设置 LED 状态。

(3) 关联 attributesysfs_ops

接下来,我们将 showstore 函数与 attribute 关联起来。为此,我们需要创建一个 sysfs_ops 结构体,并将其应用于相应的 attribute

static struct sysfs_ops led_sysfs_ops = {
    .show = led_state_show,
    .store = led_state_store,
};

(4) 创建 kobject 并注册属性

一旦 attributesysfs_ops 设置好,我们就可以创建一个 kobject,并将属性添加到它。kobjectsysfs 中的基本实体,代表一个设备或子系统。

static struct kobject *led_kobj;

static int __init led_module_init(void)
{
    int retval;

    // 创建一个 kobject,表示 LED 设备
    led_kobj = kobject_create_and_add("led_device", kernel_kobj);
    if (!led_kobj)
        return -ENOMEM;

    // 使用 sysfs_ops 创建属性并添加到 kobject 中
    retval = sysfs_create_file(led_kobj, &led_attribute);
    if (retval)
        kobject_put(led_kobj);

    return retval;
}
  • kobject_create_and_add 创建一个 kobject 对象,并将其添加到 kernel_kobj(即内核的根 kobject)中。
  • sysfs_create_file 用于将 led_attribute 作为文件创建在 led_kobj 下,用户空间可以通过此文件来读取或修改 LED 的状态。

4. 访问 sysfs 属性

通过以上的步骤,我们可以在 /sys 文件系统中看到一个新的文件 /sys/led_device/led_state。用户空间可以通过读取或写入该文件来操作 LED 状态。例如:

  • 读取 LED 状态:
cat /sys/led_device/led_state

如果 LED 开启,输出可能是:

1
  • 设置 LED 状态:
echo 1 > /sys/led_device/led_state   # 开启 LED
echo 0 > /sys/led_device/led_state   # 关闭 LED

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值