• 前言
    • 不要让时代的悲哀成为你的悲哀。

AndroidTv遥控模拟器app

  由于公司是做机顶盒开发的,经常需要定制客户的遥控功能,遥控的系统码那么多,又不可能全部都购买一个遥控器,或者让客户邮寄过来进行配置(主要是验证遥控码值的功能),由于自己刚开始配不熟悉,有时候还因为修改了Key值,导致经常配错,又懒得去找遥控验证(实在不好找),想到手机也有红外功能,能不能用手机来模拟波形,实现模拟遥控发射的波形,来检测自己的遥控是否正确,正好当时也在研究AndroidTV系统源码中如何适配SONY的遥控器,所以就开发了这个app。这里简单记录一下,软件是年初的时候做的。

  目前所支持的协议就三种,NEC,SONY,SAMSUNG。下面先简单介绍一下这三种协议以及波形(当时示波器自己倒是全部捕捉过,更换了电脑系统,没及时记录)。

1.NEC协议

  NEC协议是现在比较常用的协议,也是最简单的协议。红外接受管有3个脚,一个接地,一个接VCC,一个信号输出脚,用示波器直接接信号输出脚,可以看到波形如下图所示。

NEC波形

  其中 1-引导码,2-用户码,3-用户反码,4-数据码,5-数据反码,6-结束码。对于引导码部分是由9ms的低电平和4.5ms高电平组成,在2-5阶段每个阶段都会受到8位数据,对于低电平0则是约11.25ms(560us低电平,565us高电平),高电平1则是约2.24ms(560us低电平,1680us高电平),这就是数据位的0,1。而结束码部分则是25.60ms(5.6ms低电平,20ms这里使用作为单按的情况)。以上的高低电平都是固定的。

  • Nec协议封装代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    package com.example.remote.protocol;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * ClassName : NecPattern
    * Description : 构造NEC协议的pattern编码
    */

    public class NecPattern {

    //引导码
    private static final int startH = 9000;
    private static final int startL = 4500;

    //结束码
    private static final int endL = 560;
    private static final int endH = 2000;

    //高电平
    private static final int high8 = 560;
    //低电平0:1125
    private static final int low0 = 565;
    //低电平1:2250
    private static final int low1 = 1690;

    private static int[] pattern;
    private static List<Integer> list = new ArrayList<>();


    /**
    * 正常发码:引导码(9ms+4.5ms)+用户编码(高八位)+用户编码(低八位)+键数据码+键数据反码+结束码
    */
    public static int[] buildPattern(int userCodeH, int userCodeL, int keyCode) {
    //用户编码高八位00
    String userH = constructBinaryCode(userCodeH);
    //用户编码低八位DF
    String userL = constructBinaryCode(userCodeL);
    //数字码
    String key = constructBinaryCode(keyCode);
    //数字反码
    String keyReverse = constructBinaryCode(~keyCode);

    list.clear();
    //引导码
    list.add(startH);
    list.add(startL);
    //用户编码
    changeAdd(userH);
    changeAdd(userL);
    //键数据码
    changeAdd(key);
    //键数据反码
    changeAdd(keyReverse);
    //结束码
    list.add(endL);
    list.add(endH);

    int size = list.size();
    pattern = new int[size];
    for (int i = 0; i < size; i++) {
    //Log.d(TAG, "buildPattern: " + list.get(i));
    pattern[i] = list.get(i);
    }

    return pattern;
    }

    /**
    * 十六进制键值转化为二进制串,并逆转编码
    * @param keyCode
    * @return
    */
    private static String constructBinaryCode(int keyCode) {
    String binaryStr = convertToBinary(keyCode);
    char[] chars = binaryStr.toCharArray();
    StringBuffer sb = new StringBuffer();
    for (int i = 7; i >= 4; i--) {
    sb.append(chars[i]);
    }

    for (int i = 3; i >= 0; i--) {
    sb.append(chars[i]);
    }
    return sb.toString();
    }

    /**
    * 数字转换为长度为8位的二进制字符串
    * @return
    */
    private static String convertToBinary(int num) {
    String binary = Integer.toBinaryString(num);
    StringBuffer sb8 = new StringBuffer();
    //每个元素长度为8位,不够前面补充0
    if (binary.length() < 8) {
    for (int i = 0; i < 8 - binary.length(); i++) {
    sb8.append("0");
    }
    String binaryStr8 = sb8.append(binary).toString();
    return binaryStr8;
    }else{
    String binaryStr8 = binary.substring(binary.length() - 8);
    return binaryStr8;
    }
    }

    /**
    * 二进制转成电平
    *
    * @param code
    */
    public static void changeAdd(String code) {
    int len = code.length();
    String part;
    for (int i = 0; i < len; i++) {
    list.add(high8);
    part = code.substring(i, i + 1);
    if (part.equals("0"))
    list.add(low0);
    else
    list.add(low1);
    }
    }
    }

