Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Carrier 40MAHB #2174

Open
kmgwalker opened this issue Jan 10, 2025 · 4 comments
Open

Support for Carrier 40MAHB #2174

kmgwalker opened this issue Jan 10, 2025 · 4 comments

Comments

@kmgwalker
Copy link

kmgwalker commented Jan 10, 2025

I'm writing to share a successful decoding of the Carrier 40MAHB protocol. I have not yet massaged the code into a form suitable for incorporation into the IRremoteESP8266 library, and I don't anticipate having time to do so in the near future. But the attached code excerpt successfully controls power, temperature, mode, fan and "swing" of the unit.

Notes:

  • The unit uses a close cousin of the Bosch 144-bit protocol for temperature/fan/mode.
  • My unit is set for Fahrenheit, not Celsius. This entails an extra temperature bit in the protocol, corresponding roughly to 0.5 deg C.
  • The "swing" settings (as well as power off) use a 96-bit Coolix-style format.
  • The related Bosch format has a "quiet" bit, and I've retained this in my code, but I have not tested it.

I've attached the relevant parts of my working code.

[can't figure out how to attach, so I'm pasting it in]


/**** 
	relevant parts of working code (plus a few irrelevant parts)
*****/




#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_Bosch.h>

#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>

#include <map>
using namespace std;

String version_str = "0.5a";

// pin definitions
const int LED_PIN = 0;
const int IR_LED_PIN = 4;


// first semi-general version ///////////
  // Title of owner's manual: Carrier 40MAHB ductless unit split system (sizes 06 09 12 18 24 30 36)

void carrier_ir_send(uint8_t* buf, int len = 18);

// non-redundant bytes from 144-bit protocol
	// note: switching modes also changes fan (for modes auto and dry only?) and temp (just for fan mode?) bits; need to understand that better
union preCarrier144Protocol {
  uint8_t raw[6];
  struct {
    // section 1
    uint8_t unk1       :5;   // unknown
    uint8_t fan1       :3;   // fan speed bits part 1
    
    uint8_t unk2       :2;   // unknown
    uint8_t mode1      :2;   // operation mode bits part 1
    uint8_t temp1      :4;   // desired temperature part 1

    // section 3 (section 2 is a repeat of section 1)
    uint8_t mode2      :1;   // operation mode bits part 2
    uint8_t fan2       :6;   // fan speed bits part 2
    uint8_t unk3       :1;   // unknown (also part of fan speed bits?)
    
    uint8_t unk4       :5;   // unknown
    uint8_t temp2      :1;   // desired temperature part 2
    uint8_t unk5       :1;   // unknown
    uint8_t unk5q      :1;   // unknown (maybe silent mode)
    
    uint8_t unk6       :4;   // unknown
    uint8_t temp3      :1;   // desired temperature part 3
    uint8_t unk7       :3;   // unknown
    
    uint8_t unk8       :8;   // unknown
  };
};

struct temp_bits {
  uint8_t temp1      :4;   // desired temperature part 1
  uint8_t temp2      :1;   // desired temperature part 2
  uint8_t temp3      :1;   // desired temperature part 3
};
struct fan_bits {
  uint8_t fan1       :3;   // fan speed bits part 1
  uint8_t fan2       :6;   // fan speed bits part 2
};
struct mode_bits {
  uint8_t mode1      :2;   // operation mode bits part 1
  uint8_t mode2      :1;   // operation mode bits part 2
};


// temperature codes
const uint8_t kTempMin = 60;  // Fahrenheit
const uint8_t kTempMax = 86;  // Fahrenheit
const uint8_t kTempRange = kTempMax - kTempMin + 1;
const temp_bits kTempMap[kTempRange] = {
	{0b0000, 0b0, 0b1},		// 60F
	{0b0000, 0b1, 0b1},		// 61F...
	{0b0000, 0b0, 0b0},
	{0b0000, 0b1, 0b0},
	{0b0001, 0b0, 0b0},
	{0b0001, 0b1, 0b0},
	{0b0011, 0b0, 0b0},
	{0b0011, 0b1, 0b0},
	{0b0010, 0b0, 0b0},
	{0b0010, 0b1, 0b0},
	{0b0110, 0b0, 0b0},
	{0b0110, 0b1, 0b0},
	{0b0111, 0b0, 0b0},
	{0b0101, 0b0, 0b0},
	{0b0101, 0b1, 0b0},
	{0b0100, 0b0, 0b0},
	{0b0100, 0b1, 0b0},
	{0b1100, 0b0, 0b0},
	{0b1100, 0b1, 0b0},
	{0b1101, 0b0, 0b0},
	{0b1101, 0b1, 0b0},
	{0b1001, 0b0, 0b0},
	{0b1000, 0b0, 0b0},
	{0b1000, 0b1, 0b0},
	{0b1010, 0b0, 0b0},
	{0b1010, 0b1, 0b0},		// ...85F
	{0b1011, 0b0, 0b0},		// 86F
};

