By CrushEdge · crushedge.com

Toshiba TV IR Remote app icon

TL;DR — Toshiba TV IR Remote is a purpose-built Android app that turns any phone with a hardware IR blaster into a full working remote for a Toshiba 23PB201EJ. It ships with the complete CT-90326/CT-90327 LIRC codeset baked in, but the real story is what happens when those codes don’t work out of the box — a four-stage sweep and auto-detect toolkit that can hunt down the correct IR address, brute-force individual command codes, and fall back through NEC, NEC Extended, RC5, RCA, Sharp, and raw µs patterns until something makes the TV respond. Once you’ve found your codes, they’re saved locally and the app becomes a fully functional daily remote: twenty-six buttons, haptic feedback, hold-to-repeat on volume and d-pad, and a clean Jetpack Compose UI that gets out of your way. No ads. No internet. No account. Just photons and patience.

There is a particular kind of frustration that only hits you when a small piece of plastic — a thing you paid essentially nothing for, a thing you’ve definitely spilled something on at least twice and dropped behind the sofa more times than you can count — stops working, and you suddenly realise your entire evening television plan depends entirely on it.

That was us with the Toshiba 23PB201EJ.


The remote dies (a short, undignified story)

It was the most boring possible failure. No drama. No sparks. The remote just… stopped. One evening it worked fine. The next morning it didn’t. New batteries: nothing. Clean the contacts with the corner of a shirt: nothing. The classic “smack it firmly against your palm three times” move that has a statistically inexplicable success rate in households across Southeast Asia: also nothing. The LED on the front of the TV didn’t even flicker.

We looked up a replacement. The CT-90326 and its near-twin the CT-90327 — the official Toshiba remotes for this TV — are discontinued. Toshiba stopped making them. Third-party replacements on the usual online marketplaces ranged from “suspiciously cheap with zero reviews and a product photo that looks like it was AI-generated” to “we will ship this to you in five to eight weeks from a warehouse in a city I have never heard of.” The universal remotes at the local electronics shops took one look at our TV model, nodded sagely, and offered us a dropdown list of approximately forty different Toshiba remote profiles, none of which we had any confidence would actually work.

And sitting in my pocket the entire time was a phone with a hardware IR blaster.

You already know where this is going. I asked the most dangerous question a developer can ask themselves:

“How hard could it be to just build the remote?”

The answer, as always with these things, was: harder than it looked, more interesting than it had any right to be, and way more satisfying than waiting an indefinite number of weeks for a parcel from a city I’ve never heard of. Toshiba TV IR Remote is what came out the other end.


Why every existing IR remote app fell short

I want to be fair here. There are plenty of IR remote apps on the Play Store and I tried several before giving up and reaching for Android Studio. Let me run you through the gauntlet, because it explains every decision we made when building this one.

Generic universal remote apps. You open them, pick “Toshiba” from a scrollable list of brands, pick your model from another list, and the app loads a pre-baked profile. If you’re lucky, half the buttons work. If you’re very lucky, Power and Volume work, and the menu navigation is mysteriously wrong — Volume Up changes the TV language, OK brings up a service menu, Info does absolutely nothing. These apps work fine for common, current, mainstream TV models. A discontinued mid-range TV from a specific regional market? Not so much.

The “learning” apps. Some apps will hold your original remote in front of the phone and learn the codes by capturing the IR signal. A brilliant idea. Completely useless to us because our original remote was the reason we were here in the first place.

The LIRC database apps. More sophisticated. They pull code definitions from the community-sourced LIRC project database, which genuinely has Toshiba codes in it. But the apps that surface LIRC data give you no way to test individual candidates — you pick a profile, either the whole thing works or the whole thing doesn’t, and if it doesn’t, the app shrugs. There’s no “try address 0x02 instead of 0x40” option. No sweep. No brute-force. Just a list of profiles and a silent shrug.

Paid “advanced” remote apps. The ones that unlock “enter a custom IR hex code” behind a paywall. The paywall itself isn’t even the issue. The issue is that you still need to know the correct hex code. If the bundled profile doesn’t match your TV, having the ability to type a hex code into a field doesn’t help much when you have no idea which hex code to type.