2.SANSUNG协议

  理解了NEC协议,SAMSUNG协议就很容易理解了。最好的方式是利用示波器进行测量,也可以通过现有的代码进行分析。通过对比我们得知,SAMSUNG协议的引导码部分和NEC协议的只有引导码部分不是一样的(载波都是38k),4.5ms的低电平和4.5ms高电平。这里就不做赘述了。

  • SANSUNG协议封装代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package com.example.remote.protocol;

import java.util.ArrayList;
import java.util.List;

/**
* ClassName : NecPattern
* Description : 构造NEC协议的pattern编码
*/

public class SANSUNGPattern {

//引导码
private static final int startH = 4500;
private static final int startL = 4500;

//结束码
private static final int endL = 560;
private static final int endH = 2000;

//高电平
private static final int high8 = 560;
//低电平0:1125
private static final int low0 = 565;
//低电平1:2250
private static final int low1 = 1690;

private static int[] pattern;
private static List<Integer> list = new ArrayList<>();


/**
* 正常发码:引导码(9ms+4.5ms)+用户编码(高八位)+用户编码(低八位)+键数据码+键数据反码+结束码
*/
public static int[] buildPattern(int userCodeH, int userCodeL, int keyCode) {
//用户编码高八位00
String userH = constructBinaryCode(userCodeH);
//用户编码低八位DF
String userL = constructBinaryCode(userCodeL);
//数字码
String key = constructBinaryCode(keyCode);
//数字反码
String keyReverse = constructBinaryCode(~keyCode);

list.clear();
//引导码
list.add(startH);
list.add(startL);
//用户编码
changeAdd(userH);
changeAdd(userL);
//键数据码
changeAdd(key);
//键数据反码
changeAdd(keyReverse);
//结束码
list.add(endL);
list.add(endH);

int size = list.size();
pattern = new int[size];
for (int i = 0; i < size; i++) {
//Log.d(TAG, "buildPattern: " + list.get(i));
pattern[i] = list.get(i);
}

return pattern;
}

/**
* 十六进制键值转化为二进制串,并逆转编码
* @param keyCode
* @return
*/
private static String constructBinaryCode(int keyCode) {
String binaryStr = convertToBinary(keyCode);
char[] chars = binaryStr.toCharArray();
StringBuffer sb = new StringBuffer();
for (int i = 7; i >= 4; i--) {
sb.append(chars[i]);
}

for (int i = 3; i >= 0; i--) {
sb.append(chars[i]);
}
return sb.toString();
}

/**
* 数字转换为长度为8位的二进制字符串
* @return
*/
private static String convertToBinary(int num) {
String binary = Integer.toBinaryString(num);
StringBuffer sb8 = new StringBuffer();
//每个元素长度为8位,不够前面补充0
if (binary.length() < 8) {
for (int i = 0; i < 8 - binary.length(); i++) {
sb8.append("0");
}
String binaryStr8 = sb8.append(binary).toString();
return binaryStr8;
}else{
String binaryStr8 = binary.substring(binary.length() - 8);
return binaryStr8;
}
}

/**
* 二进制转成电平
*
* @param code
*/
public static void changeAdd(String code) {
int len = code.length();
String part;
for (int i = 0; i < len; i++) {
list.add(high8);
part = code.substring(i, i + 1);
if (part.equals("0"))
list.add(low0);
else
list.add(low1);
}
}
}

  查看NEC和SANSUNG的协议模拟代码,发现几乎是一样的。之前讨论的时候,考虑的是单按的情况,如果是重复按呢?如果是一个红外接收器既接收NEC,又接收SANSUNG,那么重复按一个键,就会错乱(甚至没有功能,下一篇文章将具体分析一下Android按键是如何解析分析匹配协议的)。

3.SIRC SONY索尼协议

  对于SONY协议,共有3各版本:12位,15位和20位。与NEC协议不同的是,使用的是40K载波对编码后的波形进行调制,位时间长度都是1.2ms或0.6ms。这里只实现模拟了12位和15位的波形发射。

  • SONY协议的发射波形逻辑0和逻辑1如下图所示:

SONY

  • 协议格式

SONY协议格式

  • 12位波形

SONY12位

  • 15位波形

    • 对于15位其实就是系统码值不够表达的,对于12位的一般系统码值是0x1,对于15位系统码值是0xA3。多了一位高位,所以需要多一个周期发射。
  • 20位波形,由于当时身边没有20位协议的遥控器,所以没有实现。

  • SONY协议模拟代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

package com.example.remote.protocol;

import java.util.ArrayList;
import java.util.List;

/**
* only support 12/15 bit
*/
public class SIRCPattern {


/**
* 12 bit
*/
int[] test = new int[]{
//start
2400, 600,
//command
1200,600,//1
600,600,//0
1200,600,//1
600,600,//0
1200,600,//1
600,600,//0

//address
600,600,//0
1200,600,//1
600,600,600,600,//00
600,600,600,600//00
};


private static final int start[] = new int[]{2400,600};
private static final int bit0[] = new int[] {600,600};
private static final int bit1[] = new int[] {1200,600};
private static int[] pattern;
private static List<Integer> list = new ArrayList<>();


public static int[] buildPattern(int address, int command) {
list.clear();
for (int level: start) {
list.add(level);
}
String addressStr;
String commandStr;
commandStr = constructBinaryCode(command,7);
if(address > 0x0F)
{
addressStr = constructBinaryCode(address,8);
}else {
addressStr = constructBinaryCode(address,5);

}
changeAdd(commandStr);
changeAdd(addressStr);
pattern = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
pattern[i] = list.get(i);
}

return pattern;
}


/**
* 数字转换为长度为length位的二进制字符串
* @param code
* @param length
* @return
*/
private static String convertToBinaryBit(int code,int length) {
String binary = Integer.toBinaryString(code);
StringBuffer sb = new StringBuffer();
//每个元素长度为8位,不够前面补充0
if (binary.length() < length) {
for (int i = 0; i < length - binary.length(); i++) {
sb.append("0");
}
return sb.append(binary).toString();
}else{
return binary.substring(binary.length() - length);
}
}


/**
* 十六进制键值转化为二进制串,并逆转编码字符串
* @param keyCode
* @return
*/
private static String constructBinaryCode(int keyCode, int length) {
String binaryStr = convertToBinaryBit(keyCode,length);
StringBuilder builder = new StringBuilder(binaryStr);
return builder.reverse().toString();
}

/**
* 二进制字符串转换成电平
*
* @param code
*/
public static void changeAdd(String code) {
for (int i = 0; i < code.length(); i++) {
if (code.charAt(i) == '0')
{
for( int level : bit0)
{
list.add(level);
}
}else if (code.charAt(i) == '1'){
for( int level : bit1)
{
list.add(level);
}
}
}
}

}

4.更多协议

  更多协议,请参考IR红外,这里只做了这三种协议的模拟。

5. 布局以及其他代码。

  UI比较简陋,就是简单的发射按钮以及输入框。

  • activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:background="?attr/colorPrimary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:menu="@menu/toolbar_menu"
app:title="@string/nec"
app:titleCentered="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.0"
/>

<TextView
android:id="@+id/user_title"
android:text="@string/user"
android:textSize="33sp"
android:textColor="@color/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="4dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintStart_toStartOf="@+id/toolbar"/>
<EditText
android:id="@+id/edit_user"
android:layout_marginTop="5dp"
android:inputType="text"
android:hint="00BF"
android:selectAllOnFocus="true"
android:singleLine="true"
android:maxLength="4"
android:digits="0123456789abcdefABCDEF"
app:layout_constraintLeft_toRightOf="@+id/user_title"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
android:layout_width="100dp"
android:layout_height="48dp"
android:importantForAutofill="no"/>

<EditText
android:id="@+id/edit_input"
android:layout_marginTop="5dp"
android:layout_marginStart="20dp"
android:inputType="text"
android:hint="01"
android:maxLength="2"
android:selectAllOnFocus="true"
android:digits="0123456789abcdefABCDEF"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintLeft_toRightOf="@+id/edit_user"
android:layout_width="100dp"
android:layout_height="48dp"
android:singleLine="true"
android:importantForAutofill="no"/>
<Button
android:id="@+id/send"
android:layout_marginTop="5dp"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintLeft_toRightOf="@+id/edit_input"
android:text="@string/send"
android:layout_width="100dp"
android:layout_height="48dp"
android:importantForAutofill="no"/>

<androidx.core.widget.NestedScrollView
android:id="@+id/input_scroll"
android:background="#C5CAE9"
app:layout_constraintTop_toBottomOf="@+id/edit_user"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="600dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="4"/>
</androidx.core.widget.NestedScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>
  • recyler_list_item.xml
1
2
3
4
5
6
7
8
9
10
11
12
<Button
android:id="@+id/key"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
app:layout_constraintBottom_toBottomOf="@+id/input_scroll"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/edit_input"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:importantForAutofill="no"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"/>
  • tobla_menu.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/nec"
android:title="@string/nec"
app:showAsAction="never"/>
<item
android:id="@+id/samsung"
android:title="@string/samsung"
app:showAsAction="never"/>
<item
android:id="@+id/sony"
android:title="@string/sony"
app:showAsAction="never"/>
</menu>
  • 对应的枚举类、适配器
    • RecyclerKeyAdapter.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      package com.example.remote.adapter;

      import android.view.LayoutInflater;
      import android.view.ViewGroup;
      import androidx.annotation.NonNull;
      import androidx.recyclerview.widget.RecyclerView;
      import com.example.remote.api.IOnClickItemCallback;
      import com.example.remote.databinding.RecyclerListItemBinding;
      import com.example.remote.protocol.EProtocol;

      import java.util.List;

      public class RecyclerKeyAdapter extends RecyclerView.Adapter<RecyclerKeyAdapter.MyViewHolder>{


      private IOnClickItemCallback onClickItemCallback;

      private final List<String> buttonText;

      public RecyclerKeyAdapter(List<String> buttonText) {
      this.buttonText = buttonText;
      }

      public void setOnClickItemCallback(IOnClickItemCallback onClickItemCallback) {
      this.onClickItemCallback = onClickItemCallback;
      }

      @NonNull
      @Override
      public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
      RecyclerListItemBinding binding = RecyclerListItemBinding.inflate(LayoutInflater.from(parent.getContext()),parent,false);
      MyViewHolder viewHolder = new MyViewHolder(binding);
      //添加点击回调
      binding.key.setOnClickListener(v -> {
      if (onClickItemCallback != null)
      {
      onClickItemCallback.onItemClick(viewHolder.getBindingAdapterPosition());
      }
      });
      return viewHolder;
      }

      @Override
      public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
      holder.binding.key.setText(buttonText.get(position));
      }

      @Override
      public int getItemCount() {
      return buttonText.size();
      }

      public List<String> getButtonText() {
      return buttonText;
      }

      public static class MyViewHolder extends RecyclerView.ViewHolder{
      RecyclerListItemBinding binding;
      public MyViewHolder(@NonNull RecyclerListItemBinding itemBinding) {
      super(itemBinding.getRoot());
      binding = itemBinding;
      }
      }
      }

    • EProtocol.java
      1
      2
      3
      4
      5
      6
      7
      8
      package com.example.remote.protocol;

      public enum EProtocol {
      NEC,
      SAMSUNG,
      SONY
      }

    • IOnClickItemCallback.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      package com.example.remote.api;

      import com.example.remote.protocol.EProtocol;

      public interface IOnClickItemCallback {

      /**
      * item点击
      * @param position 点击位置
      */
      void onItemClick(int position);
      }
        对了还需要封装一个检测当前手机是否支持红外功能类,防止应用闪退。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.example.remote.api;

