乌龟冬眠箱湿度监控系统和AI辅助建议功能的实现
liuian 2025-09-03 03:30 21 浏览
| 注:本文曾发表在博客园我的个人博客中,转载至此公众号以归档保存。
家里小朋友养了一只小乌龟,到了冬天就冬眠了,早早地准备了一个冬眠箱,铺上椰土,在室温低于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子网上的,并且由这个Private DNS负责域名解析。在Private Endpoint的DNS configuration中,将FQDN复制下来,这就是数据库中连接字符串的主机名称。
privatelink.postgres.database.azure.com
通过数据库的主页上的Connect链接,就能获得访问数据库的连接字符串,这里就不多做说明了。
Azure Function App
仍然在Azure Portal主页上,新建一个Azure Function App的资源,Azure Function App是需要由一个宿主(hosting)提供运行环境的,这个宿主环境可以有多个选择,在Azure中称为Hosting plan。Azure提供下面这些Hosting plan:
我选择的是App Servicehosting 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;}// 打开I2Cint 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[2] = { 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[2];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[0] << 8) | data[1];// 计算出电压值,仅作日志输出参考使用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." );elselog_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 == ){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, );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 == ){if ( argc == 2 ){iothub_connection_string = argv[1];}if ( iothub_connection_string == ){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 == ){log_error ( "Can't get device client handle. Check connection string." );fclose ( fp_log );return -1;}IoTHubDeviceClient_LL_SetConnectionStatusCallback ( device_handle, connection_status_callback, );// 获取湿度值float humidty_val = get_humidty_value ;if ( humidty_val > 0 ){// 基于湿度值构建一条待发送的消息RPI_MESSAGE_HANDLE message = Rpi_CreateMessage ( humidty_val );// 将消息发送到IoT Hubsend_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 -lrt
Azure 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";[Function("humidty_storing_function")]public void Run([EventHubTrigger("iothub-ehub-humidty-io-56972526-7565e081a6",Connection = "ConnectionStringSetting")] EventData events){var dbConnectionString = Environment.GetEnvironmentVariable("PostgresConnectionString");if (string.IsOrWhiteSpace(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 || v is ){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建议解决方案的各个部分进行一些简单粗略的介绍,但应该已经基本涵盖了主体流程的各个部分。如果对于某些细节问题希望能够深入展开,欢迎留言讨论。
相关推荐
- 搭建一个20人的办公网络(适用于20多人的小型办公网络环境)
-
楼主有5台机上网,则需要一个8口路由器,组网方法如下:设备:1、8口路由器一台,其中8口为LAN(局域网)端口,一个WAN(广域网)端口,价格100--400元2、网线N米,这个你自己会看了:)...
- 笔记本电脑各种参数介绍(笔记本电脑各项参数新手普及知识)
-
1、CPU:这个主要取决于频率和二级缓存,频率越高、二级缓存越大,速度越快,现在的CPU有三级缓存、四级缓存等,都影响相应速度。2、内存:内存的存取速度取决于接口、颗粒数量多少与储存大小,一般来说,内...
- 汉字上面带拼音输入法下载(字上面带拼音的输入法是哪个)
-
使用手机上的拼音输入法打成汉字的方法如下:1.打开手机上的拼音输入法,在输入框中输入汉字的拼音,例如“nihao”。2.根据输入法提示的候选词,选择正确的汉字。例如,如果输入“nihao”,输...
- xpsp3安装版系统下载(windowsxpsp3安装教程)
-
xpsp3纯净版在采用微软封装部署技术的基础上,结合作者的实际工作经验,融合了许多实用的功能。它通过一键分区、一键装系统、自动装驱动、一键设定分辨率,一键填IP,一键Ghost备份(恢复)等一系列...
- 没有备份的手机数据怎么恢复
-
手机没有备份恢复数据方法如下1、使用数据线将手机与电脑连接好,在“我的电脑”中可以看到手机的盘符。 2、将手机开启USB调试模式。在手机设置中找到开发者选项,然后点击“开启USB调试模式”。 3、...
- 电脑怎么激活windows11专业版
-
win11专业版激活方法有多种,以下提供两种常用的激活方式:方法一:使用激活密钥激活。在win11桌面上右键点击“此电脑”,选择“属性”选项。进入属性页面后,点击“更改产品密钥或升级windows”。...
- 华为手机助手下载官网(华为手机助手app下载专区)
-
华为手机助手策略调整,已不支持从应用市场下载手机助手,目前华为手机助手是需要在电脑上下载或更新手机助手到最新版本,https://consumer.huawei.com/cn/support/his...
- 光纤线断了怎么接(宽带光纤线断了怎么接)
-
宽带光纤线断了可以重接,具体操作方法如下:1、光纤连接的时候要根据束管内,同色相连,同芯相连,按顺序进行连接,由大到小。一般有三种连接方法,分别是熔接、活动连接和机械连接。2、连接的时候要开剥光缆,抛...
- win7旗舰版和专业版区别(win7旗舰版跟专业版)
-
1、功能区别:Win7旗舰版比专业版多了三个功能,分别是Bitlocker、BitlockerToGo和多语言界面; 2、用途区别:旗舰版的功能是所有版本中最全最强大的,占用的系统资源,...
- 万能连接钥匙(万能wifi连接钥匙下载)
-
1、首先打开wifi万能钥匙软件,若手机没有开启WLAN,就根据软件提示打开WLAN开关;2、打开WLAN开关后,会显示附近的WiFi,如果知道密码,可点击相应WiFi后点击‘输入密码’连接;3、若不...
- 雨林木风音乐叫什么(雨林木风是啥)
-
雨林木风的创始人是陈年鑫先生。陈年鑫先生于1999年创立了雨林木风公司,其初衷是为满足中国市场对高品质、高性能电脑的需求。在陈年鑫先生的领导下,雨林木风以技术创新、产品质量和客户服务为核心价值,不断推...
- aics6序列号永久序列号(aics6破解序列号)
-
关于AICS6这个版本,虽然是比较久远的版本,但是在功能上也是十分全面和强大的,作为一名平面设计师的话,AICS6的现有的功能已经能够应付几乎所有的设计工作了……到底AICC2019的功能是不是...
- 手机可以装电脑系统吗(手机可以装电脑系统吗怎么装)
-
答题公式1:手机可以通过数据线或无线连接的方式给电脑装系统。手机安装系统需要一定的技巧和软件支持,一般需要通过数据线或无线连接的方式与电脑连接,并下载相应的软件和系统文件进行安装。对于大部分手机用户来...
- 一周热门
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
