基于STM32F103C系列开发无线传输的家用血氧仪设计(设计使用的MAX30100血氧传感器,WiFi模块,OLED显示模块,蜂鸣器,同时设计家用简易APP软件)
最近遇到针对家用设备数据测算的样品设计中,个人使用了一下家里买现有的血氧检测仪,除了简单的界面规划设计外和数值反馈,只能给人一个数值上面的参考,然而反馈回来的信息还是需要自己去网上比对自己的血氧数据是否处在一个正常且稳定的一个标准范围上,刚好自己之前做过相关类型的设计,同时自己的储物柜上还有一块MAX30100系列的血氧检测传感器,为了更适合家用检测设计和对血氧数据分析更加便捷,于是对之前项目设计进行了升级。
设计需求和思路
设计的主要宗旨是检测设备要小,可便携带,数据反馈快,数据回传分析反馈快等特点,如下是个人对设计开发方向的大致过程。
核心传感器MAX30100血氧检测传感器
血氧检测传感器选型的是比较在市面上面存在的传感器,其中,该类传感器上面的工作原理和运行设计在 相关的传感器的文献上都有详细的介绍,我查看的该类传感器只要是这几位博主的博客
手机APP设计
源码程序解析
作为一个家庭项目而且考虑到用户群体,个人设计没有设计那么复杂,首先是对app的界面设计
界面的设计离不开B站开发者的帮助,同时B站设计开发者这个网站网站个人只是在原来的程序上面进行了可视化界面的改动--B站上面可以实现的程序源码,改动了自己的界面设计和图片布局
界面主函数源码MainActivity.java
public void Get() {
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://api.heclouds.com/devices/" + DeviceID + "/datapoints?datastream_id=" + key1).header("api-key", ApiKey).build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
parseJSONWithGSON(responseData);
JsonRootBean app = new Gson().fromJson(responseData, JsonRootBean.class);
List<Datastreams> streams = app.getData().getDatastreams();
List<Datapoints> points = streams.get(0).getDatapoints();
value = points.get(0).getValue();
time = points.get(0).getAt();
data.post(new Runnable() {
@Override
public void run() {
data.setText(String.format("血氧:%s", value));
data4.setText(String.format("时间:%s", time.substring(10, 19)));
}
});
} catch (IOException e) {
e.printStackTrace();
}
try {
OkHttpClient client1 = new OkHttpClient();
Request request1 = new Request.Builder().url("http://api.heclouds.com/devices/" + DeviceID + "/datapoints?datastream_id=" + key2).header("api-key", ApiKey).build();
Response response1 = client1.newCall(request1).execute();
String responseData1 = response1.body().string();
parseJSONWithGSON(responseData1);
JsonRootBean app = new Gson().fromJson(responseData1, JsonRootBean.class);
List<Datastreams> streams = app.getData().getDatastreams();
List<Datapoints> points = streams.get(0).getDatapoints();
value1 = points.get(0).getValue();
data1.post(new Runnable() {
@Override
public void run() {
data1.setText(String.format("心率:%s", value1));
}
});
} catch (IOException ex) {
ex.printStackTrace();
}
}
}).start();
}
public void Post() {
new Thread(new Runnable() {
@Override
public void run() {
try {
value2 = data2.getText().toString().trim();
OkHttpClient client = new OkHttpClient();
String updata = (",;" + key1 + "," + value2);
//发送type写法
RequestBody requestBody = RequestBody.create(okhttp3.MediaType.parse("application/json"), updata);
Request request = new Request.Builder().url("http://api.heclouds.com/devices/" + DeviceID + "/datapoints?type=5").post(requestBody).header("api-key", ApiKey).build();
Response response = client.newCall(request).execute();
} catch (Exception e) {
}
try {
value3 = data3.getText().toString().trim();
OkHttpClient client = new OkHttpClient();
//数据格式从官方文档看,type用5情况
String updata = (",;" + key2 + "," + value3);
//发送type写法
RequestBody requestBody = RequestBody.create(okhttp3.MediaType.parse("application/json"), updata);
Request request = new Request.Builder().url("http://api.heclouds.com/devices/" + DeviceID + "/datapoints?type=5").post(requestBody).header("api-key", ApiKey).build();
Response response = client.newCall(request).execute();
} catch (Exception e) {
}
}
}).start();
}
//解析json
private void parseJSONWithGSON(String jsonData) {
JsonRootBean app = new Gson().fromJson(jsonData, JsonRootBean.class);
List<Datastreams> streams = app.getData().getDatastreams();
List<Datapoints> points = streams.get(0).getDatapoints();
int count = app.getData().getCount();//获取数据的数量
for (int i = 0; i < points.size(); i++) {
String time = points.get(i).getAt();
String value = points.get(i).getValue();
Log.w("www","time="+time);
Log.w("www","value="+value);
}
}
private Handler handler = new Handler();
private Runnable runnable = new Runnable() {
public void run () {
Get();//获取新数据
// Toast.makeText(MainActivity.this, "数据接收成功", Toast.LENGTH_SHORT).show();
handler.postDelayed(this,15000); //15秒刷新一次
}
};
只要自己设计连上的是onenet云端,设计的安卓源码上如下:
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.cqgcxy.onenet", appContext.getPackageName());
}
}
APP软件界面布局程序代码设置
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:gravity="center_vertical"
>
<TextView
android:layout_width="374dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:hint=" 家庭血氧监测app"
android:textColor="#000000"
android:textSize="32sp"
android:textStyle="bold"
android:typeface="monospace" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:gravity="center_vertical"
>
<ImageView
android:layout_width="50sp"
android:layout_height="50sp"
android:src="@drawable/xueyang" />
<TextView
android:id="@+id/data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:hint="血氧显示区域"
android:textColor="#222222"
android:textSize="30sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:gravity="center_vertical"
>
<ImageView
android:layout_width="50sp"
android:layout_height="50sp"
android:src="@drawable/xueyang2"
app:srcCompat="@drawable/xueyang2" />
<EditText
android:id="@+id/data2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:hint="血氧修改值"
android:textColor="#222222"
android:textSize="30sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:gravity="center_vertical"
>
<ImageView
android:layout_width="50sp"
android:layout_height="50sp"
android:src="@drawable/xinlv" />
<TextView
android:id="@+id/data1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:hint="心率显示区域"
android:textColor="#222222"
android:textSize="30sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:gravity="center_vertical"
>
<ImageView
android:layout_width="50sp"
android:layout_height="50sp"
android:src="@drawable/xinlv2"
app:srcCompat="@drawable/xinlv2" />
<EditText
android:id="@+id/data3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:hint="心率修改值"
android:textColor="#222222"
android:textSize="30sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:gravity="center_vertical"
>
<ImageView
android:layout_width="50sp"
android:layout_height="50sp"
android:src="@drawable/shijian2" />
<TextView
android:id="@+id/data4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:hint="时间显示区域"
android:textColor="#222222"
android:textSize="30sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="259dp">
<Button
android:id="@+id/post"
android:layout_width="0dp"
android:layout_height="80sp"
android:layout_marginStart="40dp"
android:layout_marginTop="96dp"
android:layout_marginEnd="33dp"
android:background="@drawable/oldrondback"
android:text="修改"
android:textColor="#181616"
android:textSize="40sp"
app:layout_constraintEnd_toStartOf="@+id/get"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/get"
android:layout_width="0dp"
android:layout_height="80sp"
android:layout_marginEnd="38dp"
android:background="@drawable/allback"
android:text="获取"
android:textColor="#2C2929"
android:textSize="40sp"
app:layout_constraintBaseline_toBaselineOf="@+id/post"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/post" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>
程序的配置只要还是需要自己针对自己的开发云设计,同时自己在软件app设计上也使用了对这个阿里云方面的尝试,后面会做一个练习教程出来,目前设计软件面向的是onenet移动云看法(有一说一,移动云上面对app设置真的没有阿里云那么友好–个人观点[狗头])。
开发板硬件PCB设计
有个良好设计得益于有良好的开发平台,在设计PCB设计上面使用得是嘉立创开发环境,个人感觉画图开发嘉立创真的是帮里大忙,虽然个人设计硬件上面着实简单。如下图
硬件核心程序设计
首先系统最核心的设计借鉴了这位博主的相关文献,当然借鉴不是抄袭,同时也需要加入随这个这个传感器上自己的理解和开发,首先是自己对这个程序文件的main文件内容上面的设计,
main.c
int main(void)
{
int i=0;
u8 timeout;
delay_init();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
Usart1_Init(115200);
Usart2_Init(115200);
Delay_ms(500);
UsartPrintf(USART1,"IIC_GPIO_INIT\r\n");
IIC_GPIO_INIT();
Delay_us(500);
UsartPrintf(USART1,"OLED_Init\r\n");
OLED_Init();
OLED_Printf_EN(6,0,"MAX30102_GPIO");
MAX30102_GPIO();
OLED_Printf_EN(6,0,"Max30102_reset");
Max30102_reset();
MAX30102_Config();
UsartPrintf(USART1,"开始初始化ESP8266\r\n");
OLED_Printf_EN(6,0,"ESP8266.........");
ESP8266_Init();
UsartPrintf(USART1,"开始1\r\n");
LED_Init();
Led_flash();
while(OneNet_DevLink())
{
delay_ms(500);
}
UsartPrintf(USART1,"开始测量血氧\r\n");
for(i = 0;i <128;i++)
{
while(MAX30102_INTPin_Read()==0)
{
//读取FIFO
max30102_read_fifo();
}
}
UsartPrintf(USART1,"测量结束\r\n");
OLED_Printf_EN(6,0,"Ready.......");
while(1)
{
if(!key)
{
delay_ms(10);
if(!key)
{
while(!key);
while(1)
{
// OneNet_Publish(devPubTopic, PUB_BUF);
OLED_Printf_EN(6,0,"Waiting...");
ESP8266_Clear();
UsartPrintf(USART1,"进入主循环\r\n");
Delay_us(300);
if(++timeout >=50)
{
OLED_Printf_EN(6,0,"Dataing...");
blood_Loop();
UsartPrintf(USART1,"心率血氧测量完毕\r\n");
UsartPrintf(USART_DEBUG, "OneNet_SendData\r\n");
OneNet_SendData();
timeout=0;
ESP8266_Clear();
if((sp02 < 90||heart <= 60)&&(sp02 > 115||heart >= 120))
{
GPIO_SetBits(GPIOC,GPIO_Pin_13|GPIO_Pin_14| GPIO_Pin_15);
delay_ms(500);
GPIO_SetBits(GPIOC,GPIO_Pin_13|GPIO_Pin_14| GPIO_Pin_15);
delay_ms(500);
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
delay_ms(500);
}
else
{
GPIO_SetBits(GPIOC,GPIO_Pin_14| GPIO_Pin_15);
GPIO_SetBits(GPIOA,GPIO_Pin_1);
delay_ms(500);
GPIO_ResetBits(GPIOC,GPIO_Pin_14| GPIO_Pin_15);
delay_ms(500);
}
if(!key)
{
if(!key)
{
while(!key);
break;
}
}
}
}
}
if(++timeout >40)
{
UsartPrintf(USART_DEBUG, "OneNet_SendData\r\n");
sprintf(PUB_BUF,"{\"sp02\":%0.2f,\"heart\":%d}",sp02,heart);
Delay_us(500);
OneNet_SendData();
timeout=0;
OLED_Printf_EN(2,0,"heart:0/min ");
OLED_Printf_EN(4,0,"SpO2:0%% ");
OLED_Printf_EN(0,0,"Xue Yang Yi");
ESP8266_Clear();
}
delay_ms(10);
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
LED1 = 0;
}
}
}
程序设计逻辑未三个阶段,入下图的流程图设计表示:
MAX30102.c
void MAX30102_GPIO(void)
{
RCC_APB2PeriphClockCmd(MAX30102_INTPin_CLK,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStruct.GPIO_Pin = MAX30102_INTPin_Pin;
GPIO_Init(MAX30102_INTPin_PORT,&GPIO_InitStruct);
}
uint8_t Max30102_reset(void)
{
if(IIC_Write_Byte(MAX30102_Device_address,REG_MODE_CONFIG, 0x40))
return 1;
else
return 0;
}
void MAX30102_Config(void)
{
IIC_Write_Byte(MAX30102_Device_address,REG_INTR_ENABLE_1,0xc0); INTR setting
IIC_Write_Byte(MAX30102_Device_address,REG_INTR_ENABLE_2,0x00);//
IIC_Write_Byte(MAX30102_Device_address,REG_FIFO_WR_PTR,0x00);//选择上四位片选
IIC_Write_Byte(MAX30102_Device_address,REG_OVF_COUNTER,0x00);//选择下四位片选
IIC_Write_Byte(MAX30102_Device_address,REG_FIFO_RD_PTR,0x00);//选择前八位
IIC_Write_Byte(MAX30102_Device_address,REG_FIFO_CONFIG,0x0f);
IIC_Write_Byte(MAX30102_Device_address,REG_MODE_CONFIG,0x03);
IIC_Write_Byte(MAX30102_Device_address,REG_SPO2_CONFIG,0x27);
IIC_Write_Byte(MAX30102_Device_address,REG_LED1_PA,0x32);
IIC_Write_Byte(MAX30102_Device_address,REG_LED2_PA,0x32);
IIC_Write_Byte(MAX30102_Device_address,REG_PILOT_PA,0x7f);
}
void max30102_read_fifo(void)
{
uint16_t un_temp;
fifo_red=0;
fifo_ir=0;
uint8_t ach_i2c_data[6];
IIC_Read_Byte(MAX30102_Device_address,REG_INTR_STATUS_1);
IIC_Read_Byte(MAX30102_Device_address,REG_INTR_STATUS_2);
ach_i2c_data[0]=REG_FIFO_DATA;
IIC_Read_Array(MAX30102_Device_address,REG_FIFO_DATA,ach_i2c_data,6);
un_temp=ach_i2c_data[0];
un_temp<<=14;
fifo_red+=un_temp;
un_temp=ach_i2c_data[1];
un_temp<<=6;
fifo_red+=un_temp;
un_temp=ach_i2c_data[2];
un_temp>>=2;
fifo_red+=un_temp;
un_temp=ach_i2c_data[3];
un_temp<<=14;
fifo_ir+=un_temp;
un_temp=ach_i2c_data[4];
un_temp<<=6;
fifo_ir+=un_temp;
un_temp=ach_i2c_data[5];
un_temp>>=2;
fifo_ir+=un_temp;
if(fifo_ir<=10000)
{
fifo_ir=0;
}
if(fifo_red<=10000)
{
fifo_red=0;
}
}
传感器核心设计上面设置的数据读取的协议,设置的是从MAX30100上面的数据芯片协议上面的读取,通过对传感器的测量芯片的位选读取存储在传感器上面的数据,简单描述一下,通过iic协议对筛选芯片内容进行片选操作,先读取选型芯片的标志位0,在设置数组指针从数组位置1往后移动14位片选出额为位置,以此类推… …
blood.c
extern float sp02;
extern u8 heart;
struct
{
float Hp ; //血红蛋白
float HpO2; //氧合血红蛋白
}g_BloodWave;//血液波形数据
BloodData g_blooddata = {0}; //血液数据存储
#define CORRECTED_VALUE 47 //标定血液氧气含量
/*funcation start ------------------------------------------------------------*/
//血液检测信息更新
void blood_data_update(void)
{
//标志位被使能时 读取FIFO
g_fft_index=0;
while(g_fft_index < FFT_N)
{
while(MAX30102_INTPin_Read()==0)
{
//读取FIFO
max30102_read_fifo(); //read from MAX30102 FIFO2
//将数据写入fft输入并清除输出
if(g_fft_index < FFT_N)
{
//将数据写入fft输入并清除输出
s1[g_fft_index].real = fifo_red;
s1[g_fft_index].imag= 0;
s2[g_fft_index].real = fifo_ir;
s2[g_fft_index].imag= 0;
g_fft_index++;
}
}
}
}
//血液信息转换
void blood_data_translate(void)
{
float n_denom;
uint16_t i;
//直流滤波
float dc_red =0;
float dc_ir =0;
float ac_red =0;
float ac_ir =0;
for (i=0 ; i<FFT_N ; i++ )
{
dc_red += s1[i].real ;
dc_ir += s2[i].real ;
}
dc_red =dc_red/FFT_N ;
dc_ir =dc_ir/FFT_N ;
for (i=0 ; i<FFT_N ; i++ )
{
s1[i].real = s1[i].real - dc_red ;
s2[i].real = s2[i].real - dc_ir ;
}
//移动平均滤波
//printf("***********8 pt Moving Average red******************************************************\r\n");
UsartPrintf(USART1,"***********8 pt Moving Average red******************************************************\r\n");
for(i = 1;i < FFT_N-1;i++)
{
n_denom= ( s1[i-1].real + 2*s1[i].real + s1[i+1].real);
s1[i].real= n_denom/4.00;
n_denom= ( s2[i-1].real + 2*s2[i].real + s2[i+1].real);
s2[i].real= n_denom/4.00;
}
//八点平均滤波
for(i = 0;i < FFT_N-8;i++)
{
n_denom= ( s1[i].real+s1[i+1].real+ s1[i+2].real+ s1[i+3].real+ s1[i+4].real+ s1[i+5].real+ s1[i+6].real+ s1[i+7].real);
s1[i].real= n_denom/8.00;
n_denom= ( s2[i].real+s2[i+1].real+ s2[i+2].real+ s2[i+3].real+ s2[i+4].real+ s2[i+5].real+ s2[i+6].real+ s2[i+7].real);
s2[i].real= n_denom/8.00;
//printf("%f\r\n",s1[i].real);
UsartPrintf(USART1,"%f\r\n",s1[i].real);
}
UsartPrintf(USART1,"************8 pt Moving Average ir*************************************************************\r\n");
for(i = 0;i < FFT_N;i++)
{
//printf("%f\r\n",s2[i].real);
UsartPrintf(USART1,"%f\r\n",s2[i].real);
}
UsartPrintf(USART1,"**************************************************************************************************\r\n");
//开始变换显示
g_fft_index = 0;
//快速傅里叶变换
FFT(s1);
FFT(s2);
//解平方
UsartPrintf(USART1,"开始FFT算法*****************************************************************************************\r\n");
//代码实现开始FFT算法
for(i = 0;i < FFT_N;i++)
{
s1[i].real=sqrtf(s1[i].real*s1[i].real+s1[i].imag*s1[i].imag);
s1[i].real=sqrtf(s2[i].real*s2[i].real+s2[i].imag*s2[i].imag);
}
//计算交流分量
for (i=1 ; i<FFT_N ; i++ )
{
ac_red += s1[i].real ;
ac_ir += s2[i].real ;
}
for(i = 0;i < FFT_N/2;i++)
{
//printf("%f\r\n",s1[i].real);
UsartPrintf(USART1,"%f\r\n",s1[i].real);
}
UsartPrintf(USART1,"**************************************************************************************************\r\n");
for(i = 0;i < FFT_N/2;i++)
{
//printf("%f\r\n",s2[i].real);
UsartPrintf(USART1,"%f\r\n",s2[i].real);
}
UsartPrintf(USART1,"结束FFT算法
int s1_max_index = find_max_num_index(s1, 30);
int s2_max_index = find_max_num_index(s2, 30);
UsartPrintf(USART1,"%d\r\n",s1_max_index);
UsartPrintf(USART1,"%d\r\n",s2_max_index);
float R = (ac_ir*dc_red)/(ac_red*dc_ir);
float sp02_num =-45.060*R*R+ 30.354 *R + 94.845;
g_blooddata.SpO2 = sp02_num;
if(g_blooddata.heart == 46)
{
g_blooddata.heart = 76;
}
else g_blooddata.SpO2 = g_blooddata.SpO2;
void blood_Loop(void)
{
UsartPrintf(USART_DEBUG, "开始血液信息获取\r\n");
//血液信息获取
blood_data_update();
UsartPrintf(USART_DEBUG, "血液信息获取完毕\r\n");
UsartPrintf(USART_DEBUG, "开始血液信息转换\r\n");
//血液信息转换
blood_data_translate();
UsartPrintf(USART_DEBUG, "血液信息转换完毕\r\n");
//显示血液状态信息
OLED_Printf_EN(2,0,"heart:%3d/min ",g_blooddata.heart);
g_blooddata.SpO2 = (g_blooddata.SpO2 > 99.99) ? 99.99:g_blooddata.SpO2;
OLED_Printf_EN(4,0,"SpO2:%2.2f%% ",g_blooddata.SpO2);
UsartPrintf(USART_DEBUG, "指令心率%3d\r\n",g_blooddata.heart);
Delay_ms(10);
UsartPrintf(USART_DEBUG, "指令血氧%0.2f\r\n",g_blooddata.SpO2);
sp02 = g_blooddata.SpO2;
heart = g_blooddata.heart;
//tft显示刷新
//LED 蜂鸣器信息更新
}
这个代码实现的是的只要是对MAX30102传感器上面数据的回馈数据采集的一种处理方法,主要的设计参考了上述开发者的程序。
设计总结:个人在最后比较可惜的是没有保存好自己最终项目,不然可以展示一下自己的成功,这个项目最大的设计点是自己写的联网程序和触发模式,当然里面的代码也有一些是开发者自己已经写好的,我只是作为收益者拿来使用,感谢这些开发者给我们提供的开发环境。
这个是我的项目发布地址:项目发布地址