├── Crc.cpp ├── Crc.h ├── Makefile ├── Max.h ├── Max.ino ├── MaxRF22.cpp ├── MaxRF22.h ├── MaxRFProto.cpp ├── MaxRFProto.h ├── Pn9.cpp ├── Pn9.h ├── README.md ├── Util.cpp └── Util.h /Crc.cpp: -------------------------------------------------------------------------------- 1 | #include "Crc.h" 2 | 3 | /** 4 | * CRC code based on example from Texas Instruments DN502, matches 5 | * CC1101 implementation 6 | */ 7 | #define CRC16_POLY 0x8005 8 | static uint16_t calc_crc_step(uint8_t crcData, uint16_t crcReg) { 9 | uint8_t i; 10 | for (i = 0; i < 8; i++) { 11 | if (((crcReg & 0x8000) >> 8) ^ (crcData & 0x80)) 12 | crcReg = (crcReg << 1) ^ CRC16_POLY; 13 | else 14 | crcReg = (crcReg << 1); 15 | crcData <<= 1; 16 | } 17 | return crcReg; 18 | } // culCalcCRC 19 | 20 | #define CRC_INIT 0xFFFF 21 | uint16_t calc_crc(uint8_t *buf, size_t len) { 22 | uint16_t checksum; 23 | checksum = CRC_INIT; 24 | // Init value for CRC calculation 25 | for (size_t i = 0; i < len; i++) 26 | checksum = calc_crc_step(buf[i], checksum); 27 | return checksum; 28 | } 29 | 30 | /* vim: set sw=2 sts=2 expandtab: */ 31 | -------------------------------------------------------------------------------- /Crc.h: -------------------------------------------------------------------------------- 1 | #ifndef __MAX_CRC_H 2 | #define __MAX_CRC_H 3 | 4 | #include 5 | #include 6 | 7 | uint16_t calc_crc(uint8_t *buf, size_t len); 8 | 9 | #endif // __MAX_CRC_H 10 | 11 | /* vim: set sw=2 sts=2 expandtab: */ 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARDUINO_LIBS = SPI RF22 LiquidCrystal_I2C Wire Wire/utility TStreaming Ethernet Ethernet/utility 2 | #BOARD_TAG = uno 3 | #ARDUINO_PORT = /dev/ttyACM* 4 | BOARD_TAG = ethernet 5 | ARDUINO_PORT = /dev/ttyUSB0 6 | 7 | # Default to uploading 8 | #upload: 9 | 10 | include $(ARDMK_DIR)/arduino-mk/Arduino.mk 11 | 12 | CXXFLAGS+=-std=c++11 -flto 13 | LDFLAGS+=-flto 14 | 15 | # Make sure the tty is freed before starting an upload 16 | # This probably does not work with a clean Arduino-mk, but needs some 17 | # minor patches 18 | do_upload: size free_tty 19 | 20 | TTY_PIDS=$(shell lsof -t $(ARDUINO_PORT)) 21 | 22 | free_tty: 23 | [ -z "$(TTY_PIDS)" ] || (kill $(TTY_PIDS) && sleep 3) 24 | 25 | .PHONY: free_tty 26 | 27 | -------------------------------------------------------------------------------- /Max.h: -------------------------------------------------------------------------------- 1 | #ifndef __MAX_H 2 | #define __MAX_H 3 | 4 | // Control a relay on this pin (undef to disable) 5 | #define KETTLE_RELAY_PIN 4 6 | 7 | // Enable the LCD display (undef to disable) 8 | #define LCD_I2C 9 | 10 | // Enable the ethernet server (undef to disable) 11 | #define ETHERNET 12 | 13 | #define ETHERNET_MAC { 0x90, 0xA2, 0xDA, 0x0D, 0xb5, 0x82 } 14 | 15 | /* String stored in Flash. Type helps the Print class to autoload the 16 | * string during printing. */ 17 | typedef __FlashStringHelper FlashString; 18 | 19 | #endif // __MAX_H 20 | 21 | /* vim: set sw=2 sts=2 expandtab: */ 22 | -------------------------------------------------------------------------------- /Max.ino: -------------------------------------------------------------------------------- 1 | #include "Max.h" 2 | 3 | #include 4 | #ifdef ETHERNET 5 | #include 6 | #endif 7 | 8 | #ifdef LCD_I2C 9 | #include 10 | #endif // LCD_I2C 11 | 12 | #include "Crc.h" 13 | #include "Util.h" 14 | #include "MaxRF22.h" 15 | #include "MaxRFProto.h" 16 | #include "Pn9.h" 17 | 18 | static_assert(PN9_LEN >= RF22_MAX_MESSAGE_LEN, "Not enough pn9 bytes defined"); 19 | 20 | MaxRF22 rf(9); 21 | 22 | #ifdef LCD_I2C 23 | #define LCD_ADDR 0x20 24 | #define LCD_COLS 20 25 | #define LCD_ROWS 4 26 | LiquidCrystal_I2C lcd(LCD_ADDR, LCD_COLS, LCD_ROWS); // set the LCD address to 0x27 for a 20 chars and 4 line display 27 | #else 28 | /* Define the lcd object as a bottomless pit for prints. */ 29 | Null lcd; 30 | #endif // LCD_I2C 31 | 32 | #ifdef KETTLE_RELAY_PIN 33 | bool kettle_status; 34 | #endif // KETTLE_RELAY_PIN 35 | 36 | 37 | #ifdef ETHERNET 38 | EthernetServer server = EthernetServer(1234); //port 80 39 | 40 | DoublePrint p = (Serial & server); 41 | #else 42 | Print &p = Serial; 43 | #endif 44 | 45 | void printStatus() { 46 | #ifdef LCD_I2C 47 | int row = LCD_ROWS - 1; 48 | lcd.clear(); 49 | #endif 50 | for (int i = 0; i < lengthof(devices); ++i) { 51 | Device *d = &devices[i]; 52 | if (!d->address) break; 53 | if (d->type != DeviceType::RADIATOR && d->type != DeviceType::WALL) continue; 54 | 55 | #ifdef LCD_I2C 56 | lcd.setCursor(0, row--); 57 | #endif 58 | 59 | if (d->name) { 60 | (p & lcd) << d->name; 61 | } else { 62 | /* Only print two bytes on the lcd to save space */ 63 | lcd << V>(d->address); 64 | p << V
(d->address); 65 | } 66 | 67 | (p & lcd) << " " << V(d->actual_temp) 68 | << "/" << V(d->set_temp); 69 | if (d->type == DeviceType::RADIATOR) 70 | (p & lcd) << " " << V(d->data.radiator.valve_pos); 71 | p << endl; 72 | } 73 | p << endl; 74 | 75 | #ifdef LCD_I2C 76 | #ifdef KETTLE_RELAY_PIN 77 | lcd.home(); 78 | 79 | (p & lcd) << F("Kettle: ") << (kettle_status ? F("On") : F("Off")); 80 | #endif // KETTLE_RELAY_PIN 81 | #endif // LCD_I2C 82 | 83 | p << endl; 84 | 85 | /* Print machine-parseable status line (to draw pretty graphs) */ 86 | p << "STATUS\t" << millis() << "\t"; 87 | for (int i = 0; i < lengthof(devices); ++i) { 88 | Device *d = &devices[i]; 89 | if (!d->address) break; 90 | if (d->type != DeviceType::RADIATOR && d->type != DeviceType::WALL) continue; 91 | 92 | p << V(d->actual_temp) << "\t" << V(d->set_temp) << "\t"; 93 | if (d->type == DeviceType::RADIATOR) 94 | p << V(d->data.radiator.valve_pos); 95 | else 96 | p << "NA"; 97 | p << "\t"; 98 | } 99 | p << (kettle_status ? "1" : "0") << endl; 100 | } 101 | 102 | #ifdef KETTLE_RELAY_PIN 103 | void switchKettle() { 104 | uint32_t total = 0; 105 | uint32_t max = 0; 106 | for (int i = 0; i < lengthof(devices); ++i) { 107 | Device *d = &devices[i]; 108 | if (!d->address) break; 109 | if (d->type != DeviceType::RADIATOR) continue; 110 | if (d->data.radiator.valve_pos == VALVE_UNKNOWN) continue; 111 | total += d->data.radiator.valve_pos; 112 | if (d->data.radiator.valve_pos > max) 113 | max = d->data.radiator.valve_pos; 114 | } 115 | 116 | /* One radiator opened fairly far can turn the kettle on by itself, or 117 | * a few radiators opened a little bit. */ 118 | kettle_status = (max > 30 || total > 40); 119 | digitalWrite(KETTLE_RELAY_PIN, kettle_status ? HIGH : LOW); 120 | } 121 | #endif // KETTLE_RELAY_PIN 122 | 123 | void dump_buffer(Print &p, uint8_t *buf, uint8_t len) { 124 | /* Dump the raw received data */ 125 | int i, j; 126 | for (i = 0; i < len; i += 16) 127 | { 128 | // Hex 129 | for (j = 0; j < 16 && i+j < len; j++) 130 | { 131 | p << V(buf[i+j]) << " "; 132 | } 133 | // Padding on last block 134 | while (j++ < 16) 135 | p << " "; 136 | 137 | p << " "; 138 | // ASCII 139 | for (j = 0; j < 16 && i+j < len; j++) 140 | p << (isprint(buf[i+j]) ? (char)buf[i+j] : '.'); 141 | p << "\r\n"; 142 | } 143 | p << "\r\n"; 144 | } 145 | 146 | void setup() 147 | { 148 | Serial.begin(115200); 149 | 150 | if (rf.init()) 151 | Serial.println(F("RF init OK")); 152 | else 153 | Serial.println(F("RF init failed")); 154 | 155 | #ifdef LCD_I2C 156 | lcd.init(); 157 | lcd.backlight(); 158 | lcd.home(); 159 | lcd.clear(); 160 | Serial.println(F("LCD init complete")); 161 | 162 | #endif // LCD_I2C 163 | 164 | #ifdef KETTLE_RELAY_PIN 165 | pinMode(KETTLE_RELAY_PIN, OUTPUT); 166 | #endif // KETTLE_RELAY_PIN 167 | 168 | #ifdef ETHERNET 169 | byte mac[] = ETHERNET_MAC; 170 | if (Ethernet.begin(mac)) 171 | p << F("IP: ") << Ethernet.localIP() << "\r\n"; 172 | else 173 | p << F("DHCP Failure") << "\r\n"; 174 | 175 | server.begin(); 176 | #endif 177 | 178 | p << F("Initialized") << "\r\n"; 179 | printStatus(); 180 | } 181 | 182 | void loop() 183 | { 184 | uint8_t buf[RF22_MAX_MESSAGE_LEN]; 185 | uint8_t len = sizeof(buf); 186 | 187 | if (Serial.read() != -1) { 188 | Serial.println("OK"); 189 | printStatus(); 190 | } 191 | 192 | #ifdef ETHERNET 193 | EthernetClient c = server.available(); 194 | if (c && c.read() != -1) { 195 | c.println("OK"); 196 | printStatus(); 197 | } 198 | #endif 199 | 200 | if (rf.recv(buf, &len)) 201 | { 202 | /* Enable reception right away again, so we won't miss the next 203 | * message while processing this one. */ 204 | rf.setModeRx(); 205 | 206 | p << F("Received ") << len << F(" bytes") << "\r\n"; 207 | 208 | dump_buffer(p, buf, len); 209 | 210 | if (len < 3) { 211 | p << F("Invalid packet length (") << len << ")" << "\r\n"; 212 | return; 213 | } 214 | 215 | /* Dewhiten data */ 216 | if (xor_pn9(buf, len) < 0) { 217 | p << F("Invalid packet length (") << len << ")" << "\r\n"; 218 | return; 219 | } 220 | 221 | p << F("Dewhitened:") << "\r\n"; 222 | dump_buffer(p, buf, len); 223 | 224 | /* Calculate CRC (but don't include the CRC itself) */ 225 | uint16_t crc = calc_crc(buf, len - 2); 226 | if (buf[len - 1] != (crc & 0xff) || buf[len - 2] != (crc >> 8)) { 227 | p << F("CRC error") << "\r\n"; 228 | return; 229 | } 230 | 231 | /* Parse the message (without length byte and CRC) */ 232 | MaxRFMessage *rfm = MaxRFMessage::parse(buf + 1, len - 3); 233 | 234 | if (rfm == NULL) { 235 | p << F("Packet is invalid") << "\r\n"; 236 | } else { 237 | p << *rfm << "\r\n"; 238 | rfm->updateState(); 239 | delete rfm; 240 | } 241 | 242 | #ifdef KETTLE_RELAY_PIN 243 | switchKettle(); 244 | #endif // KETTLE_RELAY_PIN 245 | 246 | printStatus(); 247 | 248 | #if 0 249 | #ifdef LCD_I2C 250 | /* Use the first two rows of the LCD for dumped packet data */ 251 | lcd.home(); 252 | for (i = 0; i < len && i < LCD_COLS; ++i) { 253 | if (i == LCD_COLS / 2) lcd.setCursor(0, 1); 254 | printHex(NULL, buf[i], BYTE_SIZE, false, &lcd); 255 | } 256 | #endif // LCD_I2C 257 | #endif 258 | 259 | p << "\r\n"; 260 | } 261 | } 262 | 263 | /* vim: set sw=2 sts=2 expandtab filetype=cpp: */ 264 | -------------------------------------------------------------------------------- /MaxRF22.cpp: -------------------------------------------------------------------------------- 1 | #include "MaxRF22.h" 2 | #include "Util.h" 3 | 4 | const RF22::ModemConfig config = 5 | { 6 | .reg_1c = 0x01, 7 | .reg_1f = 0x03, 8 | .reg_20 = 0x90, 9 | .reg_21 = 0x20, 10 | .reg_22 = 0x51, 11 | .reg_23 = 0xea, 12 | .reg_24 = 0x00, 13 | .reg_25 = 0x58, 14 | /* 2c - 2e are only for OOK */ 15 | .reg_2c = 0x00, 16 | .reg_2d = 0x00, 17 | .reg_2e = 0x00, 18 | .reg_58 = 0x80, /* Copied from RF22 defaults */ 19 | .reg_69 = 0x60, /* Copied from RF22 defaults */ 20 | .reg_6e = 0x08, 21 | .reg_6f = 0x31, 22 | .reg_70 = 0x24, 23 | .reg_71 = RF22_DTMOD_FIFO | RF22_MODTYP_FSK, 24 | .reg_72 = 0x1e, 25 | }; 26 | 27 | /* Sync words to send / check for. Don't forget to update RF22_SYNCLEN 28 | * below if changing the length of this array. */ 29 | const uint8_t sync_words[] = { 30 | 0xc6, 31 | 0x26, 32 | 0xc6, 33 | 0x26, 34 | }; 35 | 36 | bool MaxRF22::init() { 37 | if (!RF22::init()) 38 | return false; 39 | setModemRegisters(&config); 40 | setFrequency(868.3, 0.035); 41 | /* Disable TX packet control, since the RF22 doesn't do proper 42 | * whitening so can't read the length header or CRC. We need RX packet 43 | * control so the RF22 actually sends pkvalid interrupts when the 44 | * manually set packet length is reached. */ 45 | spiWrite(RF22_REG_30_DATA_ACCESS_CONTROL, RF22_MSBFRST | RF22_ENPACRX); 46 | /* No packet headers, 4 sync words, fixed packet length */ 47 | spiWrite(RF22_REG_32_HEADER_CONTROL1, RF22_BCEN_NONE | RF22_HDCH_NONE); 48 | spiWrite(RF22_REG_33_HEADER_CONTROL2, RF22_HDLEN_0 | RF22_FIXPKLEN | RF22_SYNCLEN_4); 49 | setSyncWords(sync_words, lengthof(sync_words)); 50 | /* Detect preamble after 4 nibbles */ 51 | spiWrite(RF22_REG_35_PREAMBLE_DETECTION_CONTROL1, (0x4 << 3)); 52 | /* Send 8 bytes of preamble */ 53 | setPreambleLength(8); // in nibbles 54 | spiWrite(RF22_REG_3E_PACKET_LENGTH, 20); 55 | return true; 56 | } 57 | 58 | /* vim: set sw=2 sts=2 expandtab: */ 59 | -------------------------------------------------------------------------------- /MaxRF22.h: -------------------------------------------------------------------------------- 1 | #ifndef __MAX_RF_22_H 2 | #define __MAX_RF_22_h 3 | 4 | #include 5 | 6 | class MaxRF22 : public RF22 { 7 | public: 8 | MaxRF22(uint8_t ss = SS, uint8_t interrupt = 0) : RF22(ss, interrupt) {} 9 | bool init(); 10 | }; 11 | 12 | #endif // __MAX_RF_22_H 13 | 14 | /* vim: set sw=2 sts=2 expandtab: */ 15 | -------------------------------------------------------------------------------- /MaxRFProto.cpp: -------------------------------------------------------------------------------- 1 | #include "MaxRFProto.h" 2 | #include "Arduino.h" 3 | 4 | /* TStreaming Formatting type to use for the field titles in print 5 | * output. */ 6 | typedef Align<16> Title; 7 | 8 | /** 9 | * Static list of known devices. 10 | */ 11 | Device devices[] = { 12 | /* Add your devices here, for example: */ 13 | //{0x00b825, DeviceType::CUBE, "cube", SET_TEMP_UNKNOWN, ACTUAL_TEMP_UNKNOWN, 0}, 14 | //{0x0298e5, DeviceType::WALL, "wall", SET_TEMP_UNKNOWN, ACTUAL_TEMP_UNKNOWN, 0}, 15 | //{0x04c8dd, DeviceType::RADIATOR, "up ", SET_TEMP_UNKNOWN, ACTUAL_TEMP_UNKNOWN, 0, {.radiator = {Mode::UNKNOWN, VALVE_UNKNOWN}}}, 16 | //{0x0131b4, DeviceType::RADIATOR, "down", SET_TEMP_UNKNOWN, ACTUAL_TEMP_UNKNOWN, 0, {.radiator = {Mode::UNKNOWN, VALVE_UNKNOWN}}}, 17 | }; 18 | 19 | /* Find or assign a device struct based on the address */ 20 | static Device *get_device(uint32_t addr, DeviceType type) { 21 | for (int i = 0; i < lengthof(devices); ++i) { 22 | /* The address is not in the list yet, assign this empty slot. */ 23 | if (devices[i].address == 0 && addr != 0) { 24 | devices[i].address = addr; 25 | devices[i].type = type; 26 | devices[i].name = NULL; 27 | } 28 | /* Found it */ 29 | if (devices[i].address == addr) 30 | return &devices[i]; 31 | } 32 | /* Not found and no slots left */ 33 | return NULL; 34 | } 35 | 36 | /* MaxRFMessage */ 37 | const FlashString *MaxRFMessage::mode_to_str(Mode mode) { 38 | switch (mode) { 39 | case Mode::AUTO: return F("auto"); 40 | case Mode::MANUAL: return F("manual"); 41 | case Mode::TEMPORARY: return F("temporary"); 42 | case Mode::BOOST: return F("boost"); 43 | default: return F(""); 44 | } 45 | }; 46 | 47 | const FlashString *MaxRFMessage::display_mode_to_str(DisplayMode display_mode) { 48 | switch(display_mode) { 49 | case DisplayMode::SET_TEMP: return F("Set temperature"); 50 | case DisplayMode::ACTUAL_TEMP: return F("Actual temperature"); 51 | default: return F(""); 52 | } 53 | }; 54 | 55 | const FlashString *MaxRFMessage::type_to_str(MessageType type) { 56 | switch(type) { 57 | case MessageType::PAIR_PING: return F("PairPing"); 58 | case MessageType::PAIR_PONG: return F("PairPong"); 59 | case MessageType::ACK: return F("Ack"); 60 | case MessageType::TIME_INFORMATION: return F("TimeInformation"); 61 | case MessageType::CONFIG_WEEK_PROFILE: return F("ConfigWeekProfile"); 62 | case MessageType::CONFIG_TEMPERATURES: return F("ConfigTemperatures"); 63 | case MessageType::CONFIG_VALVE: return F("ConfigValve"); 64 | case MessageType::ADD_LINK_PARTNER: return F("AddLinkPartner"); 65 | case MessageType::REMOVE_LINK_PARTNER: return F("RemoveLinkPartner"); 66 | case MessageType::SET_GROUP_ID: return F("SetGroupId"); 67 | case MessageType::REMOVE_GROUP_ID: return F("RemoveGroupId"); 68 | case MessageType::SHUTTER_CONTACT_STATE: return F("ShutterContactState"); 69 | case MessageType::SET_TEMPERATURE: return F("SetTemperature"); 70 | case MessageType::WALL_THERMOSTAT_STATE: return F("WallThermostatState"); 71 | case MessageType::SET_COMFORT_TEMPERATURE: return F("SetComfortTemperature"); 72 | case MessageType::SET_ECO_TEMPERATURE: return F("SetEcoTemperature"); 73 | case MessageType::PUSH_BUTTON_STATE: return F("PushButtonState"); 74 | case MessageType::THERMOSTAT_STATE: return F("ThermostatState"); 75 | case MessageType::SET_DISPLAY_ACTUAL_TEMPERATURE: return F("SetDisplayActualTemperature"); 76 | case MessageType::WAKE_UP: return F("WakeUp"); 77 | case MessageType::RESET: return F("Reset"); 78 | default: return F("Unknown"); 79 | } 80 | } 81 | 82 | DeviceType MaxRFMessage::message_type_to_sender_type(MessageType type) { 83 | switch(type) { 84 | case MessageType::WALL_THERMOSTAT_STATE: return DeviceType::WALL; 85 | case MessageType::THERMOSTAT_STATE: return DeviceType::RADIATOR; 86 | case MessageType::SET_DISPLAY_ACTUAL_TEMPERATURE: return DeviceType::CUBE; 87 | default: return DeviceType::UNKNOWN; 88 | } 89 | } 90 | 91 | MaxRFMessage *MaxRFMessage::create_message_from_type(MessageType type) { 92 | switch(type) { 93 | case MessageType::SET_TEMPERATURE: return new SetTemperatureMessage(); 94 | case MessageType::WALL_THERMOSTAT_STATE: return new WallThermostatStateMessage(); 95 | case MessageType::THERMOSTAT_STATE: return new ThermostatStateMessage(); 96 | case MessageType::SET_DISPLAY_ACTUAL_TEMPERATURE: return new SetDisplayActualTemperatureMessage(); 97 | case MessageType::ACK: return new AckMessage(); 98 | default: return new UnknownMessage(); 99 | } 100 | } 101 | 102 | MaxRFMessage *MaxRFMessage::parse(const uint8_t *buf, size_t len) { 103 | if (len < 10) 104 | return NULL; 105 | 106 | MessageType type = (MessageType)buf[2]; 107 | MaxRFMessage *m = create_message_from_type(type); 108 | 109 | m->seqnum = buf[0]; 110 | m->flags = buf[1]; 111 | m->type = type; 112 | m->addr_from = getBits(buf + 3, 0, RF_ADDR_SIZE); 113 | m->addr_to = getBits(buf + 6, 0, RF_ADDR_SIZE); 114 | m->group_id = buf[9]; 115 | 116 | m->from = get_device(m->addr_from, message_type_to_sender_type(type)); 117 | m->to = get_device(m->addr_to, DeviceType::UNKNOWN); 118 | 119 | if (m->parse_payload(buf + 10, len - 10)) 120 | return m; 121 | else { 122 | delete m; 123 | return NULL; 124 | } 125 | } 126 | 127 | size_t MaxRFMessage::printTo(Print &p) const { 128 | p << V(F("Sequence num:")) << V<Hex>(this->seqnum) << "\r\n"; 129 | p << V<Title>(F("Flags:")) << V<Hex>(this->flags) << "\r\n"; 130 | p << V<Title>(F("Packet type:")) << V<Hex>(this->type) 131 | << " (" << type_to_str(this->type) << ")" << "\r\n"; 132 | p << V<Title>(F("Packet from:")) << V<Address>(this->addr_from); 133 | if (this->from) 134 | p << " (" << this->from->name << ")"; 135 | p << "\r\n"; 136 | p << V<Title>(F("Packet to:")) << V<Address>(this->addr_to); 137 | if (this->to) 138 | p << " (" << this->to->name << ")"; 139 | p << "\r\n"; 140 | p << V<Title>(F("Group id:")) << V<Hex>(this->group_id) << "\r\n"; 141 | 142 | return 0; /* XXX */ 143 | } 144 | 145 | void MaxRFMessage::updateState() { 146 | /* Nothing to do */ 147 | } 148 | 149 | /* UnknownMessage */ 150 | bool UnknownMessage::parse_payload(const uint8_t *buf, size_t len) { 151 | this->payload = buf; 152 | this->payload_len = len; 153 | return true; 154 | } 155 | 156 | size_t UnknownMessage::printTo(Print &p) const{ 157 | MaxRFMessage::printTo(p); 158 | p << V<Title>(F("Payload:")) 159 | << V<Array<Hex, TChar<' '>>>(this->payload, this->payload_len) 160 | << "\r\n"; 161 | } 162 | 163 | /* SetTemperatureMessage */ 164 | 165 | bool SetTemperatureMessage::parse_payload(const uint8_t *buf, size_t len) { 166 | if (len < 1) 167 | return false; 168 | 169 | this->set_temp = buf[0] & 0x3f; 170 | this->mode = (Mode) ((buf[0] >> 6) & 0x3); 171 | 172 | if (len >= 4) 173 | this->until = new UntilTime(buf + 1); 174 | else 175 | this->until = NULL; 176 | 177 | return true; 178 | } 179 | 180 | size_t SetTemperatureMessage::printTo(Print &p) const{ 181 | MaxRFMessage::printTo(p); 182 | p << V<Title>(F("Mode:")) << mode_to_str(this->mode) << "\r\n"; 183 | p << V<Title>(F("Set temp:")) << V<SetTemp>(this->set_temp) << "\r\n"; 184 | if (this->until) { 185 | p << V<Title>(F("Until:")) << *(this->until) << "\r\n"; 186 | } 187 | 188 | return 0; /* XXX */ 189 | } 190 | 191 | /* WallThermostatStateMessage */ 192 | 193 | bool WallThermostatStateMessage::parse_payload(const uint8_t *buf, size_t len) { 194 | if (len < 2) 195 | return false; 196 | 197 | this->set_temp = buf[0] & 0x7f; 198 | this->actual_temp = ((buf[0] & 0x80) << 1) | buf[1]; 199 | /* Note that mode and until time are not in this message */ 200 | 201 | return true; 202 | } 203 | 204 | size_t WallThermostatStateMessage::printTo(Print &p) const { 205 | MaxRFMessage::printTo(p); 206 | p << V<Title>(F("Set temp:")) << V<SetTemp>(this->set_temp) << "\r\n"; 207 | p << V<Title>(F("Actual temp:")) << V<ActualTemp>(this->actual_temp) << "\r\n"; 208 | 209 | return 0; /* XXX */ 210 | } 211 | 212 | void WallThermostatStateMessage::updateState() { 213 | MaxRFMessage::updateState(); 214 | this->from->set_temp = this->set_temp; 215 | this->from->actual_temp = this->actual_temp; 216 | this->from->actual_temp_time = millis(); 217 | } 218 | 219 | /* ThermostatStateMessage */ 220 | 221 | bool ThermostatStateMessage::parse_payload(const uint8_t *buf, size_t len) { 222 | if (len < 3) 223 | return false; 224 | 225 | this->mode = (Mode) (buf[0] & 0x3); 226 | this->dst = (buf[0] >> 2) & 0x1; 227 | this->locked = (buf[0] >> 5) & 0x1; 228 | this->battery_low = (buf[0] >> 7) & 0x1; 229 | this->valve_pos = buf[1]; 230 | this->set_temp = buf[2]; 231 | 232 | this->actual_temp = 0; 233 | if (this->mode != Mode::TEMPORARY && len >= 5) 234 | this->actual_temp = ((buf[3] & 0x1) << 8) + buf[4]; 235 | 236 | this->until = NULL; 237 | if (this->mode == Mode::TEMPORARY && len >= 6) 238 | this->until = new UntilTime(buf + 3); 239 | 240 | return true; 241 | } 242 | 243 | size_t ThermostatStateMessage::printTo(Print &p) const { 244 | MaxRFMessage::printTo(p); 245 | p << V<Title>(F("Mode:")) << mode_to_str(this->mode) << "\r\n"; 246 | p << V<Title>(F("Adjust to DST:")) << this->dst << "\r\n"; 247 | p << V<Title>(F("Locked:")) << this->locked << "\r\n"; 248 | p << V<Title>(F("Battery Low:")) << this->battery_low << "\r\n"; 249 | p << V<Title>(F("Valve position:")) << this->valve_pos << "%" << "\r\n"; 250 | p << V<Title>(F("Set temp:")) << V<SetTemp>(this->set_temp) << "\r\n"; 251 | 252 | if (this->actual_temp) 253 | p << V<Title>(F("Actual temp:")) << V<ActualTemp>(this->actual_temp) << "\r\n"; 254 | 255 | if (this->until) 256 | p << V<Title>(F("Until:")) << *(this->until) << "\r\n"; 257 | 258 | return 0; /* XXX */ 259 | } 260 | 261 | void ThermostatStateMessage::updateState() { 262 | this->from->set_temp = this->set_temp; 263 | this->from->data.radiator.valve_pos = this->valve_pos; 264 | if (this->actual_temp) { 265 | this->from->actual_temp = this->actual_temp; 266 | this->from->actual_temp_time = millis(); 267 | } 268 | } 269 | 270 | /* SetDisplayActualTemperatureMessage */ 271 | bool SetDisplayActualTemperatureMessage::parse_payload(const uint8_t *buf, size_t len) { 272 | if (len < 1) 273 | return NULL; 274 | this->display_mode = (DisplayMode) ((buf[0] >> 2) & 0x1); 275 | return true; 276 | } 277 | 278 | size_t SetDisplayActualTemperatureMessage::printTo(Print &p) const{ 279 | MaxRFMessage::printTo(p); 280 | p << V<Title>(F("Display mode:")) << display_mode_to_str(this->display_mode) << "\r\n"; 281 | } 282 | 283 | /* AckMessage */ 284 | bool AckMessage::parse_payload(const uint8_t *buf, size_t len) { 285 | if (len < 4) 286 | return false; 287 | 288 | /* XXX: Perhaps buf[0] == 0x01 can be used here instead? */ 289 | if (this->from && this->from->type == DeviceType::RADIATOR) { 290 | /* We only know about packet formats sent by radiators yet */ 291 | 292 | this->mode = (Mode) (buf[1] & 0x3); 293 | this->dst = (buf[1] >> 2) & 0x1; 294 | /* The locked and battery_low bits are unconfirmed, but they probably 295 | * match the RadiatorThermostateStateMessage. */ 296 | this->locked = (buf[1] >> 5) & 0x1; 297 | this->battery_low = (buf[1] >> 7) & 0x1; 298 | this->valve_pos = buf[2]; 299 | this->set_temp = buf[3]; 300 | 301 | this->until = NULL; 302 | if (this->mode == Mode::TEMPORARY && len >= 7) 303 | this->until = new UntilTime(buf + 4); 304 | } 305 | 306 | return true; 307 | } 308 | 309 | size_t AckMessage::printTo(Print &p) const { 310 | MaxRFMessage::printTo(p); 311 | if (this->from && this->from->type == DeviceType::RADIATOR) { 312 | p << V<Title>(F("Mode:")) << mode_to_str(this->mode) << "\r\n"; 313 | p << V<Title>(F("Adjust to DST:")) << this->dst << "\r\n"; 314 | p << V<Title>(F("Locked:")) << this->locked << "\r\n"; 315 | p << V<Title>(F("Battery Low:")) << this->battery_low << "\r\n"; 316 | p << V<Title>(F("Valve position:")) << this->valve_pos << "%" << "\r\n"; 317 | p << V<Title>(F("Set temp:")) << V<SetTemp>(this->set_temp) << "\r\n"; 318 | 319 | if (this->until) 320 | p << V<Title>(F("Until:")) << *(this->until) << "\r\n"; 321 | } 322 | 323 | return 0; /* XXX */ 324 | } 325 | 326 | void AckMessage::updateState() { 327 | if (this->from && this->from->type == DeviceType::RADIATOR) { 328 | this->from->set_temp = this->set_temp; 329 | this->from->data.radiator.valve_pos = this->valve_pos; 330 | } 331 | } 332 | 333 | /* UntilTime */ 334 | 335 | UntilTime::UntilTime(const uint8_t *buf) { 336 | this->year = buf[1] & 0x3f; 337 | this->month = ((buf[0] & 0xE0) >> 4) | (buf[1] >> 7); 338 | this->day = buf[0] & 0x1f; 339 | this->time = buf[2] & 0x3f; 340 | } 341 | 342 | size_t UntilTime::printTo(Print &p) const { 343 | p << "20" << V<Number<2>>(this->year) << "." << V<Number<2>>(this->month) << "." << V<Number<2>>(this->day); 344 | p << " " << V<Number<2>>(this->time / 2) << (this->time % 2 ? ":30" : ":00"); 345 | } 346 | 347 | 348 | /* 349 | Sequence num: E4 350 | Flags: 04 351 | Packet type: 70 (Unknown) 352 | Packet from: 0298E5 353 | Packet to: 000000 354 | Group id: 00 355 | Payload: 19 04 2A 00 CD 356 | 357 | 19: DST switch, mode = auto 358 | 0:1 mode 359 | 2 DST switch 360 | 5 ?? 361 | 04: Display mode? 362 | 2A: set temp (21°) 363 | 00 CD: Actual temp (20.5°) 364 | 365 | Sequence num: 9C 366 | Flags: 04 367 | Packet type: 70 (Unknown) 368 | Packet from: 0298E5 369 | Packet to: 000000 370 | Group id: 00 371 | Payload: 12 04 24 48 0D 1B 372 | 373 | 19: DST switch, mode = temporary 374 | 0:1 mode 375 | 2 DST switch 376 | 5 ?? 377 | 04: Display mode? 378 | 24: set temp (18°) 379 | 48 0D 1B: until time 380 | 381 | Perhaps 70 is really WallThermostatState and the curren WallThermostatState is 382 | more of a "update temp" message? It seems 70 is sent when the SetTemp of a WT 383 | changes. 384 | 385 | */ 386 | 387 | /* 388 | Set DST adjust 389 | Sequence num: E5 390 | Flags: 00 391 | Packet type: 81 (Unknown) 392 | Packet from: 00B825 393 | Packet to: 0298E5 394 | Group id: 00 395 | Payload: 00 396 | 00: Disable 397 | 01: Enable 398 | 399 | Sent to radiator thermostats only? 400 | 401 | */ 402 | 403 | /* 404 | 405 | Ack from radiator thermostat: 406 | 407 | Sequence num: 2C 408 | Flags: 02 409 | Packet type: 02 (Ack) 410 | Packet from: 0298E5 411 | Packet to: 04C8DD 412 | Group id: 00 413 | Payload: 01 11 00 28 414 | 01: 1 == more data? 0 == no data?? 415 | 11: flags, same as 11/19 in type 70? 416 | 00: Valve position / displaymode flags 417 | 28: Set temp 418 | 419 | Sequence num: 1B 420 | Flags: 02 421 | Packet type: 02 (Ack) 422 | Packet from: 0298E5 423 | Packet to: 00B825 424 | Group id: 00 425 | Payload: 01 12 04 24 48 0D 1B 426 | 427 | 01: 1 == more data? 0 == no data?? 428 | 11: flags, same as 11/19 in type 70? x2 == temporary 429 | 00: Valve position / displaymode flags 430 | 24: Set temp (18.0°) 431 | 48 0D 1B: Until time 432 | 433 | 434 | Ack from wall thermostat to SetTemperature: 435 | 436 | Sequence num: 4F 437 | Flags: 00 438 | Packet type: 02 (Ack) 439 | Packet from: 00B825 440 | Packet to: 0298E5 441 | Group id: 00 442 | Payload: 00 443 | 444 | 00: ??? 445 | */ 446 | 447 | /* vim: set sw=2 sts=2 expandtab: */ 448 | -------------------------------------------------------------------------------- /MaxRFProto.h: -------------------------------------------------------------------------------- 1 | #ifndef __MAX_RF_PROTO_H 2 | #define __MAX_RF_PROTO_H 3 | 4 | #include <stdint.h> 5 | #include <Print.h> 6 | #include <TStreaming.h> 7 | 8 | #include "Max.h" 9 | #include "Util.h" 10 | 11 | const size_t RF_ADDR_SIZE = 24; 12 | const uint16_t ACTUAL_TEMP_UNKNOWN = 0xffff; 13 | const uint8_t SET_TEMP_UNKNOWN = 0xff; 14 | const uint8_t VALVE_UNKNOWN = 0xff; 15 | 16 | // Constant string with external linkage, so it can be passed as a 17 | // template param 18 | constexpr const char na[] = "NA"; 19 | 20 | typedef HexBits<RF_ADDR_SIZE> Address; 21 | typedef SpecialValue<Fixed<10, 1>, 22 | TInt<ACTUAL_TEMP_UNKNOWN>, 23 | TStr<na>> 24 | ActualTemp; 25 | typedef SpecialValue<Fixed<2, 1>, 26 | TInt<SET_TEMP_UNKNOWN>, 27 | TStr<na>> 28 | SetTemp; 29 | typedef SpecialValue<Postfix<NoFormat, TChar<'%'>>, 30 | TInt<VALVE_UNKNOWN>, 31 | TStr<na>> 32 | ValvePos; 33 | 34 | enum class Mode : uint8_t {AUTO, MANUAL, TEMPORARY, BOOST, UNKNOWN}; 35 | enum class DisplayMode : uint8_t {SET_TEMP, ACTUAL_TEMP}; 36 | 37 | enum class DeviceType : uint8_t {UNKNOWN, CUBE, WALL, RADIATOR}; 38 | 39 | enum class MessageType : uint8_t { 40 | PAIR_PING = 0x00, 41 | PAIR_PONG = 0x01, 42 | ACK = 0x02, 43 | TIME_INFORMATION = 0x03, 44 | CONFIG_WEEK_PROFILE = 0x10, 45 | CONFIG_TEMPERATURES = 0x11, 46 | CONFIG_VALVE = 0x12, 47 | ADD_LINK_PARTNER = 0x20, 48 | REMOVE_LINK_PARTNER = 0x21, 49 | SET_GROUP_ID = 0x22, 50 | REMOVE_GROUP_ID = 0x23, 51 | SHUTTER_CONTACT_STATE = 0x30, 52 | SET_TEMPERATURE = 0x40, 53 | WALL_THERMOSTAT_STATE = 0x42, 54 | SET_COMFORT_TEMPERATURE = 0x43, 55 | SET_ECO_TEMPERATURE = 0x44, 56 | PUSH_BUTTON_STATE = 0x50, 57 | THERMOSTAT_STATE = 0x60, 58 | SET_DISPLAY_ACTUAL_TEMPERATURE = 0x82, 59 | WAKE_UP = 0xF1, 60 | RESET = 0xF0, 61 | }; 62 | 63 | /** 64 | * Current state for a specific device. 65 | */ 66 | class Device { 67 | public: 68 | uint32_t address; 69 | DeviceType type; 70 | const char *name; 71 | uint8_t set_temp; /* In 0.5° increments */ 72 | uint16_t actual_temp; /* In 0.1° increments */ 73 | unsigned long actual_temp_time; /* When was the actual_temp last updated */ 74 | union { 75 | struct { 76 | Mode mode; 77 | uint8_t valve_pos; /* 0-64 (inclusive) */ 78 | } radiator; 79 | 80 | struct { 81 | } wall; 82 | } data; 83 | }; 84 | 85 | /* 86 | * Known devices, terminated with a NULL entry. 87 | */ 88 | extern Device devices[6]; 89 | 90 | class UntilTime : public Printable { 91 | public: 92 | /* Parse an until time from three bytes from an RF packet */ 93 | UntilTime(const uint8_t *buf); 94 | 95 | virtual size_t printTo(Print &p) const; 96 | 97 | uint8_t year, month, day; 98 | 99 | /* In 30-minute increments */ 100 | uint8_t time; 101 | }; 102 | 103 | 104 | class MaxRFMessage : public Printable { 105 | public: 106 | /** 107 | * Parse a RF message. Buffer should contain only headers and 108 | * payload (so no length byte and no CRC). 109 | * 110 | * Note that the message might keep a reference to the buffer around 111 | * to prevent unnecessary copies! 112 | */ 113 | static MaxRFMessage *parse(const uint8_t *buf, size_t len); 114 | 115 | /** 116 | * Returns a string describing a given message type. 117 | */ 118 | static const FlashString *type_to_str(MessageType type); 119 | static const FlashString *mode_to_str(Mode mode); 120 | static const FlashString *display_mode_to_str(DisplayMode display_mode); 121 | 122 | virtual size_t printTo(Print &p) const; 123 | 124 | /** 125 | * Update any device states that can be derived from this message. 126 | */ 127 | virtual void updateState(); 128 | 129 | uint8_t seqnum; 130 | uint8_t flags; 131 | MessageType type; 132 | uint32_t addr_from; 133 | uint32_t addr_to; 134 | uint8_t group_id; 135 | 136 | 137 | /* The devices adressed, or NULL for broadcast messages or unknown 138 | * devices. */ 139 | Device *from; 140 | Device *to; 141 | 142 | virtual ~MaxRFMessage() {} 143 | private: 144 | static MaxRFMessage *create_message_from_type(MessageType type); 145 | static DeviceType message_type_to_sender_type(MessageType type); 146 | virtual bool parse_payload(const uint8_t *buf, size_t len) = 0; 147 | }; 148 | 149 | class UnknownMessage : public MaxRFMessage { 150 | public: 151 | virtual bool parse_payload(const uint8_t *buf, size_t len); 152 | virtual size_t printTo(Print &p) const; 153 | 154 | /** 155 | * The raw data of the message (excluding headers). 156 | * Shouldn't be freed, since this is a reference into the buffer 157 | * passed to parse. 158 | */ 159 | const uint8_t* payload; 160 | size_t payload_len; 161 | }; 162 | 163 | class SetTemperatureMessage : public MaxRFMessage { 164 | public: 165 | virtual bool parse_payload(const uint8_t *buf, size_t len); 166 | virtual size_t printTo(Print &p) const; 167 | 168 | uint8_t set_temp; /* In 0.5° units */ 169 | Mode mode; 170 | 171 | UntilTime *until; /* Only when mode is MODE_TEMPORARY */ 172 | 173 | virtual ~SetTemperatureMessage() {delete this->until; } 174 | }; 175 | 176 | class WallThermostatStateMessage : public MaxRFMessage { 177 | public: 178 | virtual bool parse_payload(const uint8_t *buf, size_t len); 179 | virtual size_t printTo(Print &p) const; 180 | virtual void updateState(); 181 | 182 | uint16_t actual_temp; /* In 0.1° units */ 183 | uint8_t set_temp; /* In 0.5° units */ 184 | }; 185 | 186 | class ThermostatStateMessage : public MaxRFMessage { 187 | public: 188 | virtual bool parse_payload(const uint8_t *buf, size_t len); 189 | virtual size_t printTo(Print &p) const; 190 | virtual void updateState(); 191 | 192 | bool dst; 193 | bool locked; 194 | bool battery_low; 195 | Mode mode; 196 | uint8_t valve_pos; /* In percent */ 197 | uint8_t set_temp; /* In 0.5° units */ 198 | uint8_t actual_temp; /* In 0.1° units, 0 when not present */ 199 | UntilTime *until; /* Only when mode is MODE_TEMPORARY */ 200 | virtual ~ThermostatStateMessage() {delete this->until; } 201 | }; 202 | 203 | class SetDisplayActualTemperatureMessage : public MaxRFMessage { 204 | public: 205 | virtual bool parse_payload(const uint8_t *buf, size_t len); 206 | virtual size_t printTo(Print &p) const; 207 | 208 | DisplayMode display_mode; 209 | }; 210 | 211 | class AckMessage : public MaxRFMessage { 212 | public: 213 | virtual bool parse_payload(const uint8_t *buf, size_t len); 214 | virtual size_t printTo(Print &p) const; 215 | virtual void updateState(); 216 | 217 | bool dst; 218 | bool locked; 219 | bool battery_low; 220 | Mode mode; 221 | uint8_t valve_pos; /* In percent */ 222 | uint8_t set_temp; /* In 0.5° units */ 223 | UntilTime *until; /* Only when mode is MODE_TEMPORARY */ 224 | virtual ~AckMessage() {delete this->until; } 225 | }; 226 | 227 | #endif // __MAX_RF_PROTO_H 228 | 229 | /* vim: set sw=2 sts=2 expandtab: */ 230 | -------------------------------------------------------------------------------- /Pn9.cpp: -------------------------------------------------------------------------------- 1 | #include <stdint.h> 2 | #include <stddef.h> 3 | #include <avr/pgmspace.h> 4 | 5 | #include "Pn9.h" 6 | #include "Util.h" 7 | 8 | /* First 255 bytes of PN9 sequence used for data whitening by the CC1101 9 | * chip. The RF22 chip is documented to support the same data whitening 10 | * algorithm, but in practice seems to use a different sequence. 11 | * 12 | * Data was generated using the following python snippet: 13 | * 14 | import itertools 15 | def pn9(state): 16 | while True: 17 | yield hex(state & 0xff) 18 | # The pn9 generator is clocked 8 times while shifting in the 19 | # next data byte 20 | for i in range(8): 21 | state = (state >> 1) + (((state & 1) ^ (state >> 5) & 1) << 8) 22 | print(list(itertools.islice(pn9(0x1ff), 255))) 23 | */ 24 | static const uint8_t PROGMEM pn9_table[] = { 25 | 0xff, 0xe1, 0x1d, 0x9a, 0xed, 0x85, 0x33, 0x24, 26 | 0xea, 0x7a, 0xd2, 0x39, 0x70, 0x97, 0x57, 0x0a, 27 | 0x54, 0x7d, 0x2d, 0xd8, 0x6d, 0x0d, 0xba, 0x8f, 28 | 0x67, 0x59, 0xc7, 0xa2, 0xbf, 0x34, 0xca, 0x18, 29 | 0x30, 0x53, 0x93, 0xdf, 0x92, 0xec, 0xa7, 0x15, 30 | 0x8a, 0xdc, 0xf4, 0x86, 0x55, 0x4e, 0x18, 0x21, 31 | 0x40, 0xc4, /* 0xc4, 0xd5, 0xc6, 0x91, 0x8a, 0xcd, 32 | 0xe7, 0xd1, 0x4e, 0x09, 0x32, 0x17, 0xdf, 0x83, 33 | 0xff, 0xf0, 0x0e, 0xcd, 0xf6, 0xc2, 0x19, 0x12, 34 | 0x75, 0x3d, 0xe9, 0x1c, 0xb8, 0xcb, 0x2b, 0x05, 35 | 0xaa, 0xbe, 0x16, 0xec, 0xb6, 0x06, 0xdd, 0xc7, 36 | 0xb3, 0xac, 0x63, 0xd1, 0x5f, 0x1a, 0x65, 0x0c, 37 | 0x98, 0xa9, 0xc9, 0x6f, 0x49, 0xf6, 0xd3, 0x0a, 38 | 0x45, 0x6e, 0x7a, 0xc3, 0x2a, 0x27, 0x8c, 0x10, 39 | 0x20, 0x62, 0xe2, 0x6a, 0xe3, 0x48, 0xc5, 0xe6, 40 | 0xf3, 0x68, 0xa7, 0x04, 0x99, 0x8b, 0xef, 0xc1, 41 | 0x7f, 0x78, 0x87, 0x66, 0x7b, 0xe1, 0x0c, 0x89, 42 | 0xba, 0x9e, 0x74, 0x0e, 0xdc, 0xe5, 0x95, 0x02, 43 | 0x55, 0x5f, 0x0b, 0x76, 0x5b, 0x83, 0xee, 0xe3, 44 | 0x59, 0xd6, 0xb1, 0xe8, 0x2f, 0x8d, 0x32, 0x06, 45 | 0xcc, 0xd4, 0xe4, 0xb7, 0x24, 0xfb, 0x69, 0x85, 46 | 0x22, 0x37, 0xbd, 0x61, 0x95, 0x13, 0x46, 0x08, 47 | 0x10, 0x31, 0x71, 0xb5, 0x71, 0xa4, 0x62, 0xf3, 48 | 0x79, 0xb4, 0x53, 0x82, 0xcc, 0xc5, 0xf7, 0xe0, 49 | 0x3f, 0xbc, 0x43, 0xb3, 0xbd, 0x70, 0x86, 0x44, 50 | 0x5d, 0x4f, 0x3a, 0x07, 0xee, 0xf2, 0x4a, 0x81, 51 | 0xaa, 0xaf, 0x05, 0xbb, 0xad, 0x41, 0xf7, 0xf1, 52 | 0x2c, 0xeb, 0x58, 0xf4, 0x97, 0x46, 0x19, 0x03, 53 | 0x66, 0x6a, 0xf2, 0x5b, 0x92, 0xfd, 0xb4, 0x42, 54 | 0x91, 0x9b, 0xde, 0xb0, 0xca, 0x09, 0x23, 0x04, 55 | 0x88, 0x98, 0xb8, 0xda, 0x38, 0x52, 0xb1, 0xf9, 56 | 0x3c, 0xda, 0x29, 0x41, 0xe6, 0xe2, 0x7b 57 | */ 58 | }; 59 | 60 | static_assert(lengthof(pn9_table) == PN9_LEN, "Not exactly PN9_LEN pn9 bytes defined"); 61 | 62 | int xor_pn9(uint8_t *buf, size_t len) { 63 | if (len > sizeof(pn9_table)) 64 | return -1; 65 | 66 | for (int i = 0; i < len; ++i) 67 | buf[i] ^= pgm_read_byte(&pn9_table[i]); 68 | 69 | return 0; 70 | } 71 | 72 | /* vim: set sw=2 sts=2 expandtab filetype=cpp: */ 73 | -------------------------------------------------------------------------------- /Pn9.h: -------------------------------------------------------------------------------- 1 | 2 | /* How many PN9 bytes are included in the lookuptable. When changing 3 | * this, also add the extra bytes in Pn9.cpp. */ 4 | #define PN9_LEN 50 5 | 6 | /** 7 | * Xor the first len bytes in buf with the PN9 sequence. 8 | * 9 | * Returns 0 if succesful or -1 if the buffer is longer than PN9_LEN. 10 | */ 11 | int xor_pn9(uint8_t *buf, size_t len); 12 | 13 | /* vim: set sw=2 sts=2 expandtab: */ 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ELV Max! Arduino sketch 2 | ----------------------- 3 | This sketch allows an Arduino to listen to messages sent by devices in 4 | the ELV Max! heating control system. 5 | 6 | The ELV Max! system allows automatic control of radiator valves, but 7 | misses a central control of the boiler, it assumes that the hot water 8 | supply is always on. 9 | 10 | This sketch allows the Arduino to listen to the messages from the ELV 11 | Max! devices and switches on the boiler (through a relay) when there is 12 | (sufficient) heat demand in the radiator valves. 13 | 14 | It also serves as a debug tool to display all received packets, which 15 | helps to reverse engineer the RF protocol. 16 | 17 | Finally, it can log all status updates it receives, so you can use other 18 | tools to draw graphs or keep other statistics. 19 | 20 | Debug and logging output is presented over serial, but can also be sent 21 | through TCP when an Arduino Ethernet or Ethernet shield is used. 22 | 23 | This tool is still a work in progress. 24 | 25 | Compiling 26 | --------- 27 | This sketch uses the RF22 library for the RF communication. However, the 28 | RF22 hardware is not perfectly suited to communicate with the ELV Max!, 29 | so it needs some patching. The patched library is available here: 30 | 31 | https://github.com/matthijskooijman/RF22 32 | 33 | Furthermore, the sketch uses the TStreaming library to get more consise 34 | printing and formatting, which can be found at: 35 | 36 | https://github.com/matthijskooijman/TStreaming 37 | 38 | 39 | Finally, the TStreaming library and parts of the sketch are programmed 40 | using new C++ features, from the C++11 standard. This requires the 41 | program to be compiled using the `-std=c++11` gcc option. Since the 42 | Arduino IDE does not support passing custom options to gcc, this sketch 43 | cannot be compiled using the Arduino IDE currently. 44 | 45 | 46 | The recommended way of compiling this sketch is to use the 47 | Arduino-mk Makefile, available at: 48 | 49 | https://github.com/mjoldfield/Arduino-Makefile 50 | 51 | This sketch ships a Makefile that includes files from the above 52 | repository to do its work. It assumes you have a `$(ARDMK_DIR)` variable 53 | in your shell that points to a checkout of the above repository. 54 | 55 | If you have that, run `make` to compile the sketch, `make size` to get a 56 | memory usage report and `make upload` to upload the sketch. 57 | 58 | Known devices 59 | ------------- 60 | Inside MaxRFProto.cpp, there is a hardcoded list of known devices, of 61 | which state is kept. Leaving the list empty will just add any devices 62 | when a message from or to them is reveived (up to a number of devices 63 | hardcoded in MaxRFProto.h). Adding devices to the list helps to give 64 | them a name and let the code know about the device type (which cannot 65 | always be determined automically). 66 | 67 | License 68 | ------- 69 | This code is licensed under the MIT license: 70 | 71 | Copyright (c) 2013 Matthijs Kooijman <matthijs@stdin.nl> 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy 74 | of this software and associated documentation files (the "Software"), to deal 75 | in the Software without restriction, including without limitation the rights 76 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77 | copies of the Software, and to permit persons to whom the Software is 78 | furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in 81 | all copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 89 | THE SOFTWARE. 90 | -------------------------------------------------------------------------------- /Util.cpp: -------------------------------------------------------------------------------- 1 | #include "Util.h" 2 | 3 | uint32_t getBits(const uint8_t *buf, uint8_t start_bit, uint8_t num_bits) { 4 | uint32_t res = 0; 5 | while (num_bits) { 6 | /* Take min(8,num_bits) bits starting at start_bit */ 7 | uint8_t mask = 0xff; 8 | if (num_bits < 8) 9 | mask <<= (8 - num_bits); 10 | if (start_bit) 11 | mask >>= start_bit; 12 | 13 | /* Select the right bits */ 14 | uint32_t select = ((uint32_t)*buf) & mask; 15 | 16 | /* Make sure the bits get shifted to the right spot in the final 17 | * result. Unfortunately we can't shift by a negative amount, so 18 | * we need this if. */ 19 | if (num_bits > 8) 20 | res += select << (num_bits - 8); 21 | else 22 | res += select >> (8 - num_bits); 23 | 24 | num_bits -= (8 - start_bit); 25 | start_bit = 0; 26 | buf++; 27 | } 28 | return res; 29 | } 30 | 31 | /* vim: set sw=2 sts=2 expandtab: */ 32 | -------------------------------------------------------------------------------- /Util.h: -------------------------------------------------------------------------------- 1 | #ifndef __MAX_UTIL_H 2 | #define __MAX_UTIL_H 3 | 4 | #include <stdint.h> 5 | 6 | /** 7 | * Get a number of bits from the given buffer, optionally skipping a few 8 | * bits at the start. 9 | */ 10 | uint32_t getBits(const uint8_t *buf, uint8_t start_bit, uint8_t num_bits); 11 | 12 | #define lengthof(x) (sizeof(x) / sizeof(*x)) 13 | 14 | #endif // __MAX_UTIL_H 15 | 16 | /* vim: set sw=2 sts=2 expandtab: */ 17 | --------------------------------------------------------------------------------