diff --git a/MODULE.md b/MODULE.md new file mode 100644 index 0000000..f148fcb --- /dev/null +++ b/MODULE.md @@ -0,0 +1,149 @@ +# Sofar2mqtt +## A smart home interface for Sofar solar and battery inverters. + +Supported models: + +ME3000SP - Full support +HYD-xx00-ES - Full support +HYD-xx00-EP - Full support +HYD-xx00-KTL - Full support + +Sofar2mqtt is a remote control interface for Sofar solar and battery inverters. +It allows remote control of the inverter and reports the invertor status, power usage, battery state etc for integration with smart home systems such as [Home Assistant](https://www.home-assistant.io/) and [Node-Red](https://nodered.org/). +For read only mode, it will send status messages without the inverter needing to be in passive mode. +It's designed to run on an ESP8266 microcontroller with a TTL to RS485 module such as MAX485 or MAX3485. +Designed to work with TTL modules with or without the DR and RE flow control pins. If your TTL module does not have these pins then just ignore the wire from D5. + +Subscribe your MQTT client to: + +Sofar2mqtt/state (where Sofar2mqtt matches the hostname you configured in settings) + +Which provides: + +running_state +grid_voltage +grid_current +grid_freq +systemIO_power (AC side of inverter) +battery_power (DC side of inverter) +battery_voltage +battery_current +batterySOC +battery_temp +battery_cycles +grid_power +consumption +solarPV +today_generation +today_exported +today_purchase +today_consumption +inverter_temp +inverterHS_temp +solarPVAmps + +With the inverter in Passive Mode, send MQTT messages to: + +Sofar2mqtt/set/standby - send value "true" +Sofar2mqtt/set/auto - send value "true" or "battery_save" +Sofar2mqtt/set/charge - send values in the range 0-3000 (watts) +Sofar2mqtt/set/discharge - send values in the range 0-3000 (watts) + +battery_save is a hybrid auto mode that will charge from excess solar but not discharge. + +(c)Colin McGerty 2021 colin@mcgerty.co.uk +Major version 2.0 rewrite by Adam Hill sidepipeukatgmaildotcom +Thanks to Rich Platts for hybrid model code and testing. +calcCRC by angelo.compagnucci@gmail.com and jpmzometa@gmail.com +Version 3.x rewrite by Igor Ybema to work on his module with TFT screen and to add more inverter types + +# How to get a pre-made module + +Just go ahead to this [Tindie](https://www.tindie.com/products/thehognl/esp12-f-with-rs485-modbus-and-optional-touch-tft/) store to get a pre-made module with this software. + +# How To Build your own module + +If you want to build your own module you should follow the rest of this readme. + +Parts List: +1. ESP8266 Microcontroller +2. MAX485 or MAX3485 TTL to RS485 board* +3. Wemos 64x48 OLED Screen (optional) +4. A small project board +5. A few wires and a little solder + +*The MAX3485 (which is red, not blue like the MAX485 shown here) is preferred as it is much more stable because it uses 3.3v logic, just like the ESP8366. The MAX485 uses 5v logic but is somewhat tolerant of 3.3v and is generally cheaper and more widely available. I use a MAX485 but many people have reported problems with this and if you can find a MAX3485 then you should use that. MAX3485 boards do not have DR and RE flow control pins, so just skip the wire from pin D5 in the wiring diagram below. + +![Parts](pics/parts.jpg) + +Cut the project board to a convenient size. + +![Board](pics/board.jpg) + +Wire the components according to this circuit diagram. + +![Wiring Diagram](pics/diagram.jpg) + +I tend to keep the wires on top of the board, poke them through and solder underneath. Your approach may be better and your soldering will almost certainly be better than mine! + +![Wiring](pics/wiring.jpg) + +Make sure you connect the DR and RE pins together. The red arrow below shows where a single wire from D5 connects to both DR and RE. (If you are using a TTL board without the DR and RE pins, ignore this step.) + +![Short these pins](pics/short.jpg) + +Use long pinned mounts on your ESP8266 if you are stacking the optional OLED on top. Trim the legs so they fit comfortably into the sockets on the circuit board below. +I don't recommend soldering the ESP8266 permanently to your circuit board. + +![Chips](pics/ICs.jpg) + +Here's how it looks when completed. + +![Finished](pics/Sofar2MQTT_completed.jpg) + +# Flashing + +Easiest to get started is to flash a pre-compiled binary. Get a [regular ESP flasher](https://github.com/esphome/esphome-flasher/releases), attach a module on your computer and flash a [binary](https://github.com/IgorYbema/Sofar2mqtt/tree/mod/binaries) to the module. +If you want to compile your own version you'll need the libraries for the ESP8266. Follow [this guide](https://randomnerdtutorials.com/how-to-install-esp8266-board-arduino-ide/) if you haven't completed that step before. + +Add a few more libraries using the Manage Libraries menu: +1. PubSubClient +2. Adafruit GFX +3. Adafruit SSD1306 Wemos Mini OLED +4. DoubleResetDetect +5. Adafruit_ILI9341 +6. XPT2046_Touchscreen + +(Even if you are not using the OLED or TFT screen, you should install the libraries or it will not compile.) + +...and upload. + +Run it on the desktop, not connected to your invertor, to test that wifi and mqtt are connected and see some messages in the serial monitor. +The OLED screen should show "Online" to indicate a connection to WiFi and MQTT. It will alternate between "RS485 Error" and "CRC-FAULT" to indicate that the inverter is not connected. + +# Connect to Inverter + +Connect the Sofar2mqtt unit to a 5v micro USB power supply. +Now connect wires A and B to the two wire RS485 input of your inverter, which is marked as 485s on the image of the inverter below. + +![ME3000SP Data Connections](pics/485s.jpg) + +# Troubleshooting + +Nothing on the OLED or TFT screen? Make sure you solder all the pins on the OLED and ESP8266, not just those with wires attached. +No communication with the inverter? Make sure the slave IDs match. Sofar2mqtt assumes slave ID 1 by default. You can change this around line 93 or in the inverter user interface. But they must be the same. + +Here's what the various things on the OLED screen tell you: + +Line 1 is the device name, nothing else. +Line 2 will display "Connecting" during start up and lines 3 and 4 will show WIFI and MQTT getting connected. +Line 2 also has a dot that slowly flashes, once every few seconds. This is when a heartbeat message is being sent to the inverter. +If a message is read from the inverter that fails the CRC checksum, line 3 will display "CRC-FAULT". This could be caused by a loose or bad RS485 wire or by unsupported features. A few of these is normal, a lot could indicate a problem. +If no response is received to a heartbeat message, lines 3 and 4 show "RS485 ERROR". This could be caused by disconnected or reversed RS485 wires. +During start-up, line 4 shows the Sofar2mqtt software version. Check that you have the latest version at https://github.com/cmcgerty/Sofar2MQTT +In normal operation, line 2 shows "Online" which indicates that both WIFI and MQTT are still connected. +In normal operation, line 3 shows the inverter run state, Standby, Charging, Discharging etc. +In normal operation, line 4 shows the power in Watts in or out of the batteries when charging or discharging. + + + diff --git a/README.md b/README.md index adc0b5f..0ede841 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Supported models: ME3000SP - Full support -HYD-xx00-ES - Full support - -![Sofar2MQTT](pics/Sofar2MQTT.jpg) +HYD-xx00-ES - Full support +HYD-xx00-EP - Full support +HYD-xx00-KTL - Full support Sofar2mqtt is a remote control interface for Sofar solar and battery inverters. It allows remote control of the inverter and reports the invertor status, power usage, battery state etc for integration with smart home systems such as [Home Assistant](https://www.home-assistant.io/) and [Node-Red](https://nodered.org/). @@ -16,7 +16,7 @@ Designed to work with TTL modules with or without the DR and RE flow control pin Subscribe your MQTT client to: -Sofar2mqtt/state +Sofar2mqtt/state (where Sofar2mqtt matches the hostname you configured in settings) Which provides: @@ -54,60 +54,31 @@ battery_save is a hybrid auto mode that will charge from excess solar but not di (c)Colin McGerty 2021 colin@mcgerty.co.uk Major version 2.0 rewrite by Adam Hill sidepipeukatgmaildotcom Thanks to Rich Platts for hybrid model code and testing. -calcCRC by angelo.compagnucci@gmail.com and jpmzometa@gmail.com - -# How To Build - -Parts List: -1. ESP8266 Microcontroller -2. MAX485 or MAX3485 TTL to RS485 board* -3. Wemos 64x48 OLED Screen (optional) -4. A small project board -5. A few wires and a little solder - -*The MAX3485 (which is red, not blue like the MAX485 shown here) is preferred as it is much more stable because it uses 3.3v logic, just like the ESP8366. The MAX485 uses 5v logic but is somewhat tolerant of 3.3v and is generally cheaper and more widely available. I use a MAX485 but many people have reported problems with this and if you can find a MAX3485 then you should use that. MAX3485 boards do not have DR and RE flow control pins, so just skip the wire from pin D5 in the wiring diagram below. - -![Parts](pics/parts.jpg) - -Cut the project board to a convenient size. - -![Board](pics/board.jpg) - -Wire the components according to this circuit diagram. +calcCRC by angelo.compagnucci@gmail.com and jpmzometa@gmail.com +Version 3.x rewrite by Igor Ybema to work on his module with TFT screen and to add more inverter types -![Wiring Diagram](pics/diagram.jpg) +# How to get a pre-made module -I tend to keep the wires on top of the board, poke them through and solder underneath. Your approach may be better and your soldering will almost certainly be better than mine! +Just go ahead to this [Tindie](https://www.tindie.com/products/thehognl/esp12-f-with-rs485-modbus-and-optional-touch-tft/) store to get a pre-made module with this software. -![Wiring](pics/wiring.jpg) +# How To Build your own module -Make sure you connect the DR and RE pins together. The red arrow below shows where a single wire from D5 connects to both DR and RE. (If you are using a TTL board without the DR and RE pins, ignore this step.) - -![Short these pins](pics/short.jpg) - -Use long pinned mounts on your ESP8266 if you are stacking the optional OLED on top. Trim the legs so they fit comfortably into the sockets on the circuit board below. -I don't recommend soldering the ESP8266 permanently to your circuit board. - -![Chips](pics/ICs.jpg) - -Here's how it looks when completed. - -![Finished](pics/Sofar2MQTT_completed.jpg) +If you want to build your own module you should follow [this readme](MODULE.md) # Flashing -Edit the file Sofar2mqtt.ino and remove the // at the start of the second OR third line as appropriate for your inverter model (ME3000SP or a Hybrid HYD model). - -Add your wifi network name and password and your mqtt server details in the section below. If you need more than one Sofar2mqtt on your network, make sure you give them unique device names. - -You'll need the libraries for the ESP8266. Follow [this guide](https://randomnerdtutorials.com/how-to-install-esp8266-board-arduino-ide/) if you haven't completed that step before. +Easiest to get started is to flash a pre-compiled binary. Get a [regular ESP flasher](https://github.com/esphome/esphome-flasher/releases), attach a module on your computer and flash a [binary](https://github.com/IgorYbema/Sofar2mqtt/tree/mod/binaries) to the module. +If you want to compile your own version you'll need the libraries for the ESP8266. Follow [this guide](https://randomnerdtutorials.com/how-to-install-esp8266-board-arduino-ide/) if you haven't completed that step before. Add a few more libraries using the Manage Libraries menu: 1. PubSubClient 2. Adafruit GFX 3. Adafruit SSD1306 Wemos Mini OLED +4. DoubleResetDetect +5. Adafruit_ILI9341 +6. XPT2046_Touchscreen -(Even if you are not using the OLED screen, you should install the Adafruit libraries or it will not compile.) +(Even if you are not using the OLED or TFT screen, you should install the libraries or it will not compile.) ...and upload. @@ -117,13 +88,13 @@ The OLED screen should show "Online" to indicate a connection to WiFi and MQTT. # Connect to Inverter Connect the Sofar2mqtt unit to a 5v micro USB power supply. -Now connect wires A and B to the two wire RS485 input of your inverter, which is marked as 485s on the image of the ME3000SP below. +Now connect wires A and B to the two wire RS485 input of your inverter, which is marked as 485s on the image of the inverter below. ![ME3000SP Data Connections](pics/485s.jpg) # Troubleshooting -Nothing on the OLED screen? Make sure you solder all the pins on the OLED and ESP8266, not just those with wires attached. +Nothing on the OLED or TFT screen? Make sure you solder all the pins on the OLED and ESP8266, not just those with wires attached. No communication with the inverter? Make sure the slave IDs match. Sofar2mqtt assumes slave ID 1 by default. You can change this around line 93 or in the inverter user interface. But they must be the same. Here's what the various things on the OLED screen tell you: diff --git a/Sofar2mqtt/Sofar2mqtt.h b/Sofar2mqtt/Sofar2mqtt.h new file mode 100644 index 0000000..d756551 --- /dev/null +++ b/Sofar2mqtt/Sofar2mqtt.h @@ -0,0 +1,1220 @@ +const unsigned char background [] PROGMEM = { + // 'Naamloos, 240x320px + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1f, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0x80, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x80, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, + 0x00, 0x0f, 0x00, 0xff, 0x80, 0x70, 0x03, 0x0c, 0x00, 0x38, 0x18, 0x38, 0x0e, 0x00, 0xe0, 0x0e, + 0x00, 0x61, 0x9f, 0xf0, 0x03, 0x80, 0x18, 0x60, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x7f, + 0xe0, 0xff, 0x83, 0xfe, 0x03, 0x3c, 0x00, 0x38, 0x18, 0xfe, 0x0f, 0x00, 0xe0, 0x7f, 0xc0, 0x67, + 0x9f, 0xf8, 0x1f, 0xf0, 0x19, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0xff, 0xf0, 0xff, + 0x87, 0xff, 0x03, 0xfc, 0x00, 0x38, 0x1f, 0xff, 0x07, 0x01, 0xe0, 0xff, 0xe0, 0x7f, 0x9f, 0xf0, + 0x3f, 0xf8, 0x1f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfc, 0x01, 0xe0, 0x70, 0x1c, 0x06, 0x07, + 0x03, 0xfc, 0x00, 0x38, 0x1f, 0x0f, 0x07, 0x01, 0xc1, 0xe0, 0xe0, 0x7f, 0x83, 0x80, 0x78, 0x38, + 0x1f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x01, 0xc0, 0x78, 0x1c, 0x00, 0x07, 0x03, 0xc0, + 0x00, 0x38, 0x1e, 0x07, 0x07, 0x81, 0xc1, 0xc0, 0x70, 0x78, 0x03, 0x80, 0x70, 0x1c, 0x1e, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x83, 0x80, 0x38, 0x1c, 0x00, 0x07, 0x03, 0x80, 0x00, 0x38, + 0x1c, 0x07, 0x03, 0x83, 0x83, 0x80, 0x70, 0x70, 0x03, 0x80, 0xe0, 0x1c, 0x1c, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1f, 0xc3, 0x80, 0x38, 0x1c, 0x00, 0x7f, 0x03, 0x80, 0x00, 0x38, 0x1c, 0x07, + 0x03, 0x83, 0x83, 0xff, 0xf0, 0x70, 0x03, 0x80, 0xff, 0xfc, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc3, 0x80, 0x38, 0x1c, 0x03, 0xff, 0x03, 0x80, 0x00, 0x38, 0x1c, 0x07, 0x03, 0x83, + 0x83, 0xff, 0xf0, 0x70, 0x03, 0x80, 0xff, 0xfc, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xc3, 0x80, 0x38, 0x1c, 0x07, 0xff, 0x03, 0x80, 0x00, 0x38, 0x1c, 0x07, 0x01, 0xc7, 0x03, 0xff, + 0xf0, 0x70, 0x03, 0x80, 0xff, 0xfc, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc3, 0x80, + 0x38, 0x1c, 0x0f, 0x07, 0x03, 0x80, 0x00, 0x38, 0x1c, 0x07, 0x01, 0xc7, 0x03, 0x80, 0x00, 0x70, + 0x03, 0x80, 0xe0, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc3, 0x80, 0x38, 0x1c, + 0x0e, 0x07, 0x03, 0x80, 0x00, 0x38, 0x1c, 0x07, 0x01, 0xc7, 0x03, 0x80, 0x00, 0x70, 0x03, 0x80, + 0xe0, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc1, 0xc0, 0x78, 0x1c, 0x0e, 0x07, + 0x03, 0x80, 0x00, 0x38, 0x1c, 0x07, 0x00, 0xee, 0x01, 0xc0, 0x00, 0x70, 0x03, 0x80, 0x70, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x81, 0xe0, 0x70, 0x1c, 0x0e, 0x0f, 0x03, 0x80, + 0x00, 0x38, 0x1c, 0x07, 0x00, 0xfe, 0x01, 0xe0, 0x00, 0x70, 0x03, 0x80, 0x78, 0x00, 0x1c, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0x80, 0xff, 0xe0, 0x1c, 0x0f, 0xff, 0x03, 0x80, 0x00, 0x38, + 0x1c, 0x07, 0x00, 0xfe, 0x00, 0xff, 0xe0, 0x70, 0x03, 0xf8, 0x3f, 0xf8, 0x1c, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xfe, 0x00, 0x7f, 0xc0, 0x1c, 0x07, 0xf3, 0x03, 0x80, 0x00, 0x38, 0x1c, 0x07, + 0x00, 0x7c, 0x00, 0x7f, 0xe0, 0x70, 0x01, 0xf8, 0x1f, 0xf8, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x0e, 0x00, 0x1c, 0x01, 0xc3, 0x03, 0x80, 0x00, 0x38, 0x1c, 0x03, 0x00, 0x7c, + 0x00, 0x0f, 0x00, 0x70, 0x00, 0x70, 0x03, 0xc0, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xf0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x0f, 0xe7, 0x9e, 0x79, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3d, + 0x97, 0x9f, 0xde, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x9e, 0xff, 0x00, + 0x09, 0x24, 0x92, 0x49, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0xa4, 0x91, + 0x12, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x92, 0x44, 0x00, 0x09, 0x2c, + 0x92, 0x49, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0xa7, 0x91, 0x1e, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x92, 0x44, 0x00, 0x09, 0x24, 0x92, 0x49, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0xe4, 0x11, 0x10, 0x40, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x92, 0x44, 0x00, 0x09, 0x27, 0x9e, 0x79, 0xee, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x67, 0x91, 0xde, 0x40, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x9e, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + + +const char index_html[] PROGMEM = R"=====( + + + + Sofar2MQTT + + + + +
+