So we made a list. A short, grumpy list of what we actually needed:

  1. Ship with the known codes for the CT-90326/CT-90327, pre-loaded, so if we’re lucky it just works.
  2. Have a real sweep tool — something that fires Power at every possible NEC address from 0x00 to 0xFF and stops when the TV responds.
  3. Let us manually fire individual address + command pairs to figure out exactly which code triggers which function.
  4. Save whatever we find, persistently, so we never have to sweep again.
  5. Actually be a usable daily remote once the hard part is done.

That list is the whole app.


What the app actually is, in one breath

Toshiba TV IR Remote is an Android 8.0+ app that requires a phone with a hardware IR blaster. It ships with a complete button layout for the Toshiba 23PB201EJ — twenty-six RemoteFunction entries, from Power and Mute through the full navigation d-pad, all the way down through the number pad and menu buttons like Home, Back, Exit, and Info. All of those buttons come pre-mapped to the CT-90326/CT-90327 codeset, NEC protocol, at address 0x40.

If 0x40 happens to be correct for your specific unit, you install the app and you have a working remote in under thirty seconds. If it isn’t — and for some Toshiba units of this generation it won’t be — you have a four-stage sweep and detection toolkit built right in, reachable in one tap from the main screen, that can find the right codes through curated candidate testing, broad address sweeping, and manual brute-force. Once you find them, they’re saved locally. Export them as JSON and you can share them with anyone who owns the same TV.

Zero network calls. One permission in the manifest: TRANSMIT_IR. That’s it.


The main remote screen

Main remote screen

The main screen. Red POWER at the top, Source and Mute flanking it, the d-pad cluster in the centre with Vol on the left and Ch on the right, navigation row below, then the number pad.

Open the app and you land on the remote. No splash screen. No onboarding carousel. No permission dialogs asking for access to your contacts. Just buttons.

The layout is deliberate. At the very top of the button grid sits POWER — rendered larger than everything else, in the error color (red) from the Material 3 theme, unmissable in a dark room, impossible to accidentally not find when you’re fumbling around at eleven at night. Source and Mute flank it, slightly smaller but still generous tap targets.

Below that, the heart of the layout: a card that holds the entire navigation cluster. Volume controls live on the left — Vol+ at the top, Vol- at the bottom — and Channel controls mirror them on the right. In the centre, a large circular OK button with the d-pad arrows (▲ ▼ ◀ ▶) surrounding it. The spatial arrangement means your thumb knows where to go without looking. You don’t have to think about it.

Below the d-pad card: a row of four outlined navigation buttons — Menu, Back, Exit, Info. Smaller, but still reachable. Below that, a standard 3×3+1 number pad for direct channel entry.

At the very bottom of the screen, a single line of status text updates whenever something happens. A successful send looks like:

NEC 0x0040:0x1A

An unmapped function tells you exactly what to do next:

Tombol VolUp belum di-map. Buka Sweep dulu.

(“The VolUp button hasn’t been mapped yet. Open Sweep first.”) That message is the app’s polite way of saying: you found some codes but not all of them — go back to the sweep and find the rest.

If the phone doesn’t have a hardware IR emitter at all, a red banner appears at the top of the screen:

“Device ini tidak punya IR emitter.” (“This device doesn’t have an IR emitter.”)

The rest of the UI remains visible and explorable. No crash, no wall of text. Just a clear explanation and then the full layout — so you can at least see what you’re missing.

Hold-to-repeat is wired into the buttons that need it. Volume, Channel, and all four d-pad arrows are marked holdable. Hold any of them and the app fires the initial command, then follows up with NEC repeat frames every ~108 milliseconds until you lift your finger. The hold duration — how long before repeating starts — is configurable in Settings and defaults to 400ms. Brief tap: one command. Sustained press: smoothly scrolling volume or channel, just like the real remote. For the d-pad, it means you can hold ▲ to scroll through a long menu rather than tapping forty times.

The top bar carries three icons: the Tune icon (sweep and auto-detect), the Edit icon (mapping editor), and the Settings cog. Three taps from anywhere to anywhere. That’s the navigation model for the whole app.


The IR address problem that nobody warns you about

Here is the thing about infrared remote controls that universal remote apps quietly smooth over, and that took me longer than I’d care to admit to fully understand.

