红外遥控及Android手机红外遥控器开发

一、红外遥控

红外遥控技术是通过红外技术、红外通信技术和遥控技术的结合实现的一种无线控制技术。由于红外线的波长较短,对障碍物的衍射能力较差,无法穿透墙壁,所以红外遥控术更适合应用在短距离直线控制的场合,也正是这样,放置在不同房间的家用电器可使用通用的遥控器而不会产生相互干扰。

红外遥控所需传输的数据量较小,一般仅为几个至几十个字节的控制码,传输距离一般小于 10 米,因其功耗小、成本低、易实现等诸多优点,被广泛应用于电视机、机顶盒、DVD 播放器、功放、空调等家用电器的遥控。

二、手机红外遥控功能

部分智能手机都配置了是红外遥控功能(即安装红了外发射器)。那么,安装了红外发射器的智能手机,可以拿来当遥控器使用,还能一部手机遥控许多家电。具有红外功能的智能手机的顶部,有的镶嵌一个或多个小灯泡,有的是一小片黑色盖子,这个黑盖子对红外线来说可是透明的,只是人的肉眼看不穿它。红外遥控带着灯泡就像一支手电筒,红外光照到哪里,哪里的电器才会接收响应,这决定了红外遥控的三个特性:

  1. 遥控器要对准电器才有反应。
  2. 遥控器不能距离电器太远,最好是五米之内。
  3. 遥控器与电器之间不能有障碍物。

安装了红外发射器的手机,可以拿来当遥控器使用,还能一部手机遥控许多家电,这就需要破解电器的信号编码了。

三、红外发射原理简介

通用红外遥控系统主要由发射和接收两大部分组成。发射部分包括单片机芯片或红外遥控发射专用芯片实现编码和调制,红外发射电路实现发射;接收部分包括一体化红外接收头电路实现接收和解调,单片机芯片实现解码。红外遥控发射专用芯片非常多,编码及调制频率也不完全一样。手机实现红外遥控功能,主要就是发射红外信号部分,这就需要了解下红外信号的编码和调制原理。

3.1.红外遥控二进制信号的编码

红外遥控器发射的信号由一串「0」和「1」的二进制代码组成,不同的芯片对「0」和「1」的编码有所不同,通常有曼彻斯特 (Manchester) 编码和脉冲宽度编码 (PWM)。家用电器使用的红外遥控器绝大部分都是脉冲宽度编码,如下图所示:


图1 脉冲宽度编码

3.2.红外遥控二进制信号的调制

二进制信号的调制由发送单片机芯片或红外遥控发射专用芯片来完成,把编码后的二进制信号调制成频率为 38kHz 的间断脉冲串,相当于用二进制信号的编码乘以频率为38kHz 的脉冲信号得到的间断脉冲串,即是调制后用于红外发射二极管发送的信号。通用红外遥控器里面常用的红外遥控发射专用芯片载波频率为 38kHz,这是由发射端所使用的 455kHz 陶瓷晶振来决定的。在发射端对晶振进行整数分频,分频系数一般取 12 所以 455kHz ÷ 12 ≈ 37.9kHz ≈ 38kHz。

四、 NEC编码协议

在红外开发中,可能最重要的就是发送二进制信号的编码协议了,各个厂家所使用的编码协议不同,所以遥控器也不能相互控制,而即使编码协议相同,使用的用户码不同,也不能被接收端接受。所以类似万能遥控器这种应用,第一个要解决的问题就是各类家用电器,各类厂家所使用的编码协议以及所使用的用户码等。但各类电器众多,这种应用是很难兼容全的,有的大厂家会将自家产品的编码及对应功能键的数据序列公布在网上,方便其他开发者开发,至于其他没有公布的,可能就要使用红外解码仪来破解它所使用的协议以及各功能键对应的数据码等。

在日常家用电器中,NEC 编码是比较常见的一种编码协议, 通用红外遥控器发出的一串二进制代码按功能可以分为分「引导码、用户码 16 位、数据码 8 位、数据反码 8 位和结束位」,编码共占 32 位,如下图2所示:


图2 NEC编码格式

其中引导码由一个 9ms 的 38kHz 载波起始码和一个 4.5ms 的无载波低电平结果码组成。用户码由低 8 位和高 8 位组成 (用户码高八位和低八位可采用原码与反码的方式,可用于纠错,但也可直接是 16 位的原码方式),不同的遥控器有不同的用户码,避免不同设备产生干扰,用户码又称为地址码或系统码。数据码采用原码和反码方式重复发送,编码时用于对数据的纠错,遥控器发射编码时,低位在前,高位在后。结束位是 0.56ms 的 38kHz 载波。而其中的「0」码由 0.56ms 的 38kHz 载波和 0.56ms 的无载波低电平组合而成,脉冲宽度为 1.125ms,「1」码由 0.56ms 的 38kHz 载波和 1.69ms 的无载波低电平组合而成,脉冲宽度为 2.25ms,如下图所示:


图3 NEC 0码和1码

五、红外遥控器解码

红外遥控器的解码仪能够分析常见家电的红外遥控信号,下面两种除外:
1、空调遥控器,空调的控制比较复杂,光光温度就可能调节十几次,难以破解。
2、灯光遥控器,灯本身发光发热,同时也会散发大量红外线,势必对外部的红外信号造成严重干扰;所以灯只能采取射频遥控器。