Sofar2MQTT - 3.3-alpha12

+
+
+

Uptime:

+

Device:

+

Battery SOC:

+

Grid power:

+

Battery power:

+

PV power:

+ + +

Battery temperature:

+

Inverter temperature:

+

Inverter heatsink temperature:

+ + + + +
+ + + +)====="; + + +const char settings_html[] PROGMEM = R"=====( + + + + + + + Sofar2MQTT - settings + + + + +
+

Sofar2MQTT - settings

+
+
+
+ Device name:
+ MQTT host:
+ MQTT port:
+ MQTT user:
+ MQTT pass:
+ Inverter type: + + + + + + + +
+ Screen type: + + + + +
+ Data mode: + + + + +
+ Screen dim timer:
+ Seperated mqtt topics per value: + + + + +
+ + +
+
+ + + +)====="; + +const char settings_html_new[] PROGMEM = R"=====( + + + + + + + Sofar2MQTT - Settings + + + + + +
+

Sofar2MQTT - Settings

+
+
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + + + +
+
+ + + +
+
+ + + + + +)====="; diff --git a/Sofar2mqtt/Sofar2mqtt.ino b/Sofar2mqtt/Sofar2mqtt.ino index 9eb72bc..9fdc5ed 100644 --- a/Sofar2mqtt/Sofar2mqtt.ino +++ b/Sofar2mqtt/Sofar2mqtt.ino @@ -1,164 +1,176 @@ -// Update these to match your inverter/network. -#define INVERTER_ME3000 // Uncomment for ME3000 -//#define INVERTER_HYBRID // Uncomment for Hybrid - // The device name is used as the MQTT base topic. If you need more than one Sofar2mqtt on your network, give them unique names. -const char* deviceName = "Sofar2mqtt"; -const char* version = "v2.1.1"; +const char* version = "v3.3-alpha12"; + +bool tftModel = true; //true means 2.8" color tft, false for oled version + +bool calculated = true; //default to pre-calculated values before sending to mqtt + +unsigned int screenDimTimer = 30; //dim screen after 30 secs +unsigned long lastScreenTouch = 0; + + +#include +#define DRD_TIMEOUT 0.1 +#define DRD_ADDRESS 0x00 +DoubleResetDetect drd(DRD_TIMEOUT, DRD_ADDRESS); + + +#include +#include +#define PORTAL_TIMEOUT 300 //reboots device if hotspot isn't configured after this time +#define WIFI_TIMEOUT 60 //try this long to connect to existing wifi before going to hotspot portal mode + +// * To be filled with EEPROM data +char deviceName[64] = "Sofar"; +char MQTT_HOST[64] = ""; +char MQTT_PORT[6] = "1883"; +char MQTT_USER[32] = ""; +char MQTT_PASS[32] = ""; +#define MQTTRECONNECTTIMER 30000 //it takes 30 secs for each mqtt server reconnect attempt +unsigned long lastMqttReconnectAttempt = 0; + -#define WIFI_SSID "xxxxx" -#define WIFI_PASSWORD "xxxxx" -#define MQTT_SERVER "mqtt" -#define MQTT_PORT 1883 -#define MQTT_USERNAME "auser" // Empty string for none. -#define MQTT_PASSWORD "apassword" /***** -Sofar2mqtt is a remote control interface for Sofar solar and battery inverters. -It allows remote control of the inverter and reports the invertor status, power usage, battery state etc for integration with smart home systems such as Home Assistant and Node-Red vi MQTT. -For read only mode, it will send status messages without the inverter needing to be in passive mode. -It's designed to run on an ESP8266 microcontroller with a TTL to RS485 module such as MAX485 or MAX3485. -Designed to work with TTL modules with or without the DR and RE flow control pins. If your TTL module does not have these pins then just ignore the wire from D5. - -Subscribe your MQTT client to: - -Sofar2mqtt/state - -Which provides: - -running_state -grid_voltage -grid_current -grid_freq -systemIO_power (AC side of inverter) -battery_power (DC side of inverter) -battery_voltage -battery_current -batterySOC -battery_temp -battery_cycles -grid_power -consumption -solarPV -today_generation -today_exported -today_purchase -today_consumption -inverter_temp -inverterHS_temp -solarPVAmps - - -With the inverter in Passive Mode, send MQTT messages to: - -Sofar2mqtt/set/standby - send value "true" -Sofar2mqtt/set/auto - send value "true" or "battery_save" -Sofar2mqtt/set/charge - send values in the range 0-3000 (watts) -Sofar2mqtt/set/discharge - send values in the range 0-3000 (watts) - -Each of the above will return a response on: -Sofar2mqtt/response/, the message containing the response from -the inverter, which has a result code in the lower byte and status in the upper byte. - -The result code will be 0 for success, 1 means "Invalid Work Mode" ( which possibly means -the inverter isn't in passive mode, ) and 3 means "Inverter busy." 2 and 4 are data errors -which shouldn't happen unless there's a cable issue or some such. - -The status bits in the upper byte indicate the following: -Bit 0 - Charge enabled -Bit 1 - Discharge enabled -Bit 2 - Battery full, charge prohibited -Bit 3 - Battery flat, discharge prohibited - -For example, a publish to Sofar2mqtt/set/charge will result in one on Sofar2mqtt/response/charge. -AND the message with 0xff to get the result code, which should be 0. - -battery_save is a hybrid auto mode that will charge from excess solar but not discharge. - -There will also be messages published to Sofar2mqtt/response/ when things happen -in the background, such as setting auto mode on startup and switching modes in battery_save mode. - -(c)Colin McGerty 2021 colin@mcgerty.co.uk -Major version 2.0 rewrite by Adam Hill sidepipeukatgmaildotcom -Thanks to Rich Platts for hybrid model code and testing. -calcCRC by angelo.compagnucci@gmail.com and jpmzometa@gmail.com + Sofar2mqtt is a remote control interface for Sofar solar and battery inverters. + It allows remote control of the inverter and reports the invertor status, power usage, battery state etc for integration with smart home systems such as Home Assistant and Node-Red vi MQTT. + For read only mode, it will send status messages without the inverter needing to be in passive mode. + It's designed to run on an ESP8266 microcontroller with a TTL to RS485 module such as MAX485 or MAX3485. + Designed to work with TTL modules with or without the DR and RE flow control pins. If your TTL module does not have these pins then just ignore the wire from D5. + + Subscribe your MQTT client to: + + Sofar2mqtt/state + + Which provides: + + running_state + grid_voltage + grid_current + grid_freq + systemIO_power (AC side of inverter) + battery_power (DC side of inverter) + battery_voltage + battery_current + batterySOC + battery_temp + battery_cycles + grid_power + consumption + solarPV + today_generation + today_exported + today_purchase + today_consumption + inverter_temp + inverterHS_temp + solarPVAmps + + With the inverter in Passive Mode, send MQTT messages to: + + Sofar2mqtt/set/standby - send value "true" + Sofar2mqtt/set/auto - send value "true" or "battery_save" + Sofar2mqtt/set/charge - send values in the range 0-3000 (watts) + Sofar2mqtt/set/discharge - send values in the range 0-3000 (watts) + + Each of the above will return a response on: + Sofar2mqtt/response/, the message containing the response from + the inverter, which has a result code in the lower byte and status in the upper byte. + + The result code will be 0 for success, 1 means "Invalid Work Mode" ( which possibly means + the inverter isn't in passive mode, ) and 3 means "Inverter busy." 2 and 4 are data errors + which shouldn't happen unless there's a cable issue or some such. + + The status bits in the upper byte indicate the following: + Bit 0 - Charge enabled + Bit 1 - Discharge enabled + Bit 2 - Battery full, charge prohibited + Bit 3 - Battery flat, discharge prohibited + + For example, a publish to Sofar2mqtt/set/charge will result in one on Sofar2mqtt/response/charge. + AND the message with 0xff to get the result code, which should be 0. + + battery_save is a hybrid auto mode that will charge from excess solar but not discharge. + + There will also be messages published to Sofar2mqtt/response/ when things happen + in the background, such as setting auto mode on startup and switching modes in battery_save mode. + + (c)Colin McGerty 2021 colin@mcgerty.co.uk + Major version 2.0 rewrite by Adam Hill sidepipeukatgmaildotcom + Thanks to Rich Platts for hybrid model code and testing. + calcCRC by angelo.compagnucci@gmail.com and jpmzometa@gmail.com *****/ -#if (! defined INVERTER_ME3000) && ! defined INVERTER_HYBRID -#error You must specify the inverter type. -#endif - #include #define SOFAR_SLAVE_ID 0x01 -#ifdef INVERTER_ME3000 -#define MAX_POWER 3000 // ME3000 is 3000W max. -#elif defined INVERTER_HYBRID -#define MAX_POWER 6000 -#endif +#define MAX_POWER 3000 //maybe change in further models #define RS485_TRIES 8 // x 50mS to wait for RS485 input chars. // Wifi parameters. #include -const char* wifiName = WIFI_SSID; WiFiClient wifi; +#include +#include +#include +ESP8266WebServer httpServer(80); +ESP8266HTTPUpdateServer httpUpdater; + +char jsonstring[1000]; + // MQTT parameters #include -const char* mqttClientID = deviceName; PubSubClient mqtt(wifi); // SoftwareSerial is used to create a second serial port, which will be deidcated to RS485. // The built-in serial port remains available for flashing and debugging. #include -#define SERIAL_COMMUNICATION_CONTROL_PIN D5 // Transmission set pin +//for OLED version default to original sofar2mqtt ports +#define SERIAL_COMMUNICATION_CONTROL_PIN D5 // Transmission set pin OLED version #define RS485_TX HIGH #define RS485_RX LOW -#define RXPin D6 // Serial Receive pin -#define TXPin D7 // Serial Transmit pin -SoftwareSerial RS485Serial(RXPin, TXPin); - -// Sofar run states -#define waiting 0 -#define check 1 - -#ifdef INVERTER_ME3000 -#define charging 2 -#define checkDischarge 3 -#define discharging 4 -#define epsState 5 -#define faultState 6 -#define permanentFaultState 7 - -#define HUMAN_CHARGING "Charging" -#define HUMAN_DISCHARGING "Discharge" - -#elif defined INVERTER_HYBRID -#define normal 2 -#define epsState 3 -#define faultState 4 -#define permanentFaultState 5 -#define normal1 6 - -// State names are a bit strange - makes sense to also match to these? -#define charging 2 -#define discharging 6 - -#define HUMAN_CHARGING "Normal" -#define HUMAN_DISCHARGING "Normal1" -#endif - -#define MAX_FRAME_SIZE 64 -#define MODBUS_FN_READSINGLEREG 0x03 -#define SOFAR_FN_PASSIVEMODE 0x42 -#define SOFAR_PARAM_STANDBY 0x5555 +#define OLEDRXPin D6 // Serial Receive pin OLED version +#define OLEDTXPin D7 // Serial Transmit pin OLED version +SoftwareSerial RS485Serial(OLEDRXPin, OLEDTXPin); + +//for TFT verion we use the hardware serial (pin 3 and 1) +#define RXPin 3 // Serial Receive pin +#define TXPin 1 // Serial Transmit pin + + unsigned int INVERTER_RUNNINGSTATE; +#define MAX_FRAME_SIZE 224 +#define MODBUS_FN_READHOLDINGREG 0x03 +#define MODBUS_FN_READINPUTREG 0x04 +#define MODBUS_FN_WRITEMULREG 0x10 +#define SOFAR_FN_PASSIVEMODE 0x42 +#define SOFAR_PARAM_STANDBY 0x5555 + // Battery Save mode is a hybrid mode where the battery will charge from excess solar but not discharge. bool BATTERYSAVE = false; +// Peakshaving will allow the inverter to charge or discharge the battery depending on a limited export or import power value, everything above will be go to or from battery +bool PEAKSHAVING = false; + +// This is the return object for the sendModbus() function. Since we are a modbus master, we +// are primarily interested in the responses to our commands. +struct modbusResponse +{ + uint8_t errorLevel; + uint8_t data[MAX_FRAME_SIZE]; + uint8_t dataSize; + char* errorMessage; +}; + +bool modbusError = true; -// SoFar ME3000 Information Registers +// SoFar Information Registers +#define ME3000_START 0x0200 +#define ME3000_END 0x0239 +#define HYBRID_START 0x0200 +#define HYBRID_END 0x0255 #define SOFAR_REG_RUNSTATE 0x0200 #define SOFAR_REG_GRIDV 0x0206 #define SOFAR_REG_GRIDA 0x0207 @@ -176,6 +188,14 @@ bool BATTERYSAVE = false; #define SOFAR_REG_EXPDAY 0x0219 #define SOFAR_REG_IMPDAY 0x021a #define SOFAR_REG_LOADDAY 0x021b +#define SOFAR_REG_PVTOTAL 0x021c +#define SOFAR_REG_EXPTOTAL 0x021e +#define SOFAR_REG_IMPTOTAL 0x0220 +#define SOFAR_REG_LOADTOTAL 0x0222 +#define SOFAR_REG_CHARGDAY 0x0224 +#define SOFAR_REG_DISCHDAY 0x0225 +#define SOFAR_REG_CHARGTOTAL 0x0226 +#define SOFAR_REG_DISCHTOTAL 0x0228 #define SOFAR_REG_BATTCYC 0x022c #define SOFAR_REG_PVA 0x0236 #define SOFAR_REG_INTTEMP 0x0238 @@ -183,93 +203,244 @@ bool BATTERYSAVE = false; #define SOFAR_REG_PV1 0x0252 #define SOFAR_REG_PV2 0x0255 +#define SOFAR_ANTIREFLUX_CONTROL 0x1242 +#define SOFAR_ANTIREFLUX_POWER 0x1243 + #define SOFAR_FN_STANDBY 0x0100 #define SOFAR_FN_DISCHARGE 0x0101 #define SOFAR_FN_CHARGE 0x0102 #define SOFAR_FN_AUTO 0x0103 +//for HYD-EP and HYD-KTL +//system (0x0400-0x047F) +#define SOFAR2_SYSTEM_BEGIN 0x0404 +#define SOFAR2_SYSTEM_END 0x041A +#define SOFAR2_REG_RUNSTATE 0x0404 +#define SOFAR2_REG_INTTEMP 0x0418 +#define SOFAR2_REG_HSTEMP 0x041A +//on grid output (0x0480-0x04FF) +#define SOFAR2_GRID_BEGIN 0x0484 +#define SOFAR2_GRID_END 0x04AF +#define SOFAR2_REG_GRIDFREQ 0x0484 +#define SOFAR2_REG_ACTW 0x0485 +#define SOFAR2_REG_EXPW 0x0488 +#define SOFAR2_REG_GRIDV 0x048D +#define SOFAR2_REG_LOADW 0x04AF +//PV INPUT (0x0580-0x05FF) +#define SOFAR2_PV_BEGIN 0x0584 +#define SOFAR2_PV_END 0x0589 +#define SOFAR2_REG_VPV1 0x0584 +#define SOFAR2_REG_APV1 0x0585 +#define SOFAR2_REG_PV1 0x0586 +#define SOFAR2_REG_VPV2 0x0587 +#define SOFAR2_REG_APV2 0x0588 +#define SOFAR2_REG_PV2 0x0589 +//seperate +#define SOFAR2_REG_PVW 0x05C4 +//Battery input (0x0600-0x067F) +#define SOFAR2_BAT_BEGIN 0x0604 +#define SOFAR2_BAT_END 0x060A +#define SOFAR2_REG_BATTV 0x0604 +#define SOFAR2_REG_BATTA 0x0605 +#define SOFAR2_REG_BATTW 0x0606 +#define SOFAR2_REG_BATTTEMP 0x0607 +#define SOFAR2_REG_BATTSOC 0x0608 +#define SOFAR2_REG_BATTCYC 0x060A +//Electric Power (0x0680-0x06BF) +#define SOFAR2_POW_BEGIN 0x0684 +#define SOFAR2_POW_END 0x069B //one more because of 32bit value stored +#define SOFAR2_REG_PVDAY 0x0684 +#define SOFAR2_REG_PVTOTAL 0x0686 +#define SOFAR2_REG_LOADDAY 0x0688 +#define SOFAR2_REG_LOADTOTAL 0x068A +#define SOFAR2_REG_IMPDAY 0x068C +#define SOFAR2_REG_IMPTOTAL 0x068E +#define SOFAR2_REG_EXPDAY 0x0690 +#define SOFAR2_REG_EXPTOTAL 0x0692 +#define SOFAR2_REG_CHARGDAY 0x0694 +#define SOFAR2_REG_CHARGTOTAL 0x0696 +#define SOFAR2_REG_DISCHDAY 0x0698 +#define SOFAR2_REG_DISCHTOTAL 0x069A +//end + +#define SOFAR2_ANTIREFLUX_CONTROL 0x1023 +#define SOFAR2_ANTIREFLUX_POWER 0x1024 + +#define SOFAR2_REG_STORAGEMODE 0x4368 +#define SOFAR2_REG_PASSIVECONTROL 0x1187 //in decimal 4487-4492, write 3x 32BIT values with first 32BIT = 0x0000 and next two are same (actually low=high limit) for the value of passive control + +enum calculatorT {NOCALC, DIV10, DIV100, MUL10, MUL100, COMBINE}; +enum valueTypeT {U16, S16, U32, S32, FLOAT}; +enum inverterModelT {ME3000, HYBRID, HYDV2}; +inverterModelT inverterModel = ME3000; //default to ME3000 + +bool separateMqttTopics = false; + struct mqtt_status_register { - uint16_t regnum; - String mqtt_name; + inverterModelT inverter; + uint16_t regnum; + String mqtt_name; + valueTypeT valueType; + calculatorT calculator; }; static struct mqtt_status_register mqtt_status_reads[] = { - { SOFAR_REG_RUNSTATE, "running_state" }, - { SOFAR_REG_GRIDV, "grid_voltage" }, - { SOFAR_REG_GRIDA, "grid_current" }, - { SOFAR_REG_GRIDFREQ, "grid_freq" }, - { SOFAR_REG_GRIDW, "grid_power" }, - { SOFAR_REG_BATTW, "battery_power" }, - { SOFAR_REG_BATTV, "battery_voltage" }, - { SOFAR_REG_BATTA, "battery_current" }, - { SOFAR_REG_SYSIOW, "systemIO_power" }, - { SOFAR_REG_BATTSOC, "batterySOC" }, - { SOFAR_REG_BATTTEMP, "battery_temp" }, - { SOFAR_REG_BATTCYC, "battery_cycles" }, - { SOFAR_REG_LOADW, "consumption" }, - { SOFAR_REG_PVW, "solarPV" }, - { SOFAR_REG_PVA, "solarPVAmps" }, - { SOFAR_REG_PVDAY, "today_generation" }, -#ifdef INVERTER_ME3000 - { SOFAR_REG_EXPDAY, "today_exported" }, - { SOFAR_REG_IMPDAY, "today_purchase" }, -#elif defined INVERTER_HYBRID - { SOFAR_REG_PV1, "Solarpv1" }, - { SOFAR_REG_PV2, "Solarpv2" }, -#endif - { SOFAR_REG_LOADDAY, "today_consumption" }, - { SOFAR_REG_INTTEMP, "inverter_temp" }, - { SOFAR_REG_HSTEMP, "inverter_HStemp" }, + { ME3000, SOFAR_REG_RUNSTATE, "running_state", U16, NOCALC}, + { ME3000, SOFAR_REG_GRIDV, "grid_voltage", U16, DIV10}, + { ME3000, SOFAR_REG_GRIDA, "grid_current", S16, DIV100}, + { ME3000, SOFAR_REG_GRIDFREQ, "grid_freq", U16, DIV100 }, + { ME3000, SOFAR_REG_GRIDW, "grid_power", S16, MUL10 }, + { ME3000, SOFAR_REG_BATTW, "battery_power", S16, MUL10 }, + { ME3000, SOFAR_REG_BATTV, "battery_voltage", U16, DIV10 }, + { ME3000, SOFAR_REG_BATTA, "battery_current", S16, DIV100 }, + { ME3000, SOFAR_REG_SYSIOW, "inverter_power", S16, MUL10 }, + { ME3000, SOFAR_REG_BATTSOC, "batterySOC", U16, NOCALC }, + { ME3000, SOFAR_REG_BATTTEMP, "battery_temp", S16, NOCALC }, + { ME3000, SOFAR_REG_BATTCYC, "battery_cycles", U16, NOCALC }, + { ME3000, SOFAR_REG_LOADW, "consumption", S16, MUL10}, + { ME3000, SOFAR_REG_PVW, "solarPV", U16, MUL10 }, + { ME3000, SOFAR_REG_PVA, "solarPVAmps", U16, NOCALC }, + { ME3000, SOFAR_REG_EXPDAY, "today_exported", U16, DIV100 }, + { ME3000, SOFAR_REG_IMPDAY, "today_purchase", U16, DIV100 }, + { ME3000, SOFAR_REG_PVDAY, "today_generation", U16, DIV100 }, + { ME3000, SOFAR_REG_LOADDAY, "today_consumption", U16, DIV100 }, + { ME3000, SOFAR_REG_EXPTOTAL, "total_exported", U32, COMBINE }, + { ME3000, SOFAR_REG_IMPTOTAL, "total_purchase", U32, COMBINE }, + { ME3000, SOFAR_REG_PVTOTAL, "total_generation", U32, COMBINE }, + { ME3000, SOFAR_REG_LOADTOTAL, "total_consumption", U32, COMBINE }, + { ME3000, SOFAR_REG_CHARGDAY, "today_charged", U16, DIV100 }, + { ME3000, SOFAR_REG_DISCHDAY, "today_discharged", U16, DIV100 }, + { ME3000, SOFAR_REG_CHARGTOTAL, "total_charged", U32, COMBINE }, + { ME3000, SOFAR_REG_DISCHTOTAL, "total_discharged", U32, COMBINE }, + { ME3000, SOFAR_REG_INTTEMP, "inverter_temp", S16, NOCALC }, + { ME3000, SOFAR_REG_HSTEMP, "inverter_HStemp", S16, NOCALC }, + { HYBRID, SOFAR_REG_RUNSTATE, "running_state", U16, NOCALC }, + { HYBRID, SOFAR_REG_GRIDV, "grid_voltage", U16, DIV10 }, + { HYBRID, SOFAR_REG_GRIDA, "grid_current", S16, DIV100 }, + { HYBRID, SOFAR_REG_GRIDFREQ, "grid_freq", U16, DIV100 }, + { HYBRID, SOFAR_REG_GRIDW, "grid_power", S16, MUL10 }, + { HYBRID, SOFAR_REG_BATTW, "battery_power", S16, MUL10 }, + { HYBRID, SOFAR_REG_BATTV, "battery_voltage", U16, DIV10 }, + { HYBRID, SOFAR_REG_BATTA, "battery_current", S16, DIV100 }, + { HYBRID, SOFAR_REG_SYSIOW, "inverter_power", S16, MUL10 }, + { HYBRID, SOFAR_REG_BATTSOC, "batterySOC", U16, NOCALC }, + { HYBRID, SOFAR_REG_BATTTEMP, "battery_temp", S16, NOCALC }, + { HYBRID, SOFAR_REG_BATTCYC, "battery_cycles", U16, NOCALC }, + { HYBRID, SOFAR_REG_LOADW, "consumption", S16, MUL10 }, + { HYBRID, SOFAR_REG_PVW, "solarPV", U16, MUL10 }, + { HYBRID, SOFAR_REG_PVA, "solarPVAmps", U16, NOCALC }, + { HYBRID, SOFAR_REG_PV1, "solarPV1", U16, MUL10 }, + { HYBRID, SOFAR_REG_PV2, "solarPV2", U16, MUL10 }, + { HYBRID, SOFAR_REG_PVDAY, "today_generation", U16, DIV100 }, + { HYBRID, SOFAR_REG_LOADDAY, "today_consumption", U16, DIV100 }, + { HYBRID, SOFAR_REG_EXPDAY, "today_exported", U16, DIV100 }, + { HYBRID, SOFAR_REG_IMPDAY, "today_purchase", U16, DIV100 }, + { HYBRID, SOFAR_REG_CHARGDAY, "today_charged", U16, DIV100 }, + { HYBRID, SOFAR_REG_DISCHDAY, "today_discharged", U16, DIV100 }, + { HYBRID, SOFAR_REG_PVTOTAL, "total_generation", U32, COMBINE }, + { HYBRID, SOFAR_REG_LOADTOTAL, "total_consumption", U32, COMBINE }, + { HYBRID, SOFAR_REG_EXPTOTAL, "total_exported", U32, COMBINE }, + { HYBRID, SOFAR_REG_IMPTOTAL, "total_purchase", U32, COMBINE }, + { HYBRID, SOFAR_REG_CHARGTOTAL, "total_charged", U32, COMBINE }, + { HYBRID, SOFAR_REG_DISCHTOTAL, "total_discharged", U32, COMBINE }, + { HYBRID, SOFAR_REG_INTTEMP, "inverter_temp", S16, NOCALC }, + { HYBRID, SOFAR_REG_HSTEMP, "inverter_HStemp", S16, NOCALC }, + { HYDV2, SOFAR2_REG_RUNSTATE, "running_state", U16, NOCALC }, + { HYDV2, SOFAR2_REG_INTTEMP, "inverter_temp", S16, NOCALC }, + { HYDV2, SOFAR2_REG_HSTEMP, "inverter_HStemp", S16, NOCALC}, + { HYDV2, SOFAR2_REG_GRIDFREQ, "grid_freq", U16, DIV100 }, + { HYDV2, SOFAR2_REG_ACTW, "inverter_power", S16, MUL10 }, + { HYDV2, SOFAR2_REG_EXPW, "grid_power", S16, MUL10}, + { HYDV2, SOFAR2_REG_GRIDV, "grid_voltage", U16, DIV10 }, + { HYDV2, SOFAR2_REG_LOADW, "consumption", S16, MUL10 }, + { HYDV2, SOFAR2_REG_VPV1, "solarPV1Volt", U16, DIV10}, + { HYDV2, SOFAR2_REG_APV1, "solarPV1Current", U16, DIV100}, + { HYDV2, SOFAR2_REG_PV1, "solarPV1", U16, MUL10}, + { HYDV2, SOFAR2_REG_VPV2, "solarPV2Volt", U16, DIV10}, + { HYDV2, SOFAR2_REG_APV2, "solarPV2Current", U16, DIV100}, + { HYDV2, SOFAR2_REG_PV2, "solarPV2", U16, MUL10}, + { HYDV2, SOFAR2_REG_PVW, "solarPV", U16, MUL100 }, + { HYDV2, SOFAR2_REG_BATTV, "battery_voltage", U16, DIV10 }, + { HYDV2, SOFAR2_REG_BATTA, "battery_current", S16, DIV100 }, + { HYDV2, SOFAR2_REG_BATTW, "battery_power", S16, MUL10 }, + { HYDV2, SOFAR2_REG_BATTTEMP, "battery_temp", S16, NOCALC }, + { HYDV2, SOFAR2_REG_BATTSOC, "batterySOC", U16, NOCALC }, + { HYDV2, SOFAR2_REG_BATTCYC, "battery_cycles", U16, NOCALC }, + { HYDV2, SOFAR2_REG_PVDAY, "today_generation", U32, DIV100 }, + { HYDV2, SOFAR2_REG_PVTOTAL, "total_generation", U32, DIV10 }, + { HYDV2, SOFAR2_REG_LOADDAY, "today_consumption", U32, DIV100 }, + { HYDV2, SOFAR2_REG_LOADTOTAL, "total_consumption", U32, DIV10 }, + { HYDV2, SOFAR2_REG_IMPDAY, "today_purchase", U32, DIV100 }, + { HYDV2, SOFAR2_REG_IMPTOTAL, "total_purchase", U32, DIV10 }, + { HYDV2, SOFAR2_REG_EXPDAY, "today_exported", U32, DIV100 }, + { HYDV2, SOFAR2_REG_EXPTOTAL, "total_exported", U32, DIV10 }, + { HYDV2, SOFAR2_REG_CHARGDAY, "today_charged", U32, DIV100 }, + { HYDV2, SOFAR2_REG_CHARGTOTAL, "total_charged", U32, DIV10 }, + { HYDV2, SOFAR2_REG_DISCHDAY, "today_discharged", U32, DIV100 }, + { HYDV2, SOFAR2_REG_DISCHTOTAL, "total_discharged", U32, DIV10 } }; -// This is the return object for the sendModbus() function. Since we are a modbus master, we -// are primarily interested in the responses to our commands. -struct modbusResponse -{ - uint8_t errorLevel; - uint8_t data[MAX_FRAME_SIZE]; - uint8_t dataSize; - char* errorMessage; -}; // These timers are used in the main loop. #define HEARTBEAT_INTERVAL 9000 #define RUNSTATE_INTERVAL 5000 #define SEND_INTERVAL 10000 #define BATTERYSAVE_INTERVAL 3000 +#define PEAKSHAVING_INTERVAL 3000 // Wemos OLED Shield set up. 64x48, pins D1 and D2 #include #include + +#include "Sofar2mqtt.h" + +//for the tft +#include // include Adafruit ILI9341 TFT library +#define TFT_CS D1 // TFT CS pin is connected to arduino pin 8 +#define TFT_DC D2 // TFT DC pin is connected to arduino pin 10 +#define TFT_LED D8 +// initialize ILI9341 TFT library +Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC); + +//for touch +#include +#define TCS_PIN 0 +#define TIRQ_PIN 2 +XPT2046_Touchscreen ts(TCS_PIN, TIRQ_PIN); + +//for the oled #include #include #define OLED_RESET 0 // GPIO0 Adafruit_SSD1306 display(OLED_RESET); + +#include + /** - * Check to see if the elapsed interval has passed since the passed in - * millis() value. If it has, return true and update the lastRun. Note - * that millis() overflows after 50 days, so we need to deal with that - * too... in our case we just zero the last run, which means the timer - * could be shorter but it's not critical... not worth the extra effort - * of doing it properly for once in 50 days. - */ + Check to see if the elapsed interval has passed since the passed in + millis() value. If it has, return true and update the lastRun. Note + that millis() overflows after 50 days, so we need to deal with that + too... in our case we just zero the last run, which means the timer + could be shorter but it's not critical... not worth the extra effort + of doing it properly for once in 50 days. +*/ bool checkTimer(unsigned long *lastRun, unsigned long interval) { - unsigned long now = millis(); + unsigned long now = millis(); - if(*lastRun > now) - *lastRun = 0; + if (*lastRun > now) + *lastRun = 0; - if(now >= *lastRun + interval) - { - *lastRun = now; - return true; - } + if (now >= *lastRun + interval) + { + *lastRun = now; + return true; + } - return false; + return false; } // Update the OLED. Use "NULL" for no change or "" for an empty line. @@ -280,659 +451,1657 @@ String oledLine4; void updateOLED(String line1, String line2, String line3, String line4) { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(WHITE); - display.setCursor(0,0); - - if(line1 != "NULL") - { - display.println(line1); - oledLine1 = line1; - } - else - display.println(oledLine1); - - display.setCursor(0,12); - - if(line2 != "NULL") - { - display.println(line2); - oledLine2 = line2; - } - else - display.println(oledLine2); - - display.setCursor(0,24); - - if(line3 != "NULL") - { - display.println(line3); - oledLine3 = line3; - } - else - display.println(oledLine3); - - display.setCursor(0,36); - - if(line4 != "NULL") - { - display.println(line4); - oledLine4 = line4; - } - else - display.println(oledLine4); - - display.display(); -} - -// Connect to WiFi -void setup_wifi() -{ - // We start by connecting to a WiFi network - Serial.println(); - Serial.print("Connecting to "); - Serial.println(wifiName); - updateOLED("NULL", "NULL", "WiFi..", "NULL"); - WiFi.mode(WIFI_STA); - WiFi.begin(wifiName, WIFI_PASSWORD); + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(WHITE); + display.setCursor(0, 0); + + if (line1 != "NULL") + { + display.println(line1); + oledLine1 = line1; + } + else + display.println(oledLine1); + + display.setCursor(0, 12); + + if (line2 != "NULL") + { + display.println(line2); + oledLine2 = line2; + } + else + display.println(oledLine2); + + display.setCursor(0, 24); + + if (line3 != "NULL") + { + display.println(line3); + oledLine3 = line3; + } + else + display.println(oledLine3); + + display.setCursor(0, 36); + + if (line4 != "NULL") + { + display.println(line4); + oledLine4 = line4; + } + else + display.println(oledLine4); + + display.display(); +} - while (WiFi.status() != WL_CONNECTED) - { - delay(500); - Serial.print("."); - updateOLED("NULL", "NULL", "WiFi...", "NULL"); - } +// ********************************** +// * EEPROM helpers * +// ********************************** - WiFi.hostname(deviceName); - Serial.println(""); - Serial.print("WiFi connected - ESP IP address: "); - Serial.println(WiFi.localIP()); - updateOLED("NULL", "NULL", "WiFi....", "NULL"); +String read_eeprom(int offset, int len) +{ + String res = ""; + for (int i = 0; i < len; ++i) + { + res += char(EEPROM.read(i + offset)); + } + return res; } -int addStateInfo(String &state, uint16_t reg, String human) +void write_eeprom(int offset, int len, String value) { - unsigned int val; - modbusResponse rs; + for (int i = 0; i < len; ++i) + { + if ((unsigned)i < value.length()) + { + EEPROM.write(i + offset, value[i]); + } + else + { + EEPROM.write(i + offset, 0); + } + } +} + +// * Gets called when WiFiManager enters configuration mode +void configModeCallback(WiFiManager *myWiFiManager) +{ + if (tftModel) { + tft.println(F("Entering config mode")); + tft.println(F("Connect your phone to WiFi: ")); + tft.println(myWiFiManager->getConfigPortalSSID()); + tft.println(F("And browse to: ")); + tft.println(WiFi.softAPIP()); + } else { + updateOLED("NULL", "hotspot", "no config", "NULL"); + } + +} - if(readSingleReg(SOFAR_SLAVE_ID, reg, &rs)) - return -1; - val = ((rs.data[0] << 8) | rs.data[1]); +bool shouldSaveConfig = false; - if (!( state == "{")) - state += ","; +// * Callback notifying us of the need to save config +void save_wifi_config_callback () +{ + shouldSaveConfig = true; +} + +void saveToEeprom() { + write_eeprom(0, 1, "1"); // * 0 --> always "1" + write_eeprom(1, 64, deviceName); // * 1-64 + write_eeprom(65, 64, MQTT_HOST); // * 65-128 + write_eeprom(129, 6, MQTT_PORT); // * 129-134 + write_eeprom(135, 32, MQTT_USER); // * 135-166 + write_eeprom(167, 32, MQTT_PASS); // * 167-198 + EEPROM.write(199, inverterModel); // * 199 + EEPROM.write(200, tftModel); // * 200 + EEPROM.write(201, calculated); // * 201 + EEPROM.write(202, screenDimTimer); // * 202 + EEPROM.write(203, separateMqttTopics); // * 203 + EEPROM.commit(); + ESP.reset(); // reset after save to activate new settings +} - state += "\"" + human + "\":" + String(val); - return 0; +bool loadFromEeprom() { + // * Get MQTT Server settings + String settings_available = read_eeprom(0, 1); + + if (settings_available == "1") + { + read_eeprom(1, 64).toCharArray(deviceName, 64); // * 1-64 + read_eeprom(65, 64).toCharArray(MQTT_HOST, 64); // * 65-128 + read_eeprom(129, 6).toCharArray(MQTT_PORT, 6); // * 129-134 + read_eeprom(135, 32).toCharArray(MQTT_USER, 32); // * 135-166 + read_eeprom(167, 32).toCharArray(MQTT_PASS, 32); // * 167 -198 + inverterModel = ME3000; + if (EEPROM.read(199) == 1) { + inverterModel = HYBRID; + } else if (EEPROM.read(199) == 2) { + inverterModel = HYDV2; + } + tftModel = EEPROM.read(200); + calculated = EEPROM.read(201); + screenDimTimer = EEPROM.read(202); + separateMqttTopics = EEPROM.read(203); + WiFi.hostname(deviceName); + return true; + } + return false; } -void sendData() +void setup_wifi() { - static unsigned long lastRun = 0; - // Update all parameters and send to MQTT. - if(checkTimer(&lastRun, SEND_INTERVAL)) - { - int l; - String state = "{"; + WiFiManagerParameter CUSTOM_MY_HOST("device", "My hostname", deviceName, 64); + WiFiManagerParameter CUSTOM_MQTT_HOST("mqtt", "MQTT hostname", MQTT_HOST, 64); + WiFiManagerParameter CUSTOM_MQTT_PORT("port", "MQTT port", MQTT_PORT, 6); + WiFiManagerParameter CUSTOM_MQTT_USER("user", "MQTT user", MQTT_USER, 32); + WiFiManagerParameter CUSTOM_MQTT_PASS("pass", "MQTT pass", MQTT_PASS, 32); + + const char *bufferStr = R"( +
+

