万字长文教你如何实现华为云IoT+OpenHarmony智能家居开发
本文分享自华为云社区《华为云IoT+OpenHarmony的智能家居开发》,作者:袁锐。
一、提示选择说明
1。主题为基于OpenHarmony的智能家居,应用场景为家庭使用,受益人群为居民。
2。开发的软件设备有智能门锁、存储精灵、光伏逆变器等,都是针对软件和硬件进行开发的。
3。解决的问题:
传统智能家居:智能物品需要手动添加到场景中,没有网络无法智能控制
创新智能家居:空间智能,发现后融入场景,分布式软总线控制
4。关键功能点:
智能门锁:密码开锁、NFC开锁、数字管家控制、服务卡控制
存储向导:密码解锁、NFC解锁、防火卷帘控制、分布式软总线控制
逆变器:单相逆变器,隔离拓扑,组件小型化,高转换率,低总谐波失真
二、赛事发展平台
1。操作系统:OpenHarmony 3.0
2。开发软件:VS code(Deveco studio工具)、DevEco Studio
3。开发板:深凯虹KHDVK-3861B、润禾DAYU200、润禾hispark AI相机
3。关于环境:
操作系统:Ubuntu 编译和构建:Python
包管理工具:HPM NPM 环境:Node.js
烧录软件:Hiburn USB串口驱动:CH341SER.exe
本地ssh:finalshell ssh文件比较:Beyond Conpare
4。虚拟机
(1)虚拟机环境
Ubuntu(华为的硬件开发一般都是在这个Linux环境下进行的)
虚拟机Vmware:下载后使用上面提到的华为云中国镜像。
下载Linux版VS code和OpenHarmony3.0源码。
(2)虚拟机环境:
修改Ubuntu Shell环境为bash:ls -l /bin/sh
下载VS code后,下载华为硬件编译插件(Device tool)
(3)HB编译插件:
安装:python3 -m pip install --user ohos-build
变量:vim ~/.bashrc
复制并粘贴到.bashrc的最后一行:export PATH=~/.local/bin:$PATH
更新变量:source ~/.bashrc
检查:pip install ohos-build
5。逆变器主要硬件选型:
(1)选材计划(选材依据)
Diode(二极管):高频检测电路,小型化,匹配二极管勿混用
电感器(电感):标称电感值、自谐振频率(与互感值成正比)直流电阻(尽可能小)、额定电流、直流重叠允许电流、温升允许电流。
电阻:贴片电阻,根据稳定性要求选择薄膜或厚膜
SPICE NMOS:封装尺寸,基本上封装越大越好,能承受的电流越大;导通电压Vgs尽量选择比实际电路中能够提供的控制电压小的值;导通电阻Rds越小越好,相应的导通电阻越小,分压越小,发热也越小。寄生电容Cgs会影响mos的开通速度。如果寄生电容太大,方波就会失真。 Rds 越小,Cgs 越大。
(2)主要材质选择
半导体材料选用:国产半导体RX65T125PS2A
电源IC选材:国产IC芯片ID7s625
DSP处理器:洞察STM32系列软性结合了DSP处理器的功能。
三,计划详情
(1) 存储向导
(1)用户视角:首先从用户角度考虑需求以及如何使用,然后从技术层面进行分析
(具体用户使用方法这里就不赘述了,具体请参考下午的开发内容)
(2)实现原理:(下面会详细介绍步骤,这里先介绍一下初步的想法)
① 关于华为云中Mqtt协议的开放(设备在线激活):使用mqttX或mqttfx。
② 华为云:根据提示创建并获取密钥等信息,获取ClientID等身份信息,然后在云端的Topic(事件主题)中自定义订阅和发布,定义产品。
③ AppGallery Connect网站:创建并注册HarmonyOS产品,按照提示流程操作。
④ 设备开发详细分析:每个设备都是一个子节点,基于OpenHarmony设备实现L0、L1、L2设备之间的互联。主控程序基于OpenHarmony JS应用框架进行设计和实现,并使用MQTT物联网通信协议接入华为云物联网平台。同时可以向华为云物联网平台发送控制指令进行云端处理。 DAYU开发板(软件+硬件)的具体实现是,中控MQTT通信过程是在内核态驱动中。 JS应用程序发起接口调用后,就进入用户态调用内核态接口的过程,JS应用程序将其需要的内容发送给云端。 MQTT协议的主体内容直接传输到内核态。内核态不进行数据处理和解析,直接将数据发布到云端。这样设计的目的是为了在添加设备时只改变JS应用的数据结构,而不需要修改设备代码来完成解耦。
NFC录入及录音:使用NFC扩展板录入。详细信息请参见下面的软总线设备通信链接。
⑤ 智能识别比对:识别对象数据库,这里的识别为单张照片识别。 vuforia服务器为目标创建一个识别数据库,需要下载并绑定到项目中。照片由相机拍摄并逐帧进行比较。
(3) 设备侧
第一步:网络连接 让设备开机后自动连接互联网
我们会在代码中预设热点名称和密码
创建包后,新建一个mqtt_entry.c地址,用于存放热点自连代码:
/home/open/Downloads/code-v3.0-LTS/OpenHarmony/applications/sample/wifi-iot/app/mqtt_demo
{ int ret; 错误号_trc; hi_wifi_assoc_request assoc_req = {0}; /* 将 SSID 复制到关联的请求 */ /* 将 SSID 复制到 assoc_req */ rc = memcpy_s(assoc_req.ssid, HI_WIFI_MAX_SSID_LEN + 1, "rui666" 、8); //热点名称 /* WPA-PSK。 CNcomment:身份验证类型:WPA2-PSK.CNend */ if (rc !=EOK) { 返回-1; } //热点加密方式 assoc_req.auth =HI_WIFI_SECURITY_WPA2PSK; memcpy(assoc_req.key, "88888888", 11); //热点密码 ret = hi_wifi_sta_connect(&assoc_req);if(ret!=HISI_OK){ 返回-1; } 返回0; } //预设热点名称和密码。设备开机后会自行连接
这里原来的能力等价物被自我发现热点所取代。
*OpenHarmony_ability的自发现和自配置网络描述如下。
第二步:报表订阅与分发,创建此包中的main函数
/home/open/Downloads/code-v3.0-LTS/OpenHarmony/applications/sample/wifi-iot/app/mqtt_demo
void mqtt_callback(MessageData *msg_data) { size_t res_len = 0; uint8_t *response_buf = NULL; char主题名称[45] = { "$crsp/" }; LOS_ASSERT(msg_data); printf("topic %.*s 收到消息\r\n", msg_data->topicName->lenstring.len, msg_data->topicName->lenstring.数据); printf("消息为 %.*s\r\n", msg_data->消息->payloadlen, msg_data->消息->有效负载); }int mqtt_connect(void) { int rc = 0; NetworkInit(&n); NetworkConnect(&n, "www.introzo.com", 1883); buf_size = 4096+1024; onenet_mqtt_buf =(无符号char *)malloc(buf_size); onenet_mqtt_readbuf =(无符号char *)malloc(buf_size); if (!(onenet_mqtt_buf && onenet_mqtt_readbuf)) { printf("MQTT客户端缓冲区没有内存!"); 返回-2; } MQTTClientInit(&mq_client, &n, 1000, onenet_mqtt_buf, buf_size, onenet_mqtt_readbuf, buf_size); MQTTStartTask(&mq_client); data.keepAliveInterval = 30; data.cleansession = 1; data.clientID.cstring = "61f6e729de9933029be57672_88888888_0_0_2022020905"; data.username.cstring = "61f6e729de9933029be57672_88888888";data.password.cstring = "43872acc0b1e6aa7bf9e6a69f12aa9b1ebc07daffb67e18cf905c847a594f813”; data.cleansession = 1; mq_client.defaultMessageHandler = mqtt_callback; //连接服务器 rc = MQTTConnect(&mq_client, &数据); //订阅消息并设置回调函数 MQTTSubscribe(&mq_client, "保时捷", 0, mqtt_callback); 同时(1) { MQTTMessage消息; 消息.qos = QOS1; message.retained = 0; message.payload = (void *)"openharmony"; message.payloadlen = strlen("openharmony"); //报告 if (MQTTPublish(&mq_client, "hi3861", &message) < ) { printf("MQTT发布失败!\r\n"); } IoTGpioSetOutputVal(9, 0); //9gpio 0灯亮 我们睡觉了(1000000); } 返回0; }
第三步:存储精灵保险模式&舵机开启
伺服解锁:
int伺服ID =0; void My_servo(uint8_tservoID,int角度) { intj=0; int k=2000/200; 角度= k*角度; 对于 (j=0;j<5;j++){ IoTGpioSetOutputVal(servoID, 1); hi_udelay(角度); IoTGpioSetOutputVal(servoID, 1); hi_udelay(20000-角度); } }
保险模式:
静态 int DealSetPassword(cJSON *objCmd) { int ret = -1; char *pstr = NULL; cJSON *objParas = NULL; cJSON *objPara = NULL; CommandParamSetPsk setLockPskParam; memset(&setLockPskParam, 0x00, sizeof(CommandParamSetPsk)); if ((objParas = cJSON_GetObjectItem(objCmd, "paras")) == 空){ RaiseLog(LOG_LEVEL_ERR, "参数不存在"); 返回ret; } if ((objPara = cJSON_GetObjectItem(objParas, "PskId")) != 空){char *id = cJSON_GetStringValue(objPara); //密钥(字符串型) if (id == NULL || strlen(id) > LOCK_ID_LENGTH) { RaiseLog(LOG_LEVEL_ERR, "检查锁ID失败!"); 返回-1; } strncpy(www.introzo.com, id, strlen(id)); } 其他 { 返回ret; } if ((objPara = cJSON_GetObjectItem(objParas, "选项")) != 空){ char *选项 = cJSON_GetStringValue(objPara); printf("选项 = %c \n", *选项); //三指令(字符串型) if(*选项=='A'){ setLockPskParam.option = OPTION_ADD; //新增密码 } else if(*选项=='U' ) { setLockPskParam.option = OPTION_UPDATE; //更新秘密 } else if(*选项=='D' ) {setLockPskParam.option = OPTION_DELETE; //删除密码 } 否则 { RaiseLog(LOG_LEVEL_ERR, "无此选项(%c)!",*选项); 返回-1; } } 其他 { 返回ret; } if ((objPara = cJSON_GetObjectItem(objParas, "LockPsk")) != 空){ char *psk = cJSON_GetStringValue(objPara); if (psk == NULL || strlen(psk) > LOCK_PSK_LENGTH) { RaiseLog(LOG_LEVEL_ERR, "检查 psk 失败!"); 返回-1; } strncpy(setLockPskParam.password, psk, strlen(psk)); } 其他 { 返回ret; } ret = IotProfile_CommandCallback(CLOUD_COMMAND_SETPSK, &setLockPskParam); 返回ret; }
第四步:标注GPIO口
识别GPIO口与接入口(这里要注意一个接是一个是接地还有一个为信号传输口)
void mqtt_test(void) { IoTGpioInit(9); IoTGpioSetDir(9, IOT_GPIO_DIR_OUT); mqtt_connect(); }
第五步:提升mqtt协议(www.introzo.com版本)
与主函数并行的www.introzo.com,挂函数和第三方库的内容:
来源= [ "mqtt_test.c", "mqtt_entry.c" ] include_dirs = [ "//utils/native/lite/include", "//kernel/liteos_m/components/cmsis/2.0", "//base/iot_hardware/interfaces/kits/wifiiot_lite", "//供应商/hisi/hi3861/hi3861/third_party/lwip_sack/include", "//基础/通信/接口/套件/wifi_lite/wifiservice", "//third_party/pahomqtt/MQTTPacket/src", "//third_party/pahomqtt/MQTTClient-C/src", "//third_party/pahomqtt/MQTTClient-C/src/liteOS", "//kernel/liteos_m/kal/cmsis", "//基础/iot_hardware/外设/接口/套件", ] 取决于 = [ "//third_party/pahomqtt:pahomqtt_static", //吊装MQTT协议 ] }
www.introzo.com:与APP并行的www.introzo.com,用于指示要编译的main函数。您可以使用启动后跟要编译的主包的方式,也可以直接选择功能。这里可以放置不需要参与编译的项目。用#注释掉它。
启动时的www.introzo.com:
导入("//build/lite/config/component/lite_component.gni") lite_component("app") { 特征 = [ T "qmqtt_demo: mqtt_test" , // 标记主函数,指定位置编译 】
Storage Wizard Pro版本(识别功能版):(使用第三方平台:Vuforia)
我们的原理是把图片上传到云端,然后逐帧分解比较(这个功能还在完善中)
(4)软件端(偏向软件,但还是嵌入式开发)
第一步:从服务器接收存储码
exports.i***ta2=函数(req,res){ console.log("iot_data:",req) const url = new URL("获取华为云提供的URL" +req.url) //转发目的地内部配置的地址 让属性 = JSON.stringify(req.body.notify_data.body.services) console.log("存储数据:",属性)让 addArr = [属性] var addSql = '插入 sesnor_Record(属性)值(?)' var回调=函数(错误,数据){ console.log("错误:"+err) console.log("属性插入结果:"+JSON.stringify(data)) 重新发送(数据) } sqlUtil.sqlContent(addSql,addArr,callBack) }
第二步:射频贴纸和复旦卡拉OK本地计划
请使用第三方软件写入复旦卡,并使用NFC射频贴应用调试助手(可在华为应用商店下载)。
void RC522_Config ( void ) { uint8_t ucStatusReturn; //返回状态 uint8_t flag_station = 1; //保留函数的标志位 同时(flag_station) { /* 寻找卡片(方法:范围内全部),第一次搜索再次失败,当搜索成功后,将卡片序列传入数组ucArray_ID*/if ( ( ucStatusReturn = PcdRequest ( PICC_REQALL, ucArray_ID ) ) != MI_OK ) ucStatusReturn = PcdRequest ( PICC_REQALL, ucArray_ID ); if ( ucStatusReturn == MI_OK ) { /* 一种防冲突操作,其中将选定的卡片序列传递到数组 ucArray_ID */ if ( PcdAnticoll ( ucArray_ID ) == MI_OK ) { if ( PcdSelect ( ucArray_ID ) == MI_OK ) { printf ("\nRC522 已就绪!\n"); flag_station = 0; } } } } }
第三步:智能窗帘轨道解决方案
因为电机可以无限前后方向拉动窗帘导轨,所以选择电机进行拉动。另外,还要注意电机的功率来决定是否加继电器。详情请前往源码仓库(购买电机时向供应商索取相关数据,同时向开发板供应商索要已发布的引脚位置图)
静态 void RtcTimeUpdate(void) { extern int SntpGetRtcTime(int localTimeZone, struct tm *rtcTime); struct tm rtcTime; SntpGetRtcTime(CONFIG_LOCAL_TIMEZONE,&rtcTime);RaiseLog(LOG_LEVEL_INFO, "年:%d 月:%d 周日:%d 日:%d 小时:%d 分钟:%d 秒:%d", \ www.introzo.com_year + BASE_YEAR_OF_TIME_CALC, www.introzo.com_mon + 1, www.introzo.com_mday,\ www.introzo.com_wday、www.introzo.com_hour、www.introzo.com_min、www.introzo.com_sec); if (www.introzo.com_wday > 0) { g_appController.curDay = www.introzo.com_wday - 1; } 其他 { g_appController.curDay = EN_SUNDAY; } g_appController.curSecondsInDay = www.introzo.com_hour * CN_SECONDS_IN_HOUR + \ www.introzo.com_min * CN_SECONDS_IN_MINUTE + www.introzo.com_sec + 8; //添加 8 毫秒偏移 } 静态 uint32_t Time2Tick(uint32_t ms) { uint64_t ret; ret = ((uint64_t)ms * osKernelGetTickFreq()) / CN_MINISECONDS_IN_SECOND; 返回 (uint32_t)ret; } #define CN_REACTION_TIME_SECONDS 1 静态 voidBoardLedButtonCallbackF1(char*arg ) { 静态 uint32_t 最后秒 = 0;uint32_t curSec = 0; RaiseLog(LOG_LEVEL_INFO, "按钮按下"); curSec = g_appController.curSecondsInDay; if((curSec) < (lastSec + CN_REACTION_TIME_SECONDS)) { RaiseLog(LOG_LEVEL_WARN, "频繁按下的按钮"); 返回; } 最后秒 = curSec; g_appController.curtainStatus = CN_BOARD_SWITCH_ON; g_appController.pwmLedDutyCycle = \ g_appController.pwmLedDutyCycle > 0 ? g_appController.pwmLedDutyCycle:CONFIG_LED_DUTYCYCLEDEFAULT; osEventFlagsSet(g_appController.curtainEvent, CN_LAMP_EVENT_SETSTATUS); 返回; } 静态voidBoardLedButtonCallbackF2(char *arg) { uint32_t 最后秒 = 0; uint32_t curSec = 0; RaiseLog(LOG_LEVEL_INFO, "按钮按下"); curSec = g_appController.curSecondsInDay; if ((curSec) < (lastSec + CN_REACTION_TIME_SECONDS)) {RaiseLog(LOG_LEVEL_WARN, "频繁按下的按钮"); 返回; } 最后秒 = curSec; g_appController.curtainStatus = CN_BOARD_SWITCH_OFF; osEventFlagsSet(g_appController.curtainEvent, CN_LAMP_EVENT_SETSTATUS); 返回; } #define CURTAIN_MOTOR_GPIO_IDX 8 #define WIFI_IOT_IO_FUNC_GPIO_8_GPIO 0 #define WIFI_IOT_IO_FUNC_GPIO_14_GPIO 4 #define MOTOR_WORK_SECOND 6 静态 void E53SC1_MotorInit(void) { IoTGpioInit(CURTAIN_MOTOR_GPIO_IDX); IoTGpioSetFunc(CURTAIN_MOTOR_GPIO_IDX, WIFI_IOT_IO_FUNC_GPIO_8_GPIO); IoTGpioSetDir(CURTAIN_MOTOR_GPIO_IDX, IOT_GPIO_DIR_OUT); //设置GPIO_8为输出模式 返回; } 静态voidE53SC1_SetCurtainStatus(int窗帘状态) { if ((curtainStatus == CN_BOARD_CURTAIN_OPEN) || (curtainStatus == CN_BOARD_CURTAIN_CLOSE)) {IoTGpioSetOutputVal(CURTAIN_MOTOR_GPIO_IDX, 1); //设置GPIO_8输出高电平,开启电机 睡眠(MOTOR_WORK_SECOND); IoTGpioSetOutputVal(CURTAIN_MOTOR_GPIO_IDX, 0); //设置GPIO_8输出低电平,关闭电机 } 返回; } 静态void数据收集和报告(constvoid *arg) { (void)arg; uint32_t 窗帘事件; uint32_t waitTicks; waitTicks = Time2Tick(CONFIG_SENSOR_SAMPLE_CYCLE); 同时 (1) { 窗帘事件 = osEventFlagsWait(g_appController.curtainEvent, CN_CURTAIN_EVENT_SETSTATUS, \ osFlagsWaitAny、waitTicks); if (curtainEvent & CN_CURTAIN_EVENT_SETSTATUS) { RaiseLog(LOG_LEVEL_INFO, "GetEvent:%08x", curtainEvent); E53SC1_SetCurtainStatus(g_appController.curtainStatus); } (void) IotProfile_Report(g_appController.curtainStatus); } return; } static int UpdateShedule(CommandParamSetShedule *shedule) { if (shedule->num == 1 && shedule->day[0] == 0) { // set the one time schedule to current weekday shedule->day[0] = (g_appController.curDay + 1); } switch (shedule->option) { case 'A': IOT_ScheduleAdd(shedule->scheduleID, shedule->day, shedule->num, shedule->startHour * CN_SECONDS_IN_HOUR +\ shedule->startMinute * CN_SECONDS_IN_MINUTE, shedule->duration, shedule->shedulecmd.cmd, 0); break; case 'U': IOT_ScheduleUpdate(shedule->scheduleID, shedule->day, shedule->num, shedule->startHour * CN_SECONDS_IN_HOUR +\ shedule->startMinute * CN_SECONDS_IN_MINUTE, shedule->duration, shedule->shedulecmd.cmd, 0); break; case 'D': IOT_ScheduleDelete(shedule->scheduleID, shedule->day, shedule->num, shedule->startHour * CN_SECONDS_IN_HOUR +\ shedule->startMinute * CN_SECONDS_IN_MINUTE, shedule->duration, shedule->shedulecmd.cmd, 0); break; default: RaiseLog(LOG_LEVEL_ERR, "the schedule has no such option!\n"); break; } return 0; } void CurrentTimeCalcTimerHander(){ g_appController.curSecondsInDay ++; } #define TimeCalcTicks_NUMBER 100 #define CN_MINISECONDS_IN_1000MS 1000 static void CurtainShedule(void) { int startSecondInDay = 0; int endSecondInDay = 0; int settingCmd = 0; int executeTaskTime = 0; // indicate the do something busy osTimerId_t CurrentTimeCalc_id; CurrentTimeCalc_id = osTimerNew(CurrentTimeCalcTimerHander, osTimerPeriodic, NULL, NULL); osTimerStart(CurrentTimeCalc_id, TimeCalcTicks_NUMBER); while (1) { osDelay(Time2Tick(CN_MINISECONDS_IN_1000MS)); if (g_appController.curSecondsInDay >= CN_SECONS_IN_DAY) { g_appController.curSecondsInDay = 0; g_appController.curDay++; if (g_appController.curDay >= EN_DAYALL) { g_appController.curDay = EN_MONDAY; } IOT_ScheduleSetUpdate(1); } // check if we need do some task here if (IOT_ScheduleIsUpdate(g_appController.curDay, g_appController.curSecondsInDay)) { if (executeTaskTime > 0) { executeTaskTime = 0; if (g_appController.curtainStatus == CN_BOARD_CURTAIN_OPEN) { g_appController.curtainStatus = CN_BOARD_CURTAIN_CLOSE; osEventFlagsSet(g_appController.curtainEvent, CN_CURTAIN_EVENT_SETSTATUS); } } startSecondInDay = IOT_ScheduleGetStartTime(); endSecondInDay = startSecondInDay + IOT_ScheduleGetDurationTime(); IOT_ScheduleGetCommand(&settingCmd, NULL); } RaiseLog(LOG_LEVEL_INFO, "start:%d end:%d cur:%d",startSecondInDay, endSecondInDay, g_appController.curSecondsInDay); if ((endSecondInDay == startSecondInDay) && (g_appController.curSecondsInDay == endSecondInDay)) { if (g_appController.curtainStatus != settingCmd) { RaiseLog(LOG_LEVEL_INFO, "Triggering"); g_appController.curtainStatus = settingCmd; osEventFlagsSet(g_appController.curtainEvent, CN_CURTAIN_EVENT_SETSTATUS); } IOT_ScheduleSetUpdate(1); } } return; } int IotProfile_CommandCallback(int command, void *buf) { CommandParamSetShedule setSheduleParam; CommandParamSetCurtain setCurtainParam; //CommandParamSetDutyCycle setDutyCycleParam; CLOUD_CommandType cmd = (CLOUD_CommandType)command; if (cmd == CLOUD_COMMAND_SETCURTAIN_STATUS) { setCurtainParam = *(CommandParamSetCurtain *)buf; g_appController.curtainStatus = setCurtainParam.status; RaiseLog(LOG_LEVEL_INFO, "setCurtainParam.status:%d\r\n", setCurtainParam.status); osEventFlagsSet(g_appController.curtainEvent, CN_LAMP_EVENT_SETSTATUS); return 0; } else if (cmd == CLOUD_COMMAND_SETSHEDULE) { setSheduleParam = *(CommandParamSetShedule *)buf; RaiseLog(LOG_LEVEL_INFO, "setshedule:day:%d hour:%d minute:%d duration:%d \r\n", \ www.introzo.com,setSheduleParam.startHour,setSheduleParam.startMinute, setSheduleParam.duration); return UpdateShedule(&setSheduleParam); } return -1; } static int IotWifiInfo_get(char *ssid, int id_size, char *pwd, int pd_size) { int retval = UtilsGetValue(SID_KEY, ssid, id_size); if (retval <= 0) { RaiseLog(LOG_LEVEL_ERR, "no such ssid stored! \n"); return 0; } if ( UtilsGetValue(PWD_KEY, pwd, pd_size) < 0) { RaiseLog(LOG_LEVEL_INFO, "ssid(%s) no password stored! \n", ssid); } else { RaiseLog(LOG_LEVEL_INFO, "ssid : %s, pwd : %s! \n", ssid, pwd); } return 1; } static void IotWifiInfo_set(char *ssid, char *pwd) { if (UtilsSetValue(SID_KEY, ssid) != 0) { RaiseLog(LOG_LEVEL_ERR, "store ssid failed! \n"); return; } if (UtilsSetValue(PWD_KEY, pwd) != 0) { RaiseLog(LOG_LEVEL_ERR, "store password failed! \n"); UtilsDeleteValue(SID_KEY); return; } RaiseLog(LOG_LEVEL_INFO, "store password success! \n"); } static void IotMainTaskEntry(const void *arg) { osThreadAttr_t attr; NfcInfo nfcInfo; (void)arg; char ssid[BUFF_SIZE] = {0}; char pwd[BUFF_SIZE] = {0}; int ret = 0; g_appController.pwmLedDutyCycle = CONFIG_LED_DUTYCYCLEDEFAULT; BOARD_InitPwmLed(); BOARD_InitWifi(); E53SC1_MotorInit(); IOT_ScheduleInit(); ret = Board_IsButtonPressedF2(); osDelay(MAIN_TASK_DELAY_TICKS); LedFlashFrequencySet(CONFIG_FLASHLED_FRENETCONFIG); nfcInfo.deviceID = "6136ceba0ad1ed02866fa3b2_Curtain01"; nfcInfo.devicePWD = "12345678"; if (ret) { RaiseLog(LOG_LEVEL_INFO, "Netconfig Button has pressed! \n"); if (BOARD_NAN_NetCfgStartConfig(SOFTAP_NAME, ssid, sizeof(ssid), pwd, sizeof(pwd)) < 0) { RaiseLog(LOG_LEVEL_ERR, "BOARD_NetCfgStartConfig failed! \n"); return; } else { ret = AFTER_NETCFG_ACTION; } } else { ret = IotWifiInfo_get(ssid, sizeof(ssid), pwd, sizeof(pwd)); if (ret == 0) { if (BOARD_NAN_NetCfgStartConfig(SOFTAP_NAME, ssid, sizeof(ssid), pwd, sizeof(pwd)) < 0) { RaiseLog(LOG_LEVEL_ERR, "BOARD_NetCfgStartConfig failed! \n"); return; } else { ret = AFTER_NETCFG_ACTION; } } } LedFlashFrequencySet(CONFIG_FLASHLED_FREWIFI); if (BOARD_ConnectWifi(ssid, pwd) != 0) { RaiseLog(LOG_LEVEL_ERR, "BOARD_ConnectWifi failed! \n"); if (ret == AFTER_NETCFG_ACTION) { NotifyNetCfgResult(NETCFG_DEV_INFO_INVALID); } hi_hard_reboot(HI_SYS_REBOOT_CAUSE_CMD); return; } if (ret == AFTER_NETCFG_ACTION) { RaiseLog(LOG_LEVEL_DEBUG, "Connect wifi success ! \n"); NotifyNetCfgResult(NETCFG_OK); osDelay(MAIN_TASK_DELAY_TICKS); RaiseLog(LOG_LEVEL_DEBUG, "StopNetCfg wifi success ! \n"); StopNetCfg(); IotWifiInfo_set(ssid, pwd); } LedFlashFrequencySet(CONFIG_FLASHLED_FRECLOUD); RtcTimeUpdate(); if (CLOUD_Init() != 0) { return; } if (CLOUD_Connect(nfcInfo.deviceID, nfcInfo.devicePWD, \ CONFIG_CLOUD_DEFAULT_SERVERIP, CONFIG_CLOUD_DEFAULT_SERVERPORT) != 0) { return; } LedFlashFrequencySet(CONFIG_FLASHLED_WORKSWELL); attr.attr_bits = 0U; attr.cb_mem = NULL; attr.cb_size = 0U; attr.stack_mem = NULL; attr.stack_size = CONFIG_TASK_DEFAULT_STACKSIZE; attr.priority = CONFIG_TASK_DEFAULT_PRIOR; www.introzo.com = "DataCollectAndReport"; if (osThreadNew((osThreadFunc_t)DataCollectAndReport, NULL, (const osThreadAttr_t *)&attr) == NULL) { return; } www.introzo.com = "CurtainShedule"; if (osThreadNew((osThreadFunc_t)CurtainShedule, NULL, (const osThreadAttr_t *)&attr) == NULL) { return; } return; } static void IotMainEntry(void) { osThreadAttr_t attr; RaiseLog(LOG_LEVEL_INFO, "DATA:%s Time:%s \r\n",__FUNCTION__, __DATE__, __TIME__); g_appController.curtainEvent = osEventFlagsNew(NULL); if ( g_appController.curtainEvent == NULL) { return; } // Create the IoT Main task attr.attr_bits = 0U; attr.cb_mem = NULL; attr.cb_size = 0U; attr.stack_mem = NULL; attr.stack_size = CONFIG_TASK_MAIN_STACKSIZE; attr.priority = CONFIG_TASK_MAIN_PRIOR; www.introzo.com = "IoTMain"; (void) osThreadNew((osThreadFunc_t)IotMainTaskEntry, NULL, (const osThreadAttr_t *)&attr); return; }
步骤四:无感配网解决方案(重要ability)
OpenHarmony设备之间的信息传递利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家与设备之间的互联。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规SoftAP配网方式共存。
相关代码: teamX/common/iot_wifi/libs/libhilinkadapter_3861.a // 无感配网相关库文件 teamX/common/iot_wifi/libs/libnetcfgdevicesdk.a // 无感配网相关库文件 teamX/common/inc/iot_netcfg_nan.h teamX/common/inc/network_config_service.h // 无感配网相关头文件 teamX/common/iot_wifi/iot_wifi.c // 相关联网接口 teamX/common/iot_wifi/iot_netcfg_nan.c // 无感配网相关实现
数字管家可以在gitee下载源码,在下载的team_X中查看详细介绍
步骤五:第三方平台接入
储物精灵Pro版(识别功能版):(使用第三方平台:Vuforia)
我们的原理就是上传画面到云端,然后逐帧分解比对(此功能目前还在完善)
第六步:实时摄像功能与智能检测光照值功能(正在实验中)
int GetLightAverageVal(unsigned char cnt) { unsigned short readVal = 0; unsigned int totalVal = 0, totalCnt = 0; for(unsigned char i=0; i) { if(LightSensorVal(&readVal) == IOT_SUCCESS) { totalVal += readVal; totalCnt++; } usleep(50000); } return (totalVal/totalCnt); } enum ENV_LIGHT_STATE GetEnvLightState(void) { enum ENV_LIGHT_STATE lightState = LIGHT_DAY; int lightVal = GetLightAverageVal(5); if(lightVal > ENV_LIGHT_LEVEL_LOWEST) { lightState = LIGHT_NIGHT; } else if(lightVal > ENV_LIGHT_LEVEL_LOW) { lightState = LIGHT_DUSK; } else { lightState = LIGHT_DAY; } return lightState; }
第七步:分布式检索功能(实验中)
传统的分布式使用的是Elasticsearch进行,鉴于OpenHarmony能力所以需要开发出对口的ability。
isCreateIfMissing() //分布式数据库创建、打开、关闭和删除 setCreateIfMissing(boolean isCreateIfMissing) //数据库不存在时是否创建 isEncrypt() //获取数据库是否加密 setEncrypt(boolean isEncrypt) //设置数据库是否加密 getStoreType() //获取分布式数据库的类型 setStoreType(KvStoreType storeType) //设置分布式数据库的类型 KvStoreType.DEVICE_COLLABORATION //设备协同分布式数据库类型 KvStoreType.SINGLE_VERSION //单版本分布式数据库类型 getKvStore(Options options, String storeId) //根据Options配置创建和打开标识符为 storeId 的分布式数据库 closeKvStore(KvStore kvStore) //关闭分布式数据库 getStoreId() //分布式数据增、删、改、查。 subscribe(SubscribeType subscribeType, KvStoreObserver observer) //订阅 sync(ListdeviceIdList, SyncMode mode) 数据同步
开发说明:(包括OH分布式文件)
1. 构造分布式数据库管理类(创建 KvManagerConfig 对象)
Context context; ... KvManagerConfig config = new KvManagerConfig(context); KvManager kvManager = KvManagerFactory.getInstance().createKvManager(config);
2. 获取/创建单版本分布式数据库(声明需要创建的单版本分布式数据库ID说明)
Options CREATE = new Options(); CREATE.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION); String storeID = "testApp"; SingleKvStore singleKvStore = kvManager.getKvStore(CREATE, storeID);
3. 订阅分布式数据更改(客户端需要实现KvStoreObserver接口&结构并注册KvStoreObserver实例)
class KvStoreObserverClient implements KvStoreObserver() { public void onChange(ChangeNotification notification) { ListinsertEntries = notification.getInsertEntries(); List updateEntries = notification.getUpdateEntries(); List deleteEntries = notification.getDeleteEntries(); } } KvStoreObserver kvStoreObserverClient = new KvStoreObserverClient(); singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_ALL, kvStoreObserverClient);
4. 构造需要写入单版本分布式数据库的Key和Value(将键值数据写入单版本分布式数据库)
String key = "todayWeather"; String value = "Sunny"; singleKvStore.putString(key, value);
5. 构造需要从单版本分布式数据库快照中查询的Key(数据取自单版本分布式数据库快照)
String key = "todayWeather";String value = singleKvStore.getString(key);
6. 获取设备列表与同步数据(PUSH_ONLY)
ListdeviceInfoList = kvManager.getConnectedDevicesInfo(www.introzo.com_FILTER); List deviceIdList = new ArrayList<>(); for (DeviceInfo deviceInfo : deviceInfoList) { deviceIdList.add(deviceInfo.getId()); } singleKvStore.sync(deviceIdList, SyncMode.PUSH_ONLY);
7. 首先get到设备数据交换权限
ohos.permission.DISTRIBUTED_DATASYNC requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC"}, 0); //然后在AbilitySlice中声明数据库并使用即可,这里不多赘述
9. 关于API的开放能力请详见官方文档,这里不再赘述。
10. 怼相关接口(正在实验的内容)
SearchAbility searchAbility = new SearchAbility(context); CountDownLatch lock = new CountDownLatch(1); searchAbility.connect(new ServiceConnectCallback() { @Override public void onConnect() { lock.countDown(); } @Override public void onDisconnect() { } }); lock.await(3000, TimeUnit.MILLISECONDS);
11. 设置搜索属性与插入索引和重构查询等将会在下一次提交中进行补充。
(二)智能门锁
(上面的储物精灵源码也包括智能门锁的功能实现,这里补充介绍开发)
1. 环境搭建:
(1) 需要手动配置在deveco tool里的用户组件
(2) 接舵机的Gpio口
这里要注意一个接的是正极一个是接地还有一个为信号传输口
(3) 云端配置
首先在华为云官方获取Client ID等身份识别信息,然后在云端的Topic中自定义订阅与发布。
在初次开发时可以使用MQTTX软件进行了命令的订阅与下发实验,显示在线成功接收到上报和订阅的消息。
这样华为云的配置就成功了
如有需要还要进行产品定义与多功能的增加与实验
2.关于编译:
(1) 在VS code编译
点击build就可以生成json文件啦,编译成功后upload就可以直接烧录进去。(注意:在编译之后如果要再编译必须点击clean以删除build产生的json文件避免报错)
(2)在Ubuntu通过命令行编译
hb set //这是用于产生json文件的
hb build //这是用于编译的,编译后则会在源码的out文件夹中产生bin文件
hb clean //在build一次以后如果如果要再build那就必须进行此命令来删除json文件
在build成功后开发者就会发现在源码中的out文件夹中看到allinone.bin,然后发送到windows下使用Hiburn进行烧录即可(波特兰最大3000000,否则会烧坏板子)下图为HiBurn的配置方法,点击Connect即可烧录。
3.碰一碰卡片(原子化服务)
数字管家需要通过在APPGallery Connect中创建项目后添加应用从而获取Json文件,然后放在码云中下在的DistSchedule\netconfig\src\main\resources中。然后按照文档开发UI界面,点击构建的Generate Key and CSR创建用户名与密钥进行签名。
用户操作界面:在slice目录下新建 www.introzo.com文件,通过addActionRoute方法为此AbilitySlice配置一条路由规则,并且在在应用配置文件(config.json)中注册。在resources/base/layout下新建对应xml布局文件,在上述两个文件中编写相应的UI。
数字管家数据处理:从slice获取deviceId:在onStart中通过调用DeviceID等,获取设备的名称等方便数字管家识别设备。从slice页面获取状态:开关锁可以直接调用intent.getBooleanParam来确定是进行开关锁还是对门锁的日程进行编排。
编写设备控制命令的解析:在CommandUtil中根据具体设备定义profile,来新增获取命令和解析命令的方法,用于设备在本地调用sendCommand来发送命令和解析。
配置设备端信息:在DeviceData的initData中,根据设备ProductID添加设备图片ID、跳转的action参数和解析方法,配置完成后设备列表页、用户页面等都能通过该配置进行图片加载、路由跳转和解析。
最后进行接口对接与NFC写入就可以了(通过应用调试助手写入NFC识别详细用于快速让手机识别到设备从而吊起数字管家实现鸿蒙的Ability)
(三)逆变器
1. 拓扑图
设计的单相逆变器,拥有隔离拓扑,通过控制GaN(HEMT)的高频开关实现逆变
关于桥臂:两个半桥产生中性点电压,另外两个半桥产生线电压,最后一个半桥作为有源滤波器。
2.现在STM32f407兼容了OpenHarmony 3.2 bata版,因为f4系列软合了dsp处理所以无需另外使用dsp从处理器。考虑到尽量减少直流侧输入电流纹波,输出的正弦波尽可能的平滑与减小总谐波失真,设计了一种并联有源滤波器,它比在输入端使用批量电容更有效地补偿纹波。
3.考虑到大部分EDA的元件库原件都不全,我在kicad按照厂家提供的数据手册画了个原件,并按照例出的参数进行了标注。
4. 关于电流与电压的总谐波失真等:有源滤波器工作在更高的电压变化下将相应的能量存储在陶瓷电容器中,陶瓷电容器的电容随着电压的降低而增加。通过算法保持Vin稳定同时允许有源滤波器产生大的波纹。输出电流结合电磁屏蔽的开环霍尔传感器形成非常紧凑的测量装置提供电流解耦并降低对共模和寄生感应噪声的敏感性。特定的GaN控制调制降低了滤波器电感中的电流可以在不达到饱和水平的情况下降低其核心尺寸。
5. 关于硬件选材:在上文的 二.竞赛开发平台 的逆变器中有介绍
6.通讯部分
(1)分布式软总线
基于UDP的coap协议,OpenHarmony特有分布式软总线。
编程步骤: 1.创建socket;
2.设置socket属性,用函数setsockopt();
3.绑定IP地址、端口等信息到socket上,用函数bind();
4.循环接收/发送数据,用函数recvfrom&sendto;
5.关闭网络连接。
创建一个socket,无论是客户端还是服务器端都需要创建一个socket。该函数返回socket文件描述符,类似于文件描述符。socket是一个结构体,被创建在内核中。
class UdpClient { private DatagramSocket client; public String sendAndReceive(String ip, int port, String msg) { String responseMsg = ""; try { //Create a client-side DatagramSocket object without having to pass in addresses and objects client = new DatagramSocket(); byte[] sendBytes = msg.getBytes(); //Encapsulates the address of the destination to be sent InetAddress address = InetAddress.getByName(ip); //Encapsulates the object to send the DatagramPacket DatagramPacket sendPacket = new DatagramPacket(sendBytes,sendBytes.length,address,port); try { //sent Data client.send(sendPacket); }catch (Exception e){ // e.printStackTrace(); } byte[] responseBytes = new byte[2048]; //Create a DatagramPacket object for the response information DatagramPacket responsePacket = new DatagramPacket(responseBytes,responseBytes.length); try { //Waiting for the response information, as on the server side, the client blocks at this step until it receives a packet client.receive(responsePacket); }catch (Exception e){ // e.printStackTrace(); } //Parse the packet contents responseMsg = new String(responsePacket.getData(),0,responsePacket.getLength()); }catch (Exception e){ // e.printStackTrace(); }finally { //Close the client if(client != null){ client.close(); client = null; } } return responseMsg; } }
DatagramSocket类代表一个发送和接收数据包的插座该类是遵循 UDP协议 实现的一个Socket类
#define _PROT_ 8800 //UDP server port number #define _SERVER_IP_ "666.66.66.666" #define TCP_BACKLOG 5 #define IP_LEN 16 #define WIFI_SSID "rui666" //WiFi name #define WIFI_PASSWORD "1145141919810" //WIFI oassword
开发板的IP与端口号
public void performClassification() { int res = classifier.getResult(accelMeasurements, gyroMeasurements); TaskDispatcher uiTaskDispatcher = this.getContext().getUITaskDispatcher(); String lab = classes[res]; result = lab; TaskDispatcher globalTaskDispatcher = getContext().getGlobalTaskDispatcher(TaskPriority.DEFAULT); globalTaskDispatcher.asyncDispatch(new Runnable() { public void run() { HiLog.warn(label, udpClient.sendAndReceive("666.66.66.666", 8800, result)); } });
相关参数意义(注意要手搓的定义内容):
sin_family //Refers to protocol families, which can only be AF_INET in socket programming sin_port //Storage port number (using network byte order) sin_addr //Store the IP address and use the in_addr this data structure sin_zero //Empty bytes are reserved in order to keep sockaddr and sockaddr_in two data structures the same size fd //socket buf //UDP datagram buffer (contains data to be sent) len //The length of the UDP datagram flags //Invocation operation mode (typically set to 0) addr //A struct that points to the host address information that receives the data (type conversion required sockaddr_in) alen //The length of the structure referred to by addr nfds //Represents the range of all file descriptors in a collection readfds //Select monitors a collection of readable file handles、 writefds //A collection of writable file handles that select monitors exceptfds //A collection of exception file handles that select monitors timeout //The timeout end time of select() this time, NULL means permanent wait
测试客户端的成功方法:通过UDP软件进行相关的发送与接收,并查看打印信息。因为与下文介绍的MQTTX软件使用原理差不多所以这里不多赘述。
(2) MQTT
Mqtt是用于设备与服务器通讯的一种协议,使设备可以上报订阅下发信息。需要下载此协议并存放在thirdparty(第三方库),并在头文件中吊起。
从开发板厂商官网下载实验demo进行实验。因为目前大多数厂商使用的都是OpenHarmony 1.0代码作为演示,不同的源码版本在编译规则和文件名上都会不同,所以在下载的源码中的头文件吊起等也要修改才能接入mqtt协议。
Mqtt最重要要吊起的功能文件在 /home/open/Downloads/code_v3.0LTS/OpenHarmony/third_party/pahomqtt/MQTTClient-C/src里,特别是liteOS中。
7.服务卡片
(1)服务卡片原理
(2)APPGallery Connect
①数字管家:
数字管家需要通过在APPGallery Connect中创建项目后添加应用从而获取Json文件,在完成下述的2后把此文件放在码云中下载的FA源码的:
DistSchedule\netconfig\src\main\resources中。然后按照文档开发UI界面,点击构建的Generate Key and CSR创建用户名与密钥进行签名。
官网在我的项目中创建项目,选择harmonyOS平台等完成填写
https://www.introzo.com/consumer/cn/service/josp/agc/index.html#/
②逻辑处理:
(i)用户操作界面:在slice目录下新建 www.introzo.com文件,通过addActionRoute方法为此AbilitySlice配置一条路由规则,并且在在应用配置文件(config.json)中注册。在resources/base/layout下新建对应xml布局文件,在上述两个文件中编写相应的UI。
(ii)数字管家数据处理:从slice获取deviceId:在onStart中通过调用DeviceID等,获取设备的名称等方便数字管家识别设备。从slice页面获取状态:开关锁可以直接调用intent.getBooleanParam来确定是进行开关锁还是对门锁的日程进行编排。
(iii)编写设备控制命令的解析:在CommandUtil中根据具体设备定义profile,来新增获取命令和解析命令的方法,用于设备在本地调用sendCommand来发送命令和解析。
(iv)配置设备端信息:在DeviceData的initData中,根据设备ProductID添加设备图片ID、跳转的action参数和解析方法,配置完成后设备列表页、用户页面等都能通过该配置进行图片加载、路由跳转和解析。
(v) NFC写入:最后进行接口对接与NFC写入就可以了(通过应用调试助手写入NFC识别详细用于快速让手机识别到设备从而吊起数字管家实现鸿蒙的Ability)可以写到开发板的NFC预存区,也可以写在huawei share的碰一碰卡片上。(目前这两种写法都可以写无数次,在下一次写入时会自动清除上一次所写的)
③开发方式:
(i) 用户操作界面:通过桌面可以在卡片中点击相关服务,卡片中可以呈现一个或多个服务。
(ii)工作原理:通过嵌入到UI界面拉起那款应用的服务(可以通过缓存实现快速打开)从而起到交互功能的原子化服务。
(iii)生命周期管理:对设备使用方的 RPC 对象进行管理,请求进行校验以及对更新后的进行回调处理。
(iv)卡片尺寸:目前官方有四种尺寸,可以在new中自己选中喜欢的尺寸。
(v)上手开发:新建一个服务卡片
选择自己所需的卡片框架
(vi)开发环节:创建完之后然后就可以看到在原有的subject中生成了config.json文件。js默认配置了卡片大小等信息,froms下的是ability中生命周期管理的核心部分(用于回调),会在主函数中实现调用。
要在这里把false改成true。
上图的文件包为主要的开发位置,开发者动的是index下的三个包。
完成签名之后在在线调试的实验机器上运行后就会产生一张纯的FA卡片了,此时环境已经搭建完毕。
本地缓存调取:src在main下的resources中建rawfile用于存放缓存,在编译时候打包进hap中怼到鸿蒙设备中即可get到。
下面以开发1*2的mini卡片为例,在本地预置了缓存文件后我们目光转向卡片,继续把播放按钮与卡片解耦开,通过hml塞入显示信息等。isWidget当true时,card_containerdiv就会变为div布局。Ispause为true时,按钮呈现播放;为false时,显示暂停按钮。
在 css 文件采用原子布局的display-index。display-index 的值越大,则越优先显示。在 main中的onCreateForm 里isMiniWidget 的data设置为 true。
在.json和main中相对应的地方添加点击事件,到此为止就可以通过点击卡片就可以得到start与stop的互动了。做完显示界面以后,接入界面与预置的本地缓存,然后封装即可。
上图上中下分别是更新(onUpdateForm),删除(onDeleteForm),事件消息(message),
更新(onUpdateForm): 卡片更新与持久化储存卡片,定时更新与请求更新时进行调用。
删除(onDeleteForm):用于删除卡片时调用。 图三:formid&massage,接收通知。一张Fa卡片创建时需要满足的基本功能:布局加载~请求数据(ohos&intent)~产生卡片(long&生成ID用于调用){通过枚举值得到}。
这样一张服务卡片就开发好了。
四、创新点描述
1. 关于智能门锁:
基于OpenHarmony开发,使用原子化服务,拥有密码解锁,NFC解锁,数字管家控制等功能。
2. 关于储物精灵
基于OpenHarmony开发,使用原子化服务,密码解锁,NFC解锁,防火帘控制,分布式软总线控制等。
3. 关于逆变器
基于OpenHarmony开发,拓扑架构大幅度缩小转换器桥臂和EMI滤波器的尺寸,在算法使用CEC加权效率设计与峰值电压追踪,通过品质因数公式FOM算出使用合适的GaN半导体选型结合五个桥臂的设计可以最小化逆变器的能量传递。
五、成果展现
1. 编译成功
2. 动图演示(导入到word中是动图,word可能无法显示出动图效果所以把相关图片动图在上传文件夹中备份了一份)
以下动图分别是门锁的舵机驱动,NFC打卡,智能门轨的演示动图。
点击关注,第一时间了解华为云新鲜技术~
相关文章
- 10-05 苹果推出iOS 15.6正式版固件:我们来看看iO
- 10-05 iPadOS 16 允许应用程序使用 M1 设备存
- 10-05 为迎接Apple Watch 10周年:Appl
- 10-05 STM32连接esp32(stm32连接esp32
- 10-05 stm32串口dma发送和接收周期数据和随机数据(
- 10-05 stm32点亮led灯ad20 (stm32点亮l
- 10-05 esp32编程接线图(esp32编程程序接线图)
- 10-05 DAC0832波形发生器课程设计报告(dac083
- 10-05 vs2010单行读取文本_VS2010-MFC获取
- 10-05 Web漏洞-SQL注入(二)
- 10-05 phpunit thinkphp模型单元测试
- 10-05 phpunit selenium 操作 html
- 10-05 【第201期】面试官:String的长度有限制吗?
- 10-05 【第256期】面试官经常测试的21条Linux命令
- 10-05 【第256期】面试官常测试的21条Linux命令
- 10-05 【第368期】为什么阿里巴巴禁止MyBatis使用
- 10-05 【第208期】我们来敲黑板,说说如何设计秒杀系统(
- 10-05 【第208期】我们来敲黑板,说说如何设计秒杀系统(
- 10-05 【344期】面试官:如何设计群聊消息的已读未读功能
- 10-05 【第328期】Spring高频面试题:如何解决循环
- 最近发表