这篇文章也是记录一下我当时为了能够测试一个MQTT方案学习设计的一个手机APP,
要特别感谢B站UP主 阿正啷个哩个啷,整个程序从0开始写,后期向同事请教,添加了部分功能模块
本文针对Java 0基础,请大神轻喷,如果后期有时间,还会把文章点点完善

导航

  • 一、环境安装
  • 二、工程结构
    • 2.1 新建工程
    • 2.2 图标和名称
    • 2.3 权限
    • 2.4 界面
      • 2.4.1 界面配置
      • 2.4.2 UI 配置
      • 2.4.3 控件
      • 2.4.4 布局
    • 2.5 导入软件包
  • 三、程序编写
    • 3.1 基本快捷键
    • 3.2 控件操作
    • 3.3 MQTT通讯
    • 3.4 界面切换
    • 3.5 前后台运行
    • 3.6 最终主界面图

一、环境安装

环境安装配置的具体步骤什么的,就不详细介绍了,网上有很多参考资料,而且现在的应用基本上都是傻瓜式安装,这边只是说明下需要安装的软件:

  1. 安装 Java JDK,从JDK 5.0开始,改名为Java SE;下载地址
  2. 安装 Android Studio; 官方下载地址
  3. 下图中有第三个文件,是Java 下的 mqtt 协议实现库,因为我这个程序中需要使用mqtt协议,所以我也放在这里了,只不过这是后面在 Android Studio 中导入的。

二、工程结构

2.1 新建工程

安装好环境以后,打开 Android Studio,点击新建一个工程 + Start a new Android Studio project,选一个界面,进入如下配置,在这里设置 project 名字,保存路径,其他的东西默认就可以,点击Finish完成设置就可以进入到编辑界面:
在左边 project 栏目中,点击下面左图红色部分,可以切换工程的框架视图(就是工程结构是按照什么模式来布局的),如果 project 栏目不小心关闭了,可以通过勾选主菜单栏目下 View - Tool Window Bars 打开,如下面右图所示:

记得修改一个地方build.gradle 文件,原本是国外的地址,国外网址的速度大家是知道的,改成阿里云的地址,如下图:
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}

2.2 图标和名称

首先看一下AndroidManifest.xml文件内容:

2.3 权限

在使用app的时候,相应的功能需要获取相应的权限,就是很多app在安装后,都会弹出向用户申请权限的那种东西,

我们这工程中得直接允许使用网络,蓝牙等权限,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.example.mqtt_project"><!--允许程序打开网络套接字--><uses-permission android:name="android.permission.INTERNET" /><!--允许程序获取网络状态--><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><!--使用蓝牙所需要的权限--><uses-permission android:name="android.permission.BLUETOOTH" /><!--使用扫描和设置蓝牙的权限(要使用这一个权限必须申明BLUETOOTH权限)--><uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="人生得意"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activity android:name=".BlueTestActivity1" android:theme="@style/Theme.AppCompat.Light.NoActionBar"></activity></application></manifest>

其中系统默认的名字本来是如下所示

android:label="@string/app_name"

那么他这个@string/app_name是怎么识别的呢:

2.4 界面

2.4.1 界面配置

我们来看 MainActivity.java 文件,这个是我们app的主界面,因为在AndroidManifest.xml 中我们就设置过

<activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar">

主界面配置:
如果需要其他界面我们可以让系统自动生成,比如我们直接在主界面对应的UI改成activity_main1,这样系统会提示错误,我们按照下图的操作可以新建一个UI界面:

选中第一种办法,在弹出的对话框,点击OK即可:

可以看到系统自动新建了一个UI界面,可以对此界面进行编辑:

2.4.2 UI 配置

接下来我们来说说 activity_main.xml :

2.4.3 控件

在UI设计的时候,需要用到很多的控件,比如按钮,图片,文本框等等,具体的种类和细节请参考对应资料,这里只介绍我使用到的:

按钮 :</Button>
图片: </ImageView>
文本框: </TextView>

</LinearLayout><Buttonandroid:layout_width="wrap_content"android:text="测试按钮"android:id="@+id/btn_1"android:layout_height="wrap_content"></Button><LinearLayoutandroid:layout_width="match_parent"android:orientation="horizontal"android:gravity="center_vertical"android:layout_height="wrap_content"><ImageViewandroid:layout_width="40dp"android:src="@drawable/th"android:layout_marginLeft="10dp"android:layout_height="40dp"></ImageView><TextViewandroid:layout_width="wrap_content"android:id="@+id/text_old"android:text="我是原来的内容"android:layout_height="wrap_content"></TextView></LinearLayout>

