Cloning an infrared remote controller on Android

In a previous article, I reverse engineered my TV's infrared protocol as a backup measure in case the remote controller stops working. A couple of years later, it finally happened. Luckily I have infrared support in my phone, a 2022 Redmi 10. It comes preinstalled with an app that supports multiple infrared protocols including the NEC one. Unfortunately, none of the installed NEC versions were compatible with my TV since it uses a modified version of the protocol. I decided to take matters into my own hands.

The infrared documentation for Android isn't lengthy, but it doesn't have to be. It boils down to sending a series of ON/OFF pulses with a given carrier frequency:


public void transmit (int carrierFrequency, int[] pattern);

To do that, I converted the functions of the previous project from C to Kotlin. The differences were minimal. In the android version, I had to construct an array of pulses whereas in the C version, I sent them immediately by switching PORTB on and off as soon as a value is set in the code. One upside of the Android version though is that I didn't need to write a modulating function because ConsumerIrManager.transmit already accepts a modulation frequency.

NEC protocol implementation. C on the right, Kotlin on the left

NEC protocol implementation. C on the right, Kotlin on the left

Porting the code constants was straightforward as well. With the help of some Vim macros, I extracted it into a Kotlin enum:

NEC hex codes. C on the right, Kotlin on the left

NEC hex codes. C on the right, Kotlin on the left

I then added the required permissions to AndroidManifest.xml:



<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.pingfrommorocco.remoteir">

    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.TRANSMIT_IR" />
    <uses-feature android:name="android.hardware.consumerir" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.RemoteIR">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

And wrapped it all in the ugliest UI you can think of. I tried to get it to mimic the layout of the physical remote controller. The keyword here is "tried":

Android on the left, physical device on the right

If you squint, you can almost see a resemblance

I then added click handlers and vibrations as a form of tactile feedback:


    override fun onCreate(savedInstanceState: Bundle?) {
        val vibrator = VibratorWrapper(this)
        val infraredService = getSystemService(CONSUMER_IR_SERVICE) as ConsumerIrManager
        val transformer = NecTransformer()

        val buttonCommandMapping = mapOf(
            R.id.power_button to NecCode.POWER,
            R.id.source_button to NecCode.SOURCE,
            R.id.channel_up_button to NecCode.CHANNEL_UP,
            R.id.channel_down_button to NecCode.CHANNEL_DOWN,
            R.id.ok_button to NecCode.OK,
            R.id.up_button to NecCode.UP,
            R.id.down_button to NecCode.DOWN,
            R.id.left_button to NecCode.LEFT,
            R.id.right_button to NecCode.RIGHT,
            R.id.play_button to NecCode.PLAY,
            R.id.stop_button to NecCode.STOP,
            R.id.fast_backward_button to NecCode.FAST_BACKWARD,
            R.id.fast_forward_button to NecCode.FAST_FORWARD,
        )

        buttonCommandMapping.entries.forEach {
            val (button, command) = it
            findViewById<Button>(button).setOnClickListener {
                val pulses = transformer.transformMessage(NecCode.ADDRESS.code, command.code)
                Log.d(TAG, "emitting: ${pulses.map { it }}")
                infraredService.transmit(38000, pulses.toIntArray())
                vibrator.vibrate(VIBRATION_DURATION)
            }
        }
    }

And that's pretty much it. I've been using it for a while now and it did the trick so far, although I'm still looking into how to get Android to keep sending pulses while the volume buttons are pressed. I'll update this article with a Github link once I figure it out.

Commentaires

Posts les plus consultés de ce blog

Writing a fast(er) youtube downloader

My experience with Win by Inwi

Porting a Golang and Rust CLI tool to D