Select LCD screen type:

+ +
+ +
+
+

Select inverter type:

+ +
+ +
+ +
+
+

Select data mode:

+ +
+ +
+
+ + )"; + WiFiManagerParameter custom_html_inputs(bufferStr); + char lcdModelString[6]; + sprintf(lcdModelString, "%u", uint8_t(tftModel)); + WiFiManagerParameter custom_hidden_lcd("key_custom_lcd", "LCD type hidden", lcdModelString, 2); + char inverterModelString[6]; + sprintf(inverterModelString, "%u", uint8_t(inverterModel)); + WiFiManagerParameter custom_hidden_inverter("key_custom_inverter", "Inverter type hidden", inverterModelString, 2); + char modeString[6]; + sprintf(modeString, "%u", uint8_t(calculated)); + WiFiManagerParameter custom_hidden_mode("key_custom_mode", "Mode type hidden", modeString, 2); + + WiFiManager wifiManager; + wifiManager.setAPCallback(configModeCallback); + wifiManager.setConfigPortalTimeout(PORTAL_TIMEOUT); + wifiManager.setSaveConfigCallback(save_wifi_config_callback); + wifiManager.addParameter(&custom_hidden_lcd); + wifiManager.addParameter(&custom_hidden_inverter); + wifiManager.addParameter(&custom_hidden_mode); + wifiManager.addParameter(&custom_html_inputs); + wifiManager.addParameter(&CUSTOM_MY_HOST); + wifiManager.addParameter(&CUSTOM_MQTT_HOST); + wifiManager.addParameter(&CUSTOM_MQTT_PORT); + wifiManager.addParameter(&CUSTOM_MQTT_USER); + wifiManager.addParameter(&CUSTOM_MQTT_PASS); + + + wifiManager.setConnectTimeout(WIFI_TIMEOUT); + if (!wifiManager.autoConnect("Sofar2Mqtt")) + { + if (tftModel) { + tft.println(F("Failed to connect to WIFI and hit timeout")); + } else { + updateOLED("NULL", "NULL", "WiFi.!.", "NULL"); + } + // * Reset and try again, or maybe put it to deep sleep + ESP.reset(); + } + + // * Read updated parameters + strcpy(deviceName, CUSTOM_MY_HOST.getValue()); + strcpy(MQTT_HOST, CUSTOM_MQTT_HOST.getValue()); + strcpy(MQTT_PORT, CUSTOM_MQTT_PORT.getValue()); + strcpy(MQTT_USER, CUSTOM_MQTT_USER.getValue()); + strcpy(MQTT_PASS, CUSTOM_MQTT_PASS.getValue()); + if (atoi(custom_hidden_lcd.getValue()) == 0) tftModel = false; + if (atoi(custom_hidden_inverter.getValue()) == 1) inverterModel = HYBRID; + if (atoi(custom_hidden_inverter.getValue()) == 2) inverterModel = HYDV2; + + // * Save the custom parameters to FS which will also initiate a reset to activate other lcd screen if necessary + if (shouldSaveConfig) saveToEeprom(); + if (tftModel) { + tft.println(F("Connected to WIFI...")); + tft.println(WiFi.localIP()); + } else { + updateOLED("NULL", "NULL", "WiFi...", "NULL"); + } + delay(500); + +} - for(l = 0; l < sizeof(mqtt_status_reads)/sizeof(struct mqtt_status_register); l++) - addStateInfo(state, mqtt_status_reads[l].regnum, mqtt_status_reads[l].mqtt_name); - state = state+"}"; - //Prefix the mqtt topic name with deviceName. - String topic(deviceName); - topic += "/state"; - sendMqtt(const_cast(topic.c_str()), state); - } +void addStateInfo(String &state, unsigned int index, unsigned int dataindex, modbusResponse *rs) +{ + String stringVal; + + if (!calculated) { + if ( (mqtt_status_reads[index].valueType == U16) || (mqtt_status_reads[index].valueType == S16)) { + uint16_t val; + val = (uint16_t)((rs->data[dataindex] << 8) | rs->data[dataindex + 1]); + stringVal = String(val); + } else { + uint32_t val; + val = (uint32_t)((rs->data[dataindex] << 24) | (rs->data[dataindex + 1] << 16) | (rs->data[dataindex + 2] << 8) | rs->data[dataindex + 3]); + stringVal = String(val); + } + } else { + if (mqtt_status_reads[index].valueType == U16) { + uint16_t val; + val = (uint16_t)((rs->data[dataindex] << 8) | rs->data[dataindex + 1]); + switch (mqtt_status_reads[index].calculator) { + case DIV10: { + stringVal = String((float)val / 10.0); + break; + } + case DIV100: { + stringVal = String((float)val / 100.0); + break; + } + case MUL10: { + stringVal = String(val * 10); + break; + } + case MUL100: { + stringVal = String(val * 100); + break; + } + default: { + stringVal = String(val); + break; + } + } + } + if (mqtt_status_reads[index].valueType == S16) { + int16_t val; + val = (int16_t)((rs->data[dataindex] << 8) | rs->data[dataindex + 1]); + switch (mqtt_status_reads[index].calculator) { + case DIV10: { + stringVal = String((float)val / 10.0); + break; + } + case DIV100: { + stringVal = String((float)val / 100.0); + break; + } + case MUL10: { + stringVal = String(val * 10); + break; + } + case MUL100: { + stringVal = String(val * 100); + break; + } + default: { + stringVal = String(val); + break; + } + } + } + if (mqtt_status_reads[index].valueType == U32) { + uint32_t val; + val = (uint32_t)((rs->data[dataindex] << 24) | (rs->data[dataindex + 1] << 16) | (rs->data[dataindex + 2] << 8) | rs->data[dataindex + 3]); + switch (mqtt_status_reads[index].calculator) { + case DIV10: { + stringVal = String((float)val / 10.0); + break; + } + case DIV100: { + stringVal = String((float)val / 100.0); + break; + } + case MUL10: { + stringVal = String(val * 10); + break; + } + case MUL100: { + stringVal = String(val * 100); + break; + } + default: { + stringVal = String(val); + break; + } + } + } + if (mqtt_status_reads[index].valueType == S32) { + int32_t val; + val = (int32_t)((rs->data[dataindex] << 24) | (rs->data[dataindex + 1] << 16) | (rs->data[dataindex + 2] << 8) | rs->data[dataindex + 3]); + switch (mqtt_status_reads[index].calculator) { + case DIV10: { + stringVal = String((float)val / 10.0); + break; + } + case DIV100: { + stringVal = String((float)val / 100.0); + break; + } + case MUL10: { + stringVal = String(val * 10); + break; + } + case MUL100: { + stringVal = String(val * 100); + break; + } + default: { + stringVal = String(val); + break; + } + } + } + } + + if (!( state == "{")) + state += ","; + + state += "\"" + mqtt_status_reads[index].mqtt_name + "\":" + stringVal; + + if ((separateMqttTopics) && (mqtt.connected())) { + String topic(deviceName); + topic += "/state/" + mqtt_status_reads[index].mqtt_name; + sendMqtt(const_cast(topic.c_str()), stringVal); + } + + if ((mqtt_status_reads[index].mqtt_name == "batterySOC") && (tftModel)) { + tft.setCursor(105, 70); + tft.setTextSize(2); + tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK); + tft.println(stringVal + "% "); + } + +} + +void retrieveData() +{ + static unsigned long lastRun = 0; + + // Update all parameters and send to MQTT. + if (checkTimer(&lastRun, SEND_INTERVAL)) + { + String state = "{\"uptime\":" + String(millis()) + ",\"deviceName\": \"" + String(deviceName) + "\""; + if (inverterModel == ME3000) { + modbusResponse rs; + if ((!modbusError) && ( readBulkReg(SOFAR_SLAVE_ID, ME3000_START, (ME3000_END - ME3000_START + 1), &rs) == 0)) { + for (unsigned int l = 0; l < sizeof(mqtt_status_reads) / sizeof(struct mqtt_status_register); l++) + if (mqtt_status_reads[l].inverter == inverterModel) { + addStateInfo(state, l, (mqtt_status_reads[l].regnum - ME3000_START) * 2, &rs); + loopRuns(); //handle some other requests while building the state info + } + } + } else if (inverterModel == HYBRID) { + modbusResponse rs; + if ((!modbusError) && ( readBulkReg(SOFAR_SLAVE_ID, HYBRID_START, (HYBRID_END - HYBRID_START + 1), &rs) == 0)) { + for (unsigned int l = 0; l < sizeof(mqtt_status_reads) / sizeof(struct mqtt_status_register); l++) + if (mqtt_status_reads[l].inverter == inverterModel) { + addStateInfo(state, l, (mqtt_status_reads[l].regnum - HYBRID_START) * 2, &rs); + loopRuns(); //handle some other requests while building the state info + } + } + } else if (inverterModel == HYDV2) { + if (!modbusError) { + modbusResponse rs; + uint8_t cached = 0; + for (unsigned int l = 0; l < sizeof(mqtt_status_reads) / sizeof(struct mqtt_status_register); l++) + if (mqtt_status_reads[l].inverter == inverterModel) { + if ((mqtt_status_reads[l].regnum >= SOFAR2_SYSTEM_BEGIN) && (mqtt_status_reads[l].regnum <= SOFAR2_SYSTEM_END)) { + if ((cached == 1) || (readBulkReg(SOFAR_SLAVE_ID, SOFAR2_SYSTEM_BEGIN, SOFAR2_SYSTEM_END - SOFAR2_SYSTEM_BEGIN + 1, &rs) == 0) ) { + cached == 1; + addStateInfo(state, l, (mqtt_status_reads[l].regnum - SOFAR2_SYSTEM_BEGIN) * 2, &rs); + } else { + cached = 0; + } + } + if ((mqtt_status_reads[l].regnum >= SOFAR2_GRID_BEGIN) && (mqtt_status_reads[l].regnum <= SOFAR2_GRID_END)) { + if ((cached == 2) || (readBulkReg(SOFAR_SLAVE_ID, SOFAR2_GRID_BEGIN, SOFAR2_GRID_END - SOFAR2_GRID_BEGIN + 1, &rs) == 0) ) { + cached == 2; + addStateInfo(state, l, (mqtt_status_reads[l].regnum - SOFAR2_GRID_BEGIN) * 2, &rs); + } else { + cached = 0; + } + } + if ((mqtt_status_reads[l].regnum >= SOFAR2_PV_BEGIN) && (mqtt_status_reads[l].regnum <= SOFAR2_PV_END)) { + if ((cached == 3) || (readBulkReg(SOFAR_SLAVE_ID, SOFAR2_PV_BEGIN, SOFAR2_PV_END - SOFAR2_PV_BEGIN + 1, &rs) == 0) ) { + cached == 3; + addStateInfo(state, l, (mqtt_status_reads[l].regnum - SOFAR2_PV_BEGIN) * 2, &rs); + } else { + cached = 0; + } + } + if ((mqtt_status_reads[l].regnum >= SOFAR2_BAT_BEGIN) && (mqtt_status_reads[l].regnum <= SOFAR2_BAT_END)) { + if ((cached == 4) || (readBulkReg(SOFAR_SLAVE_ID, SOFAR2_BAT_BEGIN, SOFAR2_BAT_END - SOFAR2_BAT_BEGIN + 1, &rs) == 0) ) { + cached == 4; + addStateInfo(state, l, (mqtt_status_reads[l].regnum - SOFAR2_BAT_BEGIN) * 2, &rs); + } else { + cached = 0; + } + } + if ((mqtt_status_reads[l].regnum >= SOFAR2_POW_BEGIN) && (mqtt_status_reads[l].regnum <= SOFAR2_POW_END)) { + if ((cached == 5) || (readBulkReg(SOFAR_SLAVE_ID, SOFAR2_POW_BEGIN, SOFAR2_POW_END - SOFAR2_POW_BEGIN + 1, &rs) == 0) ) { + cached == 5; + addStateInfo(state, l, (mqtt_status_reads[l].regnum - SOFAR2_POW_BEGIN) * 2, &rs); + } else { + cached = 0; + } + } + //for some unknown reason this register can't be get in BULK + if (mqtt_status_reads[l].regnum == SOFAR2_REG_PVW) { + cached == 0; + if (readSingleReg(SOFAR_SLAVE_ID, SOFAR2_REG_PVW, &rs) == 0) { + addStateInfo(state, l, 0, &rs); + } + } + loopRuns(); //handle some other requests while building the state info + } + } + } + + state = state + "}"; + + { //Prefix the mqtt topic name with deviceName. + String topic(deviceName); + topic += "/state"; + if (mqtt.connected()) sendMqtt(const_cast(topic.c_str()), state); + state.toCharArray(jsonstring, sizeof(jsonstring)); + } + + } } // This function is executed when an MQTT message arrives on a topic that we are subscribed to. void mqttCallback(String topic, byte *message, unsigned int length) { - if(!topic.startsWith(String(deviceName) + "/set/")) - return; - - Serial.print("Message arrived on topic: "); - Serial.print(topic); - Serial.print(". Message: "); - String messageTemp; - uint16_t fnCode = 0, fnParam = 0; - String cmd = topic.substring(topic.lastIndexOf("/") + 1); - - for(int i = 0; i < length; i++) - { - Serial.print((char)message[i]); - messageTemp += (char)message[i]; - } - - Serial.println(); - int messageValue = messageTemp.toInt(); - bool messageBool = ((messageTemp != "false") && (messageTemp != "battery_save")); - - if(cmd == "standby") - { - if(messageBool) - { - fnCode = SOFAR_FN_STANDBY; - fnParam = SOFAR_PARAM_STANDBY; - } - } - else if(cmd == "auto") - { - if(messageBool) - fnCode = SOFAR_FN_AUTO; - else if(messageTemp == "battery_save") - BATTERYSAVE = true; - } - else if((messageValue > 0) && (messageValue <= MAX_POWER)) - { - fnParam = messageValue; - - if(cmd == "charge") - fnCode = SOFAR_FN_CHARGE; - else if(cmd == "discharge") - fnCode = SOFAR_FN_DISCHARGE; - } - - if(fnCode) - { - BATTERYSAVE = false; - sendPassiveCmd(SOFAR_SLAVE_ID, fnCode, fnParam, cmd); - } + if (!topic.startsWith(String(deviceName) + "/set/")) + return; + + String messageTemp; + uint16_t fnCode = 0, fnParam = 0; + String cmd = topic.substring(topic.lastIndexOf("/") + 1); + + for (int i = 0; i < length; i++) + { + messageTemp += (char)message[i]; + } + + if (cmd == "modbus") { + + modbusResponse rs; + String retMsg; + if (sendModbus(message, length, &rs)) + retMsg = rs.errorMessage; + else if (rs.dataSize != 2) + retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; + else + { + retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); + } + + String topic(deviceName); + topic += "/response/modbus"; + sendMqtt(const_cast(topic.c_str()), retMsg); + return; + } + + if (cmd == "mode_control") { //only for HYDV2 + if (inverterModel == HYDV2) { + modbusResponse rs; + int16_t data = messageTemp.toInt(); + uint16_t addr = 0x1110; + uint8_t frame[] = { SOFAR_SLAVE_ID, MODBUS_FN_WRITEMULREG, (addr >> 8) & 0xff, addr & 0xff, 0, 1, 2, 0, data, 0, 0}; + String retMsg; + if (sendModbus(frame, sizeof(frame), &rs)) + retMsg = rs.errorMessage; + else if (rs.dataSize != 2) + retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; + else + { + retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); + } + + String topic(deviceName); + topic += "/response/mode_control"; + sendMqtt(const_cast(topic.c_str()), retMsg); + } + return; + + } + + if ((cmd == "threephaselimit") || (cmd == "antireflux")) { + uint16_t addr = inverterModel == HYDV2 ? SOFAR2_ANTIREFLUX_CONTROL : SOFAR_ANTIREFLUX_CONTROL; + if (messageTemp == "off") { + modbusResponse rs; + uint8_t frame[] = { SOFAR_SLAVE_ID, MODBUS_FN_WRITEMULREG, (addr >> 8) & 0xff, addr & 0xff, 0, 2, 4, 0, 0, 0, 0, 0, 0}; + String retMsg; + if (sendModbus(frame, sizeof(frame), &rs)) + retMsg = rs.errorMessage; + else if (rs.dataSize != 2) + retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; + else + { + retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); + } + + String topic(deviceName); + topic += "/response/" + cmd; + sendMqtt(const_cast(topic.c_str()), retMsg); + } else { + modbusResponse rs; + int16_t data = messageTemp.toInt(); + uint8_t control = ((cmd == "threephaselimit") && (inverterModel == HYDV2)) ? 2 : 1; + uint8_t frame[] = { SOFAR_SLAVE_ID, MODBUS_FN_WRITEMULREG, (addr >> 8) & 0xff, addr & 0xff, 0, 2, 4, 0, control, (data >> 8) & 0xff, data & 0xff, 0, 0}; + String retMsg; + if (sendModbus(frame, sizeof(frame), &rs)) + retMsg = rs.errorMessage; + else if (rs.dataSize != 2) + retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; + else + { + retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); + } + + String topic(deviceName); + topic += "/response/" + cmd; + sendMqtt(const_cast(topic.c_str()), retMsg); + } + return; + } + + + + if (cmd == "remote_control") { //only for HYDV2 + if (inverterModel == HYDV2) { + modbusResponse rs; + int16_t data = messageTemp.toInt(); + uint16_t addr = 0x1104; + uint8_t frame[] = { SOFAR_SLAVE_ID, MODBUS_FN_WRITEMULREG, (addr >> 8) & 0xff, addr & 0xff, 0, 1, 2, 0, data, 0, 0}; + String retMsg; + if (sendModbus(frame, sizeof(frame), &rs)) + retMsg = rs.errorMessage; + else if (rs.dataSize != 2) + retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; + else + { + retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); + } + + String topic(deviceName); + topic += "/response/remote_control"; + sendMqtt(const_cast(topic.c_str()), retMsg); + } + return; + } + + int messageValue = messageTemp.toInt(); + bool messageBool = ((messageTemp != "false") && (messageTemp != "battery_save")); + + switch (inverterModel) { + case HYDV2: { + if (cmd == "standby") { + sendPassiveCmdV2(SOFAR_SLAVE_ID, SOFAR2_REG_PASSIVECONTROL, 0, cmd); + } else if (cmd == "auto") { + } else if ((cmd == "charge") || (cmd == "discharge")) { + if (cmd == "discharge") { + messageValue = messageValue * -1; + } + sendPassiveCmdV2(SOFAR_SLAVE_ID, SOFAR2_REG_PASSIVECONTROL, messageValue, cmd); + } + break; + } + default: { + if (cmd == "standby") + { + if (messageBool) + { + fnCode = SOFAR_FN_STANDBY; + fnParam = SOFAR_PARAM_STANDBY; + } + } + else if (cmd == "auto") + { + if (messageBool) + fnCode = SOFAR_FN_AUTO; + else if (messageTemp == "battery_save") + BATTERYSAVE = true; + } + else if (messageValue > 0) + { + fnParam = messageValue; + + if (cmd == "charge") + fnCode = SOFAR_FN_CHARGE; + else if (cmd == "discharge") + fnCode = SOFAR_FN_DISCHARGE; + } + + if (fnCode) + { + BATTERYSAVE = false; + sendPassiveCmd(SOFAR_SLAVE_ID, fnCode, fnParam, cmd); + } + break; + } + + } +} + +int peakShavingLimitImport = 0; +int peakShavingLimitExport = 0; +int peakShavingBatteryPower = 0; +void peakShaving() +{ + static unsigned long lastRun = 0; + + if (checkTimer(&lastRun, PEAKSHAVING_INTERVAL) && PEAKSHAVING) + { + modbusResponse rs; + + //Get grid power + int16_t gridPower = 0; + + if (readSingleReg(SOFAR_SLAVE_ID, SOFAR_REG_GRIDW, &rs) == 0) { + gridPower = (int16_t)((rs.data[0] << 8) | rs.data[1]) * 10; + if (gridPower < peakShavingLimitImport) { + peakShavingBatteryPower += (gridPower - peakShavingLimitImport); + } else if (gridPower > peakShavingLimitExport) { + peakShavingBatteryPower += (gridPower - peakShavingLimitExport); + } + if (peakShavingBatteryPower > 0) { + //charge + if (peakShavingBatteryPower > MAX_POWER) { + peakShavingBatteryPower == MAX_POWER; + } + sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_CHARGE, (uint16_t)peakShavingBatteryPower, "charge"); + } else if (peakShavingBatteryPower < 0) { + //discharge + if (peakShavingBatteryPower < (-1 * MAX_POWER)) { + peakShavingBatteryPower == (-1 * MAX_POWER); + } + sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_DISCHARGE, (uint16_t)(peakShavingBatteryPower * -1), "discharge"); + } else { + //standby + sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_STANDBY, SOFAR_PARAM_STANDBY, "standby"); + } + } + } } void batterySave() { - static unsigned long lastRun = 0; - - if(checkTimer(&lastRun, BATTERYSAVE_INTERVAL) && BATTERYSAVE) - { - modbusResponse rs; - - //Get grid power - unsigned int p = 0; - - if(!readSingleReg(SOFAR_SLAVE_ID, SOFAR_REG_GRIDW, &rs)) - p = ((rs.data[0] << 8) | rs.data[1]); - else - Serial.println("modbus error"); - - Serial.print("Grid power: "); - Serial.println(p); - Serial.print("Battery save mode: "); - - // Switch to auto when any power flows to the grid. - // We leave a little wriggle room because once you start charging the battery, - // gridPower should be floating just above or below zero. - if((p < 65535/2 || p > 65525) && (INVERTER_RUNNINGSTATE != discharging)) - { - //exporting to the grid - if(!sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_AUTO, 0, "bsave_auto")) - Serial.println("auto"); - } - else - { - //importing from the grid - if(!sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_STANDBY, SOFAR_PARAM_STANDBY, "bsave_standby")) - Serial.println("standby"); - } - } + static unsigned long lastRun = 0; + + if (checkTimer(&lastRun, BATTERYSAVE_INTERVAL) && BATTERYSAVE) + { + modbusResponse rs; + + //Get grid power + unsigned int p = 0; + + if (readSingleReg(SOFAR_SLAVE_ID, SOFAR_REG_GRIDW, &rs) == 0) { + p = ((rs.data[0] << 8) | rs.data[1]); + + // Switch to auto when any power flows to the grid. + // We leave a little wriggle room because once you start charging the battery, + // gridPower should be floating just above or below zero. + if ((p < 65535 / 2 || p > 65525) && (((inverterModel == ME3000) && (INVERTER_RUNNINGSTATE != 4)) || ((inverterModel == HYBRID) && (INVERTER_RUNNINGSTATE != 6))) ) + { + //exporting to the grid + sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_AUTO, 0, "bsave_auto"); + + } + else + { + //importing from the grid + sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_STANDBY, SOFAR_PARAM_STANDBY, "bsave_standby"); + } + } + } } // This function reconnects the ESP8266 to the MQTT broker -void mqttReconnect() -{ - // Loop until we're reconnected - while(true) - { - mqtt.disconnect(); // Just in case. - delay(200); - Serial.print("Attempting MQTT connection..."); - updateOLED("NULL", "connecting", "NULL", "MQTT."); - delay(500); - updateOLED("NULL", "NULL", "NULL", "MQTT.."); - - // Attempt to connect - if(mqtt.connect(mqttClientID, MQTT_USERNAME, MQTT_PASSWORD)) - { - Serial.println("connected"); - delay(1000); - updateOLED("NULL", "NULL", "NULL", "MQTT...."); - delay(1000); - - //Set topic names to include the deviceName. - String standbyMode(deviceName); - standbyMode += "/set/standby"; - String autoMode(deviceName); - autoMode += "/set/auto"; - String chargeMode(deviceName); - chargeMode += "/set/charge"; - String dischargeMode(deviceName); - dischargeMode += "/set/discharge"; - - // Subscribe or resubscribe to topics. - if( - mqtt.subscribe(const_cast(standbyMode.c_str())) && - mqtt.subscribe(const_cast(autoMode.c_str())) && - mqtt.subscribe(const_cast(chargeMode.c_str())) && - mqtt.subscribe(const_cast(dischargeMode.c_str()))) - { - updateOLED("NULL", "NULL", "NULL", ""); - break; - } - } - - Serial.print("failed, rc="); - Serial.print(mqtt.state()); - Serial.println(" try again in 5 seconds"); - updateOLED("NULL", "NULL", "NULL", "MQTT..."); - - // Wait 5 seconds before retrying - delay(5000); - } +void mqttReconnect() +{ + unsigned long now = millis(); + if ((lastMqttReconnectAttempt == 0) || ((unsigned long)(now - lastMqttReconnectAttempt) > MQTTRECONNECTTIMER)) { //only try reconnect each MQTTRECONNECTTIMER seconds or on boot when lastMqttReconnectAttempt is still 0 + lastMqttReconnectAttempt = now; + if (tftModel) { + tft.fillCircle(220, 290, 10, ILI9341_RED); + } else { + updateOLED("NULL", "Offline", "NULL", "NULL"); + } + mqtt.disconnect(); // Just in case. + // Attempt to connect + if (mqtt.connect(deviceName, MQTT_USER, MQTT_PASS)) + { + if (tftModel) { + tft.fillCircle(220, 290, 10, ILI9341_GREEN); + } else { + updateOLED("NULL", "Online", "NULL", "NULL"); + } + //subscribe to set topics + String settopics(deviceName); + settopics += "/set/#"; + // Subscribe or resubscribe to topics. + mqtt.subscribe(const_cast(settopics.c_str())); + } + } } /** - * Flush the RS485 buffers in both directions. The doc for Serial.flush() implies it only - * flushes outbound characters now... I assume RS485Serial is the same. - */ + Flush the RS485 buffers in both directions. The doc for Serial.flush() implies it only + flushes outbound characters now... I assume RS485Serial is the same. +*/ void flushRS485() { - RS485Serial.flush(); - delay(200); - - while(RS485Serial.available()) - RS485Serial.read(); + if (tftModel) { + Serial.flush(); + delay(200); + + while (Serial.available()) + Serial.read(); + } else { + RS485Serial.flush(); + delay(200); + + while (RS485Serial.available()) + RS485Serial.read(); + } } int sendModbus(uint8_t frame[], byte frameSize, modbusResponse *resp) { - //Calculate the CRC and overwrite the last two bytes. - calcCRC(frame, frameSize); - - // Make sure there are no spurious characters in the in/out buffer. - flushRS485(); - - //Send - digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_TX); - RS485Serial.write(frame, frameSize); - - // It's important to reset the SERIAL_COMMUNICATION_CONTROL_PIN as soon as - // we finish sending so that the serial port can start to buffer the response. - digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX); - return listen(resp); + //Calculate the CRC and overwrite the last two bytes. + calcCRC(frame, frameSize); + + // Make sure there are no spurious characters in the in/out buffer. + flushRS485(); + + //Send + if (tftModel) { + Serial.write(frame, frameSize); + } else { + digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_TX); + RS485Serial.write(frame, frameSize); + digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX); + } + + return listen(resp); } // Listen for a response. int listen(modbusResponse *resp) { - uint8_t inFrame[64]; - uint8_t inByteNum = 0; - uint8_t inFrameSize = 0; - uint8_t inFunctionCode = 0; - uint8_t inDataBytes = 0; - int done = 0; - modbusResponse dummy; - - if(!resp) - resp = &dummy; // Just in case we ever want to interpret here. - - resp->dataSize = 0; - resp->errorLevel = 0; - - while((!done) && (inByteNum < sizeof(inFrame))) - { - int tries = 0; - - while((!RS485Serial.available()) && (tries++ < RS485_TRIES)) - delay(50); - - if(tries >= RS485_TRIES) - { - Serial.println("Timeout waiting for RS485 response."); - break; - } - - inFrame[inByteNum] = RS485Serial.read(); - - //Process the byte - switch(inByteNum) - { - case 0: - if(inFrame[inByteNum] != SOFAR_SLAVE_ID) //If we're looking for the first byte but it dosn't match the slave ID, we're just going to drop it. - inByteNum--; // Will be incremented again at the end of the loop. - break; - - case 1: - //This is the second byte in a frame, where the function code lives. - inFunctionCode = inFrame[inByteNum]; - break; - - case 2: - //This is the third byte in a frame, which tells us the number of data bytes to follow. - if((inDataBytes = inFrame[inByteNum]) > sizeof(inFrame)) - inByteNum = -1; // Frame is too big? - break; - - default: - if(inByteNum < inDataBytes + 3) - { - //This is presumed to be a data byte. - resp->data[inByteNum - 3] = inFrame[inByteNum]; - resp->dataSize++; - } - else if(inByteNum > inDataBytes + 3) - done = 1; - } - - inByteNum++; - } - - inFrameSize = inByteNum; - - /** - * Now check to see if the last two bytes are a valid CRC. - * If we don't have a response pointer we don't care. - **/ - if(inFrameSize < 5) - { - resp->errorLevel = 2; - resp->errorMessage = "Response too short"; - } - else if(checkCRC(inFrame, inFrameSize)) - { - resp->errorLevel = 0; - resp->errorMessage = "Valid data frame"; - } - else - { - resp->errorLevel = 1; - resp->errorMessage = "Error: invalid data frame"; - } - - if(resp->errorLevel) - Serial.println(resp->errorMessage); - - return -resp->errorLevel; + uint8_t inFrame[MAX_FRAME_SIZE]; + uint8_t inByteNum = 0; + uint8_t inFrameSize = 0; + uint8_t inFunctionCode = 0; + uint8_t inDataBytes = 0; + int done = 0; + modbusResponse dummy; + + if (!resp) + resp = &dummy; // Just in case we ever want to interpret here. + + resp->dataSize = 0; + resp->errorLevel = 0; + + while ((!done) && (inByteNum < sizeof(inFrame))) + { + int tries = 0; + + if (tftModel) { + while ((!Serial.available()) && (tries++ < RS485_TRIES)) + delay(50); + } else { + while ((!RS485Serial.available()) && (tries++ < RS485_TRIES)) + delay(50); + } + + if (tries >= RS485_TRIES) + { + break; + } + + if (tftModel) { + inFrame[inByteNum] = Serial.read(); + } else { + inFrame[inByteNum] = RS485Serial.read(); + } + + //Process the byte + switch (inByteNum) + { + case 0: + if (inFrame[inByteNum] != SOFAR_SLAVE_ID) //If we're looking for the first byte but it dosn't match the slave ID, we're just going to drop it. + inByteNum--; // Will be incremented again at the end of the loop. + break; + + case 1: + //This is the second byte in a frame, where the function code lives. + inFunctionCode = inFrame[inByteNum]; + break; + + case 2: + //This is the third byte in a frame, which tells us the number of data bytes to follow. + if ((inDataBytes = inFrame[inByteNum]) > sizeof(inFrame)) + inByteNum = -1; // Frame is too big? + break; + + default: + if (inByteNum < inDataBytes + 3) + { + //This is presumed to be a data byte. + resp->data[inByteNum - 3] = inFrame[inByteNum]; + resp->dataSize++; + } + else if (inByteNum > inDataBytes + 3) + done = 1; + } + + inByteNum++; + } + + inFrameSize = inByteNum; + + /** + Now check to see if the last two bytes are a valid CRC. + If we don't have a response pointer we don't care. + **/ + if (inFrameSize < 5) + { + resp->errorLevel = 2; + resp->errorMessage = "Response too short"; + } + else if (checkCRC(inFrame, inFrameSize)) + { + resp->errorLevel = 0; + resp->errorMessage = "Valid data frame"; + } + else + { + resp->errorLevel = 1; + resp->errorMessage = "Error: invalid data frame"; + } + + return -resp->errorLevel; +} + +int readBulkReg(uint8_t id, uint16_t reg, uint8_t bulkSize, modbusResponse *rs) +{ + uint8_t frame[] = { id, MODBUS_FN_READHOLDINGREG, reg >> 8, reg & 0xff, 0, bulkSize, 0, 0 }; + return sendModbus(frame, sizeof(frame), rs); } int readSingleReg(uint8_t id, uint16_t reg, modbusResponse *rs) { - uint8_t frame[] = { id, MODBUS_FN_READSINGLEREG, reg >> 8, reg & 0xff, 0, 0x01, 0, 0 }; + uint8_t frame[] = { id, MODBUS_FN_READHOLDINGREG, reg >> 8, reg & 0xff, 0, 0x01, 0, 0 }; + return sendModbus(frame, sizeof(frame), rs); +} - return sendModbus(frame, sizeof(frame), rs); +int sendPassiveCmdV2(uint8_t id, uint16_t cmd, int32_t param, String pubTopic) { + /*SOFAR2_REG_PASSIVECONTROL + need to be finished and checked + writes to 4487 - 4492 with 6x 32-bit integers + 4487 = desired PPC passive power + 4489 = min passive power + 4491 = max passive power + but 4487 isn't for forced passive mode. Set min and max to same value for that. Negative is discharging + */ + modbusResponse rs; + uint8_t frame[] = { id, MODBUS_FN_WRITEMULREG, (cmd >> 8) & 0xff, cmd & 0xff, 0, 6, 12, 0, 0, 0, 0, (param >> 24) & 0xff, (param >> 16) & 0xff, (param >> 8) & 0xff, param & 0xff, (param >> 25) & 0xff, (param >> 16) & 0xff, (param >> 8) & 0xff, param & 0xff, 0, 0 }; + int err = -1; + String retMsg; + + if (sendModbus(frame, sizeof(frame), &rs)) + retMsg = rs.errorMessage; + else if (rs.dataSize != 2) + retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; + else + { + retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); + err = 0; + } + + String topic(deviceName); + topic += "/response/" + pubTopic; + sendMqtt(const_cast(topic.c_str()), retMsg); + return err; } int sendPassiveCmd(uint8_t id, uint16_t cmd, uint16_t param, String pubTopic) { - modbusResponse rs; - uint8_t frame[] = { id, SOFAR_FN_PASSIVEMODE, cmd >> 8, cmd & 0xff, param >> 8, param & 0xff, 0, 0 }; - int err = -1; - String retMsg; + if (inverterModel == HYDV2) return 0; //no commands yet + modbusResponse rs; + uint8_t frame[] = { id, SOFAR_FN_PASSIVEMODE, cmd >> 8, cmd & 0xff, param >> 8, param & 0xff, 0, 0 }; + int err = -1; + String retMsg; + + if (sendModbus(frame, sizeof(frame), &rs)) + retMsg = rs.errorMessage; + else if (rs.dataSize != 2) + retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; + else + { + retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); + err = 0; + } + + String topic(deviceName); + topic += "/response/" + pubTopic; + sendMqtt(const_cast(topic.c_str()), retMsg); + return err; +} - if(sendModbus(frame, sizeof(frame), &rs)) - retMsg = rs.errorMessage; - else if(rs.dataSize != 2) - retMsg = "Reponse is " + String(rs.dataSize) + " bytes?"; - else - { - retMsg = String((rs.data[0] << 8) | (rs.data[1] & 0xff)); - err = 0; - } +void sendMqtt(char* topic, String msg_str) +{ + char msg[2000]; - String topic(deviceName); - topic += "/response/" + pubTopic; - sendMqtt(const_cast(topic.c_str()), retMsg); - return err; + mqtt.setBufferSize(2048); + msg_str.toCharArray(msg, msg_str.length() + 1); //packaging up the data to publish to mqtt + if (!(mqtt.publish(topic, msg))) + printScreen("MQTT publish failed"); } -void sendMqtt(char* topic, String msg_str) + +void heartbeat() { - char msg[1000]; + if (inverterModel != HYDV2) { //no heartbeat + static unsigned long lastRun = 0; - mqtt.setBufferSize(512); - msg_str.toCharArray(msg, msg_str.length() + 1); //packaging up the data to publish to mqtt + //Send a heartbeat + if (checkTimer(&lastRun, HEARTBEAT_INTERVAL)) + { + uint8_t sendHeartbeat[] = {SOFAR_SLAVE_ID, 0x49, 0x22, 0x01, 0x22, 0x02, 0x00, 0x00}; + int ret; - if (!(mqtt.publish(topic, msg))) - Serial.println("MQTT publish failed"); + sendModbus(sendHeartbeat, sizeof(sendHeartbeat), NULL); + + } + } } -void heartbeat() + +void runStateME3000() { + switch (INVERTER_RUNNINGSTATE) + { + case 0: + printScreen("Standby"); + if (BATTERYSAVE) { + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_LIGHTGREY); + } + else { + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_WHITE); + } + break; + + case 1: + printScreen("Check"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_YELLOW ); + break; + + case 2: + printScreen("Charging", String(batteryWatts()) + "W"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_BLUE); + break; + + case 3: + printScreen("Check dis"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_GREEN); + break; + + case 4: + printScreen("Discharging", String(-1 * batteryWatts()) + "W"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_GREEN); + break; + + case 5: + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_PURPLE); + break; + + case 6: + printScreen("EPS state"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_RED); + break; + + case 7: + printScreen("FAULT"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_RED); + break; + + default: + printScreen("?"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_BLACK); + break; + } + +} + +void runStateHYBRID() { //same for v2 + switch (INVERTER_RUNNINGSTATE) + { + case 0: + printScreen("Standby"); + if (BATTERYSAVE) { + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_LIGHTGREY); + } + else { + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_WHITE); + } + break; + + case 1: + printScreen("Check"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_YELLOW ); + break; + + case 2: + { + int16_t w = batteryWatts(); + if (w == 0) { + printScreen("Normal"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_WHITE); + } else if (w > 0) { + printScreen("Charging", String(w) + "W"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_BLUE); + } else { + printScreen("Discharging", String(w * -1) + "W"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_GREEN); + } + } + break; + + case 3: + printScreen("EPS state"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_PURPLE); + break; + + case 4: + printScreen("FAULT"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_RED); + break; + + case 5: + printScreen("PERM FAULT"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_RED); + break; + + case 6: + printScreen("Upgrading"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_RED); + break; + + case 7: + printScreen("Self Charging"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_RED); + break; + + default: + printScreen("?"); + if (tftModel) tft.fillCircle(120, 290, 10, ILI9341_BLACK); + break; + } + +} + + +void updateRunstate() { - static unsigned long lastRun = 0; + static unsigned long lastRun = 0; + + //Check the runstate + if (checkTimer(&lastRun, RUNSTATE_INTERVAL)) + { + modbusResponse response; + uint16_t reg = inverterModel == HYDV2 ? SOFAR2_REG_RUNSTATE : SOFAR_REG_RUNSTATE; + if (!readSingleReg(SOFAR_SLAVE_ID, reg, &response)) // 0 response is no error + { + INVERTER_RUNNINGSTATE = ((response.data[0] << 8) | response.data[1]); + if (inverterModel == 0) { //only ME3000 has different runstates + runStateME3000(); + } else { + runStateHYBRID(); + } + if (modbusError) { //fixed previous modbus error + modbusError = false; + if (tftModel) { + tft.fillCircle(20, 290, 10, ILI9341_GREEN); + } + } + } + else + { + if (!modbusError) { //new modbus error + modbusError = true; + if (tftModel) { + tft.fillCircle(20, 290, 10, ILI9341_RED); + } + printScreen("RS485 fault"); + } + } + } +} - //Send a heartbeat - if(checkTimer(&lastRun, HEARTBEAT_INTERVAL)) - { - uint8_t sendHeartbeat[] = {SOFAR_SLAVE_ID, 0x49, 0x22, 0x01, 0x22, 0x02, 0x00, 0x00}; - int ret; +int16_t batteryWatts() +{ + uint16_t reg = inverterModel == HYDV2 ? SOFAR2_REG_BATTW : SOFAR_REG_BATTW; + modbusResponse response; + if (!readSingleReg(SOFAR_SLAVE_ID, reg, &response)) + { + int16_t w = (int16_t)((response.data[0] << 8) | response.data[1]) * 10; + return w; + } + return 0; +} - Serial.println("Send heartbeat"); - // This just makes the dot on the first line of the OLED screen flash on and off with - // the heartbeat and clears any previous RS485 error massage that might still be there. - if(!(ret = sendModbus(sendHeartbeat, sizeof(sendHeartbeat), NULL))) - { - String flashDot; +void drawBitmap(int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, int16_t h, uint16_t color) { - if (oledLine2 == "Online") - flashDot = "Online."; + int16_t i, j, byteWidth = (w + 7) / 8; + uint8_t byte; - if (oledLine2 == "Online.") - flashDot = "Online"; + for (j = 0; j < h; j++) { + for (i = 0; i < w; i++) { + if (i & 7) byte <<= 1; + else byte = pgm_read_byte(bitmap + j * byteWidth + i / 8); + if (byte & 0x80) tft.drawPixel(x + i, y + j, color); + } + } +} - if (oledLine3 == "RS485") - oledLine3 = ""; +void printScreen(String text) { + if (text.length() > 10) { + int index = text.lastIndexOf(' '); + String text1 = text.substring(0, index); + String text2 = text.substring(index + 1); + printScreen(text1, text2); + } else { + if (tftModel) { + tft.fillRect(40, 135, 159, 64, ILI9341_CYAN); + int pos = 115 - 12 * (text.length() / 2); + tft.setCursor(pos, 160); + tft.setTextSize(2); + tft.setTextColor(ILI9341_BLACK, ILI9341_CYAN); + tft.println(text); + } else { + updateOLED("NULL", "NULL", text, "NULL"); + } + } +} - if (oledLine4 == "ERROR") - oledLine4 = ""; +void printScreen(String text1, String text2) { + if (tftModel) { + tft.fillRect(40, 135, 159, 64, ILI9341_CYAN); + tft.setTextSize(2); + tft.setTextColor(ILI9341_BLACK, ILI9341_CYAN); + { int pos = 115 - 12 * (text1.length() / 2); + tft.setCursor(pos, 145); + tft.println(text1); + } + { int pos = 115 - 12 * (text2.length() / 2); + tft.setCursor(pos, 175); + tft.println(text2); + } + } else { + updateOLED("NULL", "NULL", text1, text2); + } +} - updateOLED("NULL", flashDot, "NULL", "NULL"); - } - else - { - Serial.print("Bad heartbeat "); - Serial.println(ret); - updateOLED("NULL", "NULL", "RS485", "ERROR"); - } +void setupOTA() { + ArduinoOTA.setHostname(deviceName); + // ArduinoOTA.setPassword("admin"); + + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) { + type = "sketch"; + } else { // U_FS + type = "filesystem"; + } + + // NOTE: if updating FS this would be the place to unmount FS using FS.end() + //Serial.println("Start updating " + type); + }); + ArduinoOTA.onEnd([]() { + //Serial.println("\nEnd"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + //Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + //Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + //Serial.println("Auth Failed"); + } else if (error == OTA_BEGIN_ERROR) { + //Serial.println("Begin Failed"); + } else if (error == OTA_CONNECT_ERROR) { + //Serial.println("Connect Failed"); + } else if (error == OTA_RECEIVE_ERROR) { + //Serial.println("Receive Failed"); + } else if (error == OTA_END_ERROR) { + //Serial.println("End Failed"); + } + }); + ArduinoOTA.begin(); +} - //Flash the LED - digitalWrite(LED_BUILTIN, LOW); - delay(4); - digitalWrite(LED_BUILTIN, HIGH); - } +// Webserver root page +void handleRoot() { + httpServer.send_P(200, "text/html", index_html); } -void updateRunstate() +// Webserver root page +void handleSettings() { + httpServer.send_P(200, "text/html", settings_html_new); +} + +void handleJson() { - static unsigned long lastRun = 0; - - //Check the runstate - if(checkTimer(&lastRun, RUNSTATE_INTERVAL)) - { - modbusResponse response; - - Serial.print("Get runstate: "); - - if(!readSingleReg(SOFAR_SLAVE_ID, SOFAR_REG_RUNSTATE, &response)) - { - INVERTER_RUNNINGSTATE = ((response.data[0] << 8) | response.data[1]); - Serial.println(INVERTER_RUNNINGSTATE); - - switch(INVERTER_RUNNINGSTATE) - { - case waiting: - if (BATTERYSAVE) - updateOLED("NULL", "NULL", "Batt Save", "Waiting"); - else - updateOLED("NULL", "NULL", "Standby", ""); - break; - - case check: - updateOLED("NULL", "NULL", "Checking", "NULL"); - break; - - case charging: - updateOLED("NULL", "NULL", HUMAN_CHARGING, String(batteryWatts())+"W"); - break; - -#ifdef INVERTER_ME3000 - case checkDischarge: - updateOLED("NULL", "NULL", "Check Dis", "NULL"); - break; -#endif - case discharging: - updateOLED("NULL", "NULL", HUMAN_DISCHARGING, String(batteryWatts())+"W"); - break; - - case epsState: - updateOLED("NULL", "NULL", "EPS State", "NULL"); - break; - - case faultState: - updateOLED("NULL", "NULL", "FAULT", "NULL"); - break; - - case permanentFaultState: - updateOLED("NULL", "NULL", "PERMFAULT", "NULL"); - break; - - default: - updateOLED("NULL", "NULL", "Runstate?", "NULL"); - break; - } - } - else - { - Serial.println(response.errorMessage); - updateOLED("NULL", "NULL", "CRC-FAULT", "NULL"); - } - } -} - -unsigned int batteryWatts() -{ - if(INVERTER_RUNNINGSTATE == charging || INVERTER_RUNNINGSTATE == discharging) - { - modbusResponse response; - - if(!readSingleReg(SOFAR_SLAVE_ID, SOFAR_REG_BATTW, &response)) - { - unsigned int w = ((response.data[0] << 8) | response.data[1]); - - switch(INVERTER_RUNNINGSTATE) - { - case charging: - w = w*10; - break; - - case discharging: - w = (65535 - w)*10; - } - - return w; - } - else - { - Serial.println(response.errorMessage); - updateOLED("NULL", "NULL", "CRC-FAULT", "NULL"); - } - } - - return 0; + httpServer.send(200, "application/json", jsonstring); } -void setup() +void handleJsonSettings() { - Serial.begin(9600); - pinMode(LED_BUILTIN, OUTPUT); + String jsonsettingsstring; + jsonsettingsstring = "{"; + jsonsettingsstring += "\"deviceName\": \"" + String(deviceName) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"mqtthost\": \"" + String(MQTT_HOST) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"mqttport\": \"" + String(MQTT_PORT) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"mqttuser\": \"" + String(MQTT_USER) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"mqttpass\": \"" + String(MQTT_PASS) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"inverterModel\": \"" + String(inverterModel) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"tftModel\": \"" + String(tftModel) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"calculated\": \"" + String(calculated) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"screendimtimer\": \"" + String(screenDimTimer) + "\""; + jsonsettingsstring += ","; + jsonsettingsstring += "\"separateMqttTopics\": \"" + String(separateMqttTopics) + "\""; + jsonsettingsstring += "}"; + + httpServer.send(200, "application/json", jsonsettingsstring); +} + +void handleCommand() { + int num = httpServer.args(); + bool saveEeprom = false; + String message = ""; + for (int i = 0 ; i < num ; i++) { + if ((httpServer.argName(i) == "reset") || (httpServer.argName(i) == "restart") || (httpServer.argName(i) == "reboot") || ((httpServer.argName(i) == "reload"))) { + httpServer.send(200, "text/html", "Restarting!"); + delay(1000); + ESP.reset(); + } else if (httpServer.argName(i) == "factoryreset") { + httpServer.send(200, "text/html", "Factory reset! Please reconfig using wifi hotspot!"); + delay(1000); + resetConfig(); + } else if (httpServer.argName(i) == "deviceName") { + String value = httpServer.arg(i); + message += "Setting devicename to: " + value + "
"; + value.toCharArray(deviceName, sizeof(deviceName)); + saveEeprom = true; + } else if (httpServer.argName(i) == "mqtthost") { + String value = httpServer.arg(i); + message += "Setting MQTT host to: " + value + "
"; + value.toCharArray(MQTT_HOST, sizeof(MQTT_HOST)); + saveEeprom = true; + } else if (httpServer.argName(i) == "mqttport") { + String value = httpServer.arg(i); + message += "Setting MQTT port to: " + value + "
"; + value.toCharArray(MQTT_PORT, sizeof(MQTT_PORT)); + saveEeprom = true; + } else if (httpServer.argName(i) == "mqttuser") { + String value = httpServer.arg(i); + message += "Setting MQTT username to: " + value + "
"; + value.toCharArray(MQTT_USER, sizeof(MQTT_USER)); + saveEeprom = true; + } else if (httpServer.argName(i) == "mqttpass") { + String value = httpServer.arg(i); + message += "Setting MQTT password to: " + value + "
"; + value.toCharArray(MQTT_PASS, sizeof(MQTT_PASS)); + saveEeprom = true; + } else if (httpServer.argName(i) == "inverterModel") { + String value = httpServer.arg(i); + message += "Setting inverter type to: " + value + "
"; + if (value == "me3000") { + inverterModel = ME3000; + } else if (value == "hybrid") { + inverterModel = HYBRID; + } else if (value == "hydv2") { + inverterModel = HYDV2; + } + saveEeprom = true; + } else if (httpServer.argName(i) == "tftModel") { + String value = httpServer.arg(i); + message += "Setting lcd type to: " + value + "
"; + if (value == "oled") { + tftModel = false; + } else if (value == "tft") { + tftModel = true; + } + saveEeprom = true; + } else if (httpServer.argName(i) == "calculated") { + String value = httpServer.arg(i); + message += "Setting calculated mode to: " + value + "
"; + if (value == "true") { + calculated = true; + } else { + calculated = false; + } + saveEeprom = true; + } else if (httpServer.argName(i) == "screendimtimer") { + String value = httpServer.arg(i); + message += "Setting screen dim timer to: " + value + "
"; + screenDimTimer = value.toInt(); + saveEeprom = true; + } else if (httpServer.argName(i) == "separateMqttTopics") { + String value = httpServer.arg(i); + message += "Setting separateMqttTopics to: " + value + "
"; + if (value == "true") { + separateMqttTopics = true; + } else { + separateMqttTopics = false; + } + saveEeprom = true; + } + + } + + if (saveEeprom) { + httpServer.send(200, "text/html", "" + message + ""); + delay(1000); + saveToEeprom(); + } else { + httpServer.send(200, "text/html", "Nothing to do!"); + } +} + - pinMode(SERIAL_COMMUNICATION_CONTROL_PIN, OUTPUT); - digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX); - RS485Serial.begin(9600); - delay(500); +void resetConfig() { + //initiate debug led indication for factory reset + pinMode(2, FUNCTION_0); //set it as gpio + pinMode(2, OUTPUT); + digitalWrite(2, LOW); //blue led on + if (tftModel) { + analogWrite(TFT_LED, 32); //PWM on led pin to dim screen + tft.fillScreen(ILI9341_RED); + tft.fillScreen(ILI9341_BLACK); + tft.setScrollMargins(1, 10); + tft.setTextColor(ILI9341_RED, ILI9341_BLACK); // Red on black + tft.println("Double reset detected, clearing config."); + } + WiFi.persistent(true); + WiFi.disconnect(); + WiFi.persistent(false); + WiFiManager wifiManager; + wifiManager.resetSettings(); + EEPROM.begin(512); + write_eeprom(0, 1, "0"); + EEPROM.commit(); + + if (tftModel) { + tft.println("Config cleared. Please reset to configure this device..."); + } + + while (true) { + digitalWrite(2, HIGH); + delay(100); + digitalWrite(2, LOW); + delay(100); + } +} - //Turn on the OLED - display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize OLED with the I2C addr 0x3C (for the 64x48) - display.clearDisplay(); - display.display(); - updateOLED(deviceName, "connecting", "", version); +void doubleResetDetect() { + if (drd.detect()) { + if (tftModel) { + tft.begin(); + tft.setRotation(2); + } + resetConfig(); + } +} - setup_wifi(); +void setup() +{ + // * Configure EEPROM an get initial settings + EEPROM.begin(512); + if (!loadFromEeprom()) { //we don't have config yet, switch between lcd models after each reset + tftModel = true; + if (EEPROM.read(200)) tftModel = false; //previous reboot we selected TFT model, now switch to OLED + EEPROM.write(200, tftModel); // * 200 + EEPROM.commit(); + } + doubleResetDetect(); //detect factory reset first + + if (tftModel) { + tft.begin(); + tft.setRotation(2); + analogWrite(TFT_LED, 32); //PWM on led pin to dim screen + tft.fillScreen(ILI9341_CYAN); + tft.fillScreen(ILI9341_BLACK); + tft.setScrollMargins(1, 10); + tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK); // White on black + tft.println("Sofar2mqtt starting..."); + ts.begin(); + ts.setRotation(1); + + } else { + //Turn on the OLED + display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize OLED with the I2C addr 0x3C (for the 64x48) + display.clearDisplay(); + display.display(); + updateOLED(deviceName, "starting", "WiFi..", version); + } + + if (tftModel) { + Serial.begin(9600); + } else { + pinMode(SERIAL_COMMUNICATION_CONTROL_PIN, OUTPUT); + digitalWrite(SERIAL_COMMUNICATION_CONTROL_PIN, RS485_RX); + RS485Serial.begin(9600); + } + delay(500); + setup_wifi(); //set wifi and get settings, so first thing to do + + if (tftModel) { + tft.print("Running inverter model: "); + if (inverterModel == ME3000) { + tft.println("ME3000"); + } else if (inverterModel == HYBRID) { + tft.println("HYBRID"); + } else { + tft.println("HYD EP/KTL"); + } + } + delay(1000); + + mqtt.setServer(MQTT_HOST, atoi(MQTT_PORT)); + mqtt.setCallback(mqttCallback); + + setupOTA(); + MDNS.begin(deviceName); + httpUpdater.setup(&httpServer); + httpServer.begin(); + MDNS.addService("http", "tcp", 80); + httpServer.on("/", handleRoot); + httpServer.on("/settings", handleSettings); + httpServer.on("/json", handleJson); + httpServer.on("/jsonsettings", handleJsonSettings); + httpServer.on("/command", handleCommand); + + if (tftModel) { + tft.fillScreen(ILI9341_BLACK); + drawBitmap(0, 0, background, 240, 320, ILI9341_WHITE); + printScreen("Started"); + tft.fillCircle(20, 290, 10, ILI9341_RED); //turn modbus icon to red first + } + heartbeat(); + mqttReconnect(); +} + +int brightness = 32; +bool touchedBefore = false; +void tsLoop() { + if (ts.tirqTouched()) { + if (ts.touched()) { //this will run update() and therefore reset the tirqTouched flag if touch is released + if (!touchedBefore) { + touchedBefore = true; + brightness == 32 ? brightness = 0 : brightness = 32; + analogWrite(TFT_LED, brightness); + lastScreenTouch = millis(); + delay(100); + } + } else { + touchedBefore = false; + } + } + if ((screenDimTimer > 0) && (brightness > 0) && ((unsigned long)(millis() - lastScreenTouch) > (1000 * screenDimTimer))) { + brightness--; + analogWrite(TFT_LED, brightness); + delay(50); + } - mqtt.setServer(MQTT_SERVER, MQTT_PORT); - mqtt.setCallback(mqttCallback); +} - //Wake up the inverter and put it in auto mode to begin with. - heartbeat(); - mqttReconnect(); - Serial.println("Set start up mode: Auto"); - sendPassiveCmd(SOFAR_SLAVE_ID, SOFAR_FN_AUTO, 0, "startup_auto"); +void loopRuns() { + ArduinoOTA.handle(); + httpServer.handleClient(); + MDNS.update(); + if (tftModel) tsLoop(); } void loop() { - //make sure mqtt is still connected - if((!mqtt.connected()) || !mqtt.loop()) - { - updateOLED("NULL", "Offline", "NULL", "NULL"); - mqttReconnect(); - } - else - updateOLED("NULL", "Online", "NULL", "NULL"); + loopRuns(); - //Send a heartbeat to keep the inverter awake - heartbeat(); + //Check and display the runstate and update modbusError boolean + updateRunstate(); - //Check and display the runstate - updateRunstate(); + //Send a heartbeat to keep the inverter awake + if (!modbusError) heartbeat(); - //Transmit all data to MQTT - sendData(); + //make sure mqtt is still connected + if ((!mqtt.connected()) || !mqtt.loop()) + { + mqttReconnect(); + } - //Set battery save state - batterySave(); + //Get all data and send to MQTT + retrieveData(); - delay(100); + //Set battery save state + if (!modbusError) batterySave(); } //calcCRC and checkCRC are based on... //https://github.com/angeloc/simplemodbusng/blob/master/SimpleModbusMaster/SimpleModbusMaster.cpp -void calcCRC(uint8_t frame[], byte frameSize) +void calcCRC(uint8_t frame[], byte frameSize) { - unsigned int temp = 0xffff, flag; + unsigned int temp = 0xffff, flag; - for(unsigned char i = 0; i < frameSize - 2; i++) - { - temp = temp ^ frame[i]; + for (unsigned char i = 0; i < frameSize - 2; i++) + { + temp = temp ^ frame[i]; - for(unsigned char j = 1; j <= 8; j++) - { - flag = temp & 0x0001; - temp >>= 1; + for (unsigned char j = 1; j <= 8; j++) + { + flag = temp & 0x0001; + temp >>= 1; - if(flag) - temp ^= 0xA001; - } - } + if (flag) + temp ^= 0xA001; + } + } - // Bytes are reversed. - frame[frameSize - 2] = temp & 0xff; - frame[frameSize - 1] = temp >> 8; + // Bytes are reversed. + frame[frameSize - 2] = temp & 0xff; + frame[frameSize - 1] = temp >> 8; } -bool checkCRC(uint8_t frame[], byte frameSize) +bool checkCRC(uint8_t frame[], byte frameSize) { - unsigned int calculated_crc, received_crc; + unsigned int calculated_crc, received_crc; - received_crc = ((frame[frameSize-2] << 8) | frame[frameSize-1]); - calcCRC(frame, frameSize); - calculated_crc = ((frame[frameSize-2] << 8) | frame[frameSize-1]); - return (received_crc = calculated_crc); + received_crc = ((frame[frameSize - 2] << 8) | frame[frameSize - 1]); + calcCRC(frame, frameSize); + calculated_crc = ((frame[frameSize - 2] << 8) | frame[frameSize - 1]); + return (received_crc = calculated_crc); } diff --git a/binaries/Sofar2mqtt-v3.0.bin b/binaries/Sofar2mqtt-v3.0.bin new file mode 100644 index 0000000..6a4188a Binary files /dev/null and b/binaries/Sofar2mqtt-v3.0.bin differ diff --git a/binaries/Sofar2mqtt-v3.1.bin b/binaries/Sofar2mqtt-v3.1.bin new file mode 100644 index 0000000..a1800ed Binary files /dev/null and b/binaries/Sofar2mqtt-v3.1.bin differ diff --git a/binaries/Sofar2mqtt-v3.2.bin b/binaries/Sofar2mqtt-v3.2.bin new file mode 100644 index 0000000..9d1590b Binary files /dev/null and b/binaries/Sofar2mqtt-v3.2.bin differ diff --git a/binaries/Sofar2mqtt-v3.3-alpha1.bin b/binaries/Sofar2mqtt-v3.3-alpha1.bin new file mode 100644 index 0000000..f194c1d Binary files /dev/null and b/binaries/Sofar2mqtt-v3.3-alpha1.bin differ diff --git a/binaries/Sofar2mqtt-v3.3-alpha2.bin b/binaries/Sofar2mqtt-v3.3-alpha2.bin new file mode 100644 index 0000000..4484cea Binary files /dev/null and b/binaries/Sofar2mqtt-v3.3-alpha2.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha11.bin b/binaries/Sofar2mqtt.v3.3-alpha11.bin new file mode 100644 index 0000000..b3ef6e9 Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha11.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha12.bin b/binaries/Sofar2mqtt.v3.3-alpha12.bin new file mode 100644 index 0000000..7324f73 Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha12.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha3.bin b/binaries/Sofar2mqtt.v3.3-alpha3.bin new file mode 100644 index 0000000..d5d1aee Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha3.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha4.bin b/binaries/Sofar2mqtt.v3.3-alpha4.bin new file mode 100644 index 0000000..d055d58 Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha4.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha5.bin b/binaries/Sofar2mqtt.v3.3-alpha5.bin new file mode 100644 index 0000000..bc7a363 Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha5.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha7.bin b/binaries/Sofar2mqtt.v3.3-alpha7.bin new file mode 100644 index 0000000..3d3329c Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha7.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha8.bin b/binaries/Sofar2mqtt.v3.3-alpha8.bin new file mode 100644 index 0000000..0a6b34e Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha8.bin differ diff --git a/binaries/Sofar2mqtt.v3.3-alpha9.bin b/binaries/Sofar2mqtt.v3.3-alpha9.bin new file mode 100644 index 0000000..d673118 Binary files /dev/null and b/binaries/Sofar2mqtt.v3.3-alpha9.bin differ diff --git a/stl/sofar2mqtt-no-tft.stl b/stl/sofar2mqtt-no-tft.stl new file mode 100644 index 0000000..a98b5df Binary files /dev/null and b/stl/sofar2mqtt-no-tft.stl differ diff --git a/stl/sofar2mqtt-tft.stl b/stl/sofar2mqtt-tft.stl new file mode 100644 index 0000000..cb88a50 Binary files /dev/null and b/stl/sofar2mqtt-tft.stl differ