与去年不同,今年第二届HarmonyOS创新大赛要求很显然要求是比较高的,要想获得比较大的青睐,可能需要软硬件结合,南北向通吃的作品,而且最好是可以商业化落地的产品级解决方案。
这个挑战很大。
个人的想法是,寻找真正有商业价值的,可以结合现有HarmonyOS和Open Harmony技术特征的东西。幸运的是,我找到了这样的切入口,智能打蒜器。
缘由非常简单,平时喜欢吃水饺,虽然是南方人,不过偏爱北方的蘸酱和蒜泥吃法。这就遇到一个问题,蒜泥如何制作,用刀切太累了。。。作为一个程序员和纯种的懒人,就合计着如何自动化这一点。
打开拼多多,搜索蒜泥,搜到一大把的打蒜器。有手动的、有电动的,都下单买了来,居然销量超过10w+,这玩意这么受欢迎的吗?
使用情况:
1.手动反复拉的那种,用了几次感觉比刀切还累,这就尴尬了。 花费20。
2.电动蒜泥机,要一直按着开关,然后手就很酸,按了几次居然失灵了还是某个大牌电器(做电冰箱起家的),然后洗了一下,居然进水。前后用坏了5个蒜泥机,花费30×5=150。
6个月内,直接损失了170元。受不了啦,直接投降。
我来做一个App遥控的吧,这样总不会坏了吧。可能遇到此情况的用户很多,如果能做得出来,是不是也可以占领一小部分市场份额,从拼多多上那么大的销量算来,看起来挺有前景。
废话少说,经过对比,发现Neptune模块比Hi3861的成本更低,而且很容易购买,仅9.9元。嵌入一块Neptune到购买来的蒜泥原型机(主要含直流电机、3.7v电池、充放电模块)。电路设计比较简单,但暂时不会画电路图所以无法制作自定义PCB电路板,先使用简单的导线连接。
在这里感谢董昱老师的大力帮助。主要控制代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "wifiiot_gpio.h"
#include "wifiiot_gpio_ex.h"
#include "wifiiot_i2c.h"
#include "wifiiot_gpio_w800.h"
#include "net_params.h"
#include "wifi_connecter.h"
#include "net_common.h"
#include <errno.h>
#define LED_TASK_STACK_SIZE 512
#define LED_TASK_PRIO 25
enum LedState
{
LED_ON = 0,
LED_OFF,
LED_SPARK,
};
enum LedState g_ledState = LED_SPARK;
static void *GpioTask(const char *arg)
{
(void)arg;
while (1)
{
switch (g_ledState)
{
case LED_ON:
printf(" LED_ON! \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE0);
osDelay(1500);
break;
case LED_OFF:
printf(" LED_OFF! \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE1);
osDelay(1500);
break;
case LED_SPARK:
printf(" LED_SPARK! Low \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE0);
osDelay(1500);
printf(" LED_SPARK! High \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_00, WIFI_IOT_GPIO_VALUE1);
osDelay(1500);
break;
default:
osDelay(1500);
break;
}
}
return NULL;
}
static void GpioIsr(char *arg)
{
(void)arg;
enum LedState nextState = LED_SPARK;
printf(" GpioIsr entry\n");
GpioSetIsrMask(WIFI_IOT_GPIO_PB_09, 0);
switch (g_ledState)
{
case LED_ON:
nextState = LED_OFF;
break;
case LED_OFF:
nextState = LED_ON;
break;
case LED_SPARK:
nextState = LED_OFF;
break;
default:
break;
}
g_ledState = nextState;
}
void Broardcast(void *arg)
{
(void)arg;
int retval = 0;
// 建立UDP连接,这里充当了UDP的客户端
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP socket
struct sockaddr_in toAddr = {0};
toAddr.sin_family = AF_INET;
toAddr.sin_port = htons(PARAM_SERVER_PORT); // 端口号,从主机字节序转为网络字节序
if (inet_pton(AF_INET, PARAM_SERVER_ADDR, &toAddr.sin_addr) <= 0)
{ // 将主机IP地址从“点分十进制”字符串 转化为 标准格式(32位整数)
printf("inet_pton failed!\r\n");
goto do_cleanup;
}
// Broadcast me is online
while (1)
{
// 将online数据作为UDP的消息发送给手机
static char udpmessage[] = "live";
// UDP socket 是 “无连接的” ,因此每次发送都必须先指定目标主机和端口,主机可以是多播地址
retval = sendto(sockfd, udpmessage, sizeof(udpmessage), 0, (struct sockaddr *)&toAddr, sizeof(toAddr));
if (retval < 0)
{
printf("sendto failed!\r\n");
goto do_cleanup;
}
printf("send online message {%s} %ld done!\r\n", udpmessage, retval);
// 延时1秒
osDelay(1000);
}
do_cleanup:
printf("do_cleanup...\r\n");
close(sockfd);
}
void Server(void *arg)
{
(void)arg;
int retval = 0;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP socket
struct sockaddr_in clientAddr = {0};
socklen_t clientAddrLen = sizeof(clientAddr);
struct sockaddr_in serverAddr = {0};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PARAM_SERVER_PORT);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
retval = bind(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (retval < 0)
{
printf("bind failed, %ld!\r\n", retval);
goto do_cleanup;
}
printf("bind to port %d success!\r\n", PARAM_SERVER_PORT);
static char message[2] = {0};
while (1)
{
retval = recvfrom(sockfd, message, sizeof(message), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
printf("recv end\r\n");
if (retval < 0)
{
printf("recvfrom failed, %ld!\r\n", retval);
goto do_cleanup;
}
printf("recv message {%s} %ld done!\r\n", message, retval);
printf("peer info: ipaddr = %s, port = %d\r\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
if (strcmp(message, "on") == 0)
{
printf(" LED_ON! \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_01, WIFI_IOT_GPIO_VALUE0);
// GpioSetOutputVal(WIFI_IOT_GPIO_PB_08, WIFI_IOT_GPIO_VALUE0);
}
if (strcmp(message, "of") == 0)
{
printf(" LED_OFF! \n");
GpioSetOutputVal(WIFI_IOT_GPIO_PB_01, WIFI_IOT_GPIO_VALUE1);
// GpioSetOutputVal(WIFI_IOT_GPIO_PB_08, WIFI_IOT_GPIO_VALUE1);
}
retval = sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&clientAddr, sizeof(clientAddr));
if (retval <= 0)
{
printf("send failed, %ld!\r\n", retval);
goto do_cleanup;
}
printf("send message {%s} %ld done!\r\n", message, retval);
}
do_cleanup:
printf("do_cleanup...\r\n");
close(sockfd);
}
void connectWiFI()
{
WifiDeviceConfig config = {0};
// 准备AP的配置参数, 连接WiFi
strcpy(config.ssid, PARAM_HOTSPOT_SSID);
strcpy(config.preSharedKey, PARAM_HOTSPOT_PSK);
config.securityType = PARAM_HOTSPOT_TYPE;
osDelay(100);
int netId = ConnectToHotspot(&config);
}
void HT30Test(void)
{
GpioInit();
// GpioSetDir(WIFI_IOT_GPIO_PB_08, WIFI_IOT_GPIO_DIR_OUTPUT); // output is 0 PB08 control led
// GpioSetOutputVal(WIFI_IOT_GPIO_PB_08, WIFI_IOT_GPIO_VALUE0);
GpioSetDir(WIFI_IOT_GPIO_PB_01, WIFI_IOT_GPIO_DIR_OUTPUT);
GpioSetOutputVal(WIFI_IOT_GPIO_PB_01, WIFI_IOT_GPIO_VALUE1); //High level to block elec
GpioSetDir(WIFI_IOT_GPIO_PB_09, WIFI_IOT_GPIO_DIR_INPUT); // input is PB09
IoSetPull(WIFI_IOT_GPIO_PB_09, WIFI_IOT_GPIO_ATTR_PULLHIGH);
GpioRegisterIsrFunc(WIFI_IOT_GPIO_PB_09, WIFI_IOT_INT_TYPE_EDGE, WIFI_IOT_GPIO_EDGE_FALL_LEVEL_LOW, GpioIsr, NULL);
connectWiFI();
// Broardcast UDP 线程
osThreadAttr_t attr;
attr.name = "Broardcast";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 4096;
attr.priority = osPriorityNormal;
if (osThreadNew(Broardcast, NULL, &attr) == NULL)
{
printf("[Broardcast] Failed to create Broardcast!\n");
}
// Server UDP 线程
osThreadAttr_t attrRecv;
attrRecv.name = "Server";
attrRecv.attr_bits = 0U;
attrRecv.cb_mem = NULL;
attrRecv.cb_size = 0U;
attrRecv.stack_mem = NULL;
attrRecv.stack_size = 4096;
attrRecv.priority = osPriorityNormal;
if (osThreadNew(Server, NULL, &attrRecv) == NULL)
{
printf("[Server] Failed to create Server!\n");
}
// GPIO闪烁线程
osThreadAttr_t attr2;
attr2.name = "GpioTask";
attr2.attr_bits = 0U;
attr2.cb_mem = NULL;
attr2.cb_size = 0U;
attr2.stack_mem = NULL;
attr2.stack_size = 4096;
attr2.priority = osPriorityNormal;
// if (osThreadNew(GpioTask, NULL, &attr2) == NULL)
// {
// printf("[GpioTask] Failed to create GpioTask!\n");
// }
}
APP_FEATURE_INIT(HT30Test);
代码解析如下,使用UDP协议分别在Neptune上建立Server和Client两个端,Server用于监听从UDP广播地址(手机App)发送来的蒜泥机控制消息(开和关)。Client用于向UDP广播地址发送蒜泥机的在线活动信号,以便让手机App“发现”设备。
连接WiFi使用的是固定数字,真正做到产品可以开启热点和DHCP服务器,让手机主动连接设备的热点即可。
这里遇到的坑很大,主要是Neptune的控制代码,一定要从润和官方的gitee上下载,使用DevEco Tools自带的代码是无法开启Wifi热点和连接到Wifi的。
手机App端:
因为ArkUI的js和ets接口在HarmonyOS 2.0上均不支持UDP通讯,所以必须要用Java来进行桥接。出于ets目前的稳定性,暂时舍弃使用,改用JS来写UI。
这里要感谢Soon_L的大力帮助。
主要代码如下:
// 定义常量 0-Ability、1-Internal Ability
const ABILITY_TYPE_EXTERNAL = 0;//Ability调用方式
const ABILITY_TYPE_INTERNAL = 1;//Internal Ability调用方式
// 接口调用同步或者异步
const ACTION_SYNC = 0;//同步方式
const ACTION_ASYNC = 1;//异步方式
// 业务码
const ACTION_MESSAGE_UDP_UPDATE = 1001;// 主动更新
const ACTION_MESSAGE_UDP_SUBSCRIBE = 1002;// 订阅Subscribe
const ACTION_MESSAGE_UDP_UNSUBSCRIBE = 1003;// 取消订阅Unsubscribe
const ACTION_MESSAGE_UDP_SEND_ORDER = 1004;// 发送命令
const SUCCESS = 0;
var timer = null;
export default {
data: {
title: "",
message: "",
connected: false,
on: false,
},
environmentStatus: function () {
this.getEnvironmentStatus();
},
environmentStatusSubscribe: function () {
this.startEnvironmentStatusSubscribe();
},
environmentStatusUnSubscribe: function () {
this.startEnvironmentStatusUnSubscribe();
},
sendOrder: async function (order) {
var action = {};
action.bundleName = "com.example.mixerjs";
action.abilityName = "com.example.mixerjs.UpdateDataServiceAbility";
action.messageCode = ACTION_MESSAGE_UDP_SEND_ORDER
action.data = order;
action.abilityType = ABILITY_TYPE_EXTERNAL;
action.syncOption = ACTION_SYNC;
var result = await FeatureAbility.callAbility(action);
var ret = JSON.parse(result);
if (ret.code == 0) {
console.info('发送UDP命令结果:' + JSON.stringify(ret.abilityResult));
} else {
console.error('发送UDP命令错误:' + JSON.stringify(ret.code));
}
},
switchMixer(){
if (this.on) {
this.sendOrder("of")
} else {
this.sendOrder("on")
}
this.on = !this.on
},
initAction: function (code) {
var actionData = {};
var action = {};
action.bundleName = "com.example.mixerjs";
action.abilityName = "com.example.mixerjs.UpdateDataServiceAbility";
action.messageCode = code;
action.data = actionData;
action.abilityType = ABILITY_TYPE_EXTERNAL;
action.syncOption = ACTION_SYNC;
return action;
},
onInit() {
console.info('Demo App onInit')
this.environmentStatusSubscribe();
},
onDestroy(){
console.info('Demo App onDestroy')
this.environmentStatusUnSubscribe();
},
getEnvironmentStatus: async function() {
var action = this.initAction(ACTION_MESSAGE_UDP_UPDATE); //给封装好的初始化函数传递操作码,确定要调用的业务
var result = await FeatureAbility.callAbility(action);
var ret = JSON.parse(result);
if (ret.code == SUCCESS) {
this.message = ret.message;
console.info('getEnvironmentStatus message is:' + JSON.stringify(ret.message));
} else {
this.message = "NA";
console.error('getEnvironmentStatus error code:' + JSON.stringify(ret.code));
this.connected = false
}
},
startEnvironmentStatusSubscribe: async function () {
try {
var action = this.initAction(ACTION_MESSAGE_UDP_SUBSCRIBE); //给封装好的初始化函数传递操作码,确定要调用的业务
var that = this;//that没改变之前仍然是指向当时的this,这样就不会出现找不到原来的对象
var result = await FeatureAbility.subscribeAbilityEvent(action,function (requestEnvironmentStatus) { //调用订阅服务API
var envInfo = JSON.parse(requestEnvironmentStatus).data; //将Json字符串转换为对象,并获取接口返回数据
that.message = envInfo.message;
let str = that.message.substring(0,4)
console.info('startEnvironmentStatusSubscribe message is:' + JSON.stringify(str));
if( str == 'live' ) {
that.connected = true
}
});
console.info("startEnvironmentStatusSubscribe result = " + result);
} catch (pluginError) {
console.error("startEnvironmentStatusSubscribe error : result= " + result + JSON.stringify(pluginError));
}
},
startEnvironmentStatusUnSubscribe: async function () {
try {
var action = this.initAction(ACTION_MESSAGE_UDP_UNSUBSCRIBE);
var result = await FeatureAbility.unsubscribeAbilityEvent(action);
FeatureAbility.callAbility(action);
} catch (pluginError) {
console.error("startEnvironmentStatusUnSubscribe error : " + JSON.stringify(pluginError));
}
}
}
Java端的代码如下:
package com.example.mixerjs;
// ohos相关接口包
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.aafwk.content.Operation;
import ohos.event.commonevent.*;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.rpc.*;
import ohos.utils.zson.ZSONObject;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class UpdateDataServiceAbility extends Ability {
private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0, "Mixer UpdateDataServiceAbility");
private MyRemote remote = new MyRemote();
private CommonEventSubscriber subscriber;
public static String recv_string;
private DatagramSocket server_sock;
private DatagramSocket client_sock;
private DatagramPacket pac;
private byte recv_buffer[];
private byte send_buffer[];
private String message;
private String order; //待发送的命令 on/of
private static final int DEFAULT_TYPE = 0;
private static final String COMMON_UDP_INFO_CHANGED = "UDP_INFO_CHANGED";
@Override
protected void onStart(Intent intent) {
HiLog.info(LABEL, "DemoApp onStart");
UpdateDataServiceAbility.GetStatusInfo getstatusinfo = new UpdateDataServiceAbility.GetStatusInfo();
Thread t = new Thread(getstatusinfo, "getstatusinfoThread");
t.start();
HiLog.info(LABEL, "线程开始执行--> " + t.isAlive());//判断是否启动
super.onStart(intent);
}
// FA在请求PA服务时会调用Ability.connectAbility连接PA,连接成功后,需要在onConnect返回一个remote对象,供FA向PA发送消息
@Override
protected IRemoteObject onConnect(Intent intent) {
super.onConnect(intent);
return remote.asObject();
}
class MyRemote extends RemoteObject implements IRemoteBroker {
private static final int SUCCESS = 0;
private static final int ERROR = 1;
private static final int ACTION_MESSAGE_UDP_UPDATE = 1001;
private static final int ACTION_MESSAGE_UDP_SUBSCRIBE = 1002;
private static final int ACTION_MESSAGE_UDP_UNSUBSCRIBE = 1003;
private static final int ACTION_MESSAGE_UDP_SEND_ORDER = 1004;
MyRemote() {
super("MyService_MyRemote");
}
@Override
public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
switch (code) {
case ACTION_MESSAGE_UDP_UPDATE: {
reply.writeString(getEnvironmentInfo());
break;
}
case ACTION_MESSAGE_UDP_SEND_ORDER: {
order = data.readString();
HiLog.info(LABEL, "发送命令--> " + order);
try {
HiLog.info(LABEL, "发送广播数据");
client_sock = new DatagramSocket();
client_sock.setBroadcast(true);
InetAddress addr1 = InetAddress.getByName("255.255.255.255");
pac = new DatagramPacket(order.getBytes(), order.getBytes().length, addr1, 63060);//构造一个packet
client_sock.send(pac);
} catch (Exception e) {
e.printStackTrace();
}
// 返回结果当前仅支持String,对于复杂结构可以序列化为ZSON字符串上报
Map<String, Object> result = new HashMap<String, Object>();
result.put("code", SUCCESS);
result.put("abilityResult", order);
reply.writeString(ZSONObject.toZSONString(result));
break;
}
case ACTION_MESSAGE_UDP_SUBSCRIBE: {
subscribeEvent(data, reply, option);
break;
}
case ACTION_MESSAGE_UDP_UNSUBSCRIBE: {
unSubscribeEnvironmentEvent(reply);
break;
}
default: {
Map<String, Object> result = new HashMap<String, Object>();
result.put("abilityError", ERROR);
reply.writeString(ZSONObject.toZSONString(result));
return false;
}
}
return true;
}
private void subscribeEvent(MessageParcel data, MessageParcel reply, MessageOption option) {
MatchingSkills matchingSkills = new MatchingSkills();
matchingSkills.addEvent(COMMON_UDP_INFO_CHANGED);
IRemoteObject notifier = data.readRemoteObject();
CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
subscriber = new CommonEventSubscriber(subscribeInfo) {
@Override
public void onReceiveEvent(CommonEventData commonEventData) {
replyMsg(notifier);
}
};
if (option.getFlags() == MessageOption.TF_SYNC) {
reply.writeString("subscribe common event success");
}
try {
CommonEventManager.subscribeCommonEvent(subscriber);
reply.writeString(" subscribe common event success");
} catch (RemoteException e) {
HiLog.info(LABEL, "%{public}s", "RemoteException in subscribeNotificationEvents!");
}
}
private void replyMsg(IRemoteObject notifier) {
MessageParcel notifyData = MessageParcel.obtain();
notifyData.writeString(getEnvironmentInfo());
try {
notifier.sendRequest(DEFAULT_TYPE, notifyData, MessageParcel.obtain(), new MessageOption());
} catch (RemoteException exception) {
HiLog.info(LABEL, "%{public}s", "replyMsg RemoteException !");
} finally {
notifyData.reclaim();
}
}
private String getEnvironmentInfo() {
// 返回结果当前仅支持String,对于复杂结构可以序列化为ZSON字符串上报
Map<String, Object> result = new HashMap<String, Object>();
result.put("code", SUCCESS);
result.put("message", message);
return ZSONObject.toZSONString(result);
}
private void unSubscribeEnvironmentEvent(MessageParcel reply) {
try {
CommonEventManager.unsubscribeCommonEvent(subscriber);
reply.writeString("Unsubscribe common event success!");
} catch (RemoteException | IllegalArgumentException exception) {
reply.writeString("Unsubscribe failed!");
HiLog.info(LABEL, "%{public}s", "Unsubscribe failed!");
}
subscriber = null;
}
@Override
public IRemoteObject asObject() {
return this;
}
}
class GetStatusInfo implements Runnable {
@Override
public void run() {
try {
//监听端口
server_sock = new DatagramSocket(63060);
recv_buffer = new byte[1024];//接收缓冲区,byte型
pac = new DatagramPacket(recv_buffer, recv_buffer.length);//构造一个packet
recv_string = "offline";
HiLog.info(LABEL, "开始等待消息 ");
//循环接受数据
while (true) {
server_sock.receive(pac);//阻塞式接收数据
//将byte[]转化成string
recv_string = new String(recv_buffer, 0, pac.getLength());
HiLog.info(LABEL, "接受到UDP数据:" + recv_string + "长度:" + recv_string.length());
// if (recv_string.length() >= 0) {
System.out.println("Received UDP:" + recv_string);
message = recv_string;
HiLog.info(LABEL, "message:" + message);
try {
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withAction(COMMON_UDP_INFO_CHANGED)//自定义字符串类型的action
.build();
intent.setOperation(operation);
intent.setParam("result", "commonEventData");
intent.setParam("isCommonEvent", true);
CommonEventData eventData = new CommonEventData(intent);
CommonEventManager.publishCommonEvent(eventData);
HiLog.info(LABEL, "PublishCommonEvent SUCCESS");
} catch (RemoteException e) {
HiLog.info(LABEL, "Exception occurred during publishCommonEvent invocation.");
}
// }
}
} catch (IOException e) {
System.out.println("Socket Exception");
e.printStackTrace();
}
}
}
}
最终实现了这个我期待已久的智能打蒜器1.0,也作为本地大赛的参赛作品提交,希望有机会走到商业化的阶段。本人也是因为这个小项目,从头开始学硬件、电路、甚至回忆Java有关的知识,确实遭遇了巨大的困难和挫折,不过好在坚持下来,虽然很菜,但也略有信心了。
还需要进行的工作,去除导线连接Neptune模块,使用自定义的电路板,以便小型化,USB充电口改造成磁吸式(西安的同学提出的建议)。 代码全部附上,希望大家也能积极开始,构建自己心目中有用的商业化产品,丰富HarmonyOS和Open Harmony商业生态。
回头见!
附件链接:
打蒜器App端控制模块
打蒜器的Neptune模块镜像