import android.content.Context;

public class ConsumerIrManagerApi {
private static ConsumerIrManagerApi instance;
private static android.hardware.ConsumerIrManager service;

private ConsumerIrManagerApi(Context context) {
//Android4.4才开始支持红外功能
// 获取系统的红外遥控服务
service = (android.hardware.ConsumerIrManager) context.getApplicationContext().getSystemService(Context.CONSUMER_IR_SERVICE);
}

public static ConsumerIrManagerApi getConsumerIrManager(Context context) {
if (instance == null) {
instance = new ConsumerIrManagerApi(context);
}
return instance;
}

/**
* 手机是否有红外功能
*
* @return
*/
public boolean hasIrEmitter() {
//android4.4及以上版本&有红外功能
if (service != null) {
return service.hasIrEmitter();
}
//android4.4以下及4.4以上没红外功能
return false;
}

/**
* 发射红外信号
*
* @param carrierFrequency 红外频率
* @param pattern 数据码
*/
public void transmit(int carrierFrequency, int[] pattern) {
if (service != null) {
service.transmit(carrierFrequency, pattern);
}
}

/**
* 获取可支持的红外信号频率
* @return
*/
public android.hardware.ConsumerIrManager.CarrierFrequencyRange[] getCarrierFrequencies() {
if (service != null) {
return service.getCarrierFrequencies();
}
return null;
}
}

  • MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package com.example.remote;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.example.remote.adapter.RecyclerKeyAdapter;
import com.example.remote.api.ConsumerIrManagerApi;
import com.example.remote.api.IOnClickItemCallback;
import com.example.remote.databinding.ActivityMainBinding;
import com.example.remote.protocol.EProtocol;
import com.example.remote.protocol.NecPattern;
import com.example.remote.protocol.SANSUNGPattern;
import com.example.remote.protocol.SIRCPattern;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public class MainActivity extends AppCompatActivity implements IOnClickItemCallback {

ActivityMainBinding mainBinding;
ConsumerIrManagerApi consumerIrManagerApi;
EProtocol protocol = EProtocol.NEC;//默认使用NEC协议

RecyclerKeyAdapter adapter;

List<String> keyList = new ArrayList<>();

/**
* NEC or SANSUNG
*/
int userCodeH = 0x00;
int userCodeL = 0xBF;

/**
* SONY
*/
int address = 0x01;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mainBinding.getRoot());
setSupportActionBar(mainBinding.toolbar);
init();
if (!consumerIrManagerApi.hasIrEmitter()) {
Toast.makeText(this, "手机不支持红外遥控", Toast.LENGTH_LONG).show();
finish();
}

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_menu, menu);
return super.onCreateOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.nec:
protocol = EProtocol.NEC;
mainBinding.toolbar.setTitle(R.string.nec);
notifyDataButton();
break;
case R.id.samsung:
protocol = EProtocol.SAMSUNG;
mainBinding.toolbar.setTitle(R.string.samsung);
notifyDataButton();
break;
case R.id.sony:
protocol = EProtocol.SONY;
mainBinding.toolbar.setTitle(R.string.sony);
notifyDataSonyButton();
break;
default:
protocol = EProtocol.NEC;
mainBinding.toolbar.setTitle(R.string.nec);
notifyDataButton();
break;
}
return super.onOptionsItemSelected(item);
}