// fan codes
const fan_bits kFanMap[6] = {
	{0b111, 0b001010},  // 20%
	{0b100, 0b010100},  // 40%
	{0b010, 0b011110},  // 60%
	{0b001, 0b101000},  // 80%
	{0b001, 0b110010},  // 100%
	{0b101, 0b110011},  // auto
};

// mode codes
const mode_bits kModeMap[5] = {
  {0b01, 0b0},  // fan
  {0b10, 0b1},  // auto
  {0b00, 0b0},  // cool
  {0b01, 0b1},  // dry
  {0b11, 0b0},  // heat
};
const uint8_t kModeFan = 0;
const uint8_t kModeAuto = 1;
const uint8_t kModeCool = 2;
const uint8_t kModeDry = 3;
const uint8_t kModeHeat = 4;

// swing codes
const uint8_t kSwingMap[7] = {
	0x05,	// blank (meaning??)
  0x04,	// multi (swing variable?)
	0x2A,	// highest
	0x2B,	// 2nd highest
	0x2C,	// middle
	0x2D,	// 2nd lowest
	0x2E,	// lowest
};


// Carrier mini-split
struct CMS {
  uint8_t full_state[18];     // buffer for hand-off to irsend/IRBosch144AC
  preCarrier144Protocol pcp;  // intermediate buffer; no redundancies, fixed bytes or checksums

  int temp;   // deg Fahrenheit
  uint8_t mode;
  uint8_t fan;
  uint8_t swing;
  uint8_t quiet;	// not tested*****
  uint8_t power_on;

  void send144() { carrier_ir_send(full_state, 18); };      // send (already constructed) 144-bit msg
  void send96() { carrier_ir_send(full_state, 12); };      // send (already constructed) 144-bit msg
  void preconstruct144();   // params to non-redundant bytes
  void construct144();  // construct 144-bit msg; non-redundant bytes to full bytes
  void construct96(uint8_t b0, uint8_t b1, uint8_t b2); // construct 96-bit coolix-style message (add inversions and repeats)
  String dump_state();
  void send_main() { preconstruct144(); construct144(); send144(); }
  void send_swing()  { construct96(0xb9, 0xf5, kSwingMap[swing]); send96(); }
  void send_power_off() { construct96(0xb2, 0x7b, 0xe0); send96(); }

  // these return true on error (e.g. arg out of bounds or otherwise invalid)
    // second arg controls whether new settings are immediately transmitted/sent
  int set_temp(int new_temp, uint8_t no_send=0);
  int set_fan(uint8_t new_fan, uint8_t no_send=0);
  int set_mode(uint8_t new_mode, uint8_t no_send=0);   // ***** needs more work bec mode can affect other settings
  int set_quiet(uint8_t new_quiet, uint8_t no_send=0);	// not tested*****
  int set_swing(uint8_t new_swing, uint8_t no_send=0);

} cms;


void CMS::construct144() {
  construct96(0xb2, pcp.raw[0], pcp.raw[1]);

  full_state[12] = 0xd5;  // fixed value
  for (int j=0; j<4; ++j) { full_state[j+13] = pcp.raw[j+2]; }  // copy 4 bytes
  full_state[17] = full_state[12] + full_state[13] + full_state[14] + full_state[15] + full_state[16];  // check sum
}

void CMS::construct96(uint8_t b0, uint8_t b1, uint8_t b2) {
  full_state[0] = b0;
  full_state[2] = b1;
  full_state[4] = b2;
  
  // inversions
  full_state[1] = full_state[0] ^ 0xff;
  full_state[3] = full_state[2] ^ 0xff;
  full_state[5] = full_state[4] ^ 0xff;

  // repeats
  for (int j=0; j<6; ++j) { full_state[j+6] = full_state[j]; }
}

