├── .gitattributes
├── LICENSE
├── README.md
├── _config.yml
├── docs
├── MHI2MQTT_scheme.pdf
└── images
│ ├── Arduino-IDE_ESP-01-settings.jpg
│ ├── Contraption.jpg
│ ├── Installed.jpg
│ ├── MHI2MQTT_scheme.jpg
│ ├── MHI2MQTT_service-commands.jpg
│ └── MHI2MQTT_topics&values.jpg
└── src
├── MHI-ESP2MQTT.ino
└── MHI-SPI2ESP.ino
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Rob J Dekker
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MHI2MQTT
2 | Arduino-based communication interface for Mitsubishi Heavy Industries (MHI) SRK/SRF series air conditioners. It will likely work for more models but I am not able to test this.
3 | Connects to the MHI CNS connector and synchronizes to its Serial Peripheral Interface (SPI). Updates from the MHI are sent via serial to an ESP8266 running an MQTT client.
4 | Updates received on the ESP8266 via MQTT are sent to the Arduino over serial and injected into the SPI data frames to update the MHI.
5 |
6 | ## Getting started
7 | The provided code is not a library, but is rather intended as a ready-to-use firmware solution for wirelessly controlling the aircon via MQTT using an Arduino Pro Mini/ESP8266 combination.
8 |
9 | ### Circuit and connector
10 | 
11 |
12 | I would like to advise you to not solder a fixed connection between the Arduino and ESP-01. Using male and female pin headers for the connection (see figure above) will enable you to easily upload updates the Arduino at a later stage.
13 |
14 | The CNS socket on the MHI indoor unit's PCB accepts a JST-XH 5-pin female connector. It can be bought pre-wired as a 4S LiPo balance cable. In addition, I use a prefab male-to-female version of this cable as an extension cord.
15 |
16 | JST-XH pin layout (looking at the male socket on the PCB with the locking protrusions/slots downwards):
17 | Pin 1 (left) = 12V, pin 2 = SPI clock, pin 3 = SPI MOSI, pin 4 = SPI MISO, pin 5 (right) = GND.
18 |
19 | Check before connecting with a multitester. GND versus clock should be +5V. If the voltage over outer pins is -12V then connector orientation is wrong.
20 | Pins 2 - 5 are 5V and directly compatible with an Arduino Pro Mini 5V/16 MHz version. Note that the 3.3V/8MHz version is NOT directly compatible and logic level conversion is necessary. The 8 MHz is possibly to slow to keep up with SPI communication and data processing, although I didn't test this.
21 |
22 | The Arduino Pro Mini is 12V tolerant according to its specs, but using the 12V (pin 1) of the MHI unit did not work in my setup. I use a Pololu D24V5F5 step-down voltage regulator to power both the Pro Mini and the ESP8266.
23 |
24 | Oh, and while you're at it, take out the two LEDs on the Arduino. They are ridiculously bright. Really, the light will pass through the plastic casing and will light up your aircon like it's Christmas all year. It's better to use a soldering iron to do this (find good instructions on YouTube). Do not start cutting the LEDs traces as you will likely damage traces underneath.
25 |
26 | After soldering and uploading the firmware, I wrapped the entire contraption in shrink wrap to fix all the (re)movable parts and protect against causing possible short-circuits while installed in the aircon.
27 |
28 | 
29 |
30 | ### Parts
31 | * [Arduino Pro Mini 5V/16MHz](https://robotdyn.com/promini-atmega328p.html)
32 | * [ESP-01 ESP8266 WiFi Module](https://robotdyn.com/wifi-module-esp-01-esp8266-8mbit.html)
33 | * [ESP-01 Adapter](https://www.aliexpress.com/item/ESP8266-Serial-WiFi-Wireless-ESP-01-Adapter-Module-3-3V-5V-Compatible-For-Arduino/32740695540.html)
34 | * [Pololu 5V, 500mA Step-Down Voltage Regulator D24V5F5](https://www.pololu.com/product/2843)
35 | * [4S LiPo Battery Balance Charger Plug JST-XH](http://www.dx.com/p/rc-4s-lipo-battery-balance-plug-charger-cable-black-red-10cm-433660#.WyU5evZuIuU)
36 |
37 | ## Installation
38 | ### Dependencies
39 | The following additional libraries are necessary and should be added to the Arduino IDE before compiling and flashing the sketches:
40 | * [WiFiManager](https://github.com/tzapu/WiFiManager) - For managing the WiFi connection and changing MQTT settings
41 | * [PubSubClient](https://github.com/knolleary/pubsubclient) - MQTT client
42 | * [EasyTransfer](https://github.com/madsci1016/Arduino-EasyTransfer) - Takes care of serial communication between the Pro Mini and ESP-01
43 | * [ArduinoJson](https://github.com/bblanchon/ArduinoJson) - JSON library for reading and storing WiFiManager settings in flash memory
44 |
45 | ### Installing the sketches
46 | Two sketches are provided:
47 | * Arduino Pro Mini
48 | Upload MHI-SPI2ESP.ino using the Arduino IDE and an [FTDI USB-serial adapter](http://www.dx.com/nl/p/funduino-ftdi-basic-program-downloader-usb-to-ttl-et232-module-397477?tc=EUR&ta=NL&gclid=EAIaIQobChMI6cO61NDY2wIVRzbTCh0cSQHwEAQYCyABEgJ2WfD_BwE#.WyU-ePZuIuU).
49 | In the Arduino IDE, select Arduino Pro or Pro mini under Tools > Board and select ATmega328P (5V, 16 MHz) under Tools > Processor.
50 |
51 | * ESP-01 ESP8266 WiFi module
52 | Optional: Change the name (SSID) and password of the access point that is started by the ESP-01 for configuration on first boot (lines 21-22 of MHI-ESP2MQTT.ino). This is not the name of your home WiFi network (SSID)! I use the name of the room the air conditioner is in.
53 | Upload MHI-ESP2MQTT.ino using the Arduino IDE and an [ESP-01 ESP8266 USB-UART Adapter](https://www.aliexpress.com/store/product/ESP01-Programmer-Adapter-UART-GPIO0-ESP-01-Adaptaterr-ESP8266-USB-to-ESP8266-Serial-Wireless-Wifi/2221053_32704996344.html).
54 | In the Arduino IDE, select Generic ESP8266 Module under Tools > Board.
55 | I have used the following settings (running at 160 MHz is probably not necessary):
56 |
57 | 
58 |
59 | Once the sketch is flashed for the first time, future updates to the ESP-01 can also be uploaded OTA. Name and password are equal to those set for the configuration access point (Default: MHI Roomname with password mitsubishi). The Arduino Pro Mini cannot be updated OTA.
60 |
61 | ### Connecting and configuring the system
62 | * Disconnect the mains
63 | * Connect the circuit to the air conditioner's CNS connector*
64 | * Reconnect the mains
65 | * Using a tablet or smart phone, connect to the ESP-01's SSID (default: MHI Roomname, or as configured in line 21 of MHI-ESP2MQTT.ino)
66 | * Default password is mitsubishi, or as configured in line 22 of MHI-ESP2MQTT.ino
67 | * The configuration portal should display automatically within a few seconds. If not, open a browser and enter 192.168.4.1.
68 | * Select Configure WiFi
69 | * Select the home network on which the MQTT broker is running
70 | * Enter the WiFi password
71 | * Enter the IP address and port number of the MQTT broker
72 | * Enter the MQTT username and password (leave empty if not used)
73 | * Optional: Change the WiFi Timeout (max. 99 minutes; default: 5 minutes). Don't set it too high or it will take a long time before it will reconnect after the network has been down.
74 | * Set the name of the room that the aircon is in (all available MQTT topics will have the prefix Roomname/Aircon/...). You can add deeper topic levels in the Roomname field (e.g. Bedroom/John).
75 | * Optional: Change the name of the unit (default: Aircon)
76 | * Optional: Change topic names for setpoint, state, vanes, fan speed, debug and service
77 | * Topic names that start with status by default, will be updated with the current aircon settings every ~6 seconds, or directly after a new setting is acknowledged by the aircon
78 | * Select Save
79 |
80 | The system will connect to the selected WiFi network and the MQTT broker. You can quickly check if everything works by using the command line to temporarily subscribe to the relevant topics: Roomname/Aircon/#. If all is well, a successful connection will be notified on the debug topic. Within ~10 seconds after connection, the aircon's current settings will be sent to the status topics. From now on, sending payloads to the topics (see table below under Wireless operation using MQTT) should cause the aircon to respond within max. 2 seconds. All successful commands will be acknowledged by the aircon on the respective status topic.
81 |
82 | * Google for a service manual of your aircon model for instructions on how to get access to the CNS connector. The image below is of an SRK50ZS-S where I extended the CNS connector from the more difficult-to-reach side panel to the front mains connection box for easy access. Please switch off the mains before doing this and take care that the module is properly isolated (e.g. shrink wrap) and fixed with a cable tie so it can't touch the high-voltage terminals.
83 |
84 | 
85 |
86 |
87 | ## Wireless operation using MQTT
88 | The table below shows the topics and respective value range that can be used to operate the aircon:
89 |
90 | 
91 |
92 | The default topic statusRoomtemp will be updated with the ambient temperature (in degrees Celsius) every ~6 seconds.
93 | Various service commands can be send to the service topic and the system will respond as follows:
94 |
95 | 
96 |
97 | ### Behavior when the WiFi network or MQTT broker is down
98 | * When the MQTT broker becomes inaccessible, three reconnection attempts will be made by the ESP-01 with ~5 seconds in between. The system will restart if this fails and will first try to reconnect to the access point and then to the MQTT server. This behavior was chosen because if the network client running the MQTT broker disconnects from the access point, the ESP-01 will still be unable to connect to the broker after the client has reconnected. A complete restart of the ESP-01 and reconnection to the access point solves this.
99 | * When the WiFi connection between the ESP-01 and the access point is lost, the system will restart after a while and WiFiManager will try to connect to the previously configured access point. If this still fails, WiFiManager will start in access point mode (SSID default: MHI Roomname) awaiting reconfiguration by connecting to it. After the timeout previously set in the configuration portal has passed (default: 5 minutes), the ESP-01 will restart again and try to reconnect to the previously configured access point. These events will loop endlessly, giving some time to change the configuration when the router SSID, password or MQTT broker host has changed. The ESP-01 should always reconnect after a general power outage, but this might take 5 minutes or more.
100 |
101 | ## Notes
102 | ### Details about the communication protocol
103 | * Low-level SPI protocol exchanging frames of 20 bytes/bit fields using LSB first, 8 bits/transfer, clock is high when inactive (CPOL=1), data is valid on clock trailing edge (CPHA=1). Timing details of 20-byte SPI frames: 1 byte ~0.5 msec; 20-byte SPIframe ~10 msec; pause between two frames ~30 msec. Time between the start of 2 consecutive frames is ~40 msec (~25 Hz).
104 | * High-level SPI protocol where master (MHI) and slave (Arduino) exchange a repetitive pattern of special bit settings in bit fields 10, 13-16 and 18.
105 | In contrast to the low-level SPI protocol, the SPI slave now functions as a 'master' and generates what appears to be its own low frequency clock (~0.5 Hz) by toggling bit 3 of bit field 18 every 24 SPI frames.
106 | All bidirectional changes in values are synchronous to this 'clock', with few exceptions such as the room temperature in bit field 7.
107 | To successfully connect, a sequence of 3 repeating SPI frame variants should be send to the MHI. If correct, the MHI will respond by sending its own frame variations to acknowledge a valid SPI connection (?). Possibly, the frame variations send back by the MHI identify the unit type and can be used by the controller to make unit-specific functions available.
108 | Each frame variation is send 24 times with bit 3 of bit field 18 set to 1, and subsequently 24 times with bit 3 cleared. The total duration of a complete frame cycle (3 frame variations x 48 times each) is ~6 sec, then the sequence is started all over. All changes in settings should be send synchronously to the byte 18 'clock', starting at the beginning of the first SPI frame that has bit 3 of bit field 18 set to 1, and lasting the full duration of a clock cycle (= 48 SPI frames). In order to change a setting on the MHI, the correct bit fields should be set/cleared AND a function-specific 'write' bit should be set to 1 for a full 48-frame cycle in order to have the MHI accept and apply the new setting. For all subsequent frames, only the 'write' bit should be set back to 0, while the rest of the newly set bits should be unaltered. If the change is accepted, the MHI will start sending back the newly accepted settings starting from the next full 48-frame cycle.
109 |
110 | ### Notes on handling SPI communication in the code
111 | The Arduino Mini Pro had problems staying in sync with the aircon's SPI data during the early test phase. To solve this, a state machine type of approach was chosen to prevent the SPI interrupt routine from being obstructed (by e.g. UART serial communication with the ESP-01) until a full 20-byte is received (state 0). Then, two states are possible: one to update frames, calculate checksums and send values obtained from the aircon to the ESP-01 (state 1); and one where the serial connection is checked for data arriving from the ESP-01 (state 2). When debugging is enabled using the service command debugon, the debug topic will provide a cumulative count of the number of SPI synchronisation errors (until a reboot/reset). I never ran into sync issues anymore since I started using the state machine approach, so this debugging feature is obsolete unless you want to test the sketch on other Arduino boards.
112 |
113 | ## Acknowledgments
114 | This work would not have been possible in the current form without the ingenious work of others:
115 | WiFiManager by Tzapu, PubSubClient by Nick O'Leary, EasyTransfer by Bill Porter and ArduinoJson by Benoît Blanchon. In addition to using these libraries, parts of the code were obtained from the example sketches.
116 |
117 | ## License
118 | This project is licensed under the terms of the MIT license.
119 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
2 | show_downloads: true
3 |
--------------------------------------------------------------------------------
/docs/MHI2MQTT_scheme.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rjdekker/MHI2MQTT/e737f6e9f183fa3870e36682ab984b724c183614/docs/MHI2MQTT_scheme.pdf
--------------------------------------------------------------------------------
/docs/images/Arduino-IDE_ESP-01-settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rjdekker/MHI2MQTT/e737f6e9f183fa3870e36682ab984b724c183614/docs/images/Arduino-IDE_ESP-01-settings.jpg
--------------------------------------------------------------------------------
/docs/images/Contraption.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rjdekker/MHI2MQTT/e737f6e9f183fa3870e36682ab984b724c183614/docs/images/Contraption.jpg
--------------------------------------------------------------------------------
/docs/images/Installed.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rjdekker/MHI2MQTT/e737f6e9f183fa3870e36682ab984b724c183614/docs/images/Installed.jpg
--------------------------------------------------------------------------------
/docs/images/MHI2MQTT_scheme.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rjdekker/MHI2MQTT/e737f6e9f183fa3870e36682ab984b724c183614/docs/images/MHI2MQTT_scheme.jpg
--------------------------------------------------------------------------------
/docs/images/MHI2MQTT_service-commands.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rjdekker/MHI2MQTT/e737f6e9f183fa3870e36682ab984b724c183614/docs/images/MHI2MQTT_service-commands.jpg
--------------------------------------------------------------------------------
/docs/images/MHI2MQTT_topics&values.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rjdekker/MHI2MQTT/e737f6e9f183fa3870e36682ab984b724c183614/docs/images/MHI2MQTT_topics&values.jpg
--------------------------------------------------------------------------------
/src/MHI-ESP2MQTT.ino:
--------------------------------------------------------------------------------
1 | /*
2 | ##############################################################################################################################################################################################
3 | MHI ESP2MQTT Interface v1.0.0
4 | Arduino-based communication interface for Mitsubishi Heavy Industries (MHI) SRK/SRF series air conditioners.
5 | Connects to the MHI CNS connector and synchronizes to its Serial Peripheral Interface (SPI). Updates from the MHI are processed and sent via serial to an ESP8266 running an MQTT client.
6 | Updates received via MQTT are sent from the ESP8266 to the Arduino over serial and injected into the SPI data frames to update the MHI.
7 | R.J. Dekker, June 2018
8 | ##############################################################################################################################################################################################
9 | */
10 |
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include //https://github.com/tzapu/WiFiManager
18 | #include //https://github.com/bblanchon/ArduinoJson
19 | #include //https://github.com/knolleary/pubsubclient
20 | #include //https://github.com/madsci1016/Arduino-EasyTransfer
21 |
22 | //Access point that WiFiManager starts for configuration. Name and password should be set below before flashing. This is hardcoded and cannot be changed later.
23 | #define configSSID "MHI Roomname" //AP name (give every unit a unique name before flashing)
24 | #define configPW "mitsubishi" //Password to connect to the AP
25 |
26 | //Variables below are initial values that can be changed at any time from the WiFiManager configuration portal and will be stored in flash memory. If there are different values in config.json, they are overwritten.
27 | char mqtt_server[16] = "0.0.0.0";
28 | char mqtt_port[9] = "1883";
29 | char mqtt_user[20] = "";
30 | char mqtt_pass[20] = "";
31 | char wifiTimeout[3] = "5"; //Timeout in minutes (max. 99) before WiFi configuration portal is turned off and the ESP tries to connect again to the previously configured AP (if any)
32 | char Room[20] = "Roomname";
33 | char Thing[20] = "Aircon";
34 | char Setpoint[60] = "Setpoint";
35 | char statusSetpoint[60] = "statusSetpoint";
36 | char State[60] = "State";
37 | char statusState[60] = "statusState";
38 | char statusRoomtemp[60] = "statusRoomtemp";
39 | char Vanes[60] = "Vanes";
40 | char statusVanes[60] = "statusVanes";
41 | char Fanspeed[60] = "Fanspeed";
42 | char statusFanspeed[60] = "statusFanspeed";
43 | char debug[60] = "debug"; //Send only
44 | char service[60] = "service"; //Receive only
45 |
46 | //Variables below hold the current values of bit fields 4-7 and all adjustable settings to check if anything changed after receiving an update from the MHI/Arduino
47 | //Bit field variables are initialized with 255 to force an MQTT update with the most recent MHI settings directly after booting
48 | //The minimal set of bit fields needed to communicate power, mode, setpoint, roomtemp, vanes and fans speed is bit fields 4-7 and 10
49 | byte current_Bitfield4 = 255; //Power, mode and vane swing settings
50 | int current_Bitfield5 = 255; //Vanes setting 1-4 and fan speed 1-3 (4 is in bit field 10)
51 | byte current_Bitfield6 = 255; //Temperature setpoint
52 | byte current_Bitfield7 = 255; //Room temperature
53 | byte current_Mode = 255;
54 | byte current_Vanes = 255;
55 | bool current_Swing = false;
56 | byte current_Fanspeed = 255;
57 | bool current_Fanspeed4 = false;
58 |
59 | bool debugit = false; //Send some info (eg. MHI SPI bit field updates and errors) to debug topic
60 | int connectionFails = 0; //Count number of failed MQTT connection attempts for restart
61 |
62 | // HEAT COOL AUTO DRY FAN
63 | static byte modeValues[5] = { 0b00010001, 0b00001001, 0b00000001, 0b00000101, 0b00001101 }; //Used to extract current mode from bit field 4
64 |
65 | WiFiClient espClient;
66 | PubSubClient client(espClient);
67 |
68 | void callback(char* topic, byte* payload, unsigned int length); //Callback function header
69 |
70 | //Flag for saving data
71 | bool shouldSaveConfig = false;
72 |
73 | //Setup EasyTransfer (by Bill Porter)
74 | EasyTransfer ETin, ETout;
75 |
76 | struct RECEIVE_DATA_STRUCTURE { //Variables received from Arduino
77 | byte currentMHI[8]; //Contains bitfields last received from MHI for bitfields 4 - 10 (currentMHI[0]-[6]). currentMHI[7] holds the number of SPI-MHI sync errors.
78 | };
79 |
80 | struct SEND_DATA_STRUCTURE { //Variable send to Arduino
81 | byte mode; //Mode [1]OFF, [2]HEAT, [3]COOL, [4]AUTO, [5]DRY, [6]FAN, [7]ON, [64]RESET
82 | byte vanes; //Vanes [1]UP, [2]2, [3]3, [4]DOWN, [5]SWING
83 | byte fanspeed; //Fanspeed [1]1, [2]2, [3]3, [4]4
84 | byte setpoint; //Setpoint [18]18 -> [30]30 degrees Celsius
85 | } __attribute__((packed)); //Necessary for correct transfer of struct between Arduino and ESP8266
86 |
87 | RECEIVE_DATA_STRUCTURE fromArduino;
88 | SEND_DATA_STRUCTURE toArduino;
89 |
90 | //Callback notifying us of the need to save WiFiManager config to FS
91 | void saveConfigCallback ()
92 | {
93 | //Serial.println("Should save config");
94 | shouldSaveConfig = true;
95 | }
96 |
97 | template void debug2mqtt(Generic text)
98 | {
99 | if (debugit)
100 | {
101 | client.publish(debug, text, true);
102 | }
103 | }
104 |
105 | void setup()
106 | {
107 | ETin.begin(details(fromArduino), &Serial); //Start the EasyTransfer library, pass in the data details and the name of the serial port
108 | ETout.begin(details(toArduino), &Serial);
109 |
110 | //Read configuration from FS json
111 | //Serial.println("Mounting FS...");
112 |
113 | if (SPIFFS.begin())
114 | {
115 | //Serial.println("Mounted file system");
116 |
117 | if (SPIFFS.exists("/config.json"))
118 | {
119 | //File exists, reading and loading
120 | //Serial.println("Reading config file");
121 | File configFile = SPIFFS.open("/config.json", "r");
122 |
123 | if (configFile)
124 | {
125 | //Serial.println("Opened config file");
126 | size_t size = configFile.size();
127 |
128 | //Allocate a buffer to store contents of the file.
129 | std::unique_ptr buf(new char[size]);
130 |
131 | configFile.readBytes(buf.get(), size);
132 | DynamicJsonBuffer jsonBuffer;
133 | JsonObject& json = jsonBuffer.parseObject(buf.get());
134 | //json.printTo(Serial);
135 |
136 | if (json.success())
137 | {
138 | //Serial.println("\nParsed json");
139 | strcpy(mqtt_server, json["mqtt_server"]);
140 | strcpy(mqtt_port, json["mqtt_port"]);
141 | strcpy(mqtt_user, json["mqtt_user"]);
142 | strcpy(mqtt_pass, json["mqtt_pass"]);
143 | strcpy(wifiTimeout, json["wifiTimeout"]);
144 | strcpy(Room, json["Room"]);
145 | strcpy(Thing, json["Thing"]);
146 | strcpy(Setpoint, json["Setpoint"]);
147 | strcpy(statusSetpoint, json["statusSetpoint"]);
148 | strcpy(State, json["State"]);
149 | strcpy(statusState, json["statusState"]);
150 | strcpy(statusRoomtemp, json["statusRoomtemp"]);
151 | strcpy(Vanes, json["Vanes"]);
152 | strcpy(statusVanes, json["statusVanes"]);
153 | strcpy(Fanspeed, json["Fanspeed"]);
154 | strcpy(statusFanspeed, json["statusFanspeed"]);
155 | strcpy(debug, json["debug"]);
156 | strcpy(service, json["service"]);
157 | }
158 | else
159 | {
160 | //Serial.println("Failed to load json config");
161 | }
162 | }
163 | }
164 | }
165 | else
166 | {
167 | //Serial.println("Failed to mount FS");
168 | }
169 |
170 | //The extra parameters to be configured (can be either global or just in the setup)
171 | //After connecting, parameter.getValue() will get you the configured value
172 | //id/name placeholder/prompt default length
173 | WiFiManagerParameter custom_mqtt_server("server", "MQTT Server", mqtt_server, 16);
174 | WiFiManagerParameter custom_mqtt_port("port", "MQTT Port", mqtt_port, 9);
175 | WiFiManagerParameter custom_mqtt_user("user", "MQTT Username", mqtt_user, 20);
176 | WiFiManagerParameter custom_mqtt_pass("pass", "MQTT Password", mqtt_pass, 20);
177 | WiFiManagerParameter custom_wifiTimeout("timeout", "5", wifiTimeout, 3);
178 | WiFiManagerParameter custom_topic_Room("Room", "Room name", Room, 20);
179 | WiFiManagerParameter custom_topic_Thing("Thing", "Thing name", Thing, 20);
180 | WiFiManagerParameter custom_topic_Setpoint("Setpoint", "Setpoint", Setpoint, 40);
181 | WiFiManagerParameter custom_topic_statusSetpoint("statusSetpoint", "statusSetpoint", statusSetpoint, 40);
182 | WiFiManagerParameter custom_topic_State("State", "State", State, 40);
183 | WiFiManagerParameter custom_topic_statusState("statusState", "statusState", statusState, 40);
184 | WiFiManagerParameter custom_topic_statusRoomtemp("statusRoomtemp", "statusRoomtemp", statusRoomtemp, 40);
185 | WiFiManagerParameter custom_topic_Vanes("Vanes", "Vanes", Vanes, 40);
186 | WiFiManagerParameter custom_topic_statusVanes("statusVanes", "statusVanes", statusVanes, 40);
187 | WiFiManagerParameter custom_topic_Fanspeed("Fanspeed", "Fanspeed", Fanspeed, 40);
188 | WiFiManagerParameter custom_topic_statusFanspeed("statusFanspeed", "statusFanspeed", statusFanspeed, 40);
189 | WiFiManagerParameter custom_topic_debug("debug", "debug", debug, 40);
190 | WiFiManagerParameter custom_topic_service("service", "service", service, 40);
191 |
192 | //WiFiManager
193 | //Local intialization. Once its business is done, there is no need to keep it around
194 | WiFiManager wifiManager;
195 | wifiManager.setDebugOutput(false);
196 |
197 | //Set config save notify callback
198 | wifiManager.setSaveConfigCallback(saveConfigCallback);
199 |
200 | //Optional: Set static ip
201 | //wifiManager.setSTAStaticIPConfig(IPAddress(10,0,1,99), IPAddress(10,0,1,1), IPAddress(255,255,255,0));
202 |
203 | //Add all parameters here
204 | wifiManager.addParameter(&custom_mqtt_server);
205 | wifiManager.addParameter(&custom_mqtt_port);
206 | wifiManager.addParameter(&custom_mqtt_user);
207 | wifiManager.addParameter(&custom_mqtt_pass);
208 | wifiManager.addParameter(&custom_wifiTimeout);
209 | wifiManager.addParameter(&custom_topic_Room);
210 | wifiManager.addParameter(&custom_topic_Thing);
211 | wifiManager.addParameter(&custom_topic_Setpoint);
212 | wifiManager.addParameter(&custom_topic_statusSetpoint);
213 | wifiManager.addParameter(&custom_topic_State);
214 | wifiManager.addParameter(&custom_topic_statusState);
215 | wifiManager.addParameter(&custom_topic_statusRoomtemp);
216 | wifiManager.addParameter(&custom_topic_Vanes);
217 | wifiManager.addParameter(&custom_topic_statusVanes);
218 | wifiManager.addParameter(&custom_topic_Fanspeed);
219 | wifiManager.addParameter(&custom_topic_statusFanspeed);
220 | wifiManager.addParameter(&custom_topic_debug);
221 | wifiManager.addParameter(&custom_topic_service);
222 |
223 | //Set minimum quality of signal so it ignores AP's under that quality
224 | //Defaults to 8%
225 | wifiManager.setMinimumSignalQuality(5);
226 |
227 | //Sets timeout until configuration portal gets turned off
228 | //and retries connecting to the preconfigured AP
229 | wifiManager.setConfigPortalTimeout(atoi(wifiTimeout) * 60); //Convert minutes to seconds
230 |
231 | //Fetches ssid and pass and tries to connect
232 | //If it does not connect it starts an access point with the specified name
233 | //and goes into a blocking loop awaiting configuration
234 | if (!wifiManager.autoConnect(configSSID, configPW))
235 | {
236 | //Serial.println("Failed to connect and hit timeout");
237 | delay(3000);
238 | //Reset and try again, or maybe put it to deep sleep
239 | ESP.reset();
240 | delay(5000);
241 | }
242 |
243 | //If you get here you have connected to the WiFi
244 | //Serial.println("Connected...yeey :)");
245 |
246 | //Read updated parameters
247 | strcpy(mqtt_server, custom_mqtt_server.getValue());
248 | strcpy(mqtt_port, custom_mqtt_port.getValue());
249 | strcpy(mqtt_user, custom_mqtt_user.getValue());
250 | strcpy(mqtt_pass, custom_mqtt_pass.getValue());
251 | strcpy(wifiTimeout, custom_wifiTimeout.getValue());
252 | strcpy(Room, custom_topic_Room.getValue());
253 | strcpy(Thing, custom_topic_Thing.getValue());
254 |
255 | //Construct topic names
256 | //Topic prefix = "Room/Thing/"
257 | char topic_prefix[42] = "";
258 | strncpy(topic_prefix, Room, 20);
259 | strcat (topic_prefix, "/");
260 | strncat(topic_prefix, Thing, 20);
261 | strcat (topic_prefix, "/");
262 |
263 | //Start all topics with topic prefix
264 | strcpy(Setpoint, topic_prefix);
265 | strcpy(statusSetpoint, topic_prefix);
266 | strcpy(State, topic_prefix);
267 | strcpy(statusState, topic_prefix);
268 | strcpy(statusRoomtemp, topic_prefix);
269 | strcpy(Vanes, topic_prefix);
270 | strcpy(statusVanes, topic_prefix);
271 | strcpy(Fanspeed, topic_prefix);
272 | strcpy(statusFanspeed, topic_prefix);
273 | strcpy(debug, topic_prefix);
274 | strcpy(service, topic_prefix);
275 |
276 | //Append final topic level
277 | size_t maxAppend = 60 - sizeof(topic_prefix);
278 | strncat(Setpoint, custom_topic_Setpoint.getValue(), maxAppend);
279 | strncat(statusSetpoint, custom_topic_statusSetpoint.getValue(), maxAppend);
280 | strncat(State, custom_topic_State.getValue(), maxAppend);
281 | strncat(statusState, custom_topic_statusState.getValue(), maxAppend);
282 | strncat(statusRoomtemp, custom_topic_statusRoomtemp.getValue(), maxAppend);
283 | strncat(Vanes, custom_topic_Vanes.getValue(), maxAppend);
284 | strncat(statusVanes, custom_topic_statusVanes.getValue(), maxAppend);
285 | strncat(Fanspeed, custom_topic_Fanspeed.getValue(), maxAppend);
286 | strncat(statusFanspeed, custom_topic_statusFanspeed.getValue(), maxAppend);
287 | strncat(debug, custom_topic_debug.getValue(), maxAppend);
288 | strncat(service, custom_topic_service.getValue(), maxAppend);
289 |
290 | //Debug resulting topics to serial
291 | /* Serial.println("Constructed topics:");
292 | Serial.println(Setpoint);
293 | Serial.println(statusSetpoint);
294 | Serial.println(State);
295 | Serial.println(statusState);
296 | Serial.println(statusRoomtemp);
297 | Serial.println(Vanes);
298 | Serial.println(statusVanes);
299 | Serial.println(Fanspeed);
300 | Serial.println(statusFanspeed);
301 | Serial.println(debug);
302 | Serial.println(service);
303 | */
304 | //Save the custom parameters to FS
305 | if (shouldSaveConfig)
306 | {
307 | //Serial.println("Saving config...");
308 | DynamicJsonBuffer jsonBuffer;
309 | JsonObject& json = jsonBuffer.createObject();
310 | json["mqtt_server"] = mqtt_server;
311 | json["mqtt_port"] = mqtt_port;
312 | json["mqtt_user"] = mqtt_user;
313 | json["mqtt_pass"] = mqtt_pass;
314 | json["wifiTimeout"] = wifiTimeout;
315 | json["Room"] = Room;
316 | json["Thing"] = Thing;
317 | json["Setpoint"] = custom_topic_Setpoint.getValue();
318 | json["statusSetpoint"] = custom_topic_statusSetpoint.getValue();
319 | json["State"] = custom_topic_State.getValue();
320 | json["statusState"] = custom_topic_statusState.getValue();
321 | json["statusRoomtemp"] = custom_topic_statusRoomtemp.getValue();
322 | json["Vanes"] = custom_topic_Vanes.getValue();
323 | json["statusVanes"] = custom_topic_statusVanes.getValue();
324 | json["Fanspeed"] = custom_topic_Fanspeed.getValue();
325 | json["statusFanspeed"] = custom_topic_statusFanspeed.getValue();
326 | json["debug"] = custom_topic_debug.getValue();
327 | json["service"] = custom_topic_service.getValue();
328 |
329 | File configFile = SPIFFS.open("/config.json", "w");
330 | /* if (!configFile)
331 | {
332 | Serial.println("Failed to open config file for writing");
333 | }
334 | */
335 | //json.printTo(Serial);
336 | json.printTo(configFile);
337 | configFile.close();
338 | }
339 |
340 | //Serial.println();
341 | //Serial.print("Local IP adres: ");
342 | //Serial.println(WiFi.localIP());
343 |
344 | //Connect to MQTT broker and set callback
345 | client.setServer(mqtt_server, atoi(mqtt_port));
346 | client.setCallback(callback);
347 |
348 | //Configure Arduino OTA updater
349 | ArduinoOTA.setHostname(configSSID); //Set OTA hostname and password (same as local access point for WiFiManager)
350 | ArduinoOTA.setPassword((const char *)configPW);
351 | //ArduinoOTA.setPort(8266); //Port defaults to 8266
352 |
353 | ArduinoOTA.onError([](ota_error_t error) //Send OTA error messages to MQTT debug topic
354 | {
355 | if (error == OTA_AUTH_ERROR) client.publish(debug, " ERROR -> Auth failed", true);
356 | else if (error == OTA_BEGIN_ERROR) client.publish(debug, " ERROR -> Begin failed", true);
357 | else if (error == OTA_CONNECT_ERROR) client.publish(debug, " ERROR -> Connect failed", true);
358 | else if (error == OTA_RECEIVE_ERROR) client.publish(debug, " ERROR -> Receive failed", true);
359 | else if (error == OTA_END_ERROR) client.publish(debug, " ERROR -> End failed", true);
360 | });
361 |
362 | ArduinoOTA.begin();
363 | }
364 |
365 | void connect()
366 | {
367 | // Loop until we're (re)connected
368 | while (!client.connected())
369 | {
370 | //Serial.print("Attempting MQTT connection...");
371 |
372 | //MQTT connection: Attempt to connect to MQTT broker 3 times: SUCCESS -> continue | FAILED restart ESP
373 | //On restart it will first try to connect to the previously set AP. If that fails the config portal will be started.
374 | //If the config portal is not used within wifiTimeout (set in portal), the ESP will retry connecting to the previously set AP again.
375 | if (client.connect(configSSID, mqtt_user, mqtt_pass))
376 | {
377 | //Serial.println("connected!");
378 |
379 | //Subscribe to topics that control the MHI state
380 | client.subscribe(Setpoint, 1);
381 | client.subscribe(State, 1);
382 | client.subscribe(Vanes, 1);
383 | client.subscribe(Fanspeed, 1);
384 | client.subscribe(service, 1);
385 |
386 | char msg[62] = "MHI2MQTT connected to MQTT broker at ";
387 | strncat(msg, mqtt_server, 15);
388 | strcat(msg, ":");
389 | strncat(msg, mqtt_port, 8);
390 | client.publish(debug, msg , true); //Publish message to debug topic to test/notify MQTT connection
391 |
392 | connectionFails = 0;
393 |
394 | Serial.begin(500000);
395 | while(Serial.available()) Serial.read(); //Empty serial read buffer. Arduino keeps sending updates over serial during wifi configuration and connecting MQTT broker.
396 | }
397 | else
398 | {
399 | /* Serial.print("failed, rc = ");
400 | Serial.println(client.state());
401 | Serial.print("Failed connection attempts: ");
402 | Serial.println(connectionFails); */
403 |
404 | if (++connectionFails == 3)
405 | {
406 | //Serial.println("MQTT broker connection timeout...restarting!");
407 | delay(1000);
408 | ESP.restart();
409 | delay(5000);
410 | break;
411 | }
412 | //Serial.println("Try again in 5 seconds...");
413 | delay(5000);
414 | }
415 | }
416 | }
417 |
418 | void callback(char* topic, byte* payload, unsigned int length)
419 | {
420 | //Serial.print("Message arrived on topic [");
421 | //Serial.print(topic);
422 | //Serial.print("]: ");
423 |
424 | char buffer[length + 1];
425 |
426 | for (int i = 0; i < length; i++) //Copy payload to buffer string
427 | {
428 | buffer[i] = (char)payload[i];
429 | //Serial.print((char)payload[i]);
430 | }
431 |
432 | buffer[length] = '\0'; //Terminate string
433 |
434 | //Serial.println();
435 |
436 | //SERVICE COMMANDS
437 | if (strcmp(topic, service) == 0)
438 | {
439 | if (strcmp(buffer, "reboot") == 0)
440 | {
441 | toArduino.mode = 64; //Send code to restart Arduino
442 | toArduino.vanes = 0;
443 | toArduino.fanspeed = 0;
444 | toArduino.setpoint = 0;
445 | ETout.sendData();
446 |
447 | client.publish(debug, " << Rebooting... >>", true);
448 | delay(2000);
449 | ESP.restart(); //Now restart ESP
450 | delay(5000);
451 | return;
452 | }
453 |
454 | if (strcmp(buffer, "reinit") == 0) //Start WiFiManager after erasing previously stored settings
455 | {
456 | client.publish(debug, " << Reinitializing...(erase flash, reboot and start WiFiManager) >>", true);
457 | delay(500);
458 | SPIFFS.format(); //Erase flash
459 | delay(1000);
460 | WiFi.disconnect(); //Start WiFiManager for reconfiguration
461 | delay(1000);
462 | ESP.restart();
463 | delay(5000);
464 | return;
465 | }
466 |
467 | if (strcmp(buffer, "wifimanager") == 0) //Start WiFiManager with previously stored settings
468 | {
469 | client.publish(debug, " << Starting WiFiManager... >>", true);
470 | delay(500);
471 | WiFi.disconnect();
472 | delay(1000);
473 | ESP.restart();
474 | delay(5000);
475 | return;
476 | }
477 |
478 | if (strcmp(buffer, "debugon") == 0) //Send some info on debug topic, see debug2mqtt() in the code
479 | {
480 | client.publish(debug, " << Debug ON >>", true);
481 | debugit = true;
482 | return;
483 | }
484 |
485 |
486 | if (strcmp(buffer, "debugoff") == 0)
487 | {
488 | client.publish(debug, " << Debug OFF >>", true);
489 | debugit = false;
490 | return;
491 | }
492 |
493 | if (strcmp(buffer, "help") == 0)
494 | {
495 | client.publish(debug, " -> restart Arduino & ESP8266 | -> erase flash and start WiFiManager", true);
496 | client.publish(debug, " -> Start WiFiManager | -> Show some info on debug topic", true);
497 | return;
498 | }
499 |
500 | client.publish(debug, " << Unknown service command >>", true);
501 |
502 | return;
503 | }
504 |
505 | int value = atoi(buffer); //Convert payload to integer variable
506 |
507 | //POWER & MODE: payload = 0 [OFF], 1 [HEAT], 2 [COOL], 3 [AUTO], 4 [DRY], 5 [FAN], 6 [ON]
508 | if (strcmp(topic, State) == 0)
509 | {
510 | if (value >= 0 && value < 7)
511 | {
512 | toArduino.mode = value + 1;
513 | ETout.sendData(); //Send updated settings to Arduino using EasyTransfer
514 |
515 | //Serial.print("Mode change: ");
516 | //Serial.println(toArduino.mode);
517 |
518 | toArduino.mode = 0;
519 | debug2mqtt(" Updated power/mode settings send to Arduino/MHI.");
520 | }
521 | else
522 | {
523 | debug2mqtt(" << Error >> Value on MODE topic out of range [0-6]");
524 | }
525 |
526 | return;
527 | }
528 |
529 | //VANES: payload = 1 [1-UP], 2 [2], 3 [3], 4 [4-DOWN], 5 [SWING]
530 | if (strcmp(topic, Vanes) == 0)
531 | {
532 | if (value > 0 && value < 6)
533 | {
534 | toArduino.vanes = value;
535 | ETout.sendData(); //Send updated settings to Arduino using EasyTransfer
536 |
537 | //Serial.print("Vanes change: ");
538 | //Serial.println(toArduino.vanes);
539 |
540 | toArduino.vanes = 0;
541 | debug2mqtt(" Updated vanes settings send to Arduino/MHI.");
542 | }
543 | else
544 | {
545 | debug2mqtt(" << Error >> Value on VANES topic out of range [1-5]");
546 | }
547 |
548 | return;
549 | }
550 |
551 | //FAN SPEED: payload = 1 [1-LOW], 2 [2], 3 [3], 4 [4-HIGH]
552 | if (strcmp(topic, Fanspeed) == 0)
553 | {
554 | if (value > 0 && value < 5)
555 | {
556 | toArduino.fanspeed = value;
557 | ETout.sendData(); //Send updated settings to Arduino using EasyTransfer
558 |
559 | //Serial.print("Fan speed change: ");
560 | //Serial.println(toArduino.fanspeed);
561 |
562 | toArduino.fanspeed = 0;
563 | debug2mqtt(" Updated fan speed settings send to Arduino/MHI.");
564 | }
565 | else
566 | {
567 | debug2mqtt(" << Error >> Value on FAN SPEED topic out of range [1-4]");
568 | }
569 |
570 | return;
571 | }
572 |
573 |
574 | //TEMPERATURE SETPOINT: payload = temperature in degrees Celsius
575 | if (strcmp(topic, Setpoint) == 0)
576 | {
577 | if (value > 17 && value < 31)
578 | {
579 | toArduino.setpoint = value; //Bitfield containing target temperature
580 | ETout.sendData(); //Send updated settings to Arduino using EasyTransfer
581 |
582 | //Serial.print("Setpoint change: ");
583 | //Serial.println(toArduino.setpoint);
584 |
585 | toArduino.setpoint = 0;
586 | debug2mqtt(" Updated setpoint settings send to Arduino/MHI.");
587 | }
588 | else
589 | {
590 | debug2mqtt(" << Error >> Value on SETPOINT topic out of range [18-30]");
591 | }
592 |
593 | return;
594 | }
595 |
596 | }
597 |
598 | void loop()
599 | {
600 | ArduinoOTA.handle(); //Handle Arduino OTA updates
601 |
602 | if (!client.connected()) //Check MQTT connection
603 | {
604 | connect(); //Connect first time. Reconnect when connection lost.
605 | }
606 |
607 | client.loop();
608 |
609 | if (ETin.receiveData()) //Check for new data on serial (EasyTransfer). Returns false or true.
610 | {
611 | delay(1); //Make sure receive is complete. I've had occasional problems and this appears to solve them.
612 |
613 | if (debugit) //If debug = on -> send part of SPI byte frame to MQTT debug topic
614 | {
615 | char buffer[54] = " Updated bit field 4-10: ";
616 | int loc = 31;
617 |
618 | for (int i = 0; i < 7; i++)
619 | {
620 | snprintf(buffer + loc, 53 - loc , "%02X ", fromArduino.currentMHI[i]);
621 | loc += 3;
622 | }
623 |
624 | client.publish(debug, buffer, true); //Send latest MHI bit field update to MQTT broker
625 |
626 | snprintf(buffer, 53, " %d SPI resync/checksum errors", fromArduino.currentMHI[7]);
627 | client.publish(debug, buffer, true); //Send cumulative number of checksum errors on Arduino-SPI-MHI connection to MQTT
628 | }
629 |
630 | //Process MHI bit field 4-10 updates and only publish changes to corresponding MQTT statuses
631 | char msg[5] = ""; //Buffer string holding payload to publish
632 | byte buf = 0;
633 |
634 | //####### Bit field 4 >>> POWER, MODE & VANES (swing only) #######
635 | if (fromArduino.currentMHI[0] != current_Bitfield4) //Any change compared to previous bit field 4?
636 | {
637 | debug2mqtt(" Bit field 4 changed");
638 | current_Bitfield4 = fromArduino.currentMHI[0]; //Store new current bit field 4
639 |
640 | //POWER and/or MODE changed
641 | if ((current_Bitfield4 & 0b00011101) != current_Mode) //Extract mode bits (3-5) en power bit (1)
642 | {
643 | current_Mode = current_Bitfield4 & 0b00011101;
644 |
645 | //Get POWER and MODE states
646 | if (bitRead(current_Mode, 0) == 0) //Power is OFF
647 | {
648 | buf = 0;
649 | }
650 | else
651 | {
652 | for (int i = 0; i < 5; i++) //Power is ON -> get MODE
653 | {
654 | if (current_Mode == modeValues[i])
655 | {
656 | buf = i + 1;
657 | break;
658 | }
659 | }
660 | }
661 |
662 | snprintf(msg, 2, "%1d", buf);
663 | client.publish(statusState, msg, true); //Send update to MQTT broker
664 |
665 | debug2mqtt(" Mode/Power changed");
666 |
667 | /* Serial.println("State (Mode/Power) changed");
668 | Serial.print("MQTT publish [");
669 | Serial.print(statusState);
670 | Serial.print("]: ");
671 | Serial.println(msg); */
672 | }
673 |
674 | //VANES changed to swing
675 | if (bitRead(current_Bitfield4, 6)) //Check if new vanes setting is swing
676 | {
677 | if (!current_Swing) //Check if changed compared to previous setting
678 | {
679 | current_Swing = true;
680 | client.publish(statusVanes, "5", true); //Send update to MQTT broker
681 |
682 | debug2mqtt(" Vanes changed");
683 |
684 | /* Serial.println("Vanes changed");
685 | Serial.print("MQTT publish [");
686 | Serial.print(statusVanes);
687 | Serial.print("]: ");
688 | Serial.println("5"); */
689 | }
690 | }
691 | else if (current_Swing) //SWING changed from ON to OFF
692 | {
693 | current_Bitfield5 = -1; //Force update of VANES below (bit field 5) by setting out-of-range values
694 | current_Vanes = 255;
695 | current_Swing = false;
696 | }
697 | }
698 |
699 | //####### Bit field 10 >>> FAN SPEED (setting 4 only) #######
700 | if (bitRead(fromArduino.currentMHI[6], 6)) //Check if new speed setting is 4
701 | {
702 | if (!current_Fanspeed4) //Check if changed compared to current setting
703 | {
704 | debug2mqtt(" Bit field 10 changed");
705 |
706 | current_Fanspeed4 = true;
707 | client.publish(statusFanspeed, "4", true); //Send update to MQTT broker
708 |
709 | debug2mqtt(" Fan speed changed");
710 |
711 | /* Serial.println("Fanspeed changed");
712 | Serial.print("MQTT publish [");
713 | Serial.print(statusFanspeed);
714 | Serial.print("]: ");
715 | Serial.println(msg); */
716 | }
717 | }
718 | else if (current_Fanspeed4) //FAN SPEED changed from 4 to 1-3
719 | {
720 | current_Bitfield5 = -1; //Force update of FANSPEED below (bit field 5) by setting out-of-range values
721 | current_Fanspeed = 255;
722 | current_Fanspeed4 = false;
723 | }
724 |
725 | //####### Bit field 5 >>> VANES (position 1-4) & FAN SPEED (setting 1-3) #######
726 | if ((int)fromArduino.currentMHI[1] != current_Bitfield5) //Any change compared to previous bit field 5? Force update of VANES if SWING was just switched off. Same for FAN SPEED 4.
727 | {
728 | debug2mqtt(" Bit field 5 changed");
729 | current_Bitfield5 = (int)fromArduino.currentMHI[1]; //New current bit field 5
730 |
731 | //VANES changed
732 | if ((current_Bitfield5 & 0b00110000) != current_Vanes && !current_Swing) //Extract vanes bits (5-6)
733 | {
734 | current_Vanes = current_Bitfield5 & 0b00110000;
735 |
736 | //Get VANES position
737 | buf = (current_Vanes >> 4) + 1; //Convert to vanes position [1-4]
738 |
739 | snprintf(msg, 2, "%1d", buf);
740 | client.publish(statusVanes, msg, true); //Send update to MQTT broker
741 |
742 | debug2mqtt(" Vanes changed");
743 |
744 | /* Serial.println("Vanes changed");
745 | Serial.print("MQTT publish [");
746 | Serial.print(statusVanes);
747 | Serial.print("]: ");
748 | Serial.println(msg); */
749 | }
750 |
751 | //FAN SPEED (setting 1-3) changed
752 | if ((current_Bitfield5 & 0b00000111) != current_Fanspeed && !current_Fanspeed4) //Extract fan speed bits (1-3)
753 | {
754 | current_Fanspeed = current_Bitfield5 & 0b00000111;
755 |
756 | //Get FAN SPEED
757 | buf = current_Fanspeed + 1; //Convert to fan speed (1-3)
758 |
759 | snprintf(msg, 2, "%1d", buf);
760 | client.publish(statusFanspeed, msg, true); //Send update to MQTT broker
761 |
762 | debug2mqtt(" Fan speed changed");
763 |
764 | /* Serial.println("Fanspeed changed");
765 | Serial.print("MQTT publish [");
766 | Serial.print(statusFanspeed);
767 | Serial.print("]: ");
768 | Serial.println(msg); */
769 | }
770 | }
771 |
772 | //####### Bit field 6 >>> TEMPERATURE SETPOINT #######
773 | if (fromArduino.currentMHI[2] != current_Bitfield6)
774 | {
775 | debug2mqtt(" Bit field 6 changed");
776 | current_Bitfield6 = fromArduino.currentMHI[2]; //Store new setpoint byte value for reference in next SPI frame update
777 |
778 | //Extract new temperature setpoint (bitfield 6)
779 | buf = current_Bitfield6;
780 | buf = bitClear(buf, 7) >> 1; //Clear bit 8 and shift right 1 bit (= divide by 2) -> buf = temperature in degr. Celsius
781 | snprintf(msg, 3, "%2d", buf);
782 |
783 | client.publish(statusSetpoint, msg, true); //Send update to MQTT broker
784 |
785 | debug2mqtt(" Setpoint changed");
786 |
787 | /* Serial.println("Setpoint changed");
788 | Serial.print("MQTT publish [");
789 | Serial.print(statusSetpoint);
790 | Serial.print("]: ");
791 | Serial.println(msg); */
792 | }
793 |
794 | //####### Bit field 7 >>> ROOM TEMPERATURE #######
795 | if (fromArduino.currentMHI[3] != current_Bitfield7)
796 | {
797 | debug2mqtt(" Bit field 7 changed");
798 | current_Bitfield7 = fromArduino.currentMHI[3]; //Store new setpoint byte value for reference in next SPI frame update
799 |
800 | //Calculate current room temperature in degrees Celsius from bitfield 7 using:
801 | // (BF7 - 61) / 4 (note: Calibration of temperature needs to be checked further)
802 | int temp = (int)current_Bitfield7 - 61;
803 | int dec = (temp % 4) * 25; //Decimal value (can be xx.00; xx.25; xx.50; xx.75)
804 | temp /= 4; //Truncated temperature (rounded down)
805 |
806 | snprintf(msg, 6, "%d.%02d", temp, dec); //Construct temperature payload string
807 |
808 | client.publish(statusRoomtemp, msg, true); //Send update to MQTT broker
809 |
810 | debug2mqtt(" Room temperature changed");
811 |
812 | /* Serial.println("Roomtemp changed");
813 | Serial.print("MQTT publish [");
814 | Serial.print(statusRoomtemp);
815 | Serial.print("]: ");
816 | Serial.println(msg); */
817 | }
818 | }
819 | }
820 |
--------------------------------------------------------------------------------
/src/MHI-SPI2ESP.ino:
--------------------------------------------------------------------------------
1 | /*
2 | ##############################################################################################################################################################################################
3 | MHI SPI2ESP Interface v1.0.0
4 | Arduino-based communication interface for Mitsubishi Heavy Industries (MHI) SRK/SRF series air conditioners.
5 | Connects to the MHI CNS connector and synchronizes to its Serial Peripheral Interface (SPI). Updates from the MHI are sent via serial to an ESP8266 running an MQTT client.
6 | Updates received on the ESP8266 via MQTT are sent to the Arduino over serial and injected into the SPI data frames to update the MHI.
7 | R.J. Dekker, June 2018
8 | ##############################################################################################################################################################################################
9 | */
10 |
11 | //#include //Watchdog for software reset: not working due to bootloader bug, but does not seem necessary anyway
12 | #include //EasyTransfer v2.1 - Bill Porter: https://github.com/madsci1016/Arduino-EasyTransfer
13 |
14 | volatile byte state = 0; //'State machine' in loop(): [State=0] Priority is given to SPI interrupt to collect a full 20-byte SPI data frame. No other processing is allowed to keep in sync. [State=1] Pulse clock, set new/updated frames etc.. [State=2] Check for data received from ESP via serial
15 | volatile char bitfield = 0; //Bitfield position counter for sending (tx_SPIframe[]) and recceiving (rx_SPIframe[])
16 | byte variantnumber = 0; //Frame variation that is currently being sent (0, 1 or 2 in frameVariant[])
17 | byte framenumber = 1; //Counter for how many times a frame variation has been sent (max. = 48)
18 |
19 | bool updateESP = true; //Flag to ensure that an update is send to ESP only once every (repeatFrame x 2 x 3) SPI frames (~6 secs)
20 | bool checksumError = false; //Flag for checksum error in frame 2 or 47. If two errors occur at both these positions in a single 48-frame cycle -> SPI sync lost -> resync SPI
21 |
22 | byte newMode = 0; //Temporary storage of settings received from ESP
23 | byte newVanes = 0;
24 | byte newFanspeed = 0;
25 | byte newSetpoint = 0;
26 |
27 | const byte rx_frameSignature[3] = { 0x6C, 0x80, 0x04 }; //SPI frame start signature: first 3 bytes in a full SPI data frame. Used to sync to MHI SPI data in SPI_sync() routine. Might be different on other unit types!
28 |
29 | //SPI frame that is currently being sent during the SPI interrupt routine. Contains base values that will be updated after the first 48-frame cycle with values received from MHI.
30 | // Bitfield: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
31 | volatile byte tx_SPIframe[20] = { 0xA9, 0x00, 0x07, 0x4C, 0x00, 0x2A, 0xFF, 0x00, 0x00, 0x40, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x0F, 0x04, 0x05, 0xF5 };
32 |
33 | volatile byte rx_SPIframe[20]; //Array to collect a single frame of SPI data received from the MHI unit
34 |
35 | byte rx_bitfield4_10[7]; //Array containing bitfields 4-10 from rx_SPIframe, which holds current MHI mode, vanes, fans speed, ambient temperature and setpoint
36 |
37 | //Alternating bitfield 10-18 variations, each successively send for 48 frames. Bit 3 in bitfield 18 functions as a clock and is 1 for 24 frames and 0 for the subsequent 24 frames. I have never seen bit fields 11-12 changing, so don't know what they are for.
38 | // Bitfield: 10 11 12 13 14 15 16 17 18
39 | const byte frameVariant[3][9] { { 0x40, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x0F, 0x04 }, //variant number 0
40 | { 0x80, 0x00, 0x00, 0x32, 0xD6, 0x01, 0x00, 0x0F, 0x04 }, //variant number 1
41 | { 0x80, 0x00, 0x00, 0xF1, 0xF7, 0xFF, 0xFF, 0x0F, 0x04 } //variant number 2
42 | };
43 |
44 | //MODE bitmasks Bitfield #4
45 | const byte modeMask[8][2] { // CLEAR | SET
46 | { 0b00100010, 0b00000000 }, //0 = Unchanged (only clear 'write' bits)
47 | { 0b00100011, 0b00000010 }, //1 = OFF
48 | { 0b00111111, 0b00110011 }, //2 = HEAT
49 | { 0b00111111, 0b00101011 }, //3 = COOL
50 | { 0b00111111, 0b00100011 }, //4 = AUTO
51 | { 0b00111111, 0b00100111 }, //5 = DRY
52 | { 0b00111111, 0b00101111 }, //6 = FAN
53 | { 0b00100011, 0b00000011 } //7 = ON (using last mode)
54 | };
55 |
56 | //VANES bitmasks Bitfield #4 Bitfield #5
57 | const byte vanesMask[6][4] { // CLEAR | SET CLEAR | SET
58 | { 0b10000000, 0b00000000, 0b10000000, 0b00000000 }, //0 = Unchanged (only clear 'write' bits)
59 | { 0b11000000, 0b10000000, 0b10110000, 0b10000000 }, //1 = 1 (up)
60 | { 0b11000000, 0b10000000, 0b10110000, 0b10010000 }, //2 = 2
61 | { 0b11000000, 0b10000000, 0b10110000, 0b10100000 }, //3 = 3
62 | { 0b11000000, 0b10000000, 0b10110000, 0b10110000 }, //4 = 4 (down)
63 | { 0b11000000, 0b11000000, 0b10000000, 0b00000000 } //5 = swing
64 | };
65 |
66 | //FANSPEED bitmasks Bitfield #5 Bitfield #10
67 | const byte fanspeedMask[5][4] { // CLEAR | SET CLEAR | SET
68 | { 0b00001000, 0b00000000, 0b11011000, 0b00000000 }, //0 = Unchanged (only clear 'write' bits)
69 | { 0b00001111, 0b00001000, 0b11011000, 0b00000000 }, //1 = Speed 1
70 | { 0b00001111, 0b00001001, 0b11011000, 0b00000000 }, //2 = Speed 2
71 | { 0b00001111, 0b00001010, 0b11011000, 0b00000000 }, //3 = Speed 3
72 | { 0b00001111, 0b00001010, 0b11011000, 0b00010001 } //4 = Speed 4
73 | };
74 |
75 | //Setup EasyTransfer
76 | EasyTransfer ETin, ETout;
77 |
78 | struct RECEIVE_DATA_STRUCTURE { //Variables received from ESP
79 | byte mode; //Mode [1]OFF, [2]HEAT, [3]COOL, [4]AUTO, [5]DRY, [6]FAN, [7]ON, [64]RESET
80 | byte vanes; //Vanes [1]UP, [2]2, [3]3, [4]DOWN, [5]SWING
81 | byte fanspeed; //Fanspeed [1]1, [2]2, [3]3, [4]4
82 | byte setpoint; //Setpoint [18]18 -> [30]30 degrees Celsius
83 | } __attribute__((packed));
84 |
85 | struct SEND_DATA_STRUCTURE { //Variable send to ESP
86 | byte currentMHI[8]; //Contains bitfields last received from MHI for bitfields 4-10 (holding current settings for mode, vanes, fanspeed, setpoint and ambient temperature) and a SPI sync error count
87 | };
88 |
89 | RECEIVE_DATA_STRUCTURE fromESP;
90 | SEND_DATA_STRUCTURE toESP;
91 |
92 | //ROUTINES, SETUP and MAIN LOOP
93 | void setup (void)
94 | {
95 | delay(7000); //Delay to allow ESP8266 to boot
96 |
97 | Serial.begin (500000);
98 | while(Serial.available()) Serial.read(); //Empty serial read buffer from possible junk send by ESP during boot
99 |
100 | //wdt_enable(WDTO_8S); //Start Watchdog Timer (WDT) to detect hang ups
101 |
102 | ETin.begin(details(fromESP), &Serial); //Start the EasyTransfer library, pass in the data details and the name of the serial port
103 | ETout.begin(details(toESP), &Serial);
104 |
105 | pinMode(SCK, INPUT);
106 | pinMode(MISO, OUTPUT);
107 | SPCR = (0< restart system
156 |
157 | resync:
158 | //Finds the start of the first complete frame by looking for a signature.
159 | //When the first SPI transfer is started, the data sometimes starts in the middle of a frame
160 | //or out of sync with the SPI clock. This routine scans for the first 3 bytes as given
161 | //in rx_frameSignature[]. It then reads and discards the next 17 bytes before handing further
162 | //SPI data exchange to the interrupt routine ISR(SPI_STC_vect).
163 |
164 | //Serial.print("Syncing SPI to master...");
165 | int hits = 0; //Number of times consecutive signature bytes have been encountered
166 | int cycle = 0; //Stores number of bytes checked for signature. If too high -> SPI out of sync?
167 | byte r; //Used to store read byte
168 |
169 | SPCR &= ~(1< resync
171 | SPCR |= (1< 25) //If scan takes >25 SPI bytes -> SPI CLK out-of-sync -> reset SPI and try again
176 | {
177 | if (resyncAttempts > 2) //Too many resync attempts -> restart system
178 | {
179 | resyncAttempts = 0;
180 | //Serial.println("Too many SPI sync errors!");
181 | //Serial.println("Restarting system...");
182 | delay(500);
183 | softwareReset(); //softwareReset(WDTO_60MS); //Restart using Watchdog Timer
184 | delay(5000);
185 | }
186 |
187 | //Serial.println("SPI sync error!");
188 | //Serial.println("Restarting SPI hardware...");
189 |
190 | cycle = 0; //Signature took too long -> SPI bytes bit-shifted? -> 1st try = restart SPI
191 |
192 | ++resyncAttempts; //Track number of resync attempts
193 | goto resync; //Restart signature scan. Not very elegant, but it works.
194 | }
195 |
196 | hits = 0;
197 |
198 | do //Scan for 3-byte signature
199 | {
200 | SPDR = 0; //Send back zero for each read byte
201 | while(!(SPSR & (1< signature found!
207 | }
208 |
209 |
210 | for (int t = 0; t < 17; t++) //Discard the next 17 bytes after signature to skip to the start byte of the next frame
211 | {
212 | SPDR = 0;
213 | while(!(SPSR & (1< SPI signal lost -> restart system and resync
260 |
261 | switch (state)
262 | {
263 | case 0: //<<>> Do nothing (wait until a complete SPI frame has been send/received)
264 |
265 | break;
266 |
267 | case 1: //<<>> Complete frame send/received -> decide what to do based on current frame number (out of 48)
268 | switch (framenumber)
269 | {
270 | case 2: // Verify checksum on SPI frame that was just received and send to ESP8266 if correct
271 | if (verify_checksum()) //Verify checksum
272 | {
273 | if (variantnumber == 1 || updateESP) //Send updated MHI settings to ESP9288 in the 10th SPI frame repeat of frame 2 out of 3 (every ~6 seconds) or immediately after a new setting was sent to the MHI
274 | {
275 | memcpy(&toESP.currentMHI, &rx_SPIframe[3], 7); //Copy bitfields 4-10 from the most recent MHI SPI frame to new array for sending to ESP
276 | ETout.sendData(); //Send to ESP using EasyTransfer.
277 |
278 | updateESP = false; //Uncheck flag to send update only once
279 | }
280 |
281 | checksumError = false;
282 | }
283 | else
284 | {
285 | if (checksumError) //If true then the previous checksum at frame 47 was also wrong -> SPI sync lost? -> resync
286 | {
287 | toESP.currentMHI[7]++; //Count resyncs triggered by consecutive checksum errors and send to ESP for debugging
288 | updateESP = true; SPI_sync(); return;
289 | }
290 |
291 | checksumError = true;
292 | updateESP = true;
293 | }
294 |
295 | state = 0;
296 | break; //Start from beginning of loop() and wait for next complete SPI frame
297 |
298 | case 24: // Current frame variation has been sent 24 times -> clear clock bit in bit field 18 for the next 24 frames
299 | bitClear(tx_SPIframe[17], 2); //Clear clock bit 3 in bitfield 18-> update checksum and resend for 24 cycles
300 | update_checksum(); //Recalculate checksum of tx_SPIframe
301 |
302 | state = 0;
303 | break; //Start from beginning of loop() and wait for next complete SPI frame
304 |
305 | case 47: // Collect the most recent bit fields 4-10 for constructing an updated tx_SPIframe after the upcoming frame (48)
306 | if (verify_checksum()) //Verify checksum
307 | {
308 | memcpy(&rx_bitfield4_10, &rx_SPIframe[3], 7); //Get bitfields 4-10 from the last MHI SPI frame to use for the upcoming tx_SPIframe update
309 | checksumError = false;
310 | }
311 | else
312 | {
313 | if (checksumError) //If true then the previous checksum at frame 2 was also wrong -> SPI sync lost? -> resync
314 | {
315 | toESP.currentMHI[7]++; //Count resyncs triggered by consecutive checksum errors and send to ESP for debugging
316 | updateESP = true; SPI_sync(); return;
317 | }
318 |
319 | checksumError = true;
320 | }
321 |
322 | state = 0;
323 | break;
324 |
325 | case 48: // Current frame variation has been send 48 times -> construct next frame variant using most recent bit fields 4-10 collected in frame 47
326 | framenumber = 0; //Reset repeat frame counter
327 |
328 | if (++variantnumber > 2) variantnumber = 0; //Increase frame counter -> test if all 3 frames sent -> restart with frame 1
329 |
330 | memcpy(&tx_SPIframe[9], &frameVariant[variantnumber][0], 9); //Copy (part of) the next frame to the current frame for sending on the upcoming bitfield 18 clock cycle
331 |
332 | //******************* CONSTRUCTION OF UPDATED BIT FIELDS *******************
333 | //Set 'state change' bits and 'write' bits if MQTT update received from ESP
334 | //otherwise only clear 'write' bits using masks from the xxxMask[0][] arrays
335 | //Bitfields 4, 5, 6, 10 are based on the last received MHI values (frame 47)
336 | tx_SPIframe[3] = rx_bitfield4_10[0] & ~modeMask[newMode][0]; //Clear mode bits (bitfield 4)
337 | tx_SPIframe[3] |= modeMask[newMode][1]; //Set mode bits
338 |
339 | tx_SPIframe[3] &= ~vanesMask[newVanes][0]; //Clear vanes bits (bitfield 4)
340 | tx_SPIframe[3] |= vanesMask[newVanes][1]; //Set vanes bits
341 |
342 | tx_SPIframe[4] = rx_bitfield4_10[1] & ~vanesMask[newVanes][2]; //Clear vanes bits (bitfield 5)
343 | tx_SPIframe[4] |= vanesMask[newVanes][3]; //Set vanes bits
344 |
345 | tx_SPIframe[4] &= ~fanspeedMask[newFanspeed][0]; //Clear fanspeed bits (bitfield 5)
346 | tx_SPIframe[4] |= fanspeedMask[newFanspeed][1]; //Set fanspeed bits
347 |
348 | bitWrite(rx_bitfield4_10[6], 0, bitRead(rx_bitfield4_10[6], 6)); //Copy bit 7 from rx_SPIframe[9] to bit 1 as the status bits for fan speed 4 appear to be swapped (!?) between MISO and MOSI
349 | tx_SPIframe[9] &= ~0b00111111; //Clear bits 1-6 and keep variant bits 7-8
350 |
351 | tx_SPIframe[9] |= (rx_bitfield4_10[6] & ~fanspeedMask[newFanspeed][2]);
352 | tx_SPIframe[9] |= fanspeedMask[newFanspeed][3]; //Set fanspeed bits
353 |
354 | //Construct setpoint bitfield (#6) from last MHI value or MQTT update
355 | if (newSetpoint == 0)
356 | {
357 | tx_SPIframe[5] = rx_bitfield4_10[2] & ~0b10000000; //Copy last received MHI setpoint and clear the write bit
358 | }
359 | else
360 | {
361 | tx_SPIframe[5] = (newSetpoint << 1) | 0b10000000; //MQTT updated setpoint in degrees Celsius -> shift 1 bit left and set write bit (#8)
362 | }
363 |
364 | update_checksum(); //Recalculate checksum of tx_SPIframe
365 |
366 | //Reset all state changes
367 | newMode = 0;
368 | newVanes = 0;
369 | newFanspeed = 0;
370 | newSetpoint = 0;
371 |
372 | state = 0;
373 | break; //Start from beginning of loop() and wait for next complete SPI frame.
374 |
375 | default:
376 | state = 2; //Use time (~30 ms) until next frame for receiving commands from ESP
377 | }
378 |
379 | framenumber++; //Increase repeat counter to keep track of the number of times the current frame has been sent
380 |
381 | case 2: //<<>> Check once if data received from ESP via serial (EasyTransfer)
382 | if (ETin.receiveData())
383 | {
384 | delay(1); //Delay to allow fromESP.xxx to be updated
385 | if (fromESP.mode == 64) softwareReset(); //softwareReset(WDTO_60MS); //Requested reset by ESP8266 via MQTT service topic (bitfield = 32 received)
386 |
387 | //Store new commands received from ESP in newXXX, but only if not equal to zero
388 | //to prevent cancellation of previous commands that have not been send yet
389 | newMode = (fromESP.mode > 0) ? fromESP.mode : newMode;
390 | newVanes = (fromESP.vanes > 0) ? fromESP.vanes : newVanes;
391 | newFanspeed = (fromESP.fanspeed > 0) ? fromESP.fanspeed : newFanspeed;
392 | newSetpoint = (fromESP.setpoint > 0) ? fromESP.setpoint : newSetpoint;
393 |
394 | updateESP = true; //Flag for MHI status feedback: get an MHI update after frame number 2 and send it to the ESP for updating status of the MQTT topics
395 | }
396 |
397 | state = 0; //Check for serial data once between every SPI frame (approx. every 40 ms). If this is done continuously in the loop and/or after the calculations done on frames 2, 24, 47, 48, then SPI sync can be lost.
398 | break;
399 |
400 | } //End switch..case
401 |
402 | } //End of loop
403 |
--------------------------------------------------------------------------------