private void init() {
for (int i = 0; i <= 0xFF; i++) {
keyList.add("0x"+String.format("%02x", i));
}
consumerIrManagerApi = ConsumerIrManagerApi.getConsumerIrManager(this);
adapter = new RecyclerKeyAdapter(keyList);
adapter.setOnClickItemCallback(this);
mainBinding.recyclerView.setAdapter(adapter);


mainBinding.editUser.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (protocol == EProtocol.SONY && mainBinding.editUser.getText().length() > 2){
mainBinding.editUser.setText(String.format("%02x", address));
}

if (mainBinding.editUser.getText().length() == 4) {
//4位头码处理
String text = mainBinding.editUser.getText().toString();
String codeH = "0x" + text.substring(0, 2);
String codeL = "0x" + text.substring(2);
userCodeH = Integer.decode(codeH);
userCodeL = Integer.decode(codeL);
} else if ((mainBinding.editUser.getText().length() == 2)) {
String text = mainBinding.editUser.getText().toString();
address = Integer.decode("0x" + text);
}
return false;
}
});

mainBinding.editInput.setOnEditorActionListener((v, actionId, event) -> {
if (protocol == EProtocol.SONY)
{
String text = mainBinding.editInput.getText().toString();
int command = Integer.decode( "0x" + text);
if (command > 0x7F)
{
mainBinding.editInput.setText("");
}
}
return false;
});

mainBinding.send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!TextUtils.isEmpty(mainBinding.editInput.getText())) {
switch (protocol) {
case NEC:
consumerIrManagerApi.transmit(38000, NecPattern.buildPattern(
userCodeH, userCodeL, Integer.decode("0x" + mainBinding.editInput.getText().toString())));
break;
case SAMSUNG:
consumerIrManagerApi.transmit(38000, SANSUNGPattern.buildPattern(
userCodeH, userCodeL, Integer.decode("0x" + mainBinding.editInput.getText().toString())));
break;
case SONY:
consumerIrManagerApi.transmit(40000, SIRCPattern.buildPattern(
address, Integer.decode("0x" + mainBinding.editInput.getText().toString())));
break;
default:
break;
}
}
}
});
}

@Override
public void onItemClick(int position) {
switch (protocol) {
case NEC:
consumerIrManagerApi.transmit(38000, NecPattern.buildPattern(
userCodeH, userCodeL, Integer.decode(keyList.get(position))));
break;
case SAMSUNG:
consumerIrManagerApi.transmit(38000, SANSUNGPattern.buildPattern(
userCodeH, userCodeL, Integer.decode(keyList.get(position))));
break;
case SONY:
consumerIrManagerApi.transmit(40000, SIRCPattern.buildPattern(
address, Integer.decode(keyList.get(position))));
break;
default:
break;
}
}

@SuppressLint("NotifyDataSetChanged")
private void notifyDataSonyButton(){
mainBinding.editUser.setText(String.format("%02x", address));
mainBinding.editUser.setHint(String.format("%02x", address));
keyList.clear();
for (int i = 0; i <= 0x7F; i++) {
keyList.add("0x" + String.format("%02x", i));
}
adapter.notifyDataSetChanged();
}

@SuppressLint("NotifyDataSetChanged")
private void notifyDataButton(){
mainBinding.editUser.setText("00BF");
mainBinding.editUser.setHint("00BF");
keyList.clear();
for (int i = 0; i <= 0xFF; i++) {
keyList.add("0x" + String.format("%02x", i));
}
adapter.notifyDataSetChanged();
}
}

6. 总结

  对于任意一个红外遥控,每个按键都有对应的码值和系统码值,通过这些码值,可以模拟出任意一个按键。比如一个遥控的码值是0x12,系统码值是0707,那么切换对应的协议,将头码设置为0707,输入码值0x12,就可以模拟出这个按键。这里就不放效果图了,有兴趣的可以自己尝试。当然,这个模拟器是要知道对方遥控的协议按键码值才可以,如果要适配空调家用电视,这些就直接用手机自带的万能遥控使用。本工具仅用于生产测试功能。
  在做这个模拟器的时候,示波器的使用是不可缺少的。再进行App调试配对波形的时候和解析波形,产生了巨大的作用。顺便还研究了Android系统源码的红外分析,将于下篇文章分享。不得不说,懒才是第一生产力,哈哈。