// assumes temp, mode, fan etc. have valid/vetted values
  // to do: special temp and fan values for certain modes
void CMS::preconstruct144() {
  pcp.temp1 = kTempMap[temp - kTempMin].temp1;
  pcp.temp2 = kTempMap[temp - kTempMin].temp2;
  pcp.temp3 = kTempMap[temp - kTempMin].temp3;

  pcp.fan1 = kFanMap[fan].fan1;
  pcp.fan2 = kFanMap[fan].fan2;

  pcp.mode1 = kModeMap[mode].mode1;
  pcp.mode2 = kModeMap[mode].mode2;

  pcp.unk5q = quiet;  // ** will this work?			// not tested*****

  // put observed/empirical values into unknown spots
  pcp.unk1 = 0b11111;
  pcp.unk2 = 0b00;
  pcp.unk3 = 0b0;
  pcp.unk4 = 0b00000;
  pcp.unk5 = 0b0;
  //pcp.unk5q = 0b0;
  pcp.unk6 = 0b0001;
  pcp.unk7 = 0b000;
  pcp.unk8 = 0b00000000;
}

String CMS::dump_state() {
  String ret;
  ret.reserve(18*3);
  for (int j=0; j<18; ++j) {
    ret += String(full_state[j], HEX);
    ret += " ";
  }
  return ret;
}

int CMS::set_temp(int new_temp, uint8_t no_send) {
  if (new_temp < kTempMin || new_temp > kTempMax) return 1;
  temp = new_temp;
  if (!no_send) send_main();
  return 0;
}
int CMS::set_fan(uint8_t new_fan, uint8_t no_send) {
  if (new_fan > 5) return 1;    // ** fix: hard-coded
  fan = new_fan;
  if (!no_send) send_main();
  return 0;
}
int CMS::set_mode(uint8_t new_mode, uint8_t no_send) {  // ***** needs more work bec mode can affect other settings
      // for certain modes, also set fan and/or temp; when leaving those modes, restore fan and/or temp
  if (new_mode > 4) return 1;    // ** fix: hard-coded
  mode = new_mode;
  if (!no_send) send_main();
  return 0;
}
int CMS::set_quiet(uint8_t new_quiet, uint8_t no_send) {	// not tested*****
  if (new_quiet > 1) return 1;    // ** fix: hard-coded
  quiet = new_quiet;
  if (!no_send) send_main();
  return 0;
}
int CMS::set_swing(uint8_t new_swing, uint8_t no_send) {
  if (new_swing > 6) return 1;    // ** fix: hard-coded
  swing = new_swing;
  if (!no_send) send_swing();
  return 0;
}


IRBosch144AC bosch_ac(IR_LED_PIN);  // used only for sending msgs/bytes, not constructing msgs
//IRsend irsend(IR_LED_PIN);    // try to use IRsend directly instead?

// send msg to Carrier minisplit/AC; use Bosch class (perhaps should use IRsend directly and remove Bosch stuff)
void carrier_ir_send(uint8_t* buf, int len) {
  bosch_ac.setRaw(buf, len);
  bosch_ac.send();
  Serial.println(cms.dump_state().c_str());
}

/*
	[...]
*/

@kmgwalker
Copy link
Author

Pasted code looks a little messed-up. If someone can explain a better way to do this, I'll try again.

@tonhuisman
Copy link
Contributor

Pasted code looks a little messed-up. If someone can explain a better way to do this, I'll try again.

You have used a single back-tick to start and end a code block, but for multiple-line code blocks, triple-back-ticks (first one optionally having a code-high-light name, like c++) must be used. You're close 😉
You can use the Preview tab above the comment field to see the result.

@kmgwalker
Copy link
Author

kmgwalker commented Jan 11, 2025

Thank you, tonhuisman, for the helpful response. I missed the Preview tab. Here's the code again:

(Deleted code block here and edited initial comment, per suggestion.)

@tonhuisman
Copy link
Contributor

Here's the code again:

You could have edited the original comment, now you have 2x the same (rather long) code-fragment, 1 poorly readable, and 1 readable... 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants