by Professor Petabyte
Using IR control with an RPi Pico typically involves receiving signals from an IR remote using an IR receiver module (like the VS1838B or TSOP38238) and decoding them using MicroPython or C/C++.
Perhaps not immediately obvious, timing is absolutely critical in almost every form of communication, ever since the introduction of Morse code, first used in the 1840s as part of the telegraph system developed by Samuel Morse and his colleagues. It was used to transmit messages over wires by encoding letters and numbers as combinations of dots and dashes. The first message, "What hath God wrought?", was sent in 1844, marking the birth of all electronic communication.
This is significant only because it was the first form of electronic communication that relied on timing; Long beeps (dashes) and short beeps (dots), separated by short pauses to form characters. Probably the most widely known Morse Code message is SOS, notably used during the Titanic sinking in 1912, which was of course a radio transmission (not over wires).
. . . _ _ _ . . . S O S
Three dots = 'S'; Three dashes = 'O'; Three dots = 'S'.
Here is the complete Morse alphabet. There are infact three variants of it:
As you can see, all three have much in common, although some characters like 'F', 'J', 'K', 'X', 'Y' and 'Z' are quite different.
What is important though is timing. In transmitted morse, the length of the short marks (aka dots or dits) and long mark (aka dashes or dahs) must be regular and consistent to be differentiated, as must the gaps between letters (comprised of multiple short and long marks), and the gaps between words. Experienced Morse code users would have 'rhythm' when transmitting, and it is notable that experienced Morse code users (receiving) have been known to recognise and differentiate different users (transmitters), simply by their personal rhythm. In general however timings would be something like:-
The concept of dots and dashes to represent letters can also used with light rather than audible tones transmitted as radio waves. Obviously a radio transmission could be detected by a war-time enemy, and worse still the location of the transmission given away by using RDF (Radio Direction Finding) equipment. Using two sets of RDF equipment, triangulation could be used to reveal the location of the transmission.
Morse lamps, also known as signal lamps or Aldis lamps, are visual signaling devices still used by various navies to transmit covert messages over short distances using Morse code. Such lamps can flash light in patterns representing the dots and dashes, enabling communication over distances up to 8 miles (approximately 13 kilometers) in maritime and aviation contexts. The beam of light is normally quite narrow, typically only 2.6 degrees from an Aldis lamp, so the enemy has to be right in line with the beam to 'overhear' it. This is useful particularly when radio silence is necessary or radio communication is unavailable.
Special forces such as the British SAS and US Marine Corp use hand-held torches (aka flashlights) to send messages in a similar way over short distances.
Morse code proves that carefully timed signals can convey text messages, and in fact send enough text and you can transmit a large, high resolution full colour image, but that's something of a leap.
IR devices do not use Morse Code (although that is theoretically possible). Some very much more sophisticated protocols have been developed to allow close-proximity transmission of data using InfraRed light, such as the NEC protocol, which is commonly found (in numerous variations) in lots of domestic devices such as TVs, TV recorders (e.g. VHS, BetaMax, DVD), CD Audio systems, some high-end cameras, etc.
All that is needed is an LED capable of generating InfraRed light, an InfraRed light detector like the VS1838B shown below, and a microprocessor of some kind to control the transmission and receipt of data. IR works far faster and demands considerably more precision than a human could deliver manually opening and closing a switch - precision measured in billionths (1/1,000,000,000) of a second.
The ~address and ~command are compliments to the address and command used for error checking. IR signals are vulnerable to external interference, so it makes sense to robustly check the data received. e.g. Without such thorough checking an attempt to change the volume setting on a CD player could be misinterpreted as a singal to change channel on a TV.
IR Receiver Pin | Connect to Pico | Notes |
---|---|---|
OUT | GPIO (e.g., GP16) | Data signal to be read |
GND | GND | Ground |
VCC | 3.3V Power | NOT 5volts |
from machine import Pin import utime ir_pin = Pin(16, Pin.IN) def wait_for_signal_change(pin, level): while pin.value() == level: pass return utime.ticks_us() def read_ir_signal(): timings = [] print("Waiting for signal...") while ir_pin.value() == 1: pass # Wait for start start = utime.ticks_us() for i in range(100): t0 = wait_for_signal_change(ir_pin, 0) t1 = wait_for_signal_change(ir_pin, 1) pulse_duration = utime.ticks_diff(t1, t0) timings.append(pulse_duration) if utime.ticks_diff(t1, start) > 100000: # timeout ~100ms break print("Timings:", timings) while True: read_ir_signal() utime.sleep(2)
This example simply prints out the pulse durations. You would need to add logic to decode protocols like NEC based on timing patterns. The timing patterns are just time counts of the duration of flashes ff IR light, and the duration of the periods of 'darkness' between the flashes. The timing patterns look something like this....
If you point an IR control wand straight at the camera of most smartphones, you can often see the flashes of IR light when buttons on the control wand are pressed. They usually look like faint flickering purple light.
It's generally safe to look at the IR flashes from a control wand through a digital camera, but there are a few things to keep in mind. While the human eye can't see infrared light, digital cameras can, and they often display it as a bright white or purple light. This light is not harmful in short, infrequent bursts, but prolonged or very close exposure to bright IR sources could potentially cause eye strain or other minor issues.
from machine import Pin import utime ir_pin = Pin(16, Pin.IN) last_msg = "" def wait_for_edge(expected): t0 = utime.ticks_us() while ir_pin.value() == expected: if utime.ticks_diff(utime.ticks_us(), t0) > 10000: return -1 return utime.ticks_diff(utime.ticks_us(), t0) def read_nec(): global last_msg # Await start of signal while ir_pin.value(): pass # Start pulse (LOW 9ms) low = wait_for_edge(0) high = wait_for_edge(1) if (low < 8000 or high < 4000): # "Not NEC start" will appear if start pulse is not recognised print("Not NEC start") return None bits = [] # Extract binary bits from signal for i in range(32): low = wait_for_edge(0) high = wait_for_edge(1) if low == -1 or high == -1: print("Timeout") return None if high > 1000: bits.append(1) else: bits.append(0) print("Bits are:-",bits) # Convert bits to bytes def bits_to_byte(bits): return sum([b << i for i, b in enumerate(bits)]) addr = bits_to_byte(bits[0:8]) addr_inv = bits_to_byte(bits[8:16]) cmd = bits_to_byte(bits[16:24]) cmd_inv = bits_to_byte(bits[24:32]) # Check bytes are valid if addr ^ addr_inv != 0xFF or cmd ^ cmd_inv != 0xFF: print("Checksum failed") return None return cmd print("Waiting for IR signals...") while True: cmd = read_nec() if cmd is not None: print("Button code:", hex(cmd)) last_msg = "" utime.sleep(0.2)
Running this code you should start to see something more human readable, but perhaps a little more work is needed.
The HEX code we get back is 0x15, which is 21 in decimal (1x16 + 5x1) => 16 + 5 = 21.
Look now at the binary bits for the command:-
Note that the low bits PRECEED the high bits, and the low bytes PRECEED the high bytes.
The code above enables the hex code for each button press to be to be seen. The hex code for the IR control wand pictured here are as follows, but may vary depending on the manufacturer and model.
Button | Code | Button | Code | Button | Code |
---|---|---|---|---|---|
1 | 0x45 | 7 | 0x07 | UP | 0x18 |
2 | 0x46 | 8 | 0x15 | DOWN | 0x52 |
3 | 0x47 | 9 | 0x09 | LEFT | 0x08 |
4 | 0x44 | 0 | 0x19 | RIGHT | 0x5A |
5 | 0x40 | * | 0x16 | OK | 0x1C |
6 | 0x43 | # | 0x0d |
Given that the expected hex codes are predictable (and mostly reliable), from this point it is relatively straight forward to build a translation table into the program, and have it react appropriately to each keypress, which would look something like this:-
from machine import Pin import utime # Setup ir_sensor = Pin(16, Pin.IN) led = Pin(25,Pin.OUT) last_cmd = None led_state = False # Button map (based on typical HX1838 remotes) button_names = { 0x45: "1", 0x46: "2", 0x47: "3", 0x44: "4", 0x40: "5", 0x43: "6", 0x07: "7", 0x15: "8", 0x09: "9", 0x19: "0", 0x16: "*", 0x0D: "#", 0x1C: "OK", 0x18: "UP", 0x52: "DOWN", 0x08: "LEFT", 0x5A: "RIGHT" } def wait_for_edge(expected): t0 = utime.ticks_us() while ir_sensor.value() == expected: if utime.ticks_diff(utime.ticks_us(), t0) > 10000: return -1 return utime.ticks_diff(utime.ticks_us(), t0) def bits_to_byte(bits): return sum([b << i for i, b in enumerate(bits)]) def read_nec(): while ir_sensor.value(): pass # wait for LOW low = wait_for_edge(0) high = wait_for_edge(1) if low < 8000: return None # Handle repeat code if 2000 < high < 2800: return "REPEAT" if high < 4000: return None # Read 32 bits bits = [] for i in range(32): low = wait_for_edge(0) high = wait_for_edge(1) if low == -1 or high == -1: return None if high > 1000: bits.append(1) else: bits.append(0) addr = bits_to_byte(bits[0:8]) addr_inv = bits_to_byte(bits[8:16]) cmd = bits_to_byte(bits[16:24]) cmd_inv = bits_to_byte(bits[24:32]) if addr ^ addr_inv != 0xFF or cmd ^ cmd_inv != 0xFF: return None return cmd # Main loop print("Ready for IR input...") while True: cmd = read_nec() if cmd == "REPEAT": if last_cmd: cmd = last_cmd else: continue elif cmd is not None: last_cmd = cmd else: continue button = button_names.get(cmd, hex(cmd)) print("Pressed:", button) # Example action: Toggle LED on button "1" if cmd == 0x45: # "1" led_state = not led_state led.value(led_state) print(led_state) print("LED is now", "ON" if led_state else "OFF") utime.sleep(0.1)
N.B. See the 'Button map (based on typical HX1838 remotes)' near the top of this code. Different control wands from different manufacturers may transmit different hex codes than the ones shown in the table above. Using the program that reveals the hex codes per button, the button mappings section can easily be modified for a control wand other than the one used in this demonstration.
Given code that can detect and interpret IR Wand button presses, it is relatively tivial to build that into a large program that, perhaps via Relays, controls devices from a RPi Pico, e.g. Heaters, fans, pumps, motors, servos, etc.
IR signals can very easily be used to to control a huge variety of devices, and the convenience of doing so has made them hugely popular. They are very energy efficient, with most IR control wands capable of providing many months of normal service from a pair of AA batteries, and in some cases use a single button cell such as a CR2032 batteries. It increasingly common to find control wands that can control many different devices because they either use a common protocol like NEC, or can be programmed to emulate a different control wand. While infrared (IR) controls can be used outdoors, their effectiveness is limited by several factors, primarily line-of-sight and range. IR remotes require a clear line of sight to the receiver and have a limited range, typically around 10 meters. Obstacles like walls or even some types of glass can block the signal. However, for some applications, such as controlling outdoor infrared heaters or using IR repeaters, they can be a viable option.