When you press a button on a TV remote, the LED in the remote’s nose blinks. Not a single blink — a precisely timed series of on/off pulses at a carrier frequency, typically 38 kHz, sometimes 36 kHz or 56 kHz depending on the protocol. Your TV has a receiver chip tuned to that frequency, sitting there passively listening. The moment it sees a valid signal, it decodes and acts on it.

The NEC protocol — which this Toshiba TV uses — defines the pulse sequence like this. Every frame begins with a long leader burst: 9,000 µs on, then 4,500 µs off. Then 32 bits of data, transmitted LSB-first. Each bit is encoded as a 560 µs burst of IR followed by either 560 µs of silence (a zero) or 1,690 µs of silence (a one). A final 560 µs stop burst closes the frame. The whole thing takes about 67.5 milliseconds.

Those 32 bits split into four bytes:

[ Address ] [ ~Address ] [ Command ] [ ~Command ]

The address is the device identifier — think of it as the TV’s channel number on the IR spectrum. The second byte is the bitwise complement of the address, and the TV cross-checks them as an integrity guard. If they don’t match, the frame is discarded entirely. The command is the actual button code. Its complement follows, for the same integrity reason.

A valid Power press on the CT-90326, at address 0x40, looks like this:

Address: 0x40 (0100 0000)   ~Address: 0xBF (1011 1111)
Command: 0x12 (0001 0010)   ~Command: 0xED (1110 1101)

Here is where the problem hides. The address is not universal, even within the same TV model line. Toshiba manufactured the 23PB201EJ across different regional markets in multiple production batches over several years. Different batches were sometimes programmed with a different NEC address. The commands — the third byte, like 0x12 for Power — stay consistent across all of them. It’s the address byte that drifts.

The LIRC community database for the CT-90326/CT-90327 lists eight known candidate addresses:

0x40  0x02  0x80  0xBF  0xA0  0x4F  0xF0  0x10

Any one of those might be correct for a given unit. They’re all in the bundled toshiba_candidates.json asset. The default — 0x40 — is the most common, and it’s what the app uses when you first install it. But if your TV doesn’t respond to 0x40, the Sweep screen exists to find the one it does respond to.

This is the root failure of every universal remote app we tried. They ship one profile and if the address doesn’t match, there’s no next step. We built the next step.


Sweep & Auto-Detect: the four-stage detective kit

Sweep & Auto-Detect screen

The Sweep screen showing the four stage tabs. Stage 1 (shown) has two cards: Quick-Test Kandidat LIRC for the eight known addresses, and Broad NEC Power Sweep for when none of the candidates respond.

Tap the Tune icon on the main remote and you land on the Sweep screen. At the top is a scrollable row of four tabs:

  1. 1. Auto-Detect Power
  2. 2. Kandidat LIRC
  3. 3. Brute-Force NEC
  4. 4. Fallback (RC5/RCA/Sharp/Raw)

Work through them in order. Most people will be done by the end of Stage 1 or 2. The later stages are for the stubborn cases — and I say that with respect, because we were the stubborn case on the first day of testing.

Stage 1 — Auto-Detect Power

The first card on this tab is Quick-Test Kandidat LIRC. It lists all eight candidate addresses with their protocol, address in hex, and the Power command code for each. You can tap Tes (“Test”) next to any individual candidate to fire Power once at that address and wait 1.5 seconds to see if the TV reacts. Or you can tap the larger button at the bottom of the card:

“Tes semua kandidat berurutan” (“Test all candidates in sequence”)

The app walks through all eight addresses automatically, firing Power with a 1.5-second pause between each attempt, while you watch the TV. The moment the TV reacts — screen flickers, input changes, TV shuts off or turns on, anything — you know which address was active at that moment. When the sequence finishes, a dialog appears:

“Address mana yang berhasil?” (“Which address worked?”)

Tap the address that triggered a response, and the app loads the full CT-90326/CT-90327 mapping for that address — all twenty-six functions, every code from the bundled codeset, in one shot. Go back to the main remote. You’re done.

If none of the eight candidates produced any reaction at all, the second card in Stage 1 is the nuclear option: Broad NEC Power Sweep.

This card fires Power at every single NEC address from 0x00 through 0xFF, one per interval, while a live readout ticks through the address space:

Sedang mencoba: addr=0x3F cmd=0x12

(“Currently trying: addr=0x3F cmd=0x12.”)

The interval is adjustable with a slider right on the card — 300ms to 3,000ms. At 700ms (the default), sweeping all 256 addresses takes about three minutes. At 300ms you’re through it in under ninety seconds, though 300ms doesn’t leave much reaction time when you’re watching the TV.

You can also pick which Power command code to sweep. Five filter chips offer the most common Toshiba Power codes: 0x12, 0x10, 0x48, 0x08, 0x0D. The default 0x12 is correct for the CT-90326/CT-90327. If you’re trying this on a different Toshiba model and aren’t sure which Power code to use, run the sweep more than once with different chips selected.

The moment your TV does anything at all, tap the large red button:

“⛔ TV BERESPON!” (“⛔ TV RESPONDED!”)

The sweep stops. The last-tested address is displayed. A Re-test button lets you confirm it wasn’t a false positive. “Set Power & sweep dari sini” saves that address as the base for further code-hunting in Stage 3.

Stage 2 — Kandidat LIRC

Once you have a confirmed address, Stage 2 is the express lane. The same eight candidates appear here, each with a single Apply button. Tap Apply for the address that matched your TV, and the app loads the complete mapping — all twenty-six functions — instantly. Two taps from “I found the address” to “fully working remote.”

Stage 3 — Brute-Force NEC

Stage 3 handles a deeper problem: what if you want to find a command that isn’t in the bundled codeset? The CT-90326/CT-90327 database we ship covers twenty-six functions. The real remote almost certainly has more — Aspect Ratio, Sleep Timer, maybe a picture mode shortcut. Stage 3 is how you find them.

The card has two hex input fields: Address and Command. Below those, a sweep interval slider, and four action buttons:

  • Send — fire the current combination once
  • ◀ Prev — decrement the command byte and fire
  • Next ▶ — increment the command byte and fire
  • Auto — auto-increment at the configured interval until you tap Stop

At the top, a protocol picker lets you switch between NEC (8-bit address, max 0xFF) and NEC_EXT (16-bit address, max 0xFFFF). In NEC_EXT mode, the ~address integrity byte is replaced by the high byte of the 16-bit address, and the input field widens accordingly.

The key button here is “Map kode ini ke fungsi…” (“Map this code to a function…”). Found a code that triggers something on the TV? Tap it. A scrollable dialog lists every RemoteFunction by name and display label. Tap the function you want, and the code is saved permanently. Your discovery becomes part of your remote.

There is something genuinely satisfying about stepping through command codes one by one and watching a TV respond. At 0x16 the Info overlay appears. At 0x5B the menu opens. At 0x9A it jumps to the home screen. You feel like you’re reading a language that’s been sitting right there in your remote for years, completely legible if you just had the right lens.

Stage 4 — Fallback (RC5/RCA/Sharp/Raw)

Stage 4 is structurally identical to Stage 3, but with a different protocol picker: RC5, RCA, and Sharp.

This tab exists for one scenario: you’ve swept all 256 NEC addresses and the TV didn’t respond to any of them. That’s unusual for a Toshiba from this era — NEC is by far the most likely protocol — but not impossible. Before concluding that your IR blaster is dead, Stage 4 lets you probe the other common protocols. A TV that ignores NEC entirely might respond immediately to one of them. The same “Map kode ini ke fungsi” dialog is here to save whatever you find.


What’s actually happening at the microsecond level

The protocol implementations are the part of this codebase I’m most quietly proud of, and understanding them is the reason the sweep tools work reliably rather than just hoping that some IR pattern reaches the TV.

NEC in detail

A single Power command — address 0x40, command 0x12 — becomes an IntArray of 67 integers, alternating on/off durations in microseconds. Here’s what the first few entries look like:

9000, 4500,      ← leader burst
560, 560,        ← bit 0 of address = 0  (0x40 = 0100 0000, sent LSB first)
560, 560,        ← bit 1 = 0
560, 1690,       ← bit 2 = 1
560, 560,        ← bit 3 = 0
...              ← 28 more bit pairs
560              ← stop burst

0x40 in binary is 01000000, sent LSB-first: 00000010. Its complement 0xBF follows. Then command 0x12 and complement 0xED. Sixty-seven integers. 67.5 milliseconds. That’s the full NEC frame.

For hold-to-repeat, the transmitter appends a distinct repeat frame after the initial command: a 40,000 µs silence, then a 9,000 µs burst, a 2,250 µs silence, and a final 560 µs stop. The TV interprets this as “the button is still held.” Repeat frames fire every ~108 ms until the hold budget runs out. With the default 400ms setting you get about three repeat frames per press — smooth and responsive, exactly like the physical remote.

Extended NEC (NEC_EXT) is the same structure, but the ~address byte is replaced by the high byte of a 16-bit address. A device at address 0x1234 sends 0x34 (low byte, LSB-first), then 0x12 (high byte, LSB-first), then the command and complement. The encoder handles this cleanly with a single extended: Boolean parameter — the same 67-element output structure, just different bit content.

RC5: Manchester at 36 kHz

Philips RC-5 is structurally different. It uses Manchester encoding at 36 kHz — two kHz lower than NEC, which matters because the TV’s receiver is tuned to a specific carrier frequency. Each half-bit is 889 µs. A zero is a HIGH→LOW transition at the midpoint of the bit cell; a one is LOW→HIGH. The result is a self-clocking signal — the transitions themselves carry both clock and data, so there’s no separate synchronisation needed.

The 14-bit frame carries: two start bits, a toggle bit (which flips with each new distinct button press and stays constant on repeats — this is how the TV distinguishes “pressed again” from “still held”), a 5-bit system address, and a 6-bit command. RC-5X extends the command space to 7 bits by repurposing the second start bit.

The encoder builds the Manchester waveform, run-length encodes the level transitions into a duration array, drops any leading off-periods (the transmitter must begin with a burst), and trims any trailing off-period (it must also end with a burst). All the edge cases — a frame that happens to start with a zero-bit, a command whose last transition goes low — are handled correctly. Malformed frames would cause the TV to discard the signal silently, which is a deeply unhelpful failure mode to debug.

RCA: 56 kHz and a self-checking payload

RCA is older and simpler. It runs at 56 kHz — noticeably higher than NEC or RC5, which is why a TV built for one won’t respond to the other even at the right address. A 4 ms ON leader, 4 ms OFF, then 24 data bits, then a stop pulse. The 24 bits encode a 4-bit address, an 8-bit command, then ~address (4 bits) and ~command (8 bits) — all MSB-first. Bit timing: 500 µs ON, then either 1,000 µs OFF (zero) or 2,000 µs OFF (one).

The encoder packs all 24 bits into a single integer by shifting address and command into position alongside their complements, then iterates from bit 23 down to 0. Clean, compact, fast.

Sharp: two frames, inverted

Sharp is the most distinctive. It sends two consecutive frames per button press, separated by a 40,000 µs gap. The first frame: 5-bit address (MSB-first), 8-bit command (MSB-first), an expansion bit set to 1, a check bit set to 0. The second frame: same address, but command, expansion, and check bits all inverted. The TV receives both frames and cross-checks them — Sharp’s equivalent of the NEC complement integrity scheme, just split across time instead of embedded in a single frame.

Bit timing is fast: 320 µs ON, then 680 µs OFF for a zero, or 1,680 µs OFF for a one. If you’ve ever seen a Sharp TV remote’s LED seem to flash twice when you press a button — that’s the two-frame protocol in action.

RAW: the escape hatch

RAW is what happens when all other options are exhausted. If you have a pre-recorded microsecond pattern — say, captured from a working remote using external IR receiver hardware — you store it directly as an IntArray in an IrCommand with protocol = RAW and specify the carrier frequency alongside it. The transmitter fires the pattern as-is. The sweep UI doesn’t generate RAW commands, but the full data model and transmitter support them, and the export/import JSON path preserves them too. Bring your own patterns and they just work.


The Mapping Editor: showing your work

Mapping Editor screen

The Mapping Editor. Every function listed with its current protocol, address, and command in hex. A Test button per row. The clear-all at the bottom.

Tap the pencil icon on the main remote toolbar and you open the Mapping Editor. This is the transparency screen — the place that shows you exactly what each button is going to send before you trust it in daily use.