上述代码中一般带有android:layout的都是控件本身的属性,除了上面控件所定义的属性,每个控件都有很多属性,需要用到的时候再去查相关的内容。

控件ID

其中需要说明的是android:id这个属性,这个id 是 java 文件与 XML 文件通讯的介质

2.4.4 布局

界面和布局的设置,需要自己多尝试,控件的属性自己不知道的可以直接测试效果,什么东西多试试知道了

充满父控件:android:layout_width="match_parent" (这里是宽度,也有高度)
表示让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小

自适应:android:layout_height="wrap_content" (高度自适应)
表示让当前的控件大小能够刚好包含里面的内容,也就是由控件内容决定当前控件的大小

垂直居中:android:gravity="center_vertical"

水平布局:android:orientation="horizontal" (有水平垂直)

与边缘距离:android:layout_marginTop="10dp" (有上下左右)

为了简单,整体布局采用线性布局</LinearLayout> ,整体布局是指整个页面的局部,但是一个页面中有很多控件,控件线性的排放,可以是垂直布局,也可以是水平布局(直接输入字母 o ,会自动弹出语句 ),如果使用水平布局,后面的控件图标会溢出屏幕外面,不能看到,这里只有使用垂直对齐,控件自适应,就能够按照手机屏幕大小顺序往下面排列,效果如下列图示:

对于布局,是有明确一层一层包含关系的,好像是分为父控件,子控件,在控件里面包含控件的情况下需要单独的对子控件进行设置对应的布局,这个布局是在父控件的范围内进行:

2.5 导入软件包

我们需要使用 MQTT相关的函数,需要导入 MQTT Java 包,我们使用下载好导入的方式,除了这种方式,还可以使用Gradle 方式在线下载,我们按照如下步骤:

1、切换至Project视图,将下载好的 Java 包直接拖到 libs 下面:

2、右击 Java 包,点击 Add As library,默认安装在 app 下面,点击确定,等待导入完成即可:

导入完成以后可以在build.gradle中查看到:

三、程序编写

很快我们就可以开始写代码了,这边我并不会从 Jave 的基础开始介绍语法啊数据类型啊之类的,直接用到哪里改哪里,修改的地方做个说明。

3.1 基本快捷键

一些操作基本快捷键,持续补充:

**转到定义**
java中,按住Ctrl,然后单击,就能到定义了**编辑界面切换**
Alt + ← 前一个编辑的页面 Alt + → 下一个编辑的页面**自动解决错误**
Alt + Enter  就会出现建议解决办法

3.2 控件操作

控件操作是通过绑定 控件 ID 进行操作的,首先得定义一个按键 “变量”:

然后在初始化中,绑定按键变量到对应的控件ID:

    //初始化,绑定对应的控件IDprivate void ui_init() {btn_1 = findViewById(R.id.btn_1);image_open = findViewById(R.id.image_open);image_door = findViewById(R.id.image_door);image_lei = findViewById(R.id.image_lei);image_th = findViewById(R.id.image_th);image_wide = findViewById(R.id.image_wide);image_pm = findViewById(R.id.image_pm);text_old = findViewById(R.id.text_old);}

然后开始按键的相关事件代码,选择setOnClickListener,然后还要new View.OnClickListener() ,敲完之后就会自动生成一个单击事件代码框架:

完善一下单击事件,代码如下:

        ui_init();//初始化btn_1.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//这里既是单击之后的操作//System.out.println("这是一个单击操作"); java 中的打印操作/*更直观的方式用弹窗 : toastcontext:界面选择   这里为 MainActivity.this : 当前界面text: 显示的textToast.LENGTH_LONG 显示时长,这里选择  LENGTH_LONGcontext 和 text 都不需要自己敲的*/Toast.makeText(MainActivity.this)Toast.makeText(MainActivity.this,"这是一个单击操作",Toast.LENGTH_LONG).show();text_old.setText("我是新的内容"); //两个控件联动,这里把 text_old 控件里面的内容改变了}});

3.3 MQTT通讯

MQTT服务器IP,订阅发布根据自己的测试环境修改,MQTT部分代码来自阿正,这里必须得贴一下阿正的B站主页:

订阅:client.subscribe(mqtt_sub_topic,1);

发布:client.publish(topic,message); 下面代码中发布封装了一个函数。

字符串截取:
msg.obj.toString().substring(msg.obj.toString().indexOf("temp:")+5,msg.obj.toString().indexOf("h"));

MQTT部分整体代码:

package com.example.mqtt_project;import androidx.appcompat.app.AppCompatActivity;import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
//
//这里是我的安卓测试,测试一下输入设置好了没有yes or no
//
public class MainActivity extends AppCompatActivity {//    private String host = "tcp://106.13.150.28:1883";private String host = "tcp://116.62.28.158:1883";
//    private String userName = "android";
//    private String passWord = "android";private String userName = "winshine";private String passWord = "winshine123";private String mqtt_id = "523266538";private String mqtt_sub_topic = "523266538_PC"; //为了保证你不受到别人的消息private String mqtt_pub_topic = "523266538"; //为了保证你不受到别人的消息
//    private String mqtt_sub_topic = "hello"; //为了保证你不受到别人的消息
//    private String mqtt_pub_topic = "523266538"; //为了保证你不受到别人的消息private Button btn_1;private Button btn_open;private Button btn_close;private ImageView image_open;private ImageView image_door;private ImageView image_wide;private ImageView image_lei;private ImageView image_pm;private ImageView image_th;private TextView text_old;private MqttClient client;private MqttConnectOptions options;private Handler handler;private ScheduledExecutorService scheduler;private int led_flag = 1;private String open_led ="LEDON";private String close_led ="LEDOFF";private int mqtt_flag = 0;@SuppressLint("HandlerLeak")@Overrideprotected void onCreate(Bundle savedInstanceState) {//界面打开后,最先运行的地方super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//对应界面UI//用来界面初始化,控件初始化,某种意义上类似main()函数ui_init();//初始化btn_1.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//这里既是单击之后的操作//System.out.println("这是一个单击操作"); java 中的打印操作/*更直观的方式用弹窗 : toastcontext:界面选择   这里为 MainActivity.this : 当前界面text: 显示的textToast.LENGTH_LONG 显示时长,这里选择  LENGTH_LONGcontext 和 text 都不需要自己敲的*/Toast.makeText(MainActivity.this,"这是一个单击操作",Toast.LENGTH_LONG).show();text_old.setText("我是新的内容");}});/*学会按钮单击事件,图片也可以单击事件单击第一个图片,通过mqtt发布一个消息*/image_open.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {try{if(led_flag == 0) {publishmessageplus(mqtt_pub_topic, open_led);led_flag =1;Toast.makeText(MainActivity.this,"开灯",Toast.LENGTH_SHORT).show();}else{publishmessageplus(mqtt_pub_topic, close_led);led_flag =0;Toast.makeText(MainActivity.this,"关灯",Toast.LENGTH_SHORT).show();}}catch (Exception e){e.toString();}}});image_door.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this,"我是开门按钮",Toast.LENGTH_LONG).show();}});image_lei.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this,"这里做蓝牙",Toast.LENGTH_LONG).show();Intent intent = new Intent(MainActivity.this,BlueTestActivity1.class);startActivity(intent);}});btn_open.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this,"打开mqtt重连",Toast.LENGTH_LONG).show();mqtt_flag = 0;//打开重连}});btn_close.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this,"关闭mqtt重连",Toast.LENGTH_LONG).show();mqtt_flag = 1;//打开重连}});//两个控件联动,按钮单击改变textview的值//#########################################################//Mqtt_init();startReconnect();handler = new Handler() {@SuppressLint("SetTextI18n")public void handleMessage(Message msg) {super.handleMessage(msg);switch (msg.what){case 1: //开机校验更新回传break;case 2:  // 反馈回传break;case 3:  //MQTT 收到消息回传   UTF8Buffer msg=new UTF8Buffer(object.toString());String temp=null;try {temp=msg.obj.toString();if(temp!=null&&temp.contains("temp")){String T_val = msg.obj.toString().substring(msg.obj.toString().indexOf("temp:")+5,msg.obj.toString().indexOf("h"));String H_val = msg.obj.toString().substring(msg.obj.toString().indexOf("temp:")+16);Toast.makeText(MainActivity.this,msg.obj.toString(),Toast.LENGTH_LONG).show();String text_val="温度:"+T_val+""+"湿度:"+H_val;text_old.setText(text_val);}}catch (Exception e){e.toString();}break;case 30:  //连接失败Toast.makeText(MainActivity.this,"连接失败",Toast.LENGTH_LONG).show();break;case 31:   //连接成功Toast.makeText(MainActivity.this,"连接成功",Toast.LENGTH_LONG).show();try {client.subscribe(mqtt_sub_topic,1);} catch (MqttException e) {e.printStackTrace();}break;default:break;}}};}//初始化,绑定对应的控件IDprivate void ui_init() {btn_1 = findViewById(R.id.btn_1);image_open = findViewById(R.id.image_open);image_door = findViewById(R.id.image_door);image_lei = findViewById(R.id.image_lei);image_th = findViewById(R.id.image_th);image_wide = findViewById(R.id.image_wide);image_pm = findViewById(R.id.image_pm);text_old = findViewById(R.id.text_old);btn_open = findViewById(R.id.btn_open);btn_close = findViewById(R.id.btn_close);}private void Mqtt_init() {try {//host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存client = new MqttClient(host, mqtt_id,new MemoryPersistence());//MQTT的连接设置options = new MqttConnectOptions();//设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接options.setCleanSession(false);//设置连接的用户名options.setUserName(userName);//设置连接的密码options.setPassword(passWord.toCharArray());// 设置超时时间 单位为秒options.setConnectionTimeout(10);// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制options.setKeepAliveInterval(20);//设置回调client.setCallback(new MqttCallback() {@Overridepublic void connectionLost(Throwable cause) {//连接丢失后,一般在这里面进行重连System.out.println("connectionLost----------");//startReconnect();}@Overridepublic void deliveryComplete(IMqttDeliveryToken token) {//publish后会执行到这里System.out.println("deliveryComplete---------"+ token.isComplete());}@Overridepublic void messageArrived(String topicName, MqttMessage message)throws Exception {//subscribe后得到的消息会执行到这里面System.out.println("messageArrived----------");//封装message包Message msg = new Message();msg.what = 3;   //收到消息标志位msg.obj = topicName + "---" + message.toString();//发送message 到 handlerhandler.sendMessage(msg);    // hander 回传}});} catch (Exception e) {e.printStackTrace();}}private void Mqtt_connect() {new Thread(new Runnable() {@Overridepublic void run() {try {if(!(client.isConnected()) )  //如果还未连接{if(mqtt_flag == 0) {       //只有当flag == 0时才能一直重新连接client.connect(options);Message msg = new Message();msg.what = 31;handler.sendMessage(msg);}}} catch (Exception e) {e.printStackTrace();Message msg = new Message();msg.what = 30;handler.sendMessage(msg);}}}).start();}private void startReconnect() {scheduler = Executors.newSingleThreadScheduledExecutor();scheduler.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {if (!client.isConnected()) {Mqtt_connect();}}}, 0 * 1000, 10 * 1000, TimeUnit.MILLISECONDS);}private void publishmessageplus(String topic,String message2) {if (client == null || !client.isConnected()) {return;}MqttMessage message = new MqttMessage();message.setQos(0);message.setPayload(message2.getBytes());try {client.publish(topic,message);} catch (MqttException e) {e.printStackTrace();}}
}

3.4 界面切换

点击蓝牙图标,做了个界面切换,蓝牙单独的做了一个界面,这里得感谢同事 跳舞大佬,蓝牙部分的程序实现我就不放了,我只是拿过来用用,也没有时间去分析,只是了解下不同界面是如何切换的。

image_lei.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this,"这里做蓝牙",Toast.LENGTH_LONG).show();Intent intent = new Intent(MainActivity.this,BlueTestActivity1.class);startActivity(intent);}});

3.5 前后台运行

在当时程序测试整体运行过程中,即便切换到后台(就是最小化以后,没有杀掉进程的情况下),mqtt重连方法也一直有弹窗,虽然在上面给的程序中,我加了个标志位处理了一下,但是也不是根本解决办法,还是请教了跳舞,告诉我这个基础的知识,感谢跳舞。

加入前后台运行的判断:

    boolean isShow;@Overrideprotected void onResume() {super.onResume();//前台isShow = true;}@Overrideprotected void onPause() {//后台isShow = false;super.onPause();}

3.6 最终主界面图

最后主界面的效果图:

基于 MQTT 通讯一个简单的 Java工程相关推荐

  1. idea建立一个java工程_IntelliJ IDEA(三、各种工程的创建 -- 之一 -- 创建一个简单的Java工程)...

    一.创建一个简单的Java工程:HelloWorld 1. Eclipse的第一步是选择工作空间,然后创建项目: IDEA不同(没有工作空间的概念),第一步就直接创建具体的项目,项目创建过程中会选择在 ...

  2. Ant—使用Ant构建一个简单的Java工程(两)

    博客<Ant-使用Ant构建一个简单的Java项目(一)>演示了使用Ant工具构建简单的Java项目,接着这个样例来进一步学习Ant: 上面样例须要运行多条ant命令才干运行Test类中的 ...

  3. 一个简单的Java web服务器实现

    前言 一个简单的Java web服务器实现,比较简单,基于java.net.Socket和java.net.ServerSocket实现: 程序执行步骤 创建一个ServerSocket对象: 调用S ...

  4. 基于JAVAvue开发一个简单音乐播放器计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVAvue开发一个简单音乐播放器计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVAvue开发一个简单音乐播放器计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目 ...

  5. JNI开发笔记(四)--实现一个简单的JNI工程并生成so库

    实现一个简单的JNI工程并生成so库 引 前言 1. 编写C/h文件并添加到工程 2. 修改CmakeLists.txt文件 3. 编写native-lib.cpp文件 4. 在MainActivit ...

  6. 一个简单的Java EEDocker示例

    本文讲的是一个简单的Java EE&Docker示例,[编者的话]学习Docker的最好办法就是迅速在工作中应用它,本文作者使用Docker部署了一个Java EE应用,非常简单和方便.需要注 ...

  7. 我的Serverless实战—基于Serverless搭建一个简单的WordPress个人博客图文详解-JJZ

    文正在参与 "100%有奖 | 我的Serverless 实战"征稿活动 活动链接:https://marketing.csdn.net/p/15940c87f66c68188cf ...

  8. 编写一个java_Java入门篇(一)——如何编写一个简单的Java程序

    最近准备花费很长一段时间写一些关于Java的从入门到进阶再到项目开发的教程,希望对初学Java的朋友们有所帮助,更快的融入Java的学习之中. 主要内容包括JavaSE.JavaEE的基础知识以及如何 ...

  9. ava入门篇——如何编写一个简单的Java程序

    最近准备花费很长一段时间写一些关于Java的从入门到进阶再到项目开发的教程,希望对初学Java的朋友们有所帮助,更快的融入Java的学习之中. 主要内容包括JavaSE.JavaEE的基础知识以及如何 ...

最新文章

  1. STM32学习笔记9(SysTick滴答时钟)
  2. JDBC Driver常用连接方法列表
  3. 三个获取浏览器URL中参数值的方法
  4. Github标星2w+,热榜第一,如何用Python实现所有算法
  5. [转载] 应急管理体系及其业务流程研究
  6. 社区发现SLPA算法
  7. 2.2.2.进程调度的时机切换与过程、方式
  8. python构造一个二叉树_二叉树-链表存储,用二叉树构造表达式(Python实现)
  9. QGIS教程01:为什么要用QGIS?
  10. android expandablelistview横向,Android 的ExpandableListView使用总结--二级展开树结构
  11. K3 ERP 系统财务管理 - 账结法、表结法
  12. #STM32学习#6D加速度传感器测量风机震动
  13. 深度研报:回顾「NFT」和「元宇宙」冰火两重天的11月
  14. Ubiquitous Religions
  15. mysql删除通用日志_删除MySQL log bin 日志操作记录
  16. 手机ssh发送文件到服务器,使用ssh传输文件
  17. 同星T1014在线回放设置
  18. 亚马逊运营怎么做广告?六大方法!
  19. 【转】互操作性的区块链系统设计理念
  20. 写代码:假设一年期定期利率为3.25%,计算一下需要过多少年,一万元的一年定期存款连本带息能翻番?...

热门文章

  1. KylinV10上qt5.9开发应用打包步骤(四)--linuxdeployqt源码编译
  2. 年龄到底怎么算才对_怎么算年龄才是正确的
  3. php 合成图片、合成圆形图片
  4. 分分钟安装VMware,并安装linux操作系统
  5. 渔翁、魔鬼和四色鱼的故事
  6. spring源码构建时缺失spring-cglib-repack-3.2.4.jar和spring-objenesis-repack-2.4.jar
  7. iOS设备唯一标识获取策略(不定时更新)
  8. linux swap空间不足,swap空间不足问题解决
  9. 进下流行移动开发框架对比
  10. zabbix + nexmo = 电话告警