乌龟冬眠箱湿度监控系统和AI辅助建议功能的实现
家里小朋友养了一只小乌龟,到了冬天就冬眠了,早早地准备了一个冬眠箱,铺上椰土,在室温低于15℃时,就把小乌龟放到冬眠箱里,不一会儿它就自己钻入土中把自己藏了起来。按照惯例,需要每隔一定时间,对冬眠箱进行补水,以保持土壤湿润,防止小乌龟缺水,但有时候也会忘记补水的工作,造成冬眠箱过于干燥,不利于乌龟健康。翻箱倒柜,找到一个9年前买的树莓派2 Model B,32位,4核1GB的设备,正好可以利用起来,做一个冬眠箱湿度实时监控系统,设计一下用户需求,大致如下:
[*]每隔一定时间,采集冬眠箱中土壤的湿度数值,并将数据推送到网上的数据库中
[*]提供一个前端页面,这个页面负责从数据库中读取数据,并以图表形式展现湿度走势
[*]在这个前端页面上,通过人工智能AI服务,给出乌龟冬眠箱内的补水建议,比如建议几天后或者什么时机应该考虑补水等等
这个需求其实没有做到业务闭环:理论上讲,这个前端页面只不过是提供给我一个访问湿度数据并获得AI建议的一个“周边”功能而已,真正做的更为完整的话,应该是,在获得AI建议后,根据AI建议,将补水指令发送到设备,设备控制继电器完成自动补水,而不是让我看到数据后,再自己拿起喷水壶走向乌龟冬眠箱。
废话不多说,直接开整。
技术设计与实现效果
总结起来,我打算使用下面的这些硬件、技术和软件开发框架,来完成整个系统的实现:
[*]硬件:树莓派2 Model B,负责从土壤湿度传感器读入数据,然后推送到Microsoft Azure IoT Hub
[*]在数据被推送到IoT Hub前,使用ADS1115模数转换模块,将传感器模拟量转换为数字量,交由树莓派处理
[*]树莓派2 Model B中,使用C语言编程,由Azure IoT C SDK实现与Microsoft Azure IoT Hub的交互;使用pigpio实现树莓派GPIO和I2C模数转换数据采集
[*]树莓派中运行的这个数据采集程序,由cron服务负责调度,每15分钟运行一次程序,在运行时采集一次数据,推送一次数据
[*]数据推送到IoT Hub后,通过Azure Function,将数据插入到后端的Azure Database for PostgreSQL flexible server数据库
[*]使用ASP.NET Core Web App (Razor Pages)实现前端页面,访问PostgreSQL数据库,提供数据查询和呈现能力,数据趋势图表使用chartjs渲染
[*]在这个前端页面上,通过Ajax异步调用,由Microsoft Semantic Kernel访问Azure OpenAI Services,通过预先部署好的gpt-4o模型,获取补水建议,并把结果显示在页面上
在这些技术的选择上,有些地方是经过一些考量并最终决定方案的:
[*]选用C语言编程,而不是Python或者.NET,因为我对Python并不熟悉,加上树莓派2 Model B本身配置不高,所以跑.NET会比较耗费资源;因此,在现有的条件下,对于我来说,C语言是实现最快最方便的
[*]使用Cron定时任务来调度程序,而不是让程序自己长期驻留后端,在程序内部每隔一段时间做一次数据采集和上传,原因如下:
[*]Cron功能简单易用,Cron表达式灵活度非常强,可以随时调整调度时机
[*]程序长期驻留后端,更容易出现问题,比如如果编程习惯不好,产生内存泄漏,时间一长势必把系统搞挂,不利于系统稳定运行
[*]反复的GPIO I2C调用,容易产生缓存和脏数据,造成数据错误,每次调度都重新启动一次进程,可以避免这类问题的发生
[*]选择Microsoft Azure作为服务层的基础设施,因为它能提供全套所需后端服务,生态也比较成熟,而且我每个月还是有那么一点点额度在上面
[*]选择ASP.NET Core Web App (Razor Pages)作为前端技术,而不是选择所谓的单页面应用和前后端分离的架构,是因为虽然我每个月有那么一点点Azure额度,但是不多,省一个算一个,不想因为一个简单的应用把东西搞得太复杂
从整体上看,整个系统的架构如下图所示:
在整个系统完成之后,通过使用手机访问部署于Azure App Service的前端页面,我们可以看到如下的效果:
在这个页面的上半部分,提供时间区间选择功能,可以指定数据观察的起始时间和结束时间,点击【确定】按钮后,在页面的下半部分就会用曲线图来显示这个时间区间中的数据。其中“历史数据”部分显示了各个时间点(每15分钟)的湿度数据,而“湿度趋势”则是将每6个小时的数据进行平均,然后显示在曲线图上。需要注意的是,每个数据点的值并不是对应真实的物理上的“湿度”概念,它只是一个参考值,在通过I2C采集数据时,我并没有对数据进行特殊处理,所以,这个值越大,表明传感器两侧之间的电阻值越大,也就是模拟量输出端(AO)上的电压越高,这也就意味着土壤湿度越小,越干燥,根据多次实验,确定了如果土壤干燥程度很严重,这个相对值是31840(电压就是31840 * 2.048 / 32768 = 1.99V)。所以可以从上面的图表看到,随着时间的推移,土壤变得越来越干。
在这个页面的中间部分,提供了“听听AI怎么说”功能,它通过将近期的数据汇总并发送给gpt-4o大语言模型,并由gpt-4o给出建议,显示在页面上,一开始的时候,这个建议不是特别靠谱,随着时间的推移,能够给出的参考数据越来越丰富,它的推测也越来越显得合理了。
源代码
所有代码都放在了码云上了,方便国内读者访问:https://gitee.com/daxnet/humidty。代码都在src目录下:
[*]function子目录:保存了用于将Azure IoT Hub中的湿度数据保存到后端PostgreSQL数据库的Azure Function App的项目源代码
[*]iot子目录:保存了从树莓派的土壤湿度传感器读入数据,并将数据推送到Azure IoT Hub的C语言程序
[*]web子目录:保存了前端页面以及通过Semantic Kernel调用Azure OpenAI Services获取AI建议的ASP.NET Core Web Pages项目代码
技术实现
技术实现分为硬件连接与调试、Azure云环境搭建以及软件开发部分。当然这里也无法单靠一篇文章就把所有的细节都解释清楚,我会挑一些重点内容进行介绍。
硬件连接与调试
主机就是闲置的树莓派2 Model B,上网搜索了一下,这个型号的低配树莓派价格好像还很坚挺,也要小两百块,如果不是手上有个闲置,大概率我会入手一个基础版的树莓派Zero或者是Arduino开发板,价格会相对亲民。此外,土壤传感器是必不可少的,某宝上一大把,随便入手一个就行,价格也很便宜,就几块钱的事情:
当然,模数转换模块(ADC)也必不可少,因为传感器的AO端口它会输出模拟量(连续量),而树莓派GPIO本身读入的是数字量(0或者1),因此,中间需要做一个转换。可以考虑入手ADS1115的ADC模块,我是直接购买了适用于树莓派的ADS1115 ADC,这样直接往树莓派的GPIO上一插就完事儿了,省得自己还需要去排线,减少工作量。我购买的是下面这款,只是相对于基础版的ADS1115,这一款价格稍微有点高,大概3、40块的样子。
将ADS1115插在树莓派GPIO端口上,然后,按下面的图纸接线将湿度传感器接入即可:
当时购买ADS1115时,不知道是不是少发了跳线帽,手头也没有现成的,所以暂时只能用杜邦线直连0x48地址跳线(下图黄色的那根),加上也没有找到使用说明,所以当时也只能用万用表来测试是否连线正确:
调试通过后,将湿度传感器插入乌龟冬眠箱,效果如下。由于传感器顶部有裸露的接线线头,所以还特地用废弃的酒精瓶做了一个防水罩,以免补水的时候造成短路。
接下来就是配置Azure云服务和软件开发部分了。
配置Microsoft Azure云服务
在整个系统方案中,使用了下面这些Azure云服务:
[*]Azure IoT Hub(包含内建Event Hub)
[*]Azure Database for PostgreSQL flexible server
[*]Azure Function App
[*]Azure App Service
[*]Azure OpenAI Services
当然,还有一些基础服务,比如Virtual Network、DNS Zone、Private Endpoint等等,这些也就不一一列举了。事实上,配置过程内容也不少,这里也就不一步步介绍了,这里仅对其中主要的部分进行介绍。
Azure IoT Hub
直接从Azure Portal的主页上,选择IoT Hub服务新建就可以了,整个过程比较简单,在创建IoT Hub服务之后,记得添加一个IoT设备。由于我们的应用场景比较简单,所以,直接创建设备就行,在设备创建完之后,点击已创建的设备,然后在设备页面中,将Primary connection string复制保存下来,后面会用到:
此外,在IoT Hub的Hub settings中,找到Event hub-compatible name和Event hub-compatible endpoint,也复制保存下来,后续也会用到:
与IoT Hub配置相关的内容也就这些,其它选项默认即可。
Azure Database for PostgreSQL flexible server
在创建Azure Database for PostgreSQL flexible server资源之前,需要先把整个解决方案的网络拓扑设计好,否则到后面发现错误需要修改,就会变得很被动,比如如果一开始的时候网络配置不正确,就会影响后续的服务部署,或者你所使用的Subscription在有些区域有服务限制,从而造成某些资源无法创建的尴尬局面。
在创建Azure Database for PostgreSQL flexible server时,我选择了Development模式,因为这种模式最省钱,它本身也就只是为了开发测试的目的,而不是为生产环境而配置的,不过在我的场景中,已经够用了。另外为了安全起见,数据库默认是不会打开公网访问的,这也就意味着需要有对应的虚拟网络和子网的配置。在Azure中,PostgreSQL flexible server需要被部署在一个独立的子网中,这个子网至少需要有16个可用IP地址(CIDR范围:/28),在这16个地址中,Azure会使用其中的5个地址用于Azure网络相关的目的,剩下的11个地址中,如果PostgreSQL flexible server配置为高可用,它还将占用另外4个IP地址。
正如上文架构图中所述,我创建了一个Virtual Network,它包含两个子网:subnet-default和subnet-pgsql。Azure Database for PostgreSQL flexible server被部署在了subnet-pgsql子网中。为了能让Azure Function App和Azure Web App能够访问数据库,在PostgreSQL数据库上,我还配置了Private Endpoint:
这个Private Endpoint是附着在subnet-pgsql子网上的,并且由privatelink.postgres.database.azure.com这个Private DNS负责域名解析。在Private Endpoint的DNS configuration中,将FQDN复制下来,这就是数据库中连接字符串的主机名称。
通过数据库的主页上的Connect链接,就能获得访问数据库的连接字符串,这里就不多做说明了。
Azure Function App
仍然在Azure Portal主页上,新建一个Azure Function App的资源,Azure Function App是需要由一个宿主(hosting)提供运行环境的,这个宿主环境可以有多个选择,在Azure中称为Hosting plan。Azure提供下面这些Hosting plan:
我选择的是App Service hosting plan,此时,它需要在你的Subscription下创建一个App Service Plan,一个App Service Plan其实是定义了一组计算资源(如虚拟机实例、CPU、内存、存储等)和功能级别,用于支持托管在该计划下的应用程序。通过选择不同的 App Service Plan,就可以根据应用程序的需求来调整计算资源和功能级别,以满足性能、可用性和成本方面的要求。因此,请量力而行,我所选择的App Service Plan如下,仅供参考,不好意思,囊中羞涩,选了个最便宜的方案:
在创建完Azure Function App之后,别忘了启用VNet Integration,否则你的Function App无法访问PostgreSQL数据库。启用过程也比较简单,首先在Azure Database for PostgreSQL flexible server的子网所在的虚拟网络中,另外再新建一个子网,然后,将Function App的子网设置为这个新建的子网就可以了。
此外,由于我们的Azure Function App需要从IoT Hub读取IoT事件,并将事件数据写入数据库,因此,需要配置如下这些环境变量:
[*]ConnectionStringSetting:设置为IoT Hub上内建(Built-in Endpoint)的Event hub-compatible endpoint地址(上文中有提到)
[*]PostgresConnectionString:数据库的连接字符串(使用Private Endpoint的地址)
说明一下,这个“ConnectionStringSetting”的取名是任意的,你也可以选择不取这个名字,但是,它需要跟将来Azure Function App代码中的配置保持一致。
Azure App Service
与创建Azure Function App类似,直接从Azure Portal上新建App Service资源就行,在创建Azure App Service时,同样需要选择一个App Service plan,可以考虑使用上面Azure Function App相同的Service Plan,当然,如果经济条件允许,并且有另外的需求的话,则可以选择使用另一个独立的Service Plan,以使用不同的系统配置和计价模式。此外,由于我们的前端应用仍然需要访问PostgreSQL数据库,因此,与Azure Function App类似,需要启用VNet Integration,方法类似,不再赘述。
Azure App Service前端应用需要使用以下这些环境变量,这里大致介绍一下:
[*]AzureOpenAIApiKey:在完成Azure OpenAI Services大语言模型的部署之后,可以获得大语言模型的访问密钥,将密钥内容填入此处
[*]AzureOpenAIEndpoint:在完成Azure OpenAI Services大语言模型的部署之后,可以获得大语言模型的访问目标URI,从而获得Endpoint地址,填入此处
[*]AzureOpenAIModelId:在部署Azure OpenAI Services大语言模型时所选取的模型名称
[*]DbConnectionString:PostgreSQL数据库连接字符串,与上述Azure Function App的PostgresConnectionString环境变量取值相同
Azure OpenAI Services
在我之前的文章《在C#中基于Semantic Kernel的检索增强生成(RAG)实践》中,包含了如何在Azure上部署大语言模型的相关介绍,因此,这里就不再重复了。事实上,这套乌龟缸湿度监控系统中所使用的大语言模型,正是当时写那篇文章时所使用的大语言模型,因此,这里所使用的OpenAI API Key、Open AI Endpoint以及Model ID这些参数,都跟当时所使用的参数是相同的。
完成微软Azure云服务的配置之后,就可以开始进行编码开发了。
软件编码与实现
软件部分包括树莓派中收集湿度数据并推送到Azure IoT Hub的一个小程序,一个将Azure IoT Hub上的数据保存到后端PostgreSQL数据库的Azure Function App,以及一个用来显示湿度数据趋势和AI推荐的前端页面。
树莓派中应用程序的开发
在树莓派中,需要有一个应用程序专门负责收集湿度数据,然后将数据推送到Azure IoT Hub。我选择使用pigpio库来访问土壤湿度传感器,以获得湿度模拟量数据,并使用Azure IoT C SDK实现数据上传到Azure IoT Hub,编程使用C语言。首先是在树莓派中安装pigpio库,按照【官方文档】中介绍的步骤安装就可以了,安装过程基本就是下载源代码然后在本地编译安装。然后就是安装Azure IoT C SDK,并配置Visual Studio Code开发环境,我已经把详细步骤整理在代码库的文档中了,详情可以直接点击【这篇文档】获取,这里就不详细展开介绍了,重点介绍一下开发的几个要点。
第一件事情是从湿度传感器读取湿度数值,它是通过I2C(Inter-Integrated Circuit)实现的,所以需要在树莓派上启用I2C的支持,在树莓派命令提示符下,输入sudo raspi-config,打开设置界面,然后选择Interface Options:
在子菜单中,选择I2C然后启用就可以了:
下面是通过I2C访问湿度传感器获取湿度数据的主要代码:
#include <pigpio.h>#define I2C_ADDR 0x48 // 上面跳线所选择的地址#define I2C_CONFIG_HI 0xC4 // I2C的配置高位字节#define I2C_CONFIG_LO 0x83 // I2C的配置低位字节#define I2C_CONFIG_REG 0x01 // 配置数据写入寄存器static float get_humidty_value (){ // 初始化pigpio库 if ( gpioInitialise() < 0 ) { log_error ( "GPIO initialize failed." ); return -1; } // 打开I2C int i2c_handle = i2cOpen ( 1, I2C_ADDR, 0 ); if ( i2c_handle < 0 ) { log_error ( "I2C open failed, error no: %d", i2c_handle ); return -1; } // 写入配置数据,对I2C进行配置 char config = { I2C_CONFIG_HI, I2C_CONFIG_LO }; int config_res = i2cWriteI2CBlockData(i2c_handle, I2C_CONFIG_REG, config, 2); if ( config_res != 0 ) { switch ( config_res ) { case PI_I2C_WRITE_FAILED: log_error ( "I2C write failed." ); break; case PI_BAD_HANDLE: log_error ( "I2C write bad handle."); break; case PI_BAD_PARAM: log_error ( "I2C write bad parameter." ); break; } return -1; } time_sleep ( 0.2 ); // 从I2C读入数据并保存在一个字节数组中 char data; int num_bytes_read = i2cReadI2CBlockData ( i2c_handle, 0x00, data, 2 ); if ( num_bytes_read <= 0 ) { switch ( num_bytes_read ) { case PI_I2C_READ_FAILED: log_error ( "I2C read failed." ); break; case PI_BAD_HANDLE: log_error ( "I2C read bad handle."); break; case PI_BAD_PARAM: log_error ( "I2C read bad parameter." ); break; } return -1; } // 通过字节数组数据的拼装,得到湿度数据 float result = (data << 8) | data; // 计算出电压值,仅作日志输出参考使用 float voltage = result * 2.048 / 32768.0; log_info ( "ADC value: %.3f, Voltage: %.3f", result, voltage ); i2cClose ( i2c_handle ); gpioTerminate ( ); // 将结果返回 return result;}在获得湿度数据之后,就可以通过Azure IoT C SDK,将数据推送到Azure IoT Hub上。主体代码如下:
#include <stdio.h>#include <stdlib.h>#include <sys/stat.h>#include <azureiot/iothub.h>#include <azureiot/iothub_client_version.h>#include <azureiot/iothub_device_client_ll.h>#include <azureiot/iothubtransportmqtt.h>#include <azure_c_shared_utility/threadapi.h>#define CONNECTION_STRING_NAME"IOTHUB_CONNECTION_STRING"// 发送出去的消息数目static int g_message_count_send_confirmations = 0;// 消息发送之后的确认回调static void send_confirm_callback ( IOTHUB_CLIENT_CONFIRMATION_RESULT result, void* userContextCallback ){ g_message_count_send_confirmations++; log_info ( "Confirmation callback received for message %lu with status %s", ( unsigned long )g_message_count_send_confirmations, MU_ENUM_TO_STRING(IOTHUB_CLIENT_CONFIRMATION_RESULT, result) );}// 与Azure IoT Hub建立连接后的回调static void connection_status_callback(IOTHUB_CLIENT_CONNECTION_STATUS result, IOTHUB_CLIENT_CONNECTION_STATUS_REASON reason, void* user_context){ if ( result == IOTHUB_CLIENT_CONNECTION_AUTHENTICATED ) log_info ( "The device client is connected to iothub." ); else log_info ( "The device client has been disconnected." );}// 发送数据static void send_message ( IOTHUB_DEVICE_CLIENT_LL_HANDLE handle, RPI_MESSAGE_HANDLE message_handle ){ const char* serialized_message = Rpi_SerializeMessage ( message_handle ); log_debug ( "Message: %s", serialized_message ); IOTHUB_MESSAGE_HANDLE iot_message_handle = IoTHubMessage_CreateFromString( serialized_message ); if ( iot_message_handle == NULL ) { log_error ( "Failed to create message handle from String." ); return; } IOTHUB_MESSAGE_RESULT send_res = IoTHubClientCore_LL_SendEventAsync ( handle, iot_message_handle, send_confirm_callback, NULL ); if ( send_res != IOTHUB_MESSAGE_OK ) { log_error ( "IoTHubClientCore_LL_SendEventAsync call failed with status %s", MU_ENUM_TO_STRING ( IOTHUB_CLIENT_RESULT, send_res ) ); } else { do { IoTHubDeviceClient_LL_DoWork ( handle ); ThreadAPI_Sleep ( 1 ); } while (g_message_count_send_confirmations < 1); g_message_count_send_confirmations = 0; } IoTHubMessage_Destroy ( iot_message_handle );}int main ( int argc, char** argv ){ // 从环境变量或者命令行获得Azure IoT Hub的连接字符串 const char* iothub_connection_string = getenv ( CONNECTION_STRING_NAME ); if ( iothub_connection_string == NULL ) { if ( argc == 2 ) { iothub_connection_string = argv; } if ( iothub_connection_string == NULL ) { log_error ( "Error: Missing IOTHUB_CONNECTION_STRING environment variable. Terminated." ); fclose ( fp_log ); return -1; } } // 初始化IoT C SDK库 int init_res = IoTHub_Init(); if ( init_res != 0 ) { log_error ( "IoT Hub initialize failed." ); fclose ( fp_log ); return -1; } log_info ( "IoT Hub client version: %s", IoTHubClient_GetVersionString() ); // 创建设备连接 IOTHUB_DEVICE_CLIENT_LL_HANDLE device_handle = IoTHubDeviceClient_LL_CreateFromConnectionString ( iothub_connection_string, MQTT_Protocol ); if ( device_handle == NULL ) { log_error ( "Can't get device client handle. Check connection string." ); fclose ( fp_log ); return -1; } IoTHubDeviceClient_LL_SetConnectionStatusCallback ( device_handle, connection_status_callback, NULL ); // 获取湿度值 float humidty_val = get_humidty_value ( ); if ( humidty_val > 0 ) { // 基于湿度值构建一条待发送的消息 RPI_MESSAGE_HANDLE message = Rpi_CreateMessage ( humidty_val ); // 将消息发送到IoT Hub send_message ( device_handle, message ); // 释放消息所占用的资源 Rpi_DestroyMessage ( message ); } else { log_error ( "Message was not sent due to a failure in getting humidty value." ); } // 关闭IoT Hub连接并释放资源 IoTHubDeviceClient_LL_Destroy ( device_handle ); // 释放IoT Hub C SDK资源 IoTHub_Deinit(); fclose ( fp_log ); return 0;}限于文章篇幅,没有将所有代码贴出,感兴趣的话可以访问https://gitee.com/daxnet/humidty/tree/master/src/iot来阅读这部分的代码。从这部分代码可以看到,每次运行这个程序,它会收集一次数据,然后调用一次Azure IoT Hub将数据推送出去。为了达到每隔一定时间进行一次数据采集和推送的目的,我使用了Linux下的cron服务。由于调用pigpio的API需要使用root权限,因此,cron服务的配置也需要基于root用户,于是就应该使用下面的命令来编辑crontab文件:
sudo crontab -e然后在文件中加入下面这行即可,表示每15分钟执行一次:
*/15 * * * * /home/daxnet/projects/humidty/src/iot/main "<iot_hub_connection_string>"cron表达式的后面跟着的就是上面的C语言代码编译出来的可执行程序,编译命令类似如下。我并没有使用cmake等编译工具集来执行编译任务,因为我们的程序比较简单,没有必要搞得太重:
gcc -Wall -fdiagnostics-color=always -g main.c rpi_message.c log.c -o main \-liothub_client -liothub_client_mqtt_transport -lumqtt -lprov_device_client \-lprov_auth_client -lhsm_security_client -lutpm -laziotsharedutil -lpthread \-lcurl -lssl -lcrypto -lm -lparson -lprov_mqtt_transport -lpigpio -lrtAzure Function App的开发
在整个解决方案中,Azure Function App的作用是将推送到Azure IoT Hub的消息内容保存到后端的Azure Database for PostgreSQL flexible server数据库中,以便接下来的前端页面可以使用。可以直接使用Visual Studio 2022来实现Azure Function App的开发,开发需要安装Azure开发工作负载:
然后在新建项目时,选择Azure Functions项目模板:
Azure Function App的业务代码非常简单,如下:
public class HumidtyStoringFunction(ILogger<HumidtyStoringFunction> logger){ private const string DatabaseTableName = "public.humidty_history"; public void Run( EventData[] events) { var dbConnectionString = Environment.GetEnvironmentVariable("PostgresConnectionString"); if (string.IsNullOrWhiteSpace(dbConnectionString)) { logger.LogError("Database connection string is not specified, function will not proceed."); return; } try { // 初始化PostgreSQL连接 using var sqlConnection = new NpgsqlConnection(dbConnectionString); // 对于收到的每一个事件(消息) foreach (var @event in events) { var eventJson = Encoding.UTF8.GetString(@event.EventBody); logger.LogInformation($"Processing event {eventJson}"); var jobject = JObject.Parse(eventJson); // 获得时间和湿度值 var t = jobject.GetValue("t")?.Value<long>(); var v = jobject.GetValue("v")?.Value<double>(); if (t is null || v is null) { logger.LogError("Event received but the content is incorrect, function will not proceed."); return; } // 将时间和湿度值插入数据库 var sql = $@"INSERT INTO {DatabaseTableName} (""time"", ""value"") VALUES ({t}, {v})"; sqlConnection.Execute(sql); logger.LogInformation("Event processed successfully."); } } catch (Exception e) { // 如果出错,则写日志,并抛出 logger.LogError(e, "Failed to process event, exception details as below..."); throw; } }}有两点需要注意:
[*]EventHubTriggerAttribute中的Connection参数指定的是保存Azure IoT Hub连接字符串的环境变量的名称(这里是ConnectionStringSetting),而不是连接字符串本身
[*]使用try...catch合理地处理异常,此处建议在代码中完成异常处理之后,使用throw语句将异常抛出,这样,在Azure Function App的管理页面中,就会产生一个执行失败的记录
整个Function App的完整项目代码可以参考代码库:https://gitee.com/daxnet/humidty/tree/master/src/function。
Azure App Service前端应用的开发
前端应用的开发使用的是ASP.NET Core Web App (Razor Pages)项目模板,集成Chart.js实现曲线图的显示。详细代码这里就不贴出来了,可以直接访问代码库来查看完整的项目代码:https://gitee.com/daxnet/humidty/tree/master/src/web。只是在获取AI建议的时候,调用会比较慢,为了不影响页面加载和用户体验,我简单粗暴地使用Ajax实现AI建议的获取,并异步地将结果显示在界面上。在Index.cshtml代码文件中,页面加载完成时调用Ajax:
$(document).ready(function(){ $.ajax({ type: "GET", url: "/?handler=AISuggestion", contentType: "application/json", dataType: "json", success: function (result) { $('#aiSuggestion').html(result); } });});然后,在Index.cshtml.cs后台文件中,实现这个AISuggestion方法:
public async Task<JsonResult> OnGetAISuggestionAsync(){ var startTime = DateTime.Now.AddDays(-7); var endTime = DateTime.Now; var recentHumidtyData = await Utils.GetHumidtyHistory(startTime, endTime, _connectionString); var avgHumidtyData = recentHumidtyData .GroupBy(h => h.Time / 3600 / 6) .Select(g => new HumidtyHistory { Time = g.First().Time, Value = g.Average(h => h.Value) }) .ToDictionary(h => Utils.UnixTimestampToLocalDateTime(h.Time), h => h.Value); var sb = new StringBuilder(); sb.AppendLine("下面最近7天内每6小时的平均数据趋势:"); var sbResponse = new StringBuilder(); foreach (var kvp in avgHumidtyData) { sb.AppendLine($"时间:{kvp.Key.ToShortDateString()} {kvp.Key.ToShortTimeString()},数据:{kvp.Value}"); } sb.AppendLine($""" 数据越接近31840,表明冬眠箱越干燥,越需要补水,数据越接近0,表明冬眠箱越湿润,不需要补水。 现在时间是{DateTime.Now.ToShortDateString()} {DateTime.Now.ToShortTimeString()},请预测乌龟冬眠箱补水的大致时间。 """); _chat.Clear(); _chat.AddUserMessage(sb.ToString()); await foreach (var message in _chatCompletionService.GetStreamingChatMessageContentsAsync(_chat)) { sbResponse.AppendLine(message.Content); } return new JsonResult(sbResponse.ToString());}这段代码会将最近7天内的数据,每6小时做一个平均,然后作为上下文文本提供给AI,然后让AI基于现在的时间来预测乌龟冬眠箱需要补水的大致时间。
总结
通过软硬结合,借助云计算平台和AI来实现一个解决实际问题的方案,确实是一件有趣的事情。内容比较多,本文也只是在整个乌龟冬眠箱适度监控和AI建议解决方案的各个部分进行一些简单粗略的介绍,但应该已经基本涵盖了主体流程的各个部分。如果对于某些细节问题希望能够深入展开,欢迎留言讨论。
页:
[1]