The layout is a scrollable list, one row per RemoteFunction. Each row shows the function’s internal name and display label:

VolUp (Vol +)
NEC 0x0040:0x1A

Or, if that function hasn’t been mapped yet:

ChDown (Ch -)
— belum di-map —

(“— not yet mapped —”)

Every mapped row has a Test button. Tap it and the command fires once. No dialog, no confirmation. You step down the list and verify that each button does what it should on the actual TV, one tap at a time. This is where you catch the edge cases — the command code that the TV ignores entirely, the function that triggers the wrong OSD, the number button that actually sends a code for a different region’s channel numbering scheme.

At the bottom, one additional control: “Hapus semua mapping” (“Delete all mappings”). Outlined, not filled, because it’s destructive and we wanted it to look slightly less tempting than the Test buttons. Tap it and the entire mapping clears. No undo. The bundled codeset is always one tap away in Stage 2 of the Sweep.

One intentional constraint: the Mapping Editor is read-and-test only. You can’t type new hex values in here to change a mapping. If you want to reassign a function, you go to Brute-Force in the Sweep, fire the command you want, and use “Map kode ini ke fungsi.” The workflow is linear on purpose: sweep finds codes, editor verifies them, remote uses them.


Settings: the knobs you actually want

Settings screen

Settings. Two sliders, export and import JSON, a reload button, and the IR emitter diagnostic at the bottom.

The Settings screen is blessedly short. Five things, a diagnostic, and nothing else.

Hold duration. A slider from 0ms to 2,000ms, default 400ms. This is the repeat budget for the holdable buttons — volume, channel, and d-pad arrows. At 400ms a brief tap sends one command; a sustained hold starts repeating. Setting it to 0ms disables repeat entirely. Setting it to 2,000ms means holding Vol+ fires roughly eighteen NEC repeat frames before you release. Most people find 300–500ms feels natural. Tune it to match your TV’s IR receiver sensitivity and your thumb’s patience.

Sweep interval. A slider from 200ms to 3,000ms, default 700ms. The pause between shots during any auto-sweep — the broad NEC sweep, the auto-increment in Brute-Force, the candidate sequence in Auto-Detect. One setting, shared across all sweep stages.

Export mapping JSON. Opens the system file-save dialog. Writes the full MappingStore.Snapshot — the complete RemoteFunction → IrCommand mapping plus the current sweep state — as plain, human-readable JSON to wherever you point it. Send this file to someone with the same TV and they import it and have a working remote without sweeping at all.

Import mapping JSON. Opens the system file picker. Reads the JSON, deserialises it, and if it’s valid, replaces the current mapping immediately. If the JSON is malformed or unrecognised: “Gagal mengimpor mapping.” (“Failed to import mapping.”) and nothing changes. Graceful.

Reload codes (pakai address sekarang). Re-applies the bundled codeset using the address currently saved in the mapping. The “something went sideways after an update, just reset the codes” button. It reads the address from the first mapped command, finds the matching entry in the LIRC candidate list, and rebuilds the mapping from the bundled JSON. Status bar confirms: “Codes di-reload untuk addr=0x0040.”

At the very bottom of the screen, two lines of diagnostic text:

IR emitter: tersedia
Sweep terakhir: addr=0x0040 cmd=0x00 proto=NEC

Tersedia means available. TIDAK tersedia means not available — and if you see that, nothing else in this app is going to work, and you should check your phone’s spec sheet. The last sweep state persists across sessions so you can see exactly where you left off if you need to return to the sweep later.


Things we deliberately left out

A short, honest list of features that were considered and set aside.

Cloud sync or any kind of account. The app makes zero network calls. There is no Firebase SDK, no analytics library, no crash reporter phoning home. One permission in the manifest: TRANSMIT_IR. Your mapping lives on your device. Export/import handles the “I want this on two phones” case without any server involvement. This was a hard constraint from the start and it’s staying.

IR receiver / learning mode. We would have loved to build this — point a working remote at the phone, capture the codes, done. The problem is that Android’s ConsumerIrManager API is transmit-only. There is no API to read incoming IR signals from user-space. Physical hardware limitation, not a design choice.

