├── .gitignore ├── BaiMon.ino ├── EBus-adapter.pdf ├── LICENSE ├── README.md └── local_config.h.in /.gitignore: -------------------------------------------------------------------------------- 1 | /local_config.h 2 | -------------------------------------------------------------------------------- /BaiMon.ino: -------------------------------------------------------------------------------- 1 | // BaiMon - Vaillant TurboTec family boiler monitor 2 | // 3 | // 4 | 5 | #include "esp8266_peri.h" 6 | #include "uart_register.h" 7 | 8 | extern "C" { 9 | #include "user_interface.h" 10 | } 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "local_config.h" 18 | 19 | ///////////////////////////////////////////////////////////// 20 | // Defines 21 | 22 | #define BAIMON_VERSION "1.5" 23 | 24 | #define EBUS_SLAVE_ADDR(MASTER) ((MASTER)+5) 25 | 26 | enum // EBus addresses 27 | { 28 | EBUS_ADDR_HOST = CONFIG_EBUS_ADDR_HOST, 29 | EBUS_ADDR_BOILER = EBUS_SLAVE_ADDR(CONFIG_EBUS_ADDR_BOILER), 30 | }; 31 | 32 | ///////////////////////////////////////////////////////////// 33 | // History byffer sizes 34 | 35 | // EBus data history buffer size 36 | #define SERIAL_BUFFER_SIZE 0x1000 37 | // Measurement parameter history size 38 | #define MAX_PARM_HISTORY 64 39 | 40 | // Web page parameters 41 | // Show parameter measurements on page (<=MAX_PARM_HISTORY) 42 | #define WEB_PARM_HISTORY 25 43 | // Size of traffic data shown on diagnostics page (<=SERIAL_BUFFER_SIZE) 44 | #define WEB_DIAG_DATA_SIZE 0x400 45 | 46 | // Failed commands history size 47 | #define FAILED_COMMAND_HISTORY_SIZE 4 48 | // Max number of bytes to collect for failed command 49 | #define FAILED_COMMAND_BYTES_MAX 128 50 | // Number of bytes before command to display ( sizeof(m_reqData)-2) 184 | return; 185 | if (b == 0xAA || b == 0xA9) 186 | { 187 | m_reqData[m_reqSize++] = 0xA9; 188 | m_reqData[m_reqSize++] = (b == 0xA9) ? 0 : 1; 189 | } 190 | else 191 | { 192 | m_reqData[m_reqSize++] = b; 193 | } 194 | } 195 | 196 | void ReqFinish() 197 | { 198 | ReqPut(Crc8Buf(m_reqData,m_reqSize)); 199 | } 200 | 201 | bool IsSuccess() const 202 | { 203 | return m_state == CmdSuccess; 204 | } 205 | 206 | // Prepare GetParm command (B5 09 03 0D ADDR_LO ADDR_HI) 207 | void PrepGetParm(unsigned int addrFrom, unsigned int addrTo, unsigned int parmNo, unsigned int respLen) 208 | { 209 | Clear(); 210 | ReqPut(addrFrom); 211 | ReqPut(addrTo); 212 | ReqPut(0xB5); // B5 09 GetData 213 | ReqPut(0x09); 214 | ReqPut(0x03); // data len 215 | ReqPut(0x0D); // GetParm 216 | ReqPut((uint8)parmNo); 217 | ReqPut((uint8)(parmNo >> 8)); 218 | ReqFinish(); 219 | m_respLen = respLen; 220 | } 221 | 222 | void PrepGetState(unsigned int addrFrom, unsigned int addrTo) 223 | { 224 | PrepGetParm(addrFrom, addrTo, 0xAB, 1); 225 | } 226 | 227 | unsigned int GetStateValue() const 228 | { 229 | if (! IsSuccess()) 230 | return INVALID_STATE_VALUE; 231 | return m_respData[0]; 232 | } 233 | 234 | void PrepGetTemperature(unsigned int addrFrom, unsigned int addrTo) 235 | { 236 | PrepGetParm(addrFrom, addrTo, 0x18, 3); 237 | } 238 | 239 | // returns temperature (in 1/16 C) 240 | short GetTemperatureValue() const 241 | { 242 | if (! IsSuccess()) 243 | return INVALID_TEMPERATURE_VALUE; 244 | return (short)((unsigned int)m_respData[1] << 8 | m_respData[0]); 245 | } 246 | 247 | void PrepGetPressure(unsigned int addrFrom, unsigned int addrTo) 248 | { 249 | PrepGetParm(addrFrom, addrTo, 0x02, 3); 250 | } 251 | 252 | unsigned short GetPressureValue() const 253 | { 254 | if (! IsSuccess()) 255 | return INVALID_PRESSURE_VALUE; 256 | return (unsigned short)((unsigned int)m_respData[1] << 8 | m_respData[0]); 257 | } 258 | }; 259 | 260 | volatile EBusCommand * g_activeCommand = 0; 261 | 262 | bool ICACHE_RAM_ATTR RetryOrFail(volatile EBusCommand *cmd) 263 | { 264 | if (cmd->m_retryCount >= CMD_MAX_RETRIES) 265 | { 266 | cmd->m_state = EBusCommand::CmdError; 267 | g_activeCommand = cmd->m_next_cmd; 268 | return false; 269 | } 270 | // restart command at random interval 271 | uint32 r = RANDOM_REG32; 272 | 273 | cmd->m_state = EBusCommand::CmdInit; 274 | cmd->m_byteIndex = 0; 275 | cmd->m_respCrc = 0; 276 | cmd->m_retryCount++; 277 | cmd->m_count = CMD_MIN_SYNC_SKIP + (r % (CMD_MAX_SYNC_SKIP-CMD_MIN_SYNC_SKIP)); 278 | return true; 279 | } 280 | 281 | // Send byte to EBus 282 | void ICACHE_RAM_ATTR SendChar(uint8 ch) 283 | { 284 | WRITE_PERI_REG(UART_FIFO(UART_EBUS), ch); 285 | } 286 | 287 | void ICACHE_RAM_ATTR ProcessReceive(int st) 288 | { 289 | uint32 t = millis(); 290 | 291 | volatile EBusCommand *cmd = g_activeCommand; 292 | 293 | if (st < 0) // recv error 294 | { 295 | g_recvErrCnt++; 296 | g_nonSyncData++; 297 | if (cmd != 0) 298 | { 299 | if (cmd->m_state != EBusCommand::CmdInit) 300 | { 301 | RetryOrFail(cmd); 302 | } 303 | } 304 | return; 305 | } 306 | 307 | uint8 ch = (uint8)st; 308 | PUT_SERIAL_BYTE(ch); 309 | if (ch == 0xAA) 310 | { 311 | if (g_nonSyncData == 0) 312 | g_lastSyncTime = t; 313 | g_nonSyncData = 0; 314 | } 315 | else 316 | { 317 | g_nonSyncData++; 318 | } 319 | 320 | if (cmd == 0) 321 | return; 322 | 323 | switch(cmd->m_state) 324 | { 325 | case EBusCommand::CmdInit: 326 | if (ch != 0xAA) // not a sync char, skip 327 | break; 328 | // sync char received 329 | if (cmd->m_count != 0) 330 | { 331 | // skip given (random) number of syncs before restart 332 | cmd->m_count--; 333 | break; 334 | } 335 | // start processing command 336 | cmd->m_byteIndex = g_serialByteCount; 337 | if (cmd->m_reqSize == 0) 338 | { 339 | RetryOrFail(cmd); 340 | break; 341 | } 342 | else 343 | { 344 | cmd->m_state = EBusCommand::ReqSend; 345 | cmd->m_count = 0; // m_count is used as request byte counter 346 | SendChar(cmd->m_reqData[0]); 347 | } 348 | break; 349 | 350 | case EBusCommand::ReqSend: 351 | if (cmd->m_count >= cmd->m_reqSize) 352 | { 353 | RetryOrFail(cmd); 354 | break; 355 | } 356 | if (cmd->m_reqData[cmd->m_count] != ch) 357 | { 358 | RetryOrFail(cmd); 359 | break; 360 | } 361 | cmd->m_count++; 362 | if (cmd->m_count == cmd->m_reqSize) 363 | cmd->m_state = EBusCommand::ReqWaitAck; 364 | else 365 | SendChar(cmd->m_reqData[cmd->m_count]); 366 | break; 367 | 368 | case EBusCommand::ReqWaitAck: 369 | if (ch == 0) // ACK 370 | { 371 | cmd->m_state = EBusCommand::RspStart; 372 | } 373 | else 374 | { 375 | RetryOrFail(cmd); 376 | } 377 | break; 378 | 379 | case EBusCommand::RspStart: // receiving length (must match expected) 380 | if (ch == cmd->m_respLen) 381 | { 382 | cmd->m_respCrc = Crc8Byte(ch, 0); 383 | cmd->m_state = EBusCommand::RspData; 384 | cmd->m_count = 0; // m_count used as response decoded byte counter 385 | } 386 | else 387 | { 388 | RetryOrFail(cmd); 389 | } 390 | break; 391 | 392 | case EBusCommand::RspData: 393 | case EBusCommand::RspEsc: 394 | if (cmd->m_count < cmd->m_respLen) // excluding CRC byte(s) 395 | cmd->m_respCrc = Crc8Byte(ch, cmd->m_respCrc); 396 | 397 | if (cmd->m_state == EBusCommand::RspData) 398 | { 399 | if (ch == 0xA9) 400 | { 401 | cmd->m_state = EBusCommand::RspEsc; 402 | break; 403 | } 404 | } 405 | else 406 | { 407 | if (ch == 0x00) 408 | { 409 | ch = 0xA9; 410 | } 411 | else if (ch == 0x01) 412 | { 413 | ch = 0xAA; 414 | } 415 | else 416 | { 417 | RetryOrFail(cmd); 418 | break; 419 | } 420 | cmd->m_state = EBusCommand::RspData; 421 | } 422 | if (cmd->m_count == cmd->m_respLen) 423 | { 424 | // check CRC 425 | if (ch == cmd->m_respCrc) 426 | { 427 | cmd->m_state = EBusCommand::RspAck; 428 | SendChar(0x00); // send ACK 429 | } 430 | else 431 | { 432 | cmd->m_state = EBusCommand::RspNak; 433 | SendChar(0xFF); // send NAK 434 | } 435 | break; 436 | } 437 | cmd->m_respData[cmd->m_count] = ch; 438 | cmd->m_count++; 439 | break; 440 | 441 | case EBusCommand::RspAck: 442 | if (ch == 0x00) 443 | { 444 | g_activeCommand->m_state = EBusCommand::CmdSuccess; 445 | g_activeCommand = g_activeCommand->m_next_cmd; 446 | SendChar(0xAA); // send sync 447 | break; 448 | } 449 | // fall-through 450 | case EBusCommand::RspNak: 451 | RetryOrFail(cmd); 452 | break; 453 | 454 | default: 455 | g_activeCommand->m_state = EBusCommand::CmdError; 456 | break; 457 | } 458 | } 459 | 460 | ///////////////////////////////////////////////////////////// 461 | // Serial interrupt handler 462 | 463 | #define CHECK_INT_STATUS(ST, MASK) (((ST) & (MASK)) == (MASK)) 464 | 465 | void ICACHE_RAM_ATTR uart_int_handler(void *va) 466 | { 467 | for (;;) 468 | { 469 | const uint32 uartIntStatus = READ_PERI_REG(UART_INT_ST(UART_EBUS)); 470 | if (uartIntStatus == 0) 471 | break; 472 | 473 | if (CHECK_INT_STATUS(uartIntStatus, UART_FRM_ERR_INT_ST)) 474 | { 475 | ProcessReceive(RecvError); 476 | WRITE_PERI_REG(UART_INT_CLR(UART_EBUS), UART_FRM_ERR_INT_CLR); 477 | } 478 | else if (CHECK_INT_STATUS(uartIntStatus, UART_BRK_DET_INT_ST)) 479 | { 480 | ProcessReceive(RecvError); 481 | WRITE_PERI_REG(UART_INT_CLR(UART_EBUS), UART_BRK_DET_INT_CLR); 482 | } 483 | else if (CHECK_INT_STATUS(uartIntStatus, UART_RXFIFO_FULL_INT_ST)) 484 | { 485 | const uint32 fifoLen = (READ_PERI_REG(UART_STATUS(UART_EBUS)) >> UART_RXFIFO_CNT_S) & UART_RXFIFO_CNT; 486 | for (uint32 i = 0; i < fifoLen; ++i) 487 | { 488 | const uint8 ch = READ_PERI_REG(UART_FIFO(UART_EBUS)); 489 | ProcessReceive(ch); 490 | } 491 | WRITE_PERI_REG(UART_INT_CLR(UART_EBUS), UART_RXFIFO_FULL_INT_CLR); 492 | } 493 | else if (CHECK_INT_STATUS(uartIntStatus, UART_RXFIFO_TOUT_INT_ST)) 494 | { 495 | const uint32 fifoLen = (READ_PERI_REG(UART_STATUS(UART_EBUS)) >> UART_RXFIFO_CNT_S) & UART_RXFIFO_CNT; 496 | for (uint32 i = 0; i < fifoLen; ++i) 497 | { 498 | const uint8 ch = READ_PERI_REG(UART_FIFO(UART_EBUS)); 499 | ProcessReceive(ch); 500 | } 501 | ProcessReceive(RecvTimeout); 502 | WRITE_PERI_REG(UART_INT_CLR(UART_EBUS), UART_RXFIFO_TOUT_INT_CLR); 503 | } 504 | else if (CHECK_INT_STATUS(uartIntStatus, UART_TXFIFO_EMPTY_INT_ST)) 505 | { 506 | WRITE_PERI_REG(UART_INT_CLR(UART_EBUS), UART_TXFIFO_EMPTY_INT_CLR); 507 | } 508 | else 509 | { 510 | ProcessReceive(RecvUnknown); 511 | } 512 | } 513 | } 514 | 515 | void SetupEBus() 516 | { 517 | Serial.begin(2400); 518 | 519 | // Purge receive buffer 520 | while(Serial.available()) 521 | Serial.read(); 522 | 523 | // UART CONF1 (other config bits are set to 0) 524 | const uint32 conf1 = (1 << UART_RXFIFO_FULL_THRHD_S); // | (0x78 << UART_RX_TOUT_THRHD_S) | UART_RX_TOUT_EN; 525 | 526 | WRITE_PERI_REG(UART_CONF1(UART0), conf1); 527 | ETS_UART_INTR_ATTACH(uart_int_handler, 0); 528 | 529 | WRITE_PERI_REG(UART_INT_ENA(UART0), UART_RXFIFO_FULL_INT_ENA|UART_FRM_ERR_INT_ENA|UART_BRK_DET_INT_ENA); 530 | ETS_UART_INTR_ENABLE(); 531 | } 532 | 533 | ///////////////////////////////////////////////////////////// 534 | // Periodic monitoring procedure 535 | 536 | struct ParmHistData 537 | { 538 | uint32 m_timeStamp; 539 | uint32 m_byteIndex; 540 | short m_temperature; 541 | unsigned short m_pressure; 542 | uint8 m_state; 543 | uint8 m_retries; 544 | uint16 m_savedBytes; // saved byte count (for last failed commands) 545 | }; 546 | 547 | // 548 | // Indication state transition table: 549 | // 550 | // NO_SYNC -> (sync ok) -> FIRST_POLL -> (no sync) -> NO_SYNC 551 | // FIRST_POLL -> (cmd ok) -> CMD_SUCC 552 | // FIRST_POLL -> (cmd err) -> CMD_FAIL 553 | // CMD_SUCC -> (cmd err) -> CMD_FAIL -> (cmd ok) -> CMD_SUCC 554 | // CMD_FAIL -> (no sync) -> NO_SYNC 555 | // 556 | enum 557 | { 558 | STATE_NO_SYNC = 0, 559 | STATE_FIRST_POLL = 1, 560 | STATE_CMD_SUCC = 2, 561 | STATE_CMD_FAIL = 3 562 | }; 563 | 564 | enum 565 | { 566 | COMMAND_TIMEOUT = 1000, // command timeout (time to wait before declaring command failed) 567 | SYNC_TIMEOUT = 2000, // sync timeout (time to wait before entering NO_SYNC state) 568 | 569 | FIRST_POLL_DELAY = CONFIG_FIRST_POLL_DELAY*1000, // Delay before first EBus poll on startup 570 | NORM_POLL_PERIOD = CONFIG_POLL_INTERVAL_NORM*1000, // EBus poll interval when last command succeeded 571 | FAIL_POLL_PERIOD = CONFIG_POLL_INTERVAL_FAIL*1000, // EBus poll interval when last command failed 572 | }; 573 | 574 | ParmHistData g_parmHistory[MAX_PARM_HISTORY]; 575 | uint32 g_parmHistorySize = 0; // total size (must % MAX_PARM_HISTORY to get index) 576 | 577 | ParmHistData g_lastFailedCommands[FAILED_COMMAND_HISTORY_SIZE]; 578 | uint32 g_failedCommandCount = 0; // number of last failed commands (must % FAILED_COMMAND_HISTORY_SIZE to get index) 579 | 580 | // failed command buffer 581 | uint8 g_failedCommandBytes[FAILED_COMMAND_BYTES_MAX * FAILED_COMMAND_HISTORY_SIZE]; 582 | 583 | // Appends received data to last failed command data buffer (up to FAILED_COMMAND_BYTES_MAX) 584 | void SaveLastFailedCommandBytes() 585 | { 586 | if (g_failedCommandCount == 0) 587 | return; 588 | 589 | uint32 cmdIdx = (g_failedCommandCount-1) % FAILED_COMMAND_HISTORY_SIZE; 590 | ParmHistData& cmd = g_lastFailedCommands[cmdIdx]; 591 | if (cmd.m_savedBytes == FAILED_COMMAND_BYTES_MAX) 592 | return; // data buffer already filled 593 | 594 | if ((millis() - cmd.m_timeStamp) > FAILED_COMMAND_TIME_WINDOW) 595 | return; 596 | 597 | uint32 byteIndex = cmd.m_byteIndex; 598 | if (byteIndex >= FAILED_COMMAND_BYTES_BEFORE) 599 | byteIndex -= FAILED_COMMAND_BYTES_BEFORE; 600 | else 601 | byteIndex = 0; 602 | 603 | uint32 currentByteCount = g_serialByteCount; 604 | if (byteIndex > currentByteCount) 605 | return; // currentByteCount overflows, this case is not supported for simplicity 606 | uint32 availBytes = currentByteCount - byteIndex; 607 | if (availBytes > SERIAL_BUFFER_SIZE/2) // data too old, buffer may be overwritten 608 | return; 609 | if (availBytes > FAILED_COMMAND_BYTES_MAX) 610 | availBytes = FAILED_COMMAND_BYTES_MAX; 611 | 612 | uint8 *cmdBytes = g_failedCommandBytes + (cmdIdx * FAILED_COMMAND_BYTES_MAX); 613 | while (cmd.m_savedBytes < availBytes) 614 | { 615 | cmdBytes[cmd.m_savedBytes] = g_serialBuffer[(byteIndex + cmd.m_savedBytes) % SERIAL_BUFFER_SIZE]; 616 | cmd.m_savedBytes++; 617 | } 618 | } 619 | 620 | void SaveLastFailedCommand() 621 | { 622 | if (g_parmHistorySize == 0) 623 | return; 624 | 625 | uint32 cmdIdx = g_failedCommandCount % FAILED_COMMAND_HISTORY_SIZE; 626 | // save last command to failed command list 627 | const ParmHistData& lastParmData = g_parmHistory[(g_parmHistorySize - 1) % MAX_PARM_HISTORY]; 628 | g_lastFailedCommands[cmdIdx] = lastParmData; 629 | g_lastFailedCommands[cmdIdx].m_savedBytes = 0; 630 | g_failedCommandCount++; 631 | 632 | SaveLastFailedCommandBytes(); 633 | } 634 | 635 | uint32 g_monitorState = STATE_NO_SYNC; 636 | 637 | uint32 g_lastMonitorTime = 0; 638 | uint32 g_lastSuccessTime = 0; 639 | uint32 g_firstPollTime = 0; // first poll start time 640 | 641 | uint32 g_monCmdActive = false; // command active, wait MAX_CMD_DELAY after g_lastMonitorTime 642 | uint32 g_syncOk = false; // synchronization is OK 643 | uint32 g_lastCmdSucceed = false; // last command succeeded 644 | 645 | uint32 g_requestData = false; // Flag: request data immediately 646 | 647 | // EBus commands processed by interrupt handler 648 | EBusCommand g_monGetState; 649 | EBusCommand g_monGetTemp; 650 | EBusCommand g_monGetPress; 651 | 652 | // Server (narodmon.ru) data upload 653 | uint32 g_lastResultValid = false; 654 | uint32 g_lastResultSent = false; 655 | uint32 g_lastServerSendTime = 0; 656 | uint32 g_lastState = 0; 657 | uint32 g_lastTempValue = 0; 658 | uint32 g_lastPressValue = 0; 659 | 660 | void SetupMonitor() 661 | { 662 | } 663 | 664 | void ProcessMonitor() 665 | { 666 | uint32 t = millis(); 667 | bool needRequest = false; 668 | 669 | uint32 lastSyncTime = g_lastSyncTime; 670 | g_syncOk = (lastSyncTime != 0) && ((t - lastSyncTime) < SYNC_TIMEOUT); 671 | 672 | switch(g_monitorState) 673 | { 674 | case STATE_NO_SYNC: 675 | if (g_syncOk) 676 | { 677 | g_monitorState = STATE_FIRST_POLL; 678 | g_firstPollTime = t; 679 | } 680 | break; 681 | case STATE_FIRST_POLL: 682 | case STATE_CMD_FAIL: 683 | if (lastSyncTime != 0 && ! g_syncOk) 684 | g_monitorState = STATE_NO_SYNC; 685 | break; 686 | default: 687 | break; 688 | } 689 | 690 | if (g_monCmdActive) 691 | { 692 | if (g_activeCommand == 0 || t - g_lastMonitorTime > COMMAND_TIMEOUT) 693 | { 694 | // stop active monitor command, if any 695 | if (g_activeCommand == &g_monGetState || g_activeCommand == &g_monGetTemp || g_activeCommand == &g_monGetPress) 696 | g_activeCommand = 0; 697 | 698 | g_monCmdActive = false; 699 | 700 | // check last command result 701 | g_lastCmdSucceed = g_monGetState.IsSuccess() && g_monGetTemp.IsSuccess() && g_monGetPress.IsSuccess(); 702 | 703 | ParmHistData& parmData = g_parmHistory[g_parmHistorySize % MAX_PARM_HISTORY]; 704 | parmData.m_timeStamp = g_lastMonitorTime; 705 | parmData.m_byteIndex = g_monGetState.m_byteIndex; // should use first command in chain 706 | parmData.m_temperature = g_monGetTemp.GetTemperatureValue(); 707 | parmData.m_pressure = g_monGetPress.GetPressureValue(); 708 | parmData.m_state = g_monGetState.GetStateValue(); 709 | parmData.m_retries = g_monGetState.m_retryCount + g_monGetTemp.m_retryCount + g_monGetPress.m_retryCount; 710 | parmData.m_savedBytes = 0; 711 | g_parmHistorySize++; 712 | 713 | g_lastResultSent = false; 714 | g_lastState = parmData.m_state; 715 | 716 | if (parmData.m_temperature != INVALID_TEMPERATURE_VALUE && parmData.m_pressure != INVALID_PRESSURE_VALUE) 717 | { 718 | g_lastTempValue = parmData.m_temperature; 719 | g_lastPressValue = parmData.m_pressure; 720 | g_lastResultValid = true; 721 | } 722 | else 723 | { 724 | g_lastResultValid = false; 725 | } 726 | 727 | if (g_lastCmdSucceed) 728 | { 729 | g_lastSuccessTime = g_lastMonitorTime; 730 | g_monitorState = STATE_CMD_SUCC; 731 | } 732 | else 733 | { 734 | g_monitorState = g_syncOk ? STATE_CMD_FAIL : STATE_NO_SYNC; 735 | SaveLastFailedCommand(); 736 | } 737 | } 738 | } 739 | else 740 | { 741 | switch(g_monitorState) 742 | { 743 | case STATE_FIRST_POLL: 744 | if (t - g_firstPollTime > FIRST_POLL_DELAY) 745 | needRequest = true; 746 | break; 747 | case STATE_CMD_SUCC: 748 | if (t - g_lastMonitorTime > NORM_POLL_PERIOD) 749 | needRequest = true; 750 | break; 751 | case STATE_CMD_FAIL: 752 | if (t - g_lastMonitorTime > FAIL_POLL_PERIOD) 753 | needRequest = true; 754 | break; 755 | default: 756 | break; 757 | } 758 | } 759 | 760 | if (g_requestData) 761 | needRequest = true; 762 | 763 | if (needRequest) 764 | { 765 | if (g_activeCommand == 0) 766 | { 767 | g_lastMonitorTime = t; 768 | g_monCmdActive = true; 769 | g_requestData = false; 770 | 771 | // Build EBus command chain 772 | g_monGetState.PrepGetState(EBUS_ADDR_HOST, EBUS_ADDR_BOILER); 773 | g_monGetTemp.PrepGetTemperature(EBUS_ADDR_HOST, EBUS_ADDR_BOILER); 774 | g_monGetPress.PrepGetPressure(EBUS_ADDR_HOST, EBUS_ADDR_BOILER); 775 | g_monGetState.m_next_cmd = &g_monGetTemp; 776 | g_monGetTemp.m_next_cmd = &g_monGetPress; 777 | // Enable command processing by interrupt handler 778 | g_activeCommand = &g_monGetState; 779 | } 780 | } 781 | 782 | SaveLastFailedCommandBytes(); 783 | } 784 | 785 | ///////////////////////////////////////////////////////////// 786 | // Indication 787 | 788 | uint32 g_ledState = 0; // bit mask 789 | 790 | enum 791 | { 792 | // Green LED [Power/WiFi]: On - connected, Blink - trying to connect 793 | GreenLed = 12, // Wemos Mini: D6/MISO; Wemos D1: D12/MISO 794 | // Yellow LED [EBus sync]: On - EBus sync bytes (0xAA) detected, Off - not detected or mixed with garbage 795 | YellowLed = 13, // Wemos Mini: D7/MOSI; Wemos D1: D11/MOSI 796 | // Red LED [Request/Error]: On - last request failed, Off - last request OK, Blink - request is being sent 797 | RedLed = 14 // Wemos Mini: D5/SCK; Wemos D1: D13/SCK 798 | }; 799 | 800 | enum 801 | { 802 | WIFI_BLINK_PERIOD = 100, // Green LED - blink when trying to connect WiFi 803 | COMMAND_BLINK_PERIOD = 50, // Red LED - blink during EBus command processing 804 | }; 805 | 806 | bool GetLed(int no) 807 | { 808 | return (g_ledState & (1 << no)) != 0; 809 | } 810 | 811 | void SetLed(int no, bool onoff) 812 | { 813 | const uint32 mask = 1 << no; 814 | if (onoff) 815 | { 816 | g_ledState |= mask; 817 | digitalWrite(no, LOW); 818 | } 819 | else 820 | { 821 | g_ledState &= ~mask; 822 | digitalWrite(no, HIGH); 823 | } 824 | } 825 | 826 | uint32 g_lastWifiBlinkTime = 0; 827 | 828 | void SetWifiLedOn() 829 | { 830 | if (! GetLed(GreenLed)) 831 | SetLed(GreenLed, 1); 832 | } 833 | 834 | void SetWifiLedBlink() 835 | { 836 | uint32 t = millis(); 837 | uint32 dt = t - g_lastWifiBlinkTime; 838 | if (dt > WIFI_BLINK_PERIOD) 839 | { 840 | SetLed(GreenLed, !GetLed(GreenLed)); 841 | g_lastWifiBlinkTime = t; 842 | } 843 | } 844 | 845 | uint32 g_lastErrBlinkTime = 0; 846 | 847 | void SetErrLed(bool f) 848 | { 849 | if (GetLed(RedLed) != f) 850 | SetLed(RedLed, f); 851 | } 852 | 853 | void SetErrLedBlink() 854 | { 855 | uint32 t = millis(); 856 | uint32 dt = t - g_lastErrBlinkTime; 857 | if (dt > COMMAND_BLINK_PERIOD) 858 | { 859 | SetLed(RedLed, !GetLed(RedLed)); 860 | g_lastErrBlinkTime = t; 861 | } 862 | } 863 | 864 | void SetupIndication() 865 | { 866 | g_ledState = 0; 867 | pinMode(GreenLed, OUTPUT); 868 | pinMode(YellowLed, OUTPUT); 869 | pinMode(RedLed, OUTPUT); 870 | // Blink all LEDs 3 times on startup (indication test) 871 | for (int i = 0; i < 3; ++i) 872 | { 873 | if (i != 0) 874 | delay(100); 875 | SetLed(GreenLed, true); 876 | SetLed(YellowLed, true); 877 | SetLed(RedLed, true); 878 | delay(100); 879 | SetLed(GreenLed, false); 880 | SetLed(YellowLed, false); 881 | SetLed(RedLed, false); 882 | } 883 | 884 | g_lastWifiBlinkTime = g_lastErrBlinkTime = millis(); 885 | } 886 | 887 | void ProcessIndication() 888 | { 889 | uint32 t = millis(); 890 | 891 | if (WiFi.status() == WL_CONNECTED) 892 | SetWifiLedOn(); 893 | else 894 | SetWifiLedBlink(); 895 | 896 | SetLed(YellowLed, g_syncOk); 897 | 898 | if (g_activeCommand != 0) 899 | SetErrLedBlink(); 900 | else 901 | SetErrLed(!g_lastCmdSucceed); 902 | } 903 | 904 | ///////////////////////////////////////////////////////////// 905 | // Web server 906 | 907 | ESP8266WebServer g_webServer(80); 908 | 909 | void webResponseBegin(String& resp, int refresh) 910 | { 911 | resp.concat(""); 912 | if (refresh != 0) 913 | { 914 | resp.concat(""); 917 | } 918 | resp.concat("BaiMon v " BAIMON_VERSION "" 919 | ""); 922 | } 923 | 924 | void webResponseEnd(String& resp) 925 | { 926 | resp.concat(""); 927 | } 928 | 929 | void webHandleRoot() 930 | { 931 | uint32 t = millis(); 932 | 933 | uint32 sec = t / 1000; 934 | uint32 min = sec / 60; 935 | uint32 hr = min / 60; 936 | 937 | String resp; 938 | webResponseBegin(resp, 10); 939 | 940 | resp.concat("

