├── .gitignore ├── pcb └── Schematic.png ├── src ├── wifi.h ├── mqtt.h ├── credentials.h ├── main.cpp ├── ir.h ├── wifi.cpp ├── mqtt.cpp ├── ir_recv.cpp └── ir_send.cpp ├── platformio.ini ├── lib └── readme.txt ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .pioenvs 2 | .piolibdeps 3 | .vscode 4 | -------------------------------------------------------------------------------- /pcb/Schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotplot/BlastIR/HEAD/pcb/Schematic.png -------------------------------------------------------------------------------- /src/wifi.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef WIFI_h 3 | #define WIFI_h 4 | 5 | namespace wifi 6 | { 7 | extern bool connected; 8 | 9 | void setup(); 10 | } 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /src/mqtt.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef MQTT_h 3 | #define MQTT_h 4 | 5 | namespace mqtt 6 | { 7 | extern bool connected; 8 | 9 | void setup(); 10 | void connect(); 11 | 12 | void publishReceivedIRData(const char *message); 13 | void publishLog(const char *message); 14 | } 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /src/credentials.h: -------------------------------------------------------------------------------- 1 | 2 | #define HOSTNAME "" 3 | #define WIFI_SSID "" 4 | #define WIFI_PASS "" 5 | 6 | #define MQTT_PORT 1883 7 | #define MQTT_HOST "" 8 | #define MQTT_USER "" 9 | #define MQTT_PASS "" 10 | 11 | #define MQTT_IR_SEND_TOPIC "ir//send" 12 | #define MQTT_IR_RECV_TOPIC "ir//recv" 13 | #define MQTT_IR_LOG_TOPIC "ir//log" 14 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; http://docs.platformio.org/page/projectconf.html 10 | 11 | [env:d1_mini] 12 | platform = espressif8266 13 | board = d1_mini 14 | framework = arduino 15 | lib_deps = AsyncMqttClient@0.8.2 16 | 17 | upload_speed = 115200 18 | monitor_speed = 115200 19 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "ir.h" 4 | #include "mqtt.h" 5 | #include "wifi.h" 6 | 7 | void setup() 8 | { 9 | Serial.begin(115200); 10 | Serial.println(); 11 | 12 | // Setup IR receiver and transmitter 13 | ir::setupSend(); 14 | ir::setupReceive(); 15 | 16 | // Setup MQTT callbacks etc 17 | mqtt::setup(); 18 | 19 | // Establish a WiFi connection 20 | // A WiFi connection callback is responsible for establishing a MQTT connection once we have an IP 21 | wifi::setup(); 22 | } 23 | 24 | void loop() 25 | { 26 | delay(500); 27 | 28 | // If the IR recorder has finished receiving a command, process it 29 | if (ir::hasRecordedData && ir::isRecording == false) { 30 | ir::processRecordedData(); 31 | } 32 | 33 | // If the IR recorder is stopped, restart it 34 | if (ir::isRecording == false) { 35 | ir::startRecording(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organized `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | More information about PlatformIO Library Dependency Finder 36 | - http://docs.platformio.org/page/librarymanager/ldf.html 37 | -------------------------------------------------------------------------------- /src/ir.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifndef IR_h 4 | #define IR_h 5 | 6 | namespace ir 7 | { 8 | // Initialisation functions to be called at startup 9 | void setupSend(); 10 | void setupReceive(); 11 | 12 | // True if the IR command recorder is ready to receive a message, 13 | // False if a complete message has been received and is waiting to be processed. 14 | extern bool isRecording; 15 | 16 | // True if a partial or complete IR recording has been received. 17 | extern bool hasRecordedData; 18 | 19 | // True if an IR command was received but was too large to store. 20 | extern bool recordingOverflowed; 21 | 22 | // Functions for controlling the IR command recorder. 23 | void startRecording(); 24 | void stopRecording(); 25 | void processRecordedData(); 26 | 27 | // Functions for controlling the IR command transmitter. 28 | void clearCommandBuffer(); 29 | void parseDurations(const char *message, size_t len, bool messageComplete); 30 | void transmit(); 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /src/wifi.cpp: -------------------------------------------------------------------------------- 1 | #include "wifi.h" 2 | #include "mqtt.h" 3 | #include "credentials.h" 4 | 5 | #include 6 | 7 | namespace 8 | { 9 | WiFiEventHandler connectHandler, disconnectHandler; 10 | 11 | void gotIPHandler(const WiFiEventStationModeGotIP &event) 12 | { 13 | wifi::connected = true; 14 | Serial.print("WiFi connected with IP "); 15 | Serial.println(event.ip); 16 | mqtt::connect(); 17 | } 18 | 19 | void disconnectedHandler(const WiFiEventStationModeDisconnected &event) 20 | { 21 | wifi::connected = false; 22 | Serial.println("WiFi disconnected"); 23 | } 24 | } 25 | 26 | namespace wifi 27 | { 28 | bool connected = false; 29 | 30 | void setup() 31 | { 32 | WiFi.persistent(false); 33 | 34 | connectHandler = WiFi.onStationModeGotIP(&gotIPHandler); 35 | disconnectHandler = WiFi.onStationModeDisconnected(&disconnectedHandler); 36 | 37 | WiFi.mode(WIFI_STA); 38 | WiFi.hostname(HOSTNAME); 39 | WiFi.begin(WIFI_SSID, WIFI_PASS); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sam Birch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/mqtt.cpp: -------------------------------------------------------------------------------- 1 | #include "mqtt.h" 2 | #include "wifi.h" 3 | #include "ir.h" 4 | #include "credentials.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | AsyncMqttClient mqttClient; 12 | Ticker reconnectTimer; 13 | 14 | void connectHandler(bool persistentSession) 15 | { 16 | mqtt::connected = true; 17 | Serial.println("MQTT connected"); 18 | mqttClient.subscribe(MQTT_IR_SEND_TOPIC, 2); 19 | } 20 | 21 | void disconnectHandler(AsyncMqttClientDisconnectReason reason) 22 | { 23 | mqtt::connected = false; 24 | Serial.print("MQTT disconnected with reason "); 25 | Serial.println((int8_t)reason); 26 | reconnectTimer.once(2, mqtt::connect); 27 | } 28 | 29 | void messageHandler(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) 30 | { 31 | // If this is the start of a new message, clear any existing parsed durations 32 | if (index == 0) 33 | ir::clearCommandBuffer(); 34 | 35 | // Parse any durations contained in this message 36 | bool messageComplete = index + len == total; 37 | if (payload != NULL) 38 | ir::parseDurations(payload, len, messageComplete); 39 | 40 | // If this is the final message, transmit the IR command 41 | if (messageComplete) 42 | ir::transmit(); 43 | } 44 | } 45 | 46 | namespace mqtt 47 | { 48 | bool connected = false; 49 | 50 | void setup() 51 | { 52 | mqttClient.setServer(MQTT_HOST, MQTT_PORT); 53 | mqttClient.setCredentials(MQTT_USER, MQTT_PASS); 54 | 55 | mqttClient.onConnect(connectHandler); 56 | mqttClient.onDisconnect(disconnectHandler); 57 | mqttClient.onMessage(messageHandler); 58 | } 59 | 60 | void connect() 61 | { 62 | if (wifi::connected) 63 | mqttClient.connect(); 64 | } 65 | 66 | void publishReceivedIRData(const char *message) 67 | { 68 | Serial.println("Sending received data: "); 69 | Serial.println(message); 70 | 71 | if (mqtt::connected) 72 | mqttClient.publish(MQTT_IR_RECV_TOPIC, 0, false, message); 73 | } 74 | 75 | void publishLog(const char *message) 76 | { 77 | Serial.println(message); 78 | 79 | if (mqtt::connected) 80 | mqttClient.publish(MQTT_IR_LOG_TOPIC, 0, false, message); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlastIR ESP8266 IR Repeater 2 | 3 | BlastIR is a dead-simple infra-red repeater using MQTT and the ESP8266. 4 | 5 | Record transmitted IR commands by subscribing to `ir//recv` and transmit commands by publishing to `ir//send` 6 | 7 | ## Setting up a Repeater 8 | 9 | BlastIR uses [PlatformIO](http://platformio.org/platformio-ide), so make sure you have VS Code and the PlatformIO extension installed. 10 | 11 | 1. Download a copy of the BlastIR repository 12 | 2. Edit `credentials.h` to include your WiFi and MQTT details 13 | 3. Connect a WEMOS D1 Mini and use the `PlatformIO: Upload` command to program the board 14 | 4. Wire the board up according to the schematic below 15 | 16 | ## Schematic 17 | 18 | ![BlastIR Schematic](pcb/Schematic.png) 19 | 20 | ## Recording IR Commands 21 | 22 | Subscribe to the MQTT topic you specified in `credentials.h` and use your remote to transmit the desired command while pointed at the repeater. 23 | 24 | Example: 25 | 26 | > mosquitto_sub -h 172.23.1.3 -p 1883 -u -P "" -t 'ir/heatpump/recv' 27 | 28 | 29 | ir/heatpump/recv 4357 4422 476 1626 534 1652 503 ... 30 | 31 | ## Sending IR Commands 32 | 33 | To send an IR command, publish data that you recorded earlier to the MQTT topic specified in `credentials.h`. 34 | 35 | Example: 36 | 37 | > mosquitto_pub -h 172.23.1.3 -p 1883 -u -P "" -t "ir/heatpump/send" -m "4357 4422 476 1626 534 1652 503 ..." 38 | 39 | ## Home Assistant Example 40 | 41 | You can transmit IR commands using a Home Assistant automation similar to the following: 42 | 43 | ```yaml 44 | # 45 | # Transmit an IR OFF signal whenever the heatpump is supposed to turn off 46 | # 47 | - alias: Turn Heatpump Off 48 | trigger: 49 | - platform: state 50 | entity_id: input_boolean.heatpump_should_run 51 | to: 'off' 52 | action: 53 | - service: mqtt.publish 54 | data: 55 | topic: "ir/heatpump/send" 56 | payload: "4400 4400 560 1600 ..." 57 | qos: 2 58 | ``` 59 | 60 | ## Cleaning up Received Data 61 | 62 | The output timing of the TSOP4138 can be a bit variable and result in mark/space times with a lot of jitter. If required, you can adjust each duration to match a standard set of durations using the following Python script: 63 | 64 | ```python 65 | expected_durations = [ 560, 1600, 4400 ] 66 | durations = "4357 4422 476 1626 534 1652 503 ..." 67 | durations = [ int(d) for d in durations.split() ] 68 | durations = [ min(expected_durations, key = lambda usec: abs(d-usec)) for d in durations ] 69 | durations = [ str(d) for d in durations ] 70 | 71 | print(" ".join(durations)) 72 | ``` 73 | -------------------------------------------------------------------------------- /src/ir_recv.cpp: -------------------------------------------------------------------------------- 1 | #include "ir.h" 2 | #include "mqtt.h" 3 | 4 | #include 5 | #include 6 | 7 | #define IR_INPUT_PIN (0) 8 | #define MAX_RECORDED_DURATIONS (1024) 9 | 10 | namespace 11 | { 12 | uint16_t numRecordedDurations = 0; 13 | uint16_t recordedDurations[MAX_RECORDED_DURATIONS]; 14 | unsigned long lastTransitionTime = 0; 15 | 16 | Ticker recordingCompletionTimer; 17 | 18 | void ICACHE_RAM_ATTR receiverPinInterrupt() 19 | { 20 | unsigned long currentTime = micros(); 21 | unsigned long elapsedTime = currentTime - lastTransitionTime; 22 | lastTransitionTime = currentTime; 23 | 24 | // Cancel the existing completion timer 25 | recordingCompletionTimer.detach(); 26 | 27 | if (elapsedTime > 10000) 28 | { 29 | // If the duration was too high, assume this is the beginning of a new transmission and ignore the data 30 | } 31 | else if (numRecordedDurations < MAX_RECORDED_DURATIONS) 32 | { 33 | // Otherwise record the duration and schedule the completion timer for 10ms 34 | ir::hasRecordedData = true; 35 | recordedDurations[numRecordedDurations++] = elapsedTime; 36 | recordingCompletionTimer.once_ms(10, ir::stopRecording); 37 | } 38 | else 39 | { 40 | // If we have run out of buffer space, set a flag and give up 41 | ir::recordingOverflowed = true; 42 | ir::stopRecording(); 43 | } 44 | } 45 | } 46 | 47 | namespace ir 48 | { 49 | bool isRecording = false; 50 | bool hasRecordedData = false; 51 | bool recordingOverflowed = false; 52 | 53 | void setupReceive() 54 | { 55 | pinMode(IR_INPUT_PIN, INPUT); 56 | } 57 | 58 | void startRecording() 59 | { 60 | isRecording = true; 61 | hasRecordedData = recordingOverflowed = false; 62 | 63 | lastTransitionTime = 0; 64 | numRecordedDurations = 0; 65 | 66 | attachInterrupt(IR_INPUT_PIN, receiverPinInterrupt, CHANGE); 67 | } 68 | 69 | void stopRecording() 70 | { 71 | recordingCompletionTimer.detach(); 72 | detachInterrupt(IR_INPUT_PIN); 73 | ir::isRecording = false; 74 | } 75 | 76 | void processRecordedData() 77 | { 78 | String output = ""; 79 | 80 | for (int index = 0; index < numRecordedDurations; ++index) 81 | { 82 | output += recordedDurations[index]; 83 | output += " "; 84 | } 85 | 86 | mqtt::publishReceivedIRData(output.c_str()); 87 | 88 | if (recordingOverflowed) 89 | mqtt::publishLog("Received external IR command but it was too large for the receive buffer"); 90 | else 91 | mqtt::publishLog("Received external IR command"); 92 | 93 | numRecordedDurations = 0; 94 | hasRecordedData = false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ir_send.cpp: -------------------------------------------------------------------------------- 1 | #include "ir.h" 2 | #include "mqtt.h" 3 | 4 | #include 5 | 6 | extern "C" { 7 | #include 8 | #include 9 | } 10 | 11 | #define TIMER_INTERVAL_US (62) 12 | #define IR_OUTPUT_PIN (14) 13 | 14 | #define MAX_DURATIONS (1024) 15 | 16 | namespace 17 | { 18 | uint16_t currentDuration = 0; 19 | uint16_t numParsedDurations = 0; 20 | uint16_t parsedDurations[MAX_DURATIONS]; 21 | 22 | void startCarrier() 23 | { 24 | WRITE_PERI_REG(PERIPHS_IO_MUX_MTMS_U, 25 | READ_PERI_REG(PERIPHS_IO_MUX_MTMS_U) 26 | & 0xfffffe0f 27 | | (0x1 << 4) 28 | ); 29 | 30 | WRITE_PERI_REG(I2SCONF, 31 | READ_PERI_REG(I2SCONF) 32 | & 0xf0000fff | ( // Clear I2SRXFIFO, BCK_DIV and CLKM_DIV sections 33 | ((TIMER_INTERVAL_US & I2S_BCK_DIV_NUM) << I2S_BCK_DIV_NUM_S) // Set the clock frequency divider 34 | | ((2 & I2S_CLKM_DIV_NUM) << I2S_CLKM_DIV_NUM_S) // Set the clock prescaler 35 | | ((1 & I2S_BITS_MOD) << I2S_BITS_MOD_S) // ? 36 | ) 37 | ); 38 | 39 | WRITE_PERI_REG(I2SCONF, 40 | READ_PERI_REG(I2SCONF) | I2S_I2S_RX_START // Set the I2S_I2S_RX_START bit 41 | ); 42 | } 43 | 44 | void stopCarrier() 45 | { 46 | WRITE_PERI_REG(I2SCONF, 47 | READ_PERI_REG(I2SCONF) & 0xfffffdff // Clear I2S_I2S_RX_START bit 48 | ); 49 | 50 | PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTMS_U, FUNC_GPIO14); // Set the MTMS pin function to standard GPIO 51 | GPIO_OUTPUT_SET(GPIO_ID_PIN(IR_OUTPUT_PIN), 0); // Clear the output 52 | } 53 | } 54 | 55 | namespace ir 56 | { 57 | void setupSend() 58 | { 59 | stopCarrier(); 60 | clearCommandBuffer(); 61 | } 62 | 63 | void clearCommandBuffer() 64 | { 65 | currentDuration = 0; 66 | numParsedDurations = 0; 67 | } 68 | 69 | void parseDurations(const char *message, size_t len, bool messageComplete) 70 | { 71 | // Parse the supplied message and extract the carrier on/off durations. 72 | // The message should be formatted as a sequence of space-separated integers. 73 | // The first represents the ON time, the second the OFF time, and so on repeatedly. 74 | uint16_t index = 0; 75 | while (index < len && numParsedDurations < MAX_DURATIONS) 76 | { 77 | // Iterate over the next sequence of digits and update currentDuration 78 | while (index < len && isdigit(message[index])) 79 | { 80 | currentDuration = currentDuration * 10 + (message[index] - 48); 81 | ++index; 82 | } 83 | 84 | // If we found a space, or EOF in the final part of the message, then store the current duration. 85 | // Otherwise we might have a partially-parsed duration in a multipart message and can't store it yet. 86 | if (isspace(message[index]) || (index == len && messageComplete)) 87 | { 88 | parsedDurations[numParsedDurations++] = currentDuration; 89 | currentDuration = 0; 90 | } 91 | 92 | // Skip the next chars until we find another digit or EOF (theoretically after exactly one space) 93 | while (index < len && isdigit(message[index]) == false) 94 | ++index; 95 | } 96 | } 97 | 98 | void transmit() 99 | { 100 | stopRecording(); 101 | 102 | for (int index = 0; index < numParsedDurations; index += 2) 103 | { 104 | startCarrier(); 105 | delayMicroseconds(parsedDurations[index]); 106 | 107 | stopCarrier(); 108 | if (index < numParsedDurations) 109 | delayMicroseconds(parsedDurations[index + 1]); 110 | } 111 | 112 | clearCommandBuffer(); 113 | startRecording(); 114 | 115 | mqtt::publishLog("Transmitted IR command"); 116 | } 117 | } 118 | --------------------------------------------------------------------------------