Widget support. A home screen widget putting Power, Vol+, and Vol- on your launcher would be genuinely useful. The architecture supports it — IrViewModel can outlive the activity, MappingStore is accessible from any context. It’s on the list.

More TV brands and models. The app is named Toshiba TV IR Remote and it knows the 23PB201EJ well. The LIRC candidate system and sweep tools are general-purpose enough to work on other NEC-family TVs, but we haven’t curated codesets for other models. The assets/codesets/ folder structure makes adding more JSON codesets straightforward. That’s a future project.

Voice control. No. The TV is three metres away. You have a phone. Buttons are fine.


The tech stack, if you’re into that kind of thing

Kotlin, Jetpack Compose, Preferences DataStore, kotlinx.serialization, ConsumerIrManager. That’s essentially the whole dependency graph.

The IR logic lives in an ir/ package: Protocol (a six-value enum with carrier frequencies — NEC at 38 kHz, RC5 at 36 kHz, RCA at 56 kHz, NEC_EXT and Sharp and RAW at 38 kHz), IrCommand (a @Serializable data class holding protocol, address, command, and an optional raw microsecond pattern), IrTransmitter (a thin wrapper around ConsumerIrManager that picks the right encoder and calls transmit(), plus the NEC repeat-frame logic for hold), and five hand-written encoder objects — NecEncoder, Rc5Encoder, RcaEncoder, SharpEncoder, and the implicit RAW pass-through. No third-party IR library. Every encoder is a pure function: given an address and a command, return an IntArray of microsecond durations. Trivially unit-testable.

The data layer: RemoteFunction (26 entries, with a holdable companion set marking which buttons should repeat on hold), MappingStore (Preferences DataStore backing a single JSON blob, exposed as a Flow<Snapshot>), CodesetRepository (reads toshiba_candidates.json from assets, maps it to typed Candidate objects, provides buildMapping() to turn a candidate into a full RemoteFunction → IrCommand map).

The UI layer is Jetpack Compose with Material 3, wired to a single IrViewModel. Navigation is NavHost with four destinations: remote, sweep, editor, settings. The back stack is about as deep as it ever gets. No fragment manager, no backstack complexity. Four screens, popBackStack(), done.

Min SDK 26 (Android 8.0). Target SDK 36. Kotlin 2.0.21, AGP 8.12.3, Compose BOM 2025.02.00. No image processing, no networking, no media codecs. The APK is small enough that you’d forget it was installed if the app icon didn’t remind you.


Who this app is for

If you own a Toshiba 23PB201EJ and your remote has stopped working — whether the batteries failed, the IR LED burned out, or the remote has simply vanished into the same dimensional pocket where single socks and working pens go — this app is for you.

More broadly: if you have any TV for which the original remote is gone and a phone with a hardware IR blaster, the sweep tools may well find the right codes even for models we didn’t explicitly design for. The Broad NEC Power Sweep is general-purpose. The Fallback tab covers RC5, RCA, and Sharp. The export/import JSON means that if you do find a working code set for a TV not in our database, you can share it with anyone as a plain text file.

A few things worth knowing before you install:

  • You need a phone with a hardware IR blaster. Not all phones have one. Common phones that do include many Xiaomi, Redmi, and POCO models; older Samsung Galaxy S and Note series up through around the S6 era; Huawei flagships; some LG models. If you’re not sure, check your phone’s spec sheet for “IR blaster” or “IR emitter.”
  • The bundled codeset is specifically for the Toshiba 23PB201EJ. The sweep tools work on other TVs, but you’re in manual territory if yours isn’t the 23PB201EJ.
  • Minimum Android 8.0.

How to get it — and why you’ll need to send us an email first

Here is the honest situation, and we’d rather be upfront about it than bury it in small print somewhere at the bottom where nobody reads.

The app is built. It works. It’s running on the Toshiba 23PB201EJ it was built for, every single day, turning the TV off when someone fell asleep on the couch and on again when someone wants to watch something. We are proud of it. But getting it onto the Play Store as a publicly downloadable app has one more hurdle than we expected.