Boiler status

"); 941 | 942 | char tbuf[80]; 943 | sprintf(tbuf, "%d days %02d:%02d:%02d", hr / 24, hr % 24, min % 60, sec % 60); 944 | resp.concat("

Uptime:"); 945 | resp.concat(tbuf); 946 | 947 | resp.concat("

EBus sync: "); 948 | resp.concat(g_syncOk ? "OK" : "FAIL"); 949 | 950 | resp.concat("

"); 951 | resp.concat(""); 952 | resp.concat("
"); 953 | 954 | resp.concat("
Measurement history:
"); 955 | for (uint32 i = 0, cnt = (g_parmHistorySize > WEB_PARM_HISTORY) ? WEB_PARM_HISTORY : g_parmHistorySize; i != cnt; ++i) 956 | { 957 | const ParmHistData& parmData = g_parmHistory[(g_parmHistorySize - i - 1) % MAX_PARM_HISTORY]; 958 | 959 | char stateBuf[20]; 960 | const char *stateStr = stateBuf; 961 | if (parmData.m_state == INVALID_STATE_VALUE) 962 | stateStr = "ERROR"; 963 | else 964 | sprintf(stateBuf, "S%02u", parmData.m_state); 965 | 966 | char tempBuf[20]; 967 | const char *tempStr = tempBuf; 968 | if (parmData.m_temperature == INVALID_TEMPERATURE_VALUE) 969 | tempStr = "ERROR"; 970 | else 971 | sprintf(tempBuf, "%d.%02u", parmData.m_temperature/16, (parmData.m_temperature & 0xF)*100/16); 972 | 973 | char pressBuf[20]; 974 | const char *pressStr = pressBuf; 975 | if (parmData.m_pressure == INVALID_PRESSURE_VALUE) 976 | pressStr = "ERROR"; 977 | else 978 | sprintf(pressBuf, "%d.%02u", parmData.m_pressure/1000, (parmData.m_pressure%1000)/10); 979 | 980 | uint32 dt = t - parmData.m_timeStamp; 981 | uint32 dtsec = dt / 1000; 982 | uint32 dtmin = dtsec / 60; 983 | uint32 dthr = dtmin / 60; 984 | 985 | char buf[400]; 986 | sprintf(buf, "", 987 | dthr, dtmin % 60, dtsec % 60, stateStr, tempStr, pressStr, parmData.m_retries); 988 | resp.concat(buf); 989 | } 990 | resp.concat("
TimeStateTemperature,CPressure,BarRetries
-%02u:%02u:%02u%s%s%s%u
"); 991 | 992 | resp.concat("
[Diagnostics]"); 993 | 994 | webResponseEnd(resp); 995 | 996 | g_webServer.send(200, "text/html", resp); 997 | } 998 | 999 | void webHandleRequestData() 1000 | { 1001 | g_requestData = true; 1002 | 1003 | String resp; 1004 | webResponseBegin(resp, 0); 1005 | resp.concat("

