diff --git a/Getting_Started.md b/Getting_Started.md index 4d35903c..d66aeca0 100644 --- a/Getting_Started.md +++ b/Getting_Started.md @@ -217,6 +217,14 @@ Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities t ! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data! +#### Flashing on Linux with `esptool.py` (ESP32) +1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already. +2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases) +3. `cd ahoy_v && cp *esp32.bin esp32.bin` +4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin` +5. Unplug and replug your device. +6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi. + ## Connect to your Ahoy DTU When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU. diff --git a/User_Manual.md b/User_Manual.md index 242a2809..a7be8f80 100644 --- a/User_Manual.md +++ b/User_Manual.md @@ -321,6 +321,19 @@ Send Power Limit: - A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy. - You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time. - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. +### Update your AHOY-DTU Firmware +To update your AHOY-DTU, you have to download the latest firmware package. +Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip) available for download. +As soon as you have downloaded the firmware package, unzip it. On the WebUI, navigate to Update and press on select firmware file. +From the unzipped files, select the right .bin file for your hardware and needs. +- If you use an ESP8266, select the file ending with esp8266.bin +- If you use an ESP8266 with prometheus, select the file ending with esp8266_prometheus.bin +- If you use an ESP32, select the file ending with esp32.bin +- If you use an ESP32 with prometheus, select the file ending with esp32_prometheus.bin + +Note: if you want to use prometheus, the usage of an ESP32 is recommended, since the ESP8266 is at its performance limits and therefore can cause stability issues. + +After selecting the right firmware file, press update. Your AHOY-DTU will now install the new firmware and reboot. ## Additional Notes ### MI Inverters diff --git a/src/CHANGES.md b/src/CHANGES.md index 9f83f7e7..94b1d10a 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,8 @@ # Development Changes +## 0.6.13 - 2023-05-16 +* merge PR #934 (fix JSON API) and #944 (update manual) + ## 0.6.12 - 2023-04-28 * improved MqTT * fix menu active item diff --git a/src/defines.h b/src/defines.h index cf9395d5..22f5c8fb 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 6 -#define VERSION_PATCH 12 +#define VERSION_PATCH 13 //------------------------------------- typedef struct { diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 7b8250fd..dba24bb3 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -84,8 +84,8 @@ class HmRadio { DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; #ifdef ESP32 - #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 - mSpi = new SPIClass(FSPI); + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + mSpi = new SPIClass(HSPI); #else mSpi = new SPIClass(VSPI); #endif diff --git a/src/hms/esp32_3wSpi.h b/src/hms/esp32_3wSpi.h index 0d494ba6..080e2251 100644 --- a/src/hms/esp32_3wSpi.h +++ b/src/hms/esp32_3wSpi.h @@ -11,12 +11,21 @@ #include "driver/spi_master.h" #include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal +#if CONFIG_IDF_TARGET_ESP32S3 +#define CLK_PIN 6 +#define MOSI_PIN 5 +#else #define CLK_PIN 18 #define MOSI_PIN 23 -#define MISO_PIN -1 +#endif #define SPI_CLK 1 * 1000 * 1000 // 1MHz +#define SPI_PARAM_LOCK() \ + do { \ + } while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS) +#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock) + // for ESP32 this is the so-called HSPI // for ESP32-S2/S3/C3 this nomenclature does not really exist anymore, // it is simply the first externally usable hardware SPI master controller @@ -30,9 +39,10 @@ class esp32_3wSpi { } void setup(uint8_t pinCsb = CSB_PIN, uint8_t pinFcsb = FCSB_PIN) { //, uint8_t pinGpio3 = GPIO3_PIN) { + paramLock = xSemaphoreCreateMutex(); spi_bus_config_t buscfg = { .mosi_io_num = MOSI_PIN, - .miso_io_num = MISO_PIN, + .miso_io_num = -1, // single wire MOSI/MISO .sclk_io_num = CLK_PIN, .quadwp_io_num = -1, .quadhd_io_num = -1, @@ -93,7 +103,9 @@ class esp32_3wSpi { .tx_buffer = &tx_data, .rx_buffer = NULL }; + SPI_PARAM_LOCK(); ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); + SPI_PARAM_UNLOCK(); delayMicroseconds(100); } @@ -110,7 +122,10 @@ class esp32_3wSpi { .tx_buffer = NULL, .rx_buffer = &rx_data }; + + SPI_PARAM_LOCK(); ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); + SPI_PARAM_UNLOCK(); delayMicroseconds(100); return rx_data; } @@ -126,11 +141,13 @@ class esp32_3wSpi { .rx_buffer = NULL }; + SPI_PARAM_LOCK(); for(uint8_t i = 0; i < len; i++) { tx_data = ~buf[i]; // negate buffer contents ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); delayMicroseconds(4); // > 4 us } + SPI_PARAM_UNLOCK(); } void readFifo(uint8_t buf[], uint8_t len) { @@ -145,16 +162,19 @@ class esp32_3wSpi { .rx_buffer = &rx_data }; + SPI_PARAM_LOCK(); for(uint8_t i = 0; i < len; i++) { ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); delayMicroseconds(4); // > 4 us buf[i] = rx_data; } + SPI_PARAM_UNLOCK(); } private: spi_device_handle_t spi_reg, spi_fifo; bool mInitialized; + SemaphoreHandle_t paramLock = NULL; }; #else template diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 4d5c3a85..ea08f8c0 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -563,7 +563,7 @@ class RestApi { if(F("power") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); - else if(F("restart") == jsonIn[F("restart")]) + else if(F("restart") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest(Restart); else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { iv->powerLimit[0] = jsonIn["val"]; diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 9301067f..00d52511 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -2,11 +2,14 @@ ahoy: interval: 5 + transmit_retries: 5 logging: filename: 'hoymiles.log' # DEBUG, INFO, WARNING, ERROR, FATAL level: 'INFO' + max_log_filesize: 1000000 + max_log_files: 1 sunset: disabled: false diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 210bed65..688e271d 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -297,8 +297,8 @@ class InverterPacketFragment: class HoymilesNRF: """Hoymiles NRF24 Interface""" - tx_channel_id = 0 - tx_channel_list = [40] + tx_channel_id = 2 + tx_channel_list = [3,23,40,61,75] rx_channel_id = 0 rx_channel_list = [3,23,40,61,75] rx_channel_ack = False @@ -332,6 +332,12 @@ class HoymilesNRF: :rtype: bool """ + self.next_tx_channel() + + if HOYMILES_TRANSACTION_LOGGING: + c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + logging.debug(f'{c_datetime} Transmit {len(packet)} bytes channel {self.tx_channel}: {hexify_payload(packet)}') + if not txpower: txpower = self.txpower @@ -363,13 +369,13 @@ class HoymilesNRF: """ Receive Packets - :param timeout: receive timeout in nanoseconds (default: 12e8) + :param timeout: receive timeout in nanoseconds (default: 5e8) :type timeout: int :yields: fragment """ if not timeout: - timeout=12e8 + timeout=5e8 self.radio.setChannel(self.rx_channel) self.radio.setAutoAck(False) @@ -415,7 +421,7 @@ class HoymilesNRF: self.radio.setChannel(self.rx_channel) self.radio.startListening() - time.sleep(0.005) + time.sleep(0.004) def next_rx_channel(self): """ @@ -433,6 +439,15 @@ class HoymilesNRF: return True return False + def next_tx_channel(self): + """ + Select next channel from hop list + + """ + self.tx_channel_id = self.tx_channel_id + 1 + if self.tx_channel_id >= len(self.tx_channel_list): + self.tx_channel_id = 0 + @property def tx_channel(self): """ @@ -612,10 +627,6 @@ class InverterTransaction: packet = self.tx_queue.pop(0) - if HOYMILES_TRANSACTION_LOGGING: - c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") - logging.debug(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}') - self.radio.transmit(packet, txpower=self.txpower) wait = False diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 7de4a1a2..3589de1a 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -19,6 +19,7 @@ import yaml from yaml.loader import SafeLoader import hoymiles import logging +from logging.handlers import RotatingFileHandler ################################################################################ """ Signal Handler """ @@ -127,6 +128,7 @@ def main_loop(ahoy_config): dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') sunset.sun_status2mqtt(dtu_ser, dtu_name) loop_interval = ahoy_config.get('interval', 1) + transmit_retries = ahoy_config.get('transmit_retries', 5) try: do_init = True @@ -143,7 +145,7 @@ def main_loop(ahoy_config): sys.exit(999) if hoymiles.HOYMILES_DEBUG_LOGGING: logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') - poll_inverter(inverter, dtu_ser, do_init, 3) + poll_inverter(inverter, dtu_ser, do_init, transmit_retries) do_init = False if loop_interval > 0: @@ -298,6 +300,8 @@ def init_logging(ahoy_config): log_config = ahoy_config.get('logging') fn = 'hoymiles.log' lvl = logging.ERROR + max_log_filesize = 1000000 + max_log_files = 1 if log_config: fn = log_config.get('filename', fn) level = log_config.get('level', 'ERROR') @@ -311,9 +315,11 @@ def init_logging(ahoy_config): lvl = logging.ERROR elif level == 'FATAL': lvl = logging.FATAL + max_log_filesize = log_config.get('max_log_filesize', max_log_filesize) + max_log_files = log_config.get('max_log_files', max_log_files) if hoymiles.HOYMILES_TRANSACTION_LOGGING: lvl = logging.DEBUG - logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) + logging.basicConfig(handlers=[RotatingFileHandler(fn, maxBytes=max_log_filesize, backupCount=max_log_files)], format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu') logging.info(f'start logging for {dtu_name} with level: {logging.root.level}') diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index bb32fb07..ad49d664 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -515,9 +515,17 @@ class Hm300Decode0B(StatusResponse): """ reactive power """ return self.unpack('>H', 20)[0]/10 @property + def powerfactor(self): + """ Powerfactor """ + return self.unpack('>H', 24)[0]/1000 + @property def temperature(self): """ Inverter temperature in °C """ return self.unpack('>h', 26)[0]/10 + @property + def event_count(self): + """ Event counter """ + return self.unpack('>H', 28)[0] class Hm300Decode0C(Hm300Decode0B): """ 1121-series mirco-inverters status data """