红外解码仪是家电维修人员的必备仪器,常用于检测遥控器能否正常工作,开发者为了让手机实现遥控功能,也要利用解码仪捕捉每个按键对应的红外信号,红外信号由三部分组成,分别是:

  1. 用户码,表示厂商代号,每个厂家都有自己的唯一代号;
  2. 数据码,表示按键的编号,不同的数据码代表不同的按键;
  3. 电路格式,表示红外信号的编码协议,每种协议都有专门的指令格式。

比如说电路61212表示的是NEC6121协议,该协议的红外信号编码格式为:引导码+用户码+数据码+数据反码+结束码,其中引导码和结束码都是固定的,数据反码由数据码按位取反得来,真正变化的只有用户码和数据码。

六、 Android红外遥控功能开发

1、红外权限配置

在App工程的AndroidManifest.xml中补充红外权限配置

<!--红外遥控-->
<uses-permission android:name="android.permission.TRANSMIT_IR" />
<!-- 是否仅在支持红外的设备上运行 -->
<uses-feature android:name="android.hardware.ConsumerIrManager" android:required="false" /> 

2、红外功能API

红外遥控功能从Android4.4之后才开始支持,对应的管理类名叫ConsumerIrManager,常用的三个方法分别是:

hasIrEmitter (): 用于检查设备是否具有红外发射器。返回true表示有,返回false表示没有。
getCarrierFrequencies() : 获得红外发射器可用的载波频率范围。
transmit (int carrierFrequency, int[] pattern) : 用于发射红外信号。
第一个参数为信号频率,单位赫兹(Hz),家用电器的红外频率通常使用38000Hz;第二个参数为整型数组形式的信号格式。

下边是一个封装好的红外遥控管理类

package com.startimes.home.irManager.manager;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;

@TargetApi(Build.VERSION_CODES.KITKAT)
public class ConsumerIrManagerApi {

    private static ConsumerIrManagerApi instance;
    private static android.hardware.ConsumerIrManager service;

    private ConsumerIrManagerApi(Context context) {
        //Android4.4才开始支持红外功能
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
            // 获取系统的红外遥控服务
            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 static boolean hasIrEmitter() {
        //android4.4及以上版本&有红外功能
        if(service!=null){
            return service.hasIrEmitter();
        }
        //android4.4以下及4.4以上没红外功能
        return false;
    }

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

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

3、码值编码

上文提到过,一般家用电器遥控器的频率是38000,核心就在transmit()方法的信号编码参数上,上文第5部分提到过,编码由“引导码(9ms+4.5ms)+用户编码(高八位)+用户编码(低八位)+键数据码+键数据反码+结束码”组成,然后按照一定的编码规则,合成数组的形式。以NEC6122协议举例,引导码都是固定的(9000+4500),结束码即停止位可以按照(560,2000),不同遥控器差别主要在于用户码和数据码,同一个遥控器的用户码是一样的,不同按键有不同的码值,码值可以转换出对应的数据码和数据反码,以下面的遥控器码值举例。


图4 码值举例

用户码是0X08E6,按键2对应的码值是0X41,码值都是十六进制表示的,转化为二进制,用户码高八位08为00001000,用户码低八位E6为11100110,数据码41为01000001,数据反码为10111110。

由于手机与遥控器的信号编码有区别,需要逆序编码。逆序编码后,用户码高八位为00010000,用户码低八位为01100111,数据码为10000010,数据反码为01111101。

编码转换完成,但是transmit方法,参数要传递整型数组形式的信号,并不是二进制数而是电平信号数据。电平是电路中某一点电压的高低状态,在数字电路中常用高电平表示“1”,用低电平表示“0”。根据上文第四部分图三可以看出,遥控器发射红外信号之时,通过“560us低电平+1690us高电平”代表“1”,通过“560us低电平+565us低电平”代表“0”。于是编写Android代码的时候,使用“560,1690”表示二进制的1,使用“560,565”表示二进制的0,具体数组值如下所示:

int[] pattern = {9000,4500, // 开头两个数字表示引导码
    // 下面两行表示用户码
    560,565, 560,565, 560,565, 560,1690, 560,565, 560,565, 560,565, 560,565,
    560,565, 560,1690, 560,1690, 560,565, 560,565, 560,1690, 560,1690, 560,1690,
    // 下面一行表示数据码
    560,1690, 560,565, 560,565, 560,565, 560,565, 560,565, 560,1690, 560,565,
    // 下面一行表示数据反码
    560,565, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690, 560,565, 560,1690,
    560,20000}; // 末尾两个数字表示结束码

具体这些转换工作用代码写出,如下:

package com.startimes.home.irManager.ircode;

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

/**
 * Copyright  : huangr
 * Created by huangr on 2019/8/8.
 * 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++) {
            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);
        }
    }
}

4、使用

在需要发射红外信号处调用如下:

/**
 * 按下按键发射红外信号
 */
public void transmit() {
     ConsumerIrManagerApi.transmit(38000,NecPattern.buildPattern(NecPattern.buildPattern(0X08,0XE6, 0X41));
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336