Requesting data

"); 1006 | webResponseEnd(resp); 1007 | 1008 | g_webServer.sendHeader("Location", "/", false); 1009 | g_webServer.send(302, "text/html", resp); 1010 | } 1011 | 1012 | void webHandleDiag() 1013 | { 1014 | uint32 byteCount = g_serialByteCount; 1015 | uint32 byteStart = (byteCount < WEB_DIAG_DATA_SIZE) ? 0 : ((byteCount - WEB_DIAG_DATA_SIZE) & ~(0x20-1)); 1016 | 1017 | uint32 t = millis(); 1018 | 1019 | String resp; 1020 | webResponseBegin(resp, 0); 1021 | 1022 | char tbuf[40]; 1023 | 1024 | resp.concat("

Diagnostics

"); 1025 | resp.concat("MAC: "); 1026 | resp.concat(WiFi.macAddress()); 1027 | resp.concat("
Total bytes received: "); 1028 | sprintf(tbuf, "0x%08X", byteCount); 1029 | resp.concat(tbuf); 1030 | 1031 | resp.concat(" errors: "); 1032 | resp.concat(g_recvErrCnt); 1033 | 1034 | resp.concat("
Last EBus data:
"); 1035 | while (byteStart < byteCount) 1036 | { 1037 | uint32 portion = byteCount - byteStart; 1038 | if (portion > 32) 1039 | portion = 32; 1040 | sprintf(tbuf, "%08X:", byteStart); 1041 | resp.concat(tbuf); 1042 | for (uint32 i = 0; i < portion; ++i) 1043 | { 1044 | uint8 ch = GET_SERIAL_BYTE_NO(byteStart+i); 1045 | sprintf(tbuf, "%s%02X", ((i & 0x0F) == 0 ? "  " : " "), ch); 1046 | resp.concat(tbuf); 1047 | } 1048 | byteStart += portion; 1049 | resp.concat("
"); 1050 | } 1051 | resp.concat("

Failed command history:
"); 1052 | uint32 failedCmdCnt = FAILED_COMMAND_HISTORY_SIZE; 1053 | if (g_failedCommandCount < failedCmdCnt) 1054 | failedCmdCnt = g_failedCommandCount; 1055 | 1056 | for (uint32 n = 0; n < failedCmdCnt; ++n) 1057 | { 1058 | uint32 cmdIdx = (g_failedCommandCount - n - 1) % FAILED_COMMAND_HISTORY_SIZE; 1059 | const ParmHistData& cmd = g_lastFailedCommands[cmdIdx]; 1060 | const uint8 *cmdBytes = g_failedCommandBytes + cmdIdx * FAILED_COMMAND_BYTES_MAX; 1061 | 1062 | uint32 dt = t - cmd.m_timeStamp; 1063 | uint32 dtsec = dt / 1000; 1064 | uint32 dtmin = dtsec / 60; 1065 | uint32 dthr = dtmin / 60; 1066 | 1067 | char buf[200]; 1068 | sprintf(buf, "
#%u: -%02u:%02u:%02u [%u bytes] [pre: %u]
", n+1, dthr, dtmin % 60, dtsec % 60, cmd.m_savedBytes, FAILED_COMMAND_BYTES_BEFORE); 1069 | resp.concat(buf); 1070 | resp.concat("
"); 1071 | uint32 bc = 0; 1072 | while (bc < cmd.m_savedBytes) 1073 | { 1074 | uint32 portion = cmd.m_savedBytes - bc; 1075 | if (portion > 32) 1076 | portion = 32; 1077 | sprintf(tbuf, "%08X:", bc); 1078 | resp.concat(tbuf); 1079 | for (uint32 i = 0; i < portion; ++i) 1080 | { 1081 | uint8 ch = cmdBytes[bc + i]; 1082 | sprintf(tbuf, "%s%02X", ((i & 0x0F) == 0 ? "  " : " "), ch); 1083 | resp.concat(tbuf); 1084 | } 1085 | bc += portion; 1086 | resp.concat("
"); 1087 | } 1088 | resp.concat("
"); 1089 | } 1090 | 1091 | resp.concat("

[Main]"); 1092 | 1093 | webResponseEnd(resp); 1094 | g_webServer.send(200, "text/html", resp); 1095 | } 1096 | 1097 | void webHandleNotFound() 1098 | { 1099 | String message = "404 Not Found\n\n"; 1100 | message += "URI: "; 1101 | message += g_webServer.uri(); 1102 | message += "\nMethod: "; 1103 | message += (g_webServer.method() == HTTP_GET) ? "GET" : "POST"; 1104 | message += "\nArguments: "; 1105 | message += g_webServer.args(); 1106 | message += "\n"; 1107 | for (uint8_t i=0; i CONFIG_TCP_REPLY_TIMEOUT) 1203 | break; 1204 | delay(10); 1205 | } 1206 | 1207 | sock.stop(); 1208 | return true; 1209 | } 1210 | #endif 1211 | 1212 | void ProcessServerSend() 1213 | { 1214 | #if CONFIG_SERVER_UPLOAD_ENABLE 1215 | 1216 | if (g_syncOk && g_lastResultSent) 1217 | return; 1218 | 1219 | uint32 t = millis(); 1220 | 1221 | // first time interval 1222 | if (g_lastServerSendTime == 0) 1223 | { 1224 | if (t < ServerSendMinDelay) 1225 | return; 1226 | } 1227 | else 1228 | { 1229 | if ((t - g_lastServerSendTime) < ServerSendInterval) 1230 | return; 1231 | } 1232 | 1233 | g_lastServerSendTime = t; 1234 | 1235 | #if CONFIG_SERVER_USE_MQTT 1236 | bool ok = ServerUploadMqtt(); 1237 | #else 1238 | bool ok = ServerUploadTcp(); 1239 | #endif 1240 | 1241 | if (ok) 1242 | g_lastResultSent = true; 1243 | 1244 | #endif // CONFIG_SERVER_UPLOAD_ENABLE 1245 | } 1246 | 1247 | ///////////////////////////////////////////////////////////// 1248 | // main 1249 | 1250 | void setup() 1251 | { 1252 | Serial.begin(2400); 1253 | 1254 | SetupIndication(); 1255 | SetupWifi(); 1256 | SetupEBus(); 1257 | SetupWebServer(); 1258 | SetupMonitor(); 1259 | } 1260 | 1261 | void loop() 1262 | { 1263 | ProcessMonitor(); 1264 | ProcessIndication(); 1265 | ProcessServerSend(); 1266 | ProcessWebServer(); 1267 | delay(10); 1268 | } 1269 | -------------------------------------------------------------------------------- /EBus-adapter.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slavikb/BaiMon/4e6862160c9724759b3d6fc9c6cbf9dc937ad264/EBus-adapter.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vyacheslav Batenin 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 | # BaiMon 2 | 3 | ESP8266 - based monitoring device for Vaillant TurboTec boilers. 4 | 5 | Periodically monitors boiler internal state, water temperature and pressure. 6 | 7 | Publishes measured results to 'narodmon.ru', TCP and MQTT protocols are supported. 8 | 9 | Has its own web interface for viewing measured data and debugging EBus activity. 10 | 11 | This version supports Vaillant TurboTec Pro. It can be adapted to other 12 | boiler models using EBus interace. However, this requires knowledge of proprietary 13 | commands to read parameter values. 14 | 15 | Requires Arduino IDE with ESP8266 support to compile and upload to ESP8266-based controller (I run it on Wemos D1 Mini). 16 | 17 | Controller comminicates to the boiler using UART. Adapter is needed to adjust signal levels (EBus uses 9-20 volts). 18 | 19 | EBus-adapter.pdf contains schematics of a simple EBus adapter. 20 | It can be connected directly to ESP8266 outputs due to optical decoupling. 21 | 22 | -------------------------------------------------------------------------------- /local_config.h.in: -------------------------------------------------------------------------------- 1 | // Copy this file to local_config.h and edit appropriately 2 | // 3 | 4 | // WiFi settings 5 | #define CONFIG_WIFI_SSID "mywifi" 6 | #define CONFIG_WIFI_PASS "mywifipassword" 7 | 8 | // Operational settings 9 | 10 | // EBus device address for BaiMon (master) 11 | #define CONFIG_EBUS_ADDR_HOST 0 12 | // EBus device address for boiler (master) 13 | #define CONFIG_EBUS_ADDR_BOILER 3 14 | 15 | // EBus device poll intervals (seconds) 16 | 17 | // EBus poll interval when last command succeeded 18 | #define CONFIG_POLL_INTERVAL_NORM 600 19 | // EBus poll interval whan last command failed 20 | #define CONFIG_POLL_INTERVAL_FAIL 60 21 | // Delay before first poll after detecting EBus presense 22 | #define CONFIG_FIRST_POLL_DELAY 5 23 | 24 | // Narodmon.ru settings 25 | 26 | // Enable server data upload 27 | #define CONFIG_SERVER_UPLOAD_ENABLE 1 28 | // Use MQTT instead of TCP for server data upload 29 | #define CONFIG_SERVER_USE_MQTT 0 30 | 31 | // Narodmon device ID (device MAC address) 32 | // should not be too long, consider ServerUploadTcp buffer limit 33 | #define CONFIG_SERVER_DEVICE_ID "00:00:00:00:00:00" 34 | // Send interval (seconds) 35 | #define CONFIG_SERVER_SEND_INTERVAL 600 36 | // Startup delay (seconds) to avoid violating narodmon max data rate 37 | #define CONFIG_SERVER_START_DELAY 400 38 | 39 | // TCP protocol settings 40 | 41 | // TCP server host name 42 | #define CONFIG_TCP_HOST "narodmon.ru" 43 | // TCP server port 44 | #define CONFIG_TCP_PORT 8283 45 | // TCP server reply wait timeout, ms 46 | #define CONFIG_TCP_REPLY_TIMEOUT 5000 47 | 48 | // MQTT protocol settings 49 | 50 | // MQTT server host name 51 | #define CONFIG_MQTT_HOST "narodmon.ru" 52 | // MQTT server port 53 | #define CONFIG_MQTT_PORT 1883 54 | // MQTT user name and password 55 | #define CONFIG_MQTT_USERNAME "my_login" 56 | #define CONFIG_MQTT_PASSWORD "my_password" 57 | // MQTT client ID (narodmon: device MAC address) 58 | #define CONFIG_MQTT_CLIENT_ID CONFIG_SERVER_DEVICE_ID 59 | // MQTT topic name (narodmon: user/devname) 60 | #define CONFIG_MQTT_TOPIC "user/device" 61 | 62 | --------------------------------------------------------------------------------