Google’s Play Store policy requires new apps to run a closed testing phase — with a minimum number of real testers who actually install and run the app — before it becomes eligible for open, public release. The number isn’t unreasonable. But this app was built for a specific TV in a specific house, not for a mass-market launch with a newsletter campaign behind it. We don’t have a marketing team, a YouTube channel to blast the announcement to, or ten thousand Twitter followers to press the opt-in button. We have a blog, a well-functioning remote app, and the honest need for testers.

So here’s where we are: the app is live on the Play Store in closed testing, and access is invite-only for now.

If you want to be a tester — and please, if you have the TV or the IR blaster and the curiosity, be a tester — the process is about as simple as it gets:

Send your Gmail address to us via crushedge.com/contact-us/.

We’ll add you to the tester group in the Play Console. You’ll get an opt-in email from Google with a link to the Play Store listing, and from that point it installs like any other Play Store app — properly signed build, automatic updates, the whole experience. No sideloading. No enabling developer options. No sketchy APK from a file host.

Your email address goes into the Play Console tester list and nowhere else. We are not building a mailing list. We are not going to follow up with newsletters about other apps or promotional emails about things you didn’t ask for. We don’t have any of that infrastructure and we genuinely don’t want it. One email from you, one tester invite from us, done.

Once we hit the tester threshold and the Play Store lets us go public, the app will be free and open to everyone — no tier, no premium unlock, no subscription, no ads. The whole thing, free, permanently. We just need to clear the hurdle first.

If you’ve made it this far in a blog post about IR protocol microsecond timings, you are almost certainly exactly the kind of tester we want. Come join us.


Closing thoughts

There is a specific satisfaction in pressing a button that you built and watching a piece of electronics respond. It doesn’t matter that the TV would have responded to the old plastic remote just as happily. The fact that it’s responding to a 9,000 µs leader burst followed by 32 carefully encoded bits that you wrote an encoder for and transmitted through an LED in your pocket — that feels different. Better, even.

There’s also something to be said for actually understanding what a remote control is. It’s not wireless in the Bluetooth or Wi-Fi sense. It’s a flashing LED, aimed at a cheap photodiode, carrying a protocol designed in the early 1980s and still faithfully implemented on virtually every television sold since then. When you’ve spent an afternoon sweeping through 256 NEC addresses and found your TV responding at 0x02 instead of the expected 0x40, you walk away with a very concrete mental model of the whole system that no amount of reading about it would have given you.

The TV works. The remote is an app. The mapping is saved to DataStore. And we didn’t have to wait five weeks for a parcel from a warehouse city we’ve never heard of.

Thanks for reading this far. If you have the TV, the right phone, or just the right amount of curiosity — send us your email. Let’s get you in.


Quick FAQ for the people who scrolled to the bottom

Is the app free? Yes. Free to install, free to use, no in-app purchases, no premium tier, no ads. Ever.

My phone doesn’t have an IR blaster. Can I still use it? No. ConsumerIrManager requires physical IR hardware. The app will install fine and explain clearly what’s missing, but it won’t be able to transmit anything.

Will it work on a Toshiba TV that isn’t the 23PB201EJ? The bundled codeset is for the CT-90326/CT-90327 specifically. But the sweep tools — especially the Broad NEC Power Sweep — are fully general-purpose. If your TV uses the NEC protocol there’s a good chance the sweep will find its address. RC5, RCA, and Sharp are covered in Stage 4.

How do I share my codes with someone else? Settings → Export mapping JSON → share the file → they Import mapping JSON. Done. Plain text, email-able, human-readable.

Why do I need to send my email just to install an app? Play Store policy requires a closed testing phase before an app can go public. We need real testers to clear that phase. Your email is used only to add you to the Play Console tester list — nothing else.

How do I send my email? Head to crushedge.com/contact-us/. The contact details are right there.

Which phones have IR blasters? Many Xiaomi, POCO, and Redmi phones; older Samsung Galaxy S and Note series; some Huawei flagships; some LG models. Check your phone’s spec sheet. The app itself will tell you on launch: “IR emitter: tersedia” means you’re good. “TIDAK tersedia” means unfortunately not.

What’s the minimum Android version? Android 8.0 (API 26).

Is it open source? Not currently. Possibly in the future. Watch crushedge.com for updates.


Written by CrushEdge — building small, opinionated apps for specific problems. More at crushedge.com.