Merge from main and fix conflicts

This commit is contained in:
Jaakko Haakana 2025-08-03 21:54:06 +03:00
commit d8534b8fa0
71 changed files with 2202 additions and 1067 deletions

View file

@ -1,120 +0,0 @@
# This is the name of the workflow, visible on GitHub UI.
name: 🔋 Compile All Batteries
# Here we tell GitHub when to run the workflow.
on:
# The workflow is run when a commit is pushed or for a
# Pull Request.
- push
- pull_request
# This is the list of jobs that will be run concurrently.
jobs:
# This pre-job is run to skip workflows in case a workflow is already run, i.e. because the workflow is triggered by both push and pull_request
skip-duplicate-actions:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
do_not_skip: '["pull_request", "workflow_dispatch", "schedule"]'
# Since we use a build matrix, the actual number of jobs
# started depends on how many configurations the matrix
# will produce.
build-batteries:
needs: skip-duplicate-actions
if: needs.skip-duplicate-actions.outputs.should_skip != 'true'
# Here we tell GitHub that the jobs must be determined
# dynamically depending on a matrix configuration.
strategy:
# The matrix will produce one job for each combination of parameters.
matrix:
# This is the development board hardware for which the code will be compiled.
# FBQN stands for "fully qualified board name", and is used by Arduino to define the hardware to compile for.
fqbn:
- esp32:esp32:esp32
# further ESP32 chips
#- esp32:esp32:esp32c3
#- esp32:esp32:esp32c2
#- esp32:esp32:esp32c6
#- esp32:esp32:esp32h2
#- esp32:esp32:esp32s3
# These are the batteries for which the code will be compiled.
battery:
- BMW_I3_BATTERY
- BMW_IX_BATTERY
- BMW_PHEV_BATTERY
- BYD_ATTO_3_BATTERY
- CELLPOWER_BMS
- CHADEMO_BATTERY
- CMFA_EV_BATTERY
- DALY_BMS
- FOXESS_BATTERY
- GEELY_GEOMETRY_C_BATTERY
- IMIEV_CZERO_ION_BATTERY
- JAGUAR_IPACE_BATTERY
- KIA_E_GMP_BATTERY
- KIA_HYUNDAI_64_BATTERY
- KIA_HYUNDAI_HYBRID_BATTERY
- MEB_BATTERY
- MG_5_BATTERY
- NISSAN_LEAF_BATTERY
- ORION_BMS
- PYLON_BATTERY
- RJXZS_BMS
- RANGE_ROVER_PHEV_BATTERY
- RENAULT_KANGOO_BATTERY
- RENAULT_TWIZY_BATTERY
- RENAULT_ZOE_GEN1_BATTERY
- RENAULT_ZOE_GEN2_BATTERY
- SANTA_FE_PHEV_BATTERY
- STELLANTIS_ECMP_BATTERY
- TESLA_MODEL_3Y_BATTERY
- TESLA_MODEL_SX_BATTERY
- VOLVO_SPA_BATTERY
- TEST_FAKE_BATTERY
# These are the emulated inverter communication protocols for which the code will be compiled.
inverter:
- BYD_CAN
# These are the supported hardware platforms for which the code will be compiled.
hardware:
- HW_LILYGO
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
# This is the list of steps this job will run.
steps:
# First we clone the repo using the `checkout` action.
- name: Checkout
uses: actions/checkout@v4
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and
# configure the Arduino CLI on the system.
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
# Finally, we compile the sketch, using the FQBN that was set
# in the build matrix, and using build flags to define the
# battery and inverter set in the build matrix.
- name: Compile Sketch
run: arduino-cli compile --fqbn ${{ matrix.fqbn }} --build-property build.partitions=min_spiffs --build-property upload.maximum_size=1966080 --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -D${{ matrix.battery}} -D${{ matrix.inverter}} -D${{ matrix.hardware}}" ./Software

View file

@ -1,291 +0,0 @@
# This is the name of the workflow, visible on GitHub UI.
name: 🔌🔋💫 Compile All Combinations
# Here we tell GitHub when to run the workflow.
on:
# This allows you to run this workflow manually from the
# GitHub Actions tab.
workflow_dispatch:
# The workflow is run upon creating, editing,
# pre-releasing, releasing and publishing a release
release:
types: [published]
# This is the list of jobs that will be run concurrently.
jobs:
# This pre-job is run to skip workflows in case a workflow is already run, i.e. because the workflow is triggered by both push and pull_request
skip-duplicate-actions:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
do_not_skip: '["pull_request", "workflow_dispatch", "schedule"]'
# Since we use a build matrix, the actual number of jobs
# started depends on how many configurations the matrix
# will produce.
# This is the name of the job.
build-matrix-batteries-A-to-J: # we split this matrix into multiple parts, to prevent en error that is triggered when the matrix expansion exeeds 255
needs: skip-duplicate-actions
if: needs.skip-duplicate-actions.outputs.should_skip != 'true'
# Here we tell GitHub that the jobs must be determined
# dynamically depending on a matrix configuration.
strategy:
# The matrix will produce one job for each combination of parameters.
matrix:
# This is the development board hardware for which the code will be compiled.
# FBQN stands for "fully qualified board name", and is used by Arduino to define the hardware to compile for.
fqbn:
- esp32:esp32:esp32
# further ESP32 chips
#- esp32:esp32:esp32c3
#- esp32:esp32:esp32c2
#- esp32:esp32:esp32c6
#- esp32:esp32:esp32h2
#- esp32:esp32:esp32s3
# These are the batteries for which the code will be compiled.
battery:
- BMW_I3_BATTERY
- BMW_IX_BATTERY
- BMW_PHEV_BATTERY
- BYD_ATTO_3_BATTERY
- CELLPOWER_BMS
- CHADEMO_BATTERY
- FOXESS_BATTERY
- IMIEV_CZERO_ION_BATTERY
- JAGUAR_IPACE_BATTERY
# These are the emulated inverter communication protocols for which the code will be compiled.
inverter:
- AFORE_CAN
- BYD_CAN
- BYD_KOSTAL_RS485
- BYD_MODBUS
- FERROAMP_CAN
- FOXESS_CAN
- GROWATT_HV_CAN
- GROWATT_LV_CAN
- PYLON_CAN
- PYLON_LV_CAN
- SCHNEIDER_CAN
- SMA_BYD_H_CAN
- SMA_BYD_HVS_CAN
- SMA_LV_CAN
- SMA_TRIPOWER_CAN
- SOFAR_CAN
- SOLAX_CAN
- SUNGROW_CAN
# These are the supported hardware platforms for which the code will be compiled.
hardware:
- HW_LILYGO
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
# This is the list of steps this job will run.
steps:
# First we clone the repo using the `checkout` action.
- name: Checkout
uses: actions/checkout@v4
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and
# configure the Arduino CLI on the system.
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
# Finally, we compile the sketch, using the FQBN that was set
# in the build matrix, and using build flags to define the
# battery and inverter set in the build matrix.
- name: Compile Sketch
run: arduino-cli compile --fqbn ${{ matrix.fqbn }} --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -D${{ matrix.battery}} -D${{ matrix.inverter}} -D${{ matrix.hardware}}" ./Software
# This is the name of the job.
build-matrix-batteries-K-to-P:
needs: skip-duplicate-actions
if: needs.skip-duplicate-actions.outputs.should_skip != 'true'
# Here we tell GitHub that the jobs must be determined
# dynamically depending on a matrix configuration.
strategy:
# The matrix will produce one job for each combination of parameters.
matrix:
# This is the development board hardware for which the code will be compiled.
# FBQN stands for "fully qualified board name", and is used by Arduino to define the hardware to compile for.
fqbn:
- esp32:esp32:esp32
# further ESP32 chips
#- esp32:esp32:esp32c3
#- esp32:esp32:esp32c2
#- esp32:esp32:esp32c6
#- esp32:esp32:esp32h2
#- esp32:esp32:esp32s3
# These are the batteries for which the code will be compiled.
battery:
- KIA_E_GMP_BATTERY
- KIA_HYUNDAI_64_BATTERY
- KIA_HYUNDAI_HYBRID_BATTERY
- MEB_BATTERY
- MG_5_BATTERY
- NISSAN_LEAF_BATTERY
- ORION_BMS
- PYLON_BATTERY
# These are the emulated inverter communication protocols for which the code will be compiled.
inverter:
- AFORE_CAN
- BYD_CAN
- BYD_KOSTAL_RS485
- BYD_MODBUS
- FERROAMP_CAN
- FOXESS_CAN
- GROWATT_HV_CAN
- GROWATT_LV_CAN
- PYLON_CAN
- PYLON_LV_CAN
- SCHNEIDER_CAN
- SMA_BYD_H_CAN
- SMA_BYD_HVS_CAN
- SMA_LV_CAN
- SMA_TRIPOWER_CAN
- SOFAR_CAN
- SOLAX_CAN
- SUNGROW_CAN
# These are the supported hardware platforms for which the code will be compiled.
hardware:
- HW_LILYGO
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
# This is the list of steps this job will run.
steps:
# First we clone the repo using the `checkout` action.
- name: Checkout
uses: actions/checkout@v4
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and
# configure the Arduino CLI on the system.
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
# Finally, we compile the sketch, using the FQBN that was set
# in the build matrix, and using build flags to define the
# battery and inverter set in the build matrix.
- name: Compile Sketch
run: arduino-cli compile --fqbn ${{ matrix.fqbn }} --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -D${{ matrix.battery}} -D${{ matrix.inverter}} -D${{ matrix.hardware}}" ./Software
# This is the name of the job.
build-matrix-batteries-R-to-Z:
needs: skip-duplicate-actions
if: needs.skip-duplicate-actions.outputs.should_skip != 'true'
# Here we tell GitHub that the jobs must be determined
# dynamically depending on a matrix configuration.
strategy:
# The matrix will produce one job for each combination of parameters.
matrix:
# This is the development board hardware for which the code will be compiled.
# FBQN stands for "fully qualified board name", and is used by Arduino to define the hardware to compile for.
fqbn:
- esp32:esp32:esp32
# further ESP32 chips
#- esp32:esp32:esp32c3
#- esp32:esp32:esp32c2
#- esp32:esp32:esp32c6
#- esp32:esp32:esp32h2
#- esp32:esp32:esp32s3
# These are the batteries for which the code will be compiled.
battery:
- RANGE_ROVER_PHEV_BATTERY
- RENAULT_KANGOO_BATTERY
- RENAULT_TWIZY_BATTERY
- RENAULT_ZOE_GEN1_BATTERY
- RENAULT_ZOE_GEN2_BATTERY
- RJXZS_BMS
- SANTA_FE_PHEV_BATTERY
- STELLANTIS_ECMP_BATTERY
- TESLA_MODEL_3Y_BATTERY
- TESLA_MODEL_SX_BATTERY
- VOLVO_SPA_BATTERY
- TEST_FAKE_BATTERY
# These are the emulated inverter communication protocols for which the code will be compiled.
inverter:
- AFORE_CAN
- BYD_CAN
- BYD_KOSTAL_RS485
- BYD_MODBUS
- FERROAMP_CAN
- FOXESS_CAN
- GROWATT_HV_CAN
- GROWATT_LV_CAN
- PYLON_CAN
- PYLON_LV_CAN
- SCHNEIDER_CAN
- SMA_BYD_H_CAN
- SMA_BYD_HVS_CAN
- SMA_LV_CAN
- SMA_TRIPOWER_CAN
- SOFAR_CAN
- SOLAX_CAN
- SUNGROW_CAN
# These are the supported hardware platforms for which the code will be compiled.
hardware:
- HW_LILYGO
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
# This is the list of steps this job will run.
steps:
# First we clone the repo using the `checkout` action.
- name: Checkout
uses: actions/checkout@v4
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and
# configure the Arduino CLI on the system.
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
# Finally, we compile the sketch, using the FQBN that was set
# in the build matrix, and using build flags to define the
# battery and inverter set in the build matrix.
- name: Compile Sketch
run: arduino-cli compile --fqbn ${{ matrix.fqbn }} --build-property build.partitions=min_spiffs --build-property upload.maximum_size=1966080 --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -D${{ matrix.battery}} -D${{ matrix.inverter}} -D${{ matrix.hardware}}" ./Software

View file

@ -1,94 +0,0 @@
# This is the name of the workflow, visible on GitHub UI.
name: 🔋🔋 Compile All Double Batteries
# Here we tell GitHub when to run the workflow.
on:
# The workflow is run when a commit is pushed or for a
# Pull Request.
- push
- pull_request
# This is the list of jobs that will be run concurrently.
jobs:
# This pre-job is run to skip workflows in case a workflow is already run, i.e. because the workflow is triggered by both push and pull_request
skip-duplicate-actions:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
do_not_skip: '["pull_request", "workflow_dispatch", "schedule"]'
# Since we use a build matrix, the actual number of jobs
# started depends on how many configurations the matrix
# will produce.
build-batteries:
needs: skip-duplicate-actions
if: needs.skip-duplicate-actions.outputs.should_skip != 'true'
# Here we tell GitHub that the jobs must be determined
# dynamically depending on a matrix configuration.
strategy:
# The matrix will produce one job for each combination of parameters.
matrix:
# This is the development board hardware for which the code will be compiled.
# FBQN stands for "fully qualified board name", and is used by Arduino to define the hardware to compile for.
fqbn:
- esp32:esp32:esp32
# further ESP32 chips
#- esp32:esp32:esp32c3
#- esp32:esp32:esp32c2
#- esp32:esp32:esp32c6
#- esp32:esp32:esp32h2
#- esp32:esp32:esp32s3
# These are the batteries for which the code will be compiled.
battery:
- BMW_I3_BATTERY
- BYD_ATTO_3_BATTERY
- KIA_HYUNDAI_64_BATTERY
- PYLON_BATTERY
- SANTA_FE_PHEV_BATTERY
- TEST_FAKE_BATTERY
# These are the emulated inverter communication protocols for which the code will be compiled.
inverter:
- BYD_CAN
# These are the supported hardware platforms for which the code will be compiled.
hardware:
- HW_LILYGO
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
# This is the list of steps this job will run.
steps:
# First we clone the repo using the `checkout` action.
- name: Checkout
uses: actions/checkout@v4
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and
# configure the Arduino CLI on the system.
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
# Finally, we compile the sketch, using the FQBN that was set
# in the build matrix, and using build flags to define the
# battery and inverter set in the build matrix.
- name: Compile Sketch
run: arduino-cli compile --fqbn ${{ matrix.fqbn }} --build-property build.partitions=min_spiffs --build-property upload.maximum_size=1966080 --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -DDOUBLE_BATTERY -D${{ matrix.battery}} -D${{ matrix.inverter}} -D${{ matrix.hardware}}" ./Software

View file

@ -1,108 +0,0 @@
# This is the name of the workflow, visible on GitHub UI.
name: 🔌 Compile All Inverters
# Here we tell GitHub when to run the workflow.
on:
# The workflow is run when a commit is pushed or for a
# Pull Request.
- push
- pull_request
# This is the list of jobs that will be run concurrently.
jobs:
# This pre-job is run to skip workflows in case a workflow is already run, i.e. because the workflow is triggered by both push and pull_request
skip-duplicate-actions:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
do_not_skip: '["pull_request", "workflow_dispatch", "schedule"]'
# Since we use a build matrix, the actual number of jobs
# started depends on how many configurations the matrix
# will produce.
# This is the name of the job.
build-inverters:
needs: skip-duplicate-actions
if: needs.skip-duplicate-actions.outputs.should_skip != 'true'
# Here we tell GitHub that the jobs must be determined
# dynamically depending on a matrix configuration.
strategy:
# The matrix will produce one job for each combination of parameters.
matrix:
# This is the development board hardware for which the code will be compiled.
# FBQN stands for "fully qualified board name", and is used by Arduino to define the hardware to compile for.
fqbn:
- esp32:esp32:esp32
# further ESP32 chips
#- esp32:esp32:esp32c3
#- esp32:esp32:esp32c2
#- esp32:esp32:esp32c6
#- esp32:esp32:esp32h2
#- esp32:esp32:esp32s3
# These are the batteries for which the code will be compiled.
battery:
- NISSAN_LEAF_BATTERY
# These are the emulated inverter communication protocols for which the code will be compiled.
inverter:
- AFORE_CAN
- BYD_CAN
- BYD_KOSTAL_RS485
- BYD_MODBUS
- FERROAMP_CAN
- FOXESS_CAN
- GROWATT_HV_CAN
- GROWATT_LV_CAN
- PYLON_CAN
- PYLON_LV_CAN
- SCHNEIDER_CAN
- SMA_BYD_H_CAN
- SMA_BYD_HVS_CAN
- SMA_LV_CAN
- SMA_TRIPOWER_CAN
- SOFAR_CAN
- SOLAX_CAN
- SUNGROW_CAN
- NISSANLEAF_CHARGER # Last element is not an inverter, but good to also test if the charger compiles
# These are the supported hardware platforms for which the code will be compiled.
hardware:
- HW_LILYGO
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
# This is the list of steps this job will run.
steps:
# First we clone the repo using the `checkout` action.
- name: Checkout
uses: actions/checkout@v4
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and
# configure the Arduino CLI on the system.
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
# Finally, we compile the sketch, using the FQBN that was set
# in the build matrix, and using build flags to define the
# battery and inverter set in the build matrix.
- name: Compile Sketch
run: arduino-cli compile --fqbn ${{ matrix.fqbn }} --build-property build.partitions=min_spiffs --build-property upload.maximum_size=1966080 --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -D${{ matrix.battery}} -D${{ matrix.inverter}} -D${{ matrix.hardware}}" ./Software

View file

@ -1,58 +0,0 @@
# This is the name of the workflow, visible on GitHub UI.
name: 🔋 Compile Common Image for Lilygo with debug logging
# Here we tell GitHub when to run the workflow.
on:
# The workflow is run when a commit is pushed or for a
# Pull Request.
- push
- pull_request
# This is the list of jobs that will be run concurrently.
jobs:
# This pre-job is run to skip workflows in case a workflow is already run, i.e. because the workflow is triggered by both push and pull_request
skip-duplicate-actions:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
do_not_skip: '["pull_request", "workflow_dispatch", "schedule"]'
build-common-image:
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and
# configure the Arduino CLI on the system.
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
- name: Compile Sketch
run: arduino-cli compile --output-dir ./ --fqbn esp32:esp32:esp32 --build-property build.partitions=min_spiffs --build-property upload.maximum_size=1966080 --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -DDEBUG_VIA_USB -DCOMMON_IMAGE -DHW_LILYGO" ./Software
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: battery-emulator-lilygo-debug.bin
path: Software.ino.bin

View file

@ -1,7 +1,5 @@
# This is the name of the workflow, visible on GitHub UI.
name: 🔋 Compile Common Image for Lilygo name: 🔋 Compile Common Image for Lilygo
# Here we tell GitHub when to run the workflow.
on: on:
# The workflow is run when a commit is pushed or for a # The workflow is run when a commit is pushed or for a
# Pull Request. # Pull Request.
@ -30,29 +28,31 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4 name: Checkout code
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h # Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h - name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
# We use the `arduino/setup-arduino-cli` action to install and - name: Build image for Lilygo
# configure the Arduino CLI on the system. run: pio run -e lilygo_330
- name: Setup Arduino CLI
uses: arduino/setup-arduino-cli@v2
# We then install the platform.
- name: Install platform
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32
- name: Compile Sketch
run: arduino-cli compile --output-dir ./ --fqbn esp32:esp32:esp32 --build-property build.partitions=min_spiffs --build-property upload.maximum_size=1966080 --build-property "build.extra_flags=-Wall -Wextra -Wpedantic -Werror -DESP32 -DCOMMON_IMAGE -DHW_LILYGO" ./Software
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: battery-emulator-lilygo.bin name: battery-emulator-lilygo.bin
path: Software.ino.bin path: .pio/build/lilygo_330/firmware.bin

View file

@ -0,0 +1,58 @@
name: 🔋 Compile Common Image for Stark CMR
on:
# The workflow is run when a commit is pushed or for a
# Pull Request.
- push
- pull_request
# This is the list of jobs that will be run concurrently.
jobs:
# This pre-job is run to skip workflows in case a workflow is already run, i.e. because the workflow is triggered by both push and pull_request
skip-duplicate-actions:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
do_not_skip: '["pull_request", "workflow_dispatch", "schedule"]'
build-common-image:
# This is the platform GitHub will use to run our workflow.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
name: Checkout code
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
# Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
- name: Copy USER_SECRETS.TEMPLATE.h to USER_SECRETS.h
run: cp ./Software/USER_SECRETS.TEMPLATE.h ./Software/USER_SECRETS.h
- name: Build image for Stark CMR
run: pio run -e stark_330
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: battery-emulator-stark.bin
path: .pio/build/stark_330/firmware.bin

View file

@ -2,7 +2,7 @@
![GitHub release (with filter)](https://img.shields.io/github/v/release/dalathegreat/BYD-Battery-Emulator-For-Gen24?color=%23008000) ![GitHub release (with filter)](https://img.shields.io/github/v/release/dalathegreat/BYD-Battery-Emulator-For-Gen24?color=%23008000)
![GitHub Repo stars](https://img.shields.io/github/stars/dalathegreat/Battery-Emulator?style=flat&color=%23128512) ![GitHub Repo stars](https://img.shields.io/github/stars/dalathegreat/Battery-Emulator?style=flat&color=%23128512)
![GitHub forks](https://img.shields.io/github/forks/dalathegreat/Battery-Emulator?style=flat&color=%23128512) ![GitHub forks](https://img.shields.io/github/forks/dalathegreat/Battery-Emulator?style=flat&color=%23128512)
![GitHub actions](https://img.shields.io/github/actions/workflow/status/dalathegreat/BYD-Battery-Emulator-For-Gen24/compile-all-batteries.yml?color=0E810E) ![GitHub actions](https://img.shields.io/github/actions/workflow/status/dalathegreat/BYD-Battery-Emulator-For-Gen24/compile-common-image-lilygo.yml?color=0E810E)
![Static Badge](https://img.shields.io/badge/made-with_love-blue?color=%23008000) ![Static Badge](https://img.shields.io/badge/made-with_love-blue?color=%23008000)
## What is Battery Emulator? ## What is Battery Emulator?

View file

@ -19,6 +19,7 @@
//#define CELLPOWER_BMS //#define CELLPOWER_BMS
//#define CHADEMO_BATTERY //NOTE: inherently enables CONTACTOR_CONTROL below //#define CHADEMO_BATTERY //NOTE: inherently enables CONTACTOR_CONTROL below
//#define GEELY_GEOMETRY_C_BATTERY //#define GEELY_GEOMETRY_C_BATTERY
//#define HYUNDAI_IONIQ_28_BATTERY
//#define CMFA_EV_BATTERY //#define CMFA_EV_BATTERY
//#define IMIEV_CZERO_ION_BATTERY //#define IMIEV_CZERO_ION_BATTERY
//#define JAGUAR_IPACE_BATTERY //#define JAGUAR_IPACE_BATTERY
@ -39,6 +40,7 @@
//#define RENAULT_ZOE_GEN1_BATTERY //#define RENAULT_ZOE_GEN1_BATTERY
//#define RENAULT_ZOE_GEN2_BATTERY //#define RENAULT_ZOE_GEN2_BATTERY
//#define SONO_BATTERY //#define SONO_BATTERY
//#define SAMSUNG_SDI_LV_BATTERY
//#define SANTA_FE_PHEV_BATTERY //#define SANTA_FE_PHEV_BATTERY
//#define SIMPBMS_BATTERY //#define SIMPBMS_BATTERY
//#define STELLANTIS_ECMP_BATTERY //#define STELLANTIS_ECMP_BATTERY

View file

@ -74,6 +74,8 @@ const char* name_for_battery_type(BatteryType type) {
return FoxessBattery::Name; return FoxessBattery::Name;
case BatteryType::GeelyGeometryC: case BatteryType::GeelyGeometryC:
return GeelyGeometryCBattery::Name; return GeelyGeometryCBattery::Name;
case BatteryType::HyundaiIoniq28:
return HyundaiIoniq28Battery::Name;
case BatteryType::OrionBms: case BatteryType::OrionBms:
return OrionBms::Name; return OrionBms::Name;
case BatteryType::Sono: case BatteryType::Sono:
@ -114,6 +116,8 @@ const char* name_for_battery_type(BatteryType type) {
return RenaultZoeGen1Battery::Name; return RenaultZoeGen1Battery::Name;
case BatteryType::RenaultZoe2: case BatteryType::RenaultZoe2:
return RenaultZoeGen2Battery::Name; return RenaultZoeGen2Battery::Name;
case BatteryType::SamsungSdiLv:
return SamsungSdiLVBattery::Name;
case BatteryType::SantaFePhev: case BatteryType::SantaFePhev:
return SantaFePhevBattery::Name; return SantaFePhevBattery::Name;
case BatteryType::SimpBms: case BatteryType::SimpBms:
@ -171,6 +175,8 @@ Battery* create_battery(BatteryType type) {
return new FoxessBattery(); return new FoxessBattery();
case BatteryType::GeelyGeometryC: case BatteryType::GeelyGeometryC:
return new GeelyGeometryCBattery(); return new GeelyGeometryCBattery();
case BatteryType::HyundaiIoniq28:
return new HyundaiIoniq28Battery();
case BatteryType::OrionBms: case BatteryType::OrionBms:
return new OrionBms(); return new OrionBms();
case BatteryType::Sono: case BatteryType::Sono:
@ -211,6 +217,8 @@ Battery* create_battery(BatteryType type) {
return new RenaultZoeGen1Battery(); return new RenaultZoeGen1Battery();
case BatteryType::RenaultZoe2: case BatteryType::RenaultZoe2:
return new RenaultZoeGen2Battery(); return new RenaultZoeGen2Battery();
case BatteryType::SamsungSdiLv:
return new SamsungSdiLVBattery();
case BatteryType::SantaFePhev: case BatteryType::SantaFePhev:
return new SantaFePhevBattery(); return new SantaFePhevBattery();
case BatteryType::SimpBms: case BatteryType::SimpBms:

View file

@ -26,6 +26,7 @@ void setup_can_shunt();
#include "ECMP-BATTERY.h" #include "ECMP-BATTERY.h"
#include "FOXESS-BATTERY.h" #include "FOXESS-BATTERY.h"
#include "GEELY-GEOMETRY-C-BATTERY.h" #include "GEELY-GEOMETRY-C-BATTERY.h"
#include "HYUNDAI-IONIQ-28-BATTERY.h"
#include "IMIEV-CZERO-ION-BATTERY.h" #include "IMIEV-CZERO-ION-BATTERY.h"
#include "JAGUAR-IPACE-BATTERY.h" #include "JAGUAR-IPACE-BATTERY.h"
#include "KIA-E-GMP-BATTERY.h" #include "KIA-E-GMP-BATTERY.h"
@ -43,6 +44,7 @@ void setup_can_shunt();
#include "RENAULT-ZOE-GEN1-BATTERY.h" #include "RENAULT-ZOE-GEN1-BATTERY.h"
#include "RENAULT-ZOE-GEN2-BATTERY.h" #include "RENAULT-ZOE-GEN2-BATTERY.h"
#include "RJXZS-BMS.h" #include "RJXZS-BMS.h"
#include "SAMSUNG-SDI-LV-BATTERY.h"
#include "SANTA-FE-PHEV-BATTERY.h" #include "SANTA-FE-PHEV-BATTERY.h"
#include "SIMPBMS-BATTERY.h" #include "SIMPBMS-BATTERY.h"
#include "SONO-BATTERY.h" #include "SONO-BATTERY.h"

View file

@ -43,6 +43,8 @@ enum class BatteryType {
VolvoSpa = 35, VolvoSpa = 35,
VolvoSpaHybrid = 36, VolvoSpaHybrid = 36,
MgHsPhev = 37, MgHsPhev = 37,
SamsungSdiLv = 38,
HyundaiIoniq28 = 39,
Highest Highest
}; };

View file

@ -123,6 +123,7 @@ void EcmpBattery::update_values() {
datalayer_extended.stellantisECMP.pid_time_spent_over_55c = pid_time_spent_over_55c; datalayer_extended.stellantisECMP.pid_time_spent_over_55c = pid_time_spent_over_55c;
datalayer_extended.stellantisECMP.pid_contactor_closing_counter = pid_contactor_closing_counter; datalayer_extended.stellantisECMP.pid_contactor_closing_counter = pid_contactor_closing_counter;
datalayer_extended.stellantisECMP.pid_date_of_manufacture = pid_date_of_manufacture; datalayer_extended.stellantisECMP.pid_date_of_manufacture = pid_date_of_manufacture;
datalayer_extended.stellantisECMP.pid_SOH_cell_1 = pid_SOH_cell_1;
if (battery_InterlockOpen) { if (battery_InterlockOpen) {
set_event(EVENT_HVIL_FAILURE, 0); set_event(EVENT_HVIL_FAILURE, 0);
@ -135,6 +136,12 @@ void EcmpBattery::update_values() {
} else { } else {
clear_event(EVENT_12V_LOW); clear_event(EVENT_12V_LOW);
} }
if (pid_reason_open == 7) { //Invalid status
set_event(EVENT_CONTACTOR_OPEN, 0);
} else {
clear_event(EVENT_CONTACTOR_OPEN);
}
} }
void EcmpBattery::handle_incoming_can_frame(CAN_frame rx_frame) { void EcmpBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
@ -578,8 +585,7 @@ void EcmpBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
(rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]); (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
break; break;
case PID_ENERGY_CAPACITY: case PID_ENERGY_CAPACITY:
pid_energy_capacity = ((rx_frame.data.u8[4] << 24) | (rx_frame.data.u8[5] << 16) | pid_energy_capacity = (rx_frame.data.u8[4] << 16) | (rx_frame.data.u8[5] << 8) | (rx_frame.data.u8[6]);
(rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
break; break;
case PID_HIGH_CELL_NUM: case PID_HIGH_CELL_NUM:
pid_highest_cell_voltage_num = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; pid_highest_cell_voltage_num = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
@ -720,6 +726,33 @@ void EcmpBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (incoming_poll) //Multiframe responses switch (incoming_poll) //Multiframe responses
{ {
case PID_ALL_CELL_SOH:
switch (rx_frame.data.u8[0]) {
case 0x10:
pid_SOH_cell_1 = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]);
break;
case 0x21:
break;
case 0x22:
break;
case 0x23:
break;
case 0x24:
break;
case 0x25:
break;
case 0x26:
break;
case 0x27:
break;
case 0x28:
break;
case 0x29:
break;
default:
break;
}
break;
case PID_ALL_CELL_VOLTAGES: case PID_ALL_CELL_VOLTAGES:
switch (rx_frame.data.u8[0]) { switch (rx_frame.data.u8[0]) {
case 0x10: case 0x10:
@ -1291,6 +1324,11 @@ void EcmpBattery::transmit_can(unsigned long currentMillis) {
case PID_DATE_OF_MANUFACTURE: case PID_DATE_OF_MANUFACTURE:
ECMP_POLL.data.u8[2] = (uint8_t)((PID_DATE_OF_MANUFACTURE & 0xFF00) >> 8); ECMP_POLL.data.u8[2] = (uint8_t)((PID_DATE_OF_MANUFACTURE & 0xFF00) >> 8);
ECMP_POLL.data.u8[3] = (uint8_t)(PID_DATE_OF_MANUFACTURE & 0x00FF); ECMP_POLL.data.u8[3] = (uint8_t)(PID_DATE_OF_MANUFACTURE & 0x00FF);
poll_state = PID_ALL_CELL_SOH;
break;
case PID_ALL_CELL_SOH:
ECMP_POLL.data.u8[2] = (uint8_t)((PID_ALL_CELL_SOH & 0xFF00) >> 8);
ECMP_POLL.data.u8[3] = (uint8_t)(PID_ALL_CELL_SOH & 0x00FF);
poll_state = PID_WELD_CHECK; // Loop back to beginning poll_state = PID_WELD_CHECK; // Loop back to beginning
break; break;
default: default:

View file

@ -134,6 +134,7 @@ class EcmpBattery : public CanBattery {
uint32_t pid_time_spent_over_55c = NOT_SAMPLED_YET; uint32_t pid_time_spent_over_55c = NOT_SAMPLED_YET;
uint32_t pid_contactor_closing_counter = NOT_SAMPLED_YET; uint32_t pid_contactor_closing_counter = NOT_SAMPLED_YET;
uint32_t pid_date_of_manufacture = NOT_SAMPLED_YET; uint32_t pid_date_of_manufacture = NOT_SAMPLED_YET;
uint16_t pid_SOH_cell_1 = NOT_SAMPLED_YET;
unsigned long previousMillis10 = 0; // will store last time a 10ms CAN Message was sent unsigned long previousMillis10 = 0; // will store last time a 10ms CAN Message was sent
unsigned long previousMillis20 = 0; // will store last time a 20ms CAN Message was sent unsigned long previousMillis20 = 0; // will store last time a 20ms CAN Message was sent
@ -195,7 +196,7 @@ class EcmpBattery : public CanBattery {
static const uint16_t PID_SW_VERSION_NUM = 0xF195; //Not supported on all batteris static const uint16_t PID_SW_VERSION_NUM = 0xF195; //Not supported on all batteris
static const uint16_t PID_FACTORY_MODE_CONTROL = 0xD900; static const uint16_t PID_FACTORY_MODE_CONTROL = 0xD900;
static const uint16_t PID_BATTERY_SERIAL = 0xD901; static const uint16_t PID_BATTERY_SERIAL = 0xD901;
static const uint16_t PID_ALL_CELL_SOH = 0xD4B5; //Very long message reply, too much data for this integration static const uint16_t PID_ALL_CELL_SOH = 0xD4B5;
static const uint16_t PID_AUX_FUSE_STATE = 0xD86C; static const uint16_t PID_AUX_FUSE_STATE = 0xD86C;
static const uint16_t PID_BATTERY_STATE = 0xD811; static const uint16_t PID_BATTERY_STATE = 0xD811;
static const uint16_t PID_PRECHARGE_SHORT_CIRCUIT = 0xD4D8; static const uint16_t PID_PRECHARGE_SHORT_CIRCUIT = 0xD4D8;

View file

@ -377,6 +377,11 @@ class EcmpHtmlRenderer : public BatteryHtmlRenderer {
? "N/A" ? "N/A"
: String(datalayer_extended.stellantisECMP.pid_contactor_closing_counter)) + : String(datalayer_extended.stellantisECMP.pid_contactor_closing_counter)) +
" cycles</h4>"; " cycles</h4>";
content += "<h4>State of Health Cell-1: " +
(datalayer_extended.stellantisECMP.pid_SOH_cell_1 == 255
? "N/A"
: String(datalayer_extended.stellantisECMP.pid_SOH_cell_1)) +
"</h4>";
return content; return content;
} }

View file

@ -0,0 +1,13 @@
#include "HYUNDAI-IONIQ-28-BATTERY-HTML.h"
#include "HYUNDAI-IONIQ-28-BATTERY.h"
String HyundaiIoniq28BatteryHtmlRenderer::get_status_html() {
String content;
content += "<h4>12V voltage: " + String(batt.get_lead_acid_voltage() / 10.0, 1) + "</h4>";
content += "<h4>Temperature, power relay: " + String(batt.get_power_relay_temperature()) + "</h4>";
content += "<h4>Battery relay: " + String(batt.get_battery_relay_mode()) + "</h4>";
content += "<h4>Batterymanagement mode: " + String(batt.get_battery_management_mode()) + "</h4>";
content += "<h4>BMS ignition: " + String(batt.get_battery_ignition_mode()) + "</h4>";
return content;
}

View file

@ -0,0 +1,19 @@
#ifndef _HYUNDAI_IONIQ_28_BATTERY_HTML_H
#define _HYUNDAI_IONIQ_28_BATTERY_HTML_H
#include "../datalayer/datalayer.h"
#include "../devboard/webserver/BatteryHtmlRenderer.h"
class HyundaiIoniq28Battery;
class HyundaiIoniq28BatteryHtmlRenderer : public BatteryHtmlRenderer {
private:
HyundaiIoniq28Battery& batt;
public:
HyundaiIoniq28BatteryHtmlRenderer(HyundaiIoniq28Battery& b) : batt(b) {}
String get_status_html();
};
#endif

View file

@ -0,0 +1,387 @@
#include "HYUNDAI-IONIQ-28-BATTERY.h"
#include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h"
#include "../datalayer/datalayer_extended.h"
#include "../devboard/utils/events.h"
void HyundaiIoniq28Battery::
update_values() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
datalayer_battery->status.real_soc = (SOC_Display * 10); //increase SOC range from 0-100.0 -> 100.00
datalayer_battery->status.soh_pptt = (batterySOH * 10); //Increase decimals from 100.0% -> 100.00%
datalayer_battery->status.voltage_dV = batteryVoltage; //value is *10 (3700 = 370.0)
datalayer_battery->status.current_dA = -batteryAmps; //value is *10 (150 = 15.0) , invert the sign
datalayer_battery->status.remaining_capacity_Wh = static_cast<uint32_t>(
(static_cast<double>(datalayer_battery->status.real_soc) / 10000) * datalayer_battery->info.total_capacity_Wh);
datalayer_battery->status.max_charge_power_W = allowedChargePower * 10;
datalayer_battery->status.max_discharge_power_W = allowedDischargePower * 10;
datalayer_battery->status.temperature_min_dC = (int8_t)temperatureMin * 10; //Increase decimals, 17C -> 17.0C
datalayer_battery->status.temperature_max_dC = (int8_t)temperatureMax * 10; //Increase decimals, 18C -> 18.0C
datalayer_battery->status.cell_max_voltage_mV = CellVoltMax_mV;
datalayer_battery->status.cell_min_voltage_mV = CellVoltMin_mV;
//Map all cell voltages to the global array
memcpy(datalayer_battery->status.cell_voltages_mV, cellvoltages_mv, 96 * sizeof(uint16_t));
if (leadAcidBatteryVoltage < 110) {
set_event(EVENT_12V_LOW, leadAcidBatteryVoltage);
}
}
void HyundaiIoniq28Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x4DE:
startedUp = true;
break;
case 0x542: //BMS SOC
startedUp = true;
datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE;
SOC_Display = rx_frame.data.u8[0] * 5; //100% = 200 ( 200 * 5 = 1000 )
break;
case 0x594:
startedUp = true;
allowedChargePower = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]);
allowedDischargePower = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]);
SOC_BMS = rx_frame.data.u8[5] * 5; //100% = 200 ( 200 * 5 = 1000 )
break;
case 0x595:
startedUp = true;
batteryVoltage = (rx_frame.data.u8[7] << 8) + rx_frame.data.u8[6];
batteryAmps = (rx_frame.data.u8[5] << 8) + rx_frame.data.u8[4];
if (counter_200 > 3) {
IONIQ_524.data.u8[0] = (uint8_t)(batteryVoltage / 10);
IONIQ_524.data.u8[1] = (uint8_t)((batteryVoltage / 10) >> 8);
} //VCU measured voltage sent back to bms
break;
case 0x596:
startedUp = true;
leadAcidBatteryVoltage = rx_frame.data.u8[1]; //12v Battery Volts
temperatureMin = rx_frame.data.u8[6]; //Lowest temp in battery
temperatureMax = rx_frame.data.u8[7]; //Highest temp in battery
break;
case 0x598:
startedUp = true;
break;
case 0x5D5:
startedUp = true;
powerRelayTemperature = rx_frame.data.u8[7];
break;
case 0x5D8:
startedUp = true;
break;
case 0x7EC:
//We only poll multiframe messages
switch (rx_frame.data.u8[0]) {
case 0x10: //"PID Header"
transmit_can_frame(&IONIQ_7E4_ACK);
incoming_poll_group = rx_frame.data.u8[3];
break;
case 0x21: //First frame in PID group
if (incoming_poll_group == 1) { //21 14 13 24 13 24 04 00
batteryRelay = rx_frame.data.u8[7];
} else if (incoming_poll_group == 2) { //21 AD AD AD AD AD AD AC
cellvoltages_mv[0] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[1] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[2] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[3] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[4] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[5] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[6] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 3) { //21 AD AD AD AD AD AD AD
cellvoltages_mv[32] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[33] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[34] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[35] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[36] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[37] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[38] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 4) { //21 AD AD AD AD AD AD AD
cellvoltages_mv[64] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[65] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[66] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[67] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[68] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[69] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[70] = (rx_frame.data.u8[7] * 20);
}
break;
case 0x22: //Second datarow in PID group
if (incoming_poll_group == 1) { //22 00 0C FF 17 16 17 17
} else if (incoming_poll_group == 2) { //22 AD AC AC AD AD AD AD
cellvoltages_mv[7] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[8] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[9] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[10] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[11] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[12] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[13] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 3) { //22 AD AD AD AD AD AD AD
cellvoltages_mv[39] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[40] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[41] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[42] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[43] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[44] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[45] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 4) { //22 AD AD AD AD AD AD AD
cellvoltages_mv[71] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[72] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[73] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[74] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[75] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[76] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[77] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 6) {
batteryManagementMode = rx_frame.data.u8[5];
}
break;
case 0x23: //Third datarow in PID group
if (incoming_poll_group == 1) { //23 17 17 17 00 17 AD 25
CellVoltMax_mV = (rx_frame.data.u8[6] * 20); //(volts *50) *20 =mV
} else if (incoming_poll_group == 2) { //23 AD AD AD AD AB AD AD
cellvoltages_mv[14] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[15] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[16] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[17] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[18] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[19] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[20] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 3) { //23 AD AD AC AD AD AD AD
cellvoltages_mv[46] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[47] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[48] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[49] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[50] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[51] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[52] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 4) { //23 AA AD AD AD AD AD AC
cellvoltages_mv[78] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[79] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[80] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[81] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[82] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[83] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[84] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 5) {
heatertemp = rx_frame.data.u8[7];
}
break;
case 0x24: //Fourth datarow in PID group
if (incoming_poll_group == 1) { //24 AA 4F 00 00 77 00 14
CellVoltMin_mV = (rx_frame.data.u8[1] * 20); //(volts *50) *20 =mV
} else if (incoming_poll_group == 2) { //24 AD AD AD AD AD AD AB
cellvoltages_mv[21] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[22] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[23] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[24] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[25] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[26] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[27] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 3) { //24 AD AD AD AD AC AD AD
cellvoltages_mv[53] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[54] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[55] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[56] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[57] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[58] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[59] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 4) { //24 AD AC AC AD AC AD AD
cellvoltages_mv[85] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[86] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[87] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[88] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[89] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[90] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[91] = (rx_frame.data.u8[7] * 20);
} else if (incoming_poll_group == 5) {
batterySOH = ((rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3]);
}
break;
case 0x25: //Fifth datarow in PID group
if (incoming_poll_group == 1) { //25 5C A9 00 14 5F D3 00
} else if (incoming_poll_group == 2) { //25 AD AD AD AD 00 00 00
cellvoltages_mv[28] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[29] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[30] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[31] = (rx_frame.data.u8[4] * 20);
} else if (incoming_poll_group == 3) { //25 AD AD AD AD 00 00 00
cellvoltages_mv[60] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[61] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[62] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[63] = (rx_frame.data.u8[4] * 20);
} else if (incoming_poll_group == 4) { //25 AD AD AD AD 00 00 00
cellvoltages_mv[92] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[93] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[94] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[95] = (rx_frame.data.u8[4] * 20);
} else if (incoming_poll_group == 5) {
}
break;
case 0x26: //Sixth datarow in PID group
if (incoming_poll_group == 1) { //26 07 84 F9 00 07 42 8F
} else if (incoming_poll_group == 5) {
}
break;
case 0x27: //Seventh datarow in PID group
if (incoming_poll_group == 1) { //27 03 3F A1 EB 00 19 99
BMS_ign = rx_frame.data.u8[6];
inverterVoltageFrameHigh = rx_frame.data.u8[7];
}
break;
case 0x28: //Eighth datarow in PID group
if (incoming_poll_group == 1) { //28 7F FF 7F FF 03 E8 00
inverterVoltage = (inverterVoltageFrameHigh << 8) + rx_frame.data.u8[1];
}
break;
}
break;
default:
break;
}
}
void HyundaiIoniq28Battery::transmit_can(unsigned long currentMillis) {
if (!startedUp) {
return; // Don't send any CAN messages towards battery until it has started up
}
//Send 250ms message
if (currentMillis - previousMillis250 >= INTERVAL_250_MS) {
previousMillis250 = currentMillis;
switch (poll_group) //Swap between the 5 groups every 250ms
{
case 0:
IONIQ_7E4_POLL.data.u8[2] = 0x01;
poll_group = 1;
break;
case 1:
IONIQ_7E4_POLL.data.u8[2] = 0x02;
poll_group = 2;
break;
case 2:
IONIQ_7E4_POLL.data.u8[2] = 0x03;
poll_group = 3;
break;
case 3:
IONIQ_7E4_POLL.data.u8[2] = 0x04;
poll_group = 4;
break;
case 4:
IONIQ_7E4_POLL.data.u8[2] = 0x05;
poll_group = 0;
break;
default:
//Unhandlex exception
poll_group = 0;
break;
}
transmit_can_frame(&IONIQ_7E4_POLL);
}
//Send 100ms message
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
previousMillis100 = currentMillis;
transmit_can_frame(&IONIQ_553);
transmit_can_frame(&IONIQ_57F);
transmit_can_frame(&IONIQ_2A1);
}
// Send 10ms CAN Message
if (currentMillis - previousMillis10 >= INTERVAL_10_MS) {
previousMillis10 = currentMillis;
switch (counter_200) {
case 0:
IONIQ_200.data.u8[5] = 0x17;
++counter_200;
break;
case 1:
IONIQ_200.data.u8[5] = 0x57;
++counter_200;
break;
case 2:
IONIQ_200.data.u8[5] = 0x97;
++counter_200;
break;
case 3:
IONIQ_200.data.u8[5] = 0xD7;
++counter_200;
break;
case 4:
IONIQ_200.data.u8[3] = 0x10;
IONIQ_200.data.u8[5] = 0xFF;
++counter_200;
break;
case 5:
IONIQ_200.data.u8[5] = 0x3B;
++counter_200;
break;
case 6:
IONIQ_200.data.u8[5] = 0x7B;
++counter_200;
break;
case 7:
IONIQ_200.data.u8[5] = 0xBB;
++counter_200;
break;
case 8:
IONIQ_200.data.u8[5] = 0xFB;
counter_200 = 5;
break;
}
transmit_can_frame(&IONIQ_200);
transmit_can_frame(&IONIQ_523);
transmit_can_frame(&IONIQ_524);
}
}
void HyundaiIoniq28Battery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer_battery->info.total_capacity_Wh = 28000;
datalayer_battery->info.number_of_cells = 96;
datalayer_battery->info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
datalayer_battery->info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
datalayer_battery->info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
datalayer_battery->info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
datalayer.system.status.battery_allows_contactor_closing = true;
}
uint16_t HyundaiIoniq28Battery::get_lead_acid_voltage() const {
return leadAcidBatteryVoltage;
}
int16_t HyundaiIoniq28Battery::get_power_relay_temperature() const {
return powerRelayTemperature * 2;
}
uint8_t HyundaiIoniq28Battery::get_battery_management_mode() const {
return batteryManagementMode;
}
uint8_t HyundaiIoniq28Battery::get_battery_ignition_mode() const {
return BMS_ign;
}
uint8_t HyundaiIoniq28Battery::get_battery_relay_mode() const {
return batteryRelay;
}

View file

@ -0,0 +1,118 @@
#ifndef HYUNDAI_IONIQ_28_BATTERY_H
#define HYUNDAI_IONIQ_28_BATTERY_H
#include <Arduino.h>
#include "../datalayer/datalayer.h"
#include "../datalayer/datalayer_extended.h"
#include "CanBattery.h"
#include "HYUNDAI-IONIQ-28-BATTERY-HTML.h"
#ifdef HYUNDAI_IONIQ_28_BATTERY
#define SELECTED_BATTERY_CLASS HyundaiIoniq28Battery
#endif
class HyundaiIoniq28Battery : public CanBattery {
public:
HyundaiIoniq28Battery() : renderer(*this) {}
BatteryHtmlRenderer& get_status_renderer() { return renderer; }
virtual void setup(void);
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr const char* Name = "Hyundai Ioniq Electric 28kWh";
// Getter methods for HTML renderer
uint16_t get_lead_acid_voltage() const;
int16_t get_power_relay_temperature() const;
uint8_t get_battery_management_mode() const;
uint8_t get_battery_ignition_mode() const;
uint8_t get_battery_relay_mode() const;
private:
HyundaiIoniq28BatteryHtmlRenderer renderer;
DATALAYER_BATTERY_TYPE* datalayer_battery;
static const int MAX_PACK_VOLTAGE_DV = 4050; //5000 = 500.0V
static const int MIN_PACK_VOLTAGE_DV = 2880;
static const int MAX_CELL_DEVIATION_MV = 150;
static const int MAX_CELL_VOLTAGE_MV = 4250; //Battery is put into emergency stop if one cell goes over this value
static const int MIN_CELL_VOLTAGE_MV = 2950; //Battery is put into emergency stop if one cell goes below this value
unsigned long previousMillis250 = 0; // will store last time a 250ms CAN Message was send
unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
unsigned long previousMillis10 = 0; // will store last time a 10s CAN Message was send
uint16_t SOC_BMS = 0;
uint16_t SOC_Display = 0;
uint16_t batterySOH = 1000;
uint16_t CellVoltMax_mV = 3700;
uint16_t CellVoltMin_mV = 3700;
uint16_t allowedDischargePower = 0;
uint16_t allowedChargePower = 0;
uint16_t batteryVoltage = 3700;
uint16_t inverterVoltageFrameHigh = 0;
uint16_t inverterVoltage = 0;
uint16_t cellvoltages_mv[96];
uint16_t leadAcidBatteryVoltage = 120;
int16_t batteryAmps = 0;
int16_t temperatureMax = 0;
int16_t temperatureMin = 0;
uint8_t batteryManagementMode = 0;
uint8_t BMS_ign = 0;
uint8_t batteryRelay = 0;
uint8_t counter_200 = 0;
int8_t heatertemp = 0;
int8_t powerRelayTemperature = 0;
bool startedUp = false;
uint8_t incoming_poll_group = 0xFF;
uint8_t poll_group = 0;
CAN_frame IONIQ_200 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x200,
.data = {0x00, 0x80, 0xD8, 0x04, 0x00, 0x17, 0xD0, 0x00}};
CAN_frame IONIQ_523 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x523,
.data = {0x08, 0x38, 0x36, 0x36, 0x33, 0x34, 0x00, 0x01}};
CAN_frame IONIQ_524 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x524,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
//553 Needed frame 200ms
CAN_frame IONIQ_553 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x553,
.data = {0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00}};
//57F Needed frame 100ms
CAN_frame IONIQ_57F = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x57F,
.data = {0x80, 0x0A, 0x72, 0x00, 0x00, 0x00, 0x00, 0x72}};
//Needed frame 100ms
CAN_frame IONIQ_2A1 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x2A1,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame IONIQ_7E4_POLL = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x02, 0x21, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame IONIQ_7E4_ACK = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
};
#endif

View file

@ -48,66 +48,19 @@ void KiaHyundai64Battery::
datalayer_battery_extended->batteryManagementMode = batteryManagementMode; datalayer_battery_extended->batteryManagementMode = batteryManagementMode;
datalayer_battery_extended->BMS_ign = BMS_ign; datalayer_battery_extended->BMS_ign = BMS_ign;
datalayer_battery_extended->batteryRelay = batteryRelay; datalayer_battery_extended->batteryRelay = batteryRelay;
datalayer_battery_extended->inverterVoltage = inverterVoltage;
//Perform logging if configured to do so memcpy(datalayer_battery_extended->ecu_serial_number, ecu_serial_number, sizeof(ecu_serial_number));
#ifdef DEBUG_LOG memcpy(datalayer_battery_extended->ecu_version_number, ecu_version_number, sizeof(ecu_version_number));
logging.println(); //sepatator datalayer_battery_extended->cumulative_charge_current_ah = cumulative_charge_current_ah;
logging.println("Values from battery: "); datalayer_battery_extended->cumulative_discharge_current_ah = cumulative_discharge_current_ah;
logging.print("SOC BMS: "); datalayer_battery_extended->cumulative_energy_charged_kWh = cumulative_energy_charged_kWh;
logging.print((uint16_t)SOC_BMS / 10.0, 1); datalayer_battery_extended->cumulative_energy_discharged_kWh = cumulative_energy_discharged_kWh;
logging.print("% | SOC Display: "); datalayer_battery_extended->powered_on_total_time = powered_on_total_time;
logging.print((uint16_t)SOC_Display / 10.0, 1); datalayer_battery_extended->isolation_resistance_kOhm = isolation_resistance_kOhm;
logging.print("% | SOH "); datalayer_battery_extended->number_of_standard_charging_sessions = number_of_standard_charging_sessions;
logging.print((uint16_t)batterySOH / 10.0, 1); datalayer_battery_extended->number_of_fastcharging_sessions = number_of_fastcharging_sessions;
logging.println("%"); datalayer_battery_extended->accumulated_normal_charging_energy_kWh = accumulated_normal_charging_energy_kWh;
logging.print((int16_t)batteryAmps / 10.0, 1); datalayer_battery_extended->accumulated_fastcharging_energy_kWh = accumulated_fastcharging_energy_kWh;
logging.print(" Amps | ");
logging.print((uint16_t)batteryVoltage / 10.0, 1);
logging.print(" Volts | ");
logging.print((int16_t)datalayer_battery->status.active_power_W);
logging.println(" Watts");
logging.print("Allowed Charge ");
logging.print((uint16_t)allowedChargePower * 10);
logging.print(" W | Allowed Discharge ");
logging.print((uint16_t)allowedDischargePower * 10);
logging.println(" W");
logging.print("MaxCellVolt ");
logging.print(CellVoltMax_mV);
logging.print(" mV No ");
logging.print(CellVmaxNo);
logging.print(" | MinCellVolt ");
logging.print(CellVoltMin_mV);
logging.print(" mV No ");
logging.println(CellVminNo);
logging.print("TempHi ");
logging.print((int16_t)temperatureMax);
logging.print("°C TempLo ");
logging.print((int16_t)temperatureMin);
logging.print("°C WaterInlet ");
logging.print((int8_t)temperature_water_inlet);
logging.print("°C PowerRelay ");
logging.print((int8_t)powerRelayTemperature * 2);
logging.println("°C");
logging.print("Aux12volt: ");
logging.print((int16_t)leadAcidBatteryVoltage / 10.0, 1);
logging.println("V | ");
logging.print("BmsManagementMode ");
logging.print((uint8_t)batteryManagementMode, BIN);
if (bitRead((uint8_t)BMS_ign, 2) == 1) {
logging.print(" | BmsIgnition ON");
} else {
logging.print(" | BmsIgnition OFF");
}
if (bitRead((uint8_t)batteryRelay, 0) == 1) {
logging.print(" | PowerRelay ON");
} else {
logging.print(" | PowerRelay OFF");
}
logging.print(" | Inverter ");
logging.print(inverterVoltage);
logging.println(" Volts");
#endif
} }
void KiaHyundai64Battery::update_number_of_cells() { void KiaHyundai64Battery::update_number_of_cells() {
@ -129,6 +82,7 @@ void KiaHyundai64Battery::update_number_of_cells() {
void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) { void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (rx_frame.ID) { switch (rx_frame.ID) {
case 0x4DE: case 0x4DE:
datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE;
startedUp = true; startedUp = true;
break; break;
case 0x542: //BMS SOC case 0x542: //BMS SOC
@ -168,65 +122,118 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
case 0x5D8: case 0x5D8:
startedUp = true; startedUp = true;
//PID data is polled after last message sent from battery every other time: //PID data is polled after last message sent from battery every other time this 0x5D8 message arrives:
if (holdPidCounter == true) { if (holdPidCounter == true) {
holdPidCounter = false; holdPidCounter = false;
} else { } else {
holdPidCounter = true; holdPidCounter = true;
if (poll_data_pid >= 6) { //polling one of six PIDs at 100ms*2, resolution = 1200ms
poll_data_pid = 0;
}
poll_data_pid++; poll_data_pid++;
if (poll_data_pid == 1) { if (poll_data_pid == 1) {
transmit_can_frame(&KIA64_7E4_id1); KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_1 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_1 & 0x00FF);
} else if (poll_data_pid == 2) { } else if (poll_data_pid == 2) {
transmit_can_frame(&KIA64_7E4_id2); KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_2 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_2 & 0x00FF);
} else if (poll_data_pid == 3) { } else if (poll_data_pid == 3) {
transmit_can_frame(&KIA64_7E4_id3); KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_3 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_3 & 0x00FF);
} else if (poll_data_pid == 4) { } else if (poll_data_pid == 4) {
transmit_can_frame(&KIA64_7E4_id4); KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_4 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_4 & 0x00FF);
} else if (poll_data_pid == 5) { } else if (poll_data_pid == 5) {
transmit_can_frame(&KIA64_7E4_id5); KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_5 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_5 & 0x00FF);
} else if (poll_data_pid == 6) { } else if (poll_data_pid == 6) {
transmit_can_frame(&KIA64_7E4_id6); KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_6 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_6 & 0x00FF);
} else if (poll_data_pid == 7) {
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_GROUP_11 & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_GROUP_11 & 0x00FF);
} else if (poll_data_pid == 8) {
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_ECU_SERIAL & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_ECU_SERIAL & 0x00FF);
} else if (poll_data_pid == 9) {
KIA64_7E4_poll.data.u8[2] = (uint8_t)((POLL_ECU_VERSION & 0xFF00) >> 8);
KIA64_7E4_poll.data.u8[3] = (uint8_t)(POLL_ECU_VERSION & 0x00FF);
poll_data_pid = 0;
} }
transmit_can_frame(&KIA64_7E4_poll);
} }
break; break;
case 0x7EC: //Data From polled PID group, BigEndian case 0x7EC: //Data From polled PID group, BigEndian
switch (rx_frame.data.u8[0]) {
case 0x10: //"PID Header" if (rx_frame.data.u8[0] < 0x10) { //One line response
if (rx_frame.data.u8[4] == poll_data_pid) { pid_reply = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
transmit_can_frame(&KIA64_7E4_ack); //Send ack to BMS if the same frame is sent as polled }
if (rx_frame.data.u8[0] == 0x10) { //Multiframe response, send ACK
transmit_can_frame(&KIA64_7E4_ack);
pid_reply = (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4];
}
switch (rx_frame.data.u8[0]) { //Multiframe responses
case 0x10: //Header frame sometimes has data
if (pid_reply == POLL_ECU_SERIAL) {
ecu_serial_number[0] = rx_frame.data.u8[5];
ecu_serial_number[1] = rx_frame.data.u8[6];
ecu_serial_number[2] = rx_frame.data.u8[7];
} else if (pid_reply == POLL_ECU_VERSION) {
ecu_version_number[0] = rx_frame.data.u8[5];
ecu_version_number[1] = rx_frame.data.u8[6];
ecu_version_number[2] = rx_frame.data.u8[7];
} }
break; break;
case 0x21: //First frame in PID group case 0x21: //First frame in PID group
if (poll_data_pid == 1) { if (pid_reply == POLL_GROUP_1) {
batteryRelay = rx_frame.data.u8[7]; batteryRelay = rx_frame.data.u8[7];
} else if (poll_data_pid == 2) { } else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[0] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[0] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[1] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[1] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[2] = (rx_frame.data.u8[4] * 20); cellvoltages_mv[2] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[3] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[3] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[4] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[4] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[5] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[5] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) { } else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[32] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[32] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[33] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[33] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[34] = (rx_frame.data.u8[4] * 20); cellvoltages_mv[34] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[35] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[35] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[36] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[36] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[37] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[37] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) { } else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[64] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[64] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[65] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[65] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[66] = (rx_frame.data.u8[4] * 20); cellvoltages_mv[66] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[67] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[67] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[68] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[68] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[69] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[69] = (rx_frame.data.u8[7] * 20);
} else if (pid_reply == POLL_GROUP_11) {
number_of_standard_charging_sessions = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
} else if (pid_reply == POLL_ECU_SERIAL) {
ecu_serial_number[3] = rx_frame.data.u8[1];
ecu_serial_number[4] = rx_frame.data.u8[2];
ecu_serial_number[5] = rx_frame.data.u8[3];
ecu_serial_number[6] = rx_frame.data.u8[4];
ecu_serial_number[7] = rx_frame.data.u8[5];
ecu_serial_number[8] = rx_frame.data.u8[6];
ecu_serial_number[9] = rx_frame.data.u8[7];
} else if (pid_reply == POLL_ECU_VERSION) {
ecu_version_number[3] = rx_frame.data.u8[1];
ecu_version_number[4] = rx_frame.data.u8[2];
ecu_version_number[5] = rx_frame.data.u8[3];
ecu_version_number[6] = rx_frame.data.u8[4];
ecu_version_number[7] = rx_frame.data.u8[5];
ecu_version_number[8] = rx_frame.data.u8[6];
ecu_version_number[9] = rx_frame.data.u8[7];
} }
break; break;
case 0x22: //Second datarow in PID group case 0x22: //Second datarow in PID group
if (poll_data_pid == 2) { if (pid_reply == POLL_GROUP_1) {
//battery_max_temperature = rx_frame.data.u8[5];
//battery_min_temperature = rx_frame.data.u8[6];
//module_1_temperature = rx_frame.data.u8[7];
} else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[6] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[6] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[7] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[7] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[8] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[8] = (rx_frame.data.u8[3] * 20);
@ -234,7 +241,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[10] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[10] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[11] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[11] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[12] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[12] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) { } else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[38] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[38] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[39] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[39] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[40] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[40] = (rx_frame.data.u8[3] * 20);
@ -242,7 +249,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[42] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[42] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[43] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[43] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[44] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[44] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) { } else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[70] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[70] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[71] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[71] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[72] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[72] = (rx_frame.data.u8[3] * 20);
@ -250,15 +257,35 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[74] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[74] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[75] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[75] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[76] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[76] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 6) { } else if (pid_reply == POLL_GROUP_6) {
batteryManagementMode = rx_frame.data.u8[5]; batteryManagementMode = rx_frame.data.u8[5];
} else if (pid_reply == POLL_GROUP_11) {
number_of_fastcharging_sessions = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[2]);
accumulated_normal_charging_energy_kWh = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]);
} else if (pid_reply == POLL_ECU_SERIAL) {
ecu_serial_number[10] = rx_frame.data.u8[1];
ecu_serial_number[11] = rx_frame.data.u8[2];
ecu_serial_number[12] = rx_frame.data.u8[3];
ecu_serial_number[13] = rx_frame.data.u8[4];
ecu_serial_number[14] = rx_frame.data.u8[5];
ecu_serial_number[15] = rx_frame.data.u8[6];
} else if (pid_reply == POLL_ECU_VERSION) {
ecu_version_number[10] = rx_frame.data.u8[1];
ecu_version_number[11] = rx_frame.data.u8[2];
ecu_version_number[12] = rx_frame.data.u8[3];
ecu_version_number[13] = rx_frame.data.u8[4];
ecu_version_number[14] = rx_frame.data.u8[5];
ecu_version_number[15] = rx_frame.data.u8[6];
} }
break; break;
case 0x23: //Third datarow in PID group case 0x23: //Third datarow in PID group
if (poll_data_pid == 1) { if (pid_reply == POLL_GROUP_1) {
//module_2_temperature = rx_frame.data.u8[1];
//module_3_temperature = rx_frame.data.u8[2];
//module_4_temperature = rx_frame.data.u8[3];
temperature_water_inlet = rx_frame.data.u8[6]; temperature_water_inlet = rx_frame.data.u8[6];
CellVoltMax_mV = (rx_frame.data.u8[7] * 20); //(volts *50) *20 =mV CellVoltMax_mV = (rx_frame.data.u8[7] * 20); //(volts *50) *20 =mV
} else if (poll_data_pid == 2) { } else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[13] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[13] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[14] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[14] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[15] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[15] = (rx_frame.data.u8[3] * 20);
@ -266,7 +293,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[17] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[17] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[18] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[18] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[19] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[19] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) { } else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[45] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[45] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[46] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[46] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[47] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[47] = (rx_frame.data.u8[3] * 20);
@ -274,7 +301,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[49] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[49] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[50] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[50] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[51] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[51] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) { } else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[77] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[77] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[78] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[78] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[79] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[79] = (rx_frame.data.u8[3] * 20);
@ -282,16 +309,18 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[81] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[81] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[82] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[82] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[83] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[83] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 5) { } else if (pid_reply == POLL_GROUP_5) {
heatertemp = rx_frame.data.u8[7]; heatertemp = rx_frame.data.u8[7];
} else if (pid_reply == POLL_GROUP_11) {
accumulated_fastcharging_energy_kWh = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
} }
break; break;
case 0x24: //Fourth datarow in PID group case 0x24: //Fourth datarow in PID group
if (poll_data_pid == 1) { if (pid_reply == POLL_GROUP_1) {
CellVmaxNo = rx_frame.data.u8[1]; CellVmaxNo = rx_frame.data.u8[1];
CellVminNo = rx_frame.data.u8[3]; CellVminNo = rx_frame.data.u8[3];
CellVoltMin_mV = (rx_frame.data.u8[2] * 20); //(volts *50) *20 =mV CellVoltMin_mV = (rx_frame.data.u8[2] * 20); //(volts *50) *20 =mV
} else if (poll_data_pid == 2) { } else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[20] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[20] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[21] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[21] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[22] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[22] = (rx_frame.data.u8[3] * 20);
@ -299,7 +328,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[24] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[24] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[25] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[25] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[26] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[26] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) { } else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[52] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[52] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[53] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[53] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[54] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[54] = (rx_frame.data.u8[3] * 20);
@ -307,7 +336,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
cellvoltages_mv[56] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[56] = (rx_frame.data.u8[5] * 20);
cellvoltages_mv[57] = (rx_frame.data.u8[6] * 20); cellvoltages_mv[57] = (rx_frame.data.u8[6] * 20);
cellvoltages_mv[58] = (rx_frame.data.u8[7] * 20); cellvoltages_mv[58] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) { } else if (pid_reply == POLL_GROUP_4) {
cellvoltages_mv[84] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[84] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[85] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[85] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[86] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[86] = (rx_frame.data.u8[3] * 20);
@ -317,24 +346,29 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
if (rx_frame.data.u8[7] > 4) { // Data only valid on 98S if (rx_frame.data.u8[7] > 4) { // Data only valid on 98S
cellvoltages_mv[90] = (rx_frame.data.u8[7] * 20); // Perform extra checks cellvoltages_mv[90] = (rx_frame.data.u8[7] * 20); // Perform extra checks
} }
} else if (poll_data_pid == 5) { } else if (pid_reply == POLL_GROUP_5) {
batterySOH = ((rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3]); batterySOH = ((rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3]);
} }
break; break;
case 0x25: //Fifth datarow in PID group case 0x25: //Fifth datarow in PID group
if (poll_data_pid == 2) { if (pid_reply == POLL_GROUP_1) {
cumulative_charge_current_ah =
((rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
cumulative_discharge_current_ah =
((rx_frame.data.u8[5] << 16) | (rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
} else if (pid_reply == POLL_GROUP_2) {
cellvoltages_mv[27] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[27] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[28] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[28] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[29] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[29] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[30] = (rx_frame.data.u8[4] * 20); cellvoltages_mv[30] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[31] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[31] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 3) { } else if (pid_reply == POLL_GROUP_3) {
cellvoltages_mv[59] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[59] = (rx_frame.data.u8[1] * 20);
cellvoltages_mv[60] = (rx_frame.data.u8[2] * 20); cellvoltages_mv[60] = (rx_frame.data.u8[2] * 20);
cellvoltages_mv[61] = (rx_frame.data.u8[3] * 20); cellvoltages_mv[61] = (rx_frame.data.u8[3] * 20);
cellvoltages_mv[62] = (rx_frame.data.u8[4] * 20); cellvoltages_mv[62] = (rx_frame.data.u8[4] * 20);
cellvoltages_mv[63] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[63] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 4) { // Data only valid on 98S } else if (pid_reply == POLL_GROUP_4) { // Data only valid on 98S
if (rx_frame.data.u8[1] > 4) { // Perform extra checks if (rx_frame.data.u8[1] > 4) { // Perform extra checks
cellvoltages_mv[91] = (rx_frame.data.u8[1] * 20); cellvoltages_mv[91] = (rx_frame.data.u8[1] * 20);
} }
@ -350,7 +384,7 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
if (rx_frame.data.u8[5] > 4) { // Perform extra checks if (rx_frame.data.u8[5] > 4) { // Perform extra checks
cellvoltages_mv[95] = (rx_frame.data.u8[5] * 20); cellvoltages_mv[95] = (rx_frame.data.u8[5] * 20);
} }
} else if (poll_data_pid == 5) { // Data only valid on 98S } else if (pid_reply == POLL_GROUP_5) { // Data only valid on 98S
if (rx_frame.data.u8[4] > 4) { // Perform extra checks if (rx_frame.data.u8[4] > 4) { // Perform extra checks
cellvoltages_mv[96] = (rx_frame.data.u8[4] * 20); cellvoltages_mv[96] = (rx_frame.data.u8[4] * 20);
} }
@ -360,7 +394,11 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
} }
break; break;
case 0x26: //Sixth datarow in PID group case 0x26: //Sixth datarow in PID group
if (poll_data_pid == 5) { if (pid_reply == POLL_GROUP_1) {
cumulative_energy_charged_kWh =
((rx_frame.data.u8[2] << 16) | (rx_frame.data.u8[3] << 8) | rx_frame.data.u8[4]);
cumulative_energy_discharged_HIGH_BYTE = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
} else if (pid_reply == POLL_GROUP_5) {
//We have read all cells, check that content is valid: //We have read all cells, check that content is valid:
for (uint8_t i = 85; i < 97; ++i) { for (uint8_t i = 85; i < 97; ++i) {
if (cellvoltages_mv[i] < 300) { // Zero the value if it's below 300 if (cellvoltages_mv[i] < 300) { // Zero the value if it's below 300
@ -374,17 +412,24 @@ void KiaHyundai64Battery::handle_incoming_can_frame(CAN_frame rx_frame) {
} }
break; break;
case 0x27: //Seventh datarow in PID group case 0x27: //Seventh datarow in PID group
if (poll_data_pid == 1) { if (pid_reply == POLL_GROUP_1) {
cumulative_energy_discharged_kWh = ((cumulative_energy_discharged_HIGH_BYTE << 8) | rx_frame.data.u8[1]);
powered_on_total_time = ((rx_frame.data.u8[2] << 24) | (rx_frame.data.u8[3] << 16) |
(rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
BMS_ign = rx_frame.data.u8[6]; BMS_ign = rx_frame.data.u8[6];
inverterVoltageFrameHigh = rx_frame.data.u8[7]; inverterVoltageFrameHigh = rx_frame.data.u8[7];
} }
break; break;
case 0x28: //Eighth datarow in PID group case 0x28: //Eighth datarow in PID group
if (poll_data_pid == 1) { if (pid_reply == POLL_GROUP_1) {
inverterVoltage = (inverterVoltageFrameHigh << 8) + rx_frame.data.u8[1]; inverterVoltage = (inverterVoltageFrameHigh << 8) + rx_frame.data.u8[1];
isolation_resistance_kOhm = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
} }
break; break;
default:
break;
} }
break; break;
default: default:
break; break;
@ -401,7 +446,8 @@ void KiaHyundai64Battery::transmit_can(unsigned long currentMillis) {
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) { if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
previousMillis100 = currentMillis; previousMillis100 = currentMillis;
if (contactor_closing_allowed == nullptr || *contactor_closing_allowed) { if ((contactor_closing_allowed == nullptr || *contactor_closing_allowed) &&
datalayer.system.status.inverter_allows_contactor_closing) {
transmit_can_frame(&KIA64_553); transmit_can_frame(&KIA64_553);
transmit_can_frame(&KIA64_57F); transmit_can_frame(&KIA64_57F);
transmit_can_frame(&KIA64_2A1); transmit_can_frame(&KIA64_2A1);
@ -412,7 +458,8 @@ void KiaHyundai64Battery::transmit_can(unsigned long currentMillis) {
if (currentMillis - previousMillis10 >= INTERVAL_10_MS) { if (currentMillis - previousMillis10 >= INTERVAL_10_MS) {
previousMillis10 = currentMillis; previousMillis10 = currentMillis;
if (contactor_closing_allowed == nullptr || *contactor_closing_allowed) { if ((contactor_closing_allowed == nullptr || *contactor_closing_allowed) &&
datalayer.system.status.inverter_allows_contactor_closing) {
switch (counter_200) { switch (counter_200) {
case 0: case 0:

View file

@ -78,7 +78,8 @@ class KiaHyundai64Battery : public CanBattery {
int16_t batteryAmps = 0; int16_t batteryAmps = 0;
int16_t temperatureMax = 0; int16_t temperatureMax = 0;
int16_t temperatureMin = 0; int16_t temperatureMin = 0;
int16_t poll_data_pid = 0; uint8_t poll_data_pid = 0;
uint16_t pid_reply = 0;
bool holdPidCounter = false; bool holdPidCounter = false;
uint8_t CellVmaxNo = 0; uint8_t CellVmaxNo = 0;
uint8_t CellVminNo = 0; uint8_t CellVminNo = 0;
@ -91,6 +92,19 @@ class KiaHyundai64Battery : public CanBattery {
int8_t heatertemp = 0; int8_t heatertemp = 0;
int8_t powerRelayTemperature = 0; int8_t powerRelayTemperature = 0;
bool startedUp = false; bool startedUp = false;
uint8_t ecu_serial_number[16] = {0};
uint8_t ecu_version_number[16] = {0};
uint32_t cumulative_charge_current_ah = 0;
uint32_t cumulative_discharge_current_ah = 0;
uint32_t cumulative_energy_charged_kWh = 0;
uint16_t cumulative_energy_discharged_HIGH_BYTE = 0;
uint32_t cumulative_energy_discharged_kWh = 0;
uint32_t powered_on_total_time = 0;
uint16_t isolation_resistance_kOhm = 0;
uint16_t number_of_standard_charging_sessions = 0;
uint16_t number_of_fastcharging_sessions = 0;
uint16_t accumulated_normal_charging_energy_kWh = 0;
uint16_t accumulated_fastcharging_energy_kWh = 0;
CAN_frame KIA_HYUNDAI_200 = {.FD = false, CAN_frame KIA_HYUNDAI_200 = {.FD = false,
.ext_ID = false, .ext_ID = false,
@ -125,42 +139,26 @@ class KiaHyundai64Battery : public CanBattery {
.DLC = 8, .DLC = 8,
.ID = 0x2A1, .ID = 0x2A1,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame KIA64_7E4_id1 = {.FD = false, CAN_frame KIA64_7E4_poll = {.FD = false,
.ext_ID = false, .ext_ID = false,
.DLC = 8, .DLC = 8,
.ID = 0x7E4, .ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 01 .data = {0x03, 0x22, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00}};
CAN_frame KIA64_7E4_id2 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 02
CAN_frame KIA64_7E4_id3 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 03
CAN_frame KIA64_7E4_id4 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 04
CAN_frame KIA64_7E4_id5 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x05, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 05
CAN_frame KIA64_7E4_id6 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x7E4,
.data = {0x03, 0x22, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00}}; //Poll PID 03 22 01 06
CAN_frame KIA64_7E4_ack = { CAN_frame KIA64_7E4_ack = {
.FD = false, .FD = false,
.ext_ID = false, .ext_ID = false,
.DLC = 8, .DLC = 8,
.ID = 0x7E4, .ID = 0x7E4,
.data = {0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Ack frame, correct PID is returned .data = {0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Ack frame, correct PID is returned
static const int POLL_GROUP_1 = 0x0101;
static const int POLL_GROUP_2 = 0x0102;
static const int POLL_GROUP_3 = 0x0103;
static const int POLL_GROUP_4 = 0x0104;
static const int POLL_GROUP_5 = 0x0105;
static const int POLL_GROUP_6 = 0x0106;
static const int POLL_GROUP_11 = 0x0111;
static const int POLL_ECU_SERIAL = 0xF18C;
static const int POLL_ECU_VERSION = 0xF191;
}; };
#endif #endif

View file

@ -11,16 +11,45 @@ class KiaHyundai64HtmlRenderer : public BatteryHtmlRenderer {
String get_status_html() { String get_status_html() {
String content; String content;
auto print_hyundai = [&content](DATALAYER_INFO_KIAHYUNDAI64& data) { auto print_hyundai = [&content](DATALAYER_INFO_KIAHYUNDAI64& data) {
content += "<h4>Cells: " + String(data.total_cell_count) + "S</h4>"; char readableSerialNumber[17]; // One extra space for null terminator
content += "<h4>12V voltage: " + String(data.battery_12V / 10.0, 1) + "</h4>"; memcpy(readableSerialNumber, data.ecu_serial_number, sizeof(data.ecu_serial_number));
content += "<h4>Waterleakage: " + String(data.waterleakageSensor) + "</h4>"; readableSerialNumber[16] = '\0'; // Null terminate the string
content += "<h4>Temperature, water inlet: " + String(data.temperature_water_inlet) + "</h4>"; char readableVersionNumber[17]; // One extra space for null terminator
content += "<h4>Temperature, power relay: " + String(data.powerRelayTemperature) + "</h4>"; memcpy(readableVersionNumber, data.ecu_version_number, sizeof(data.ecu_version_number));
readableVersionNumber[16] = '\0'; // Null terminate the string
content += "<h4>BMS serial number: " + String(readableSerialNumber) + "</h4>";
content += "<h4>BMS software version: " + String(readableVersionNumber) + "</h4>";
content += "<h4>Cells: " + String(data.total_cell_count) + " S</h4>";
content += "<h4>12V voltage: " + String(data.battery_12V / 10.0, 1) + " V</h4>";
content += "<h4>Waterleakage: ";
if (data.waterleakageSensor == 0) {
content += " LEAK DETECTED</h4>";
} else if (data.waterleakageSensor == 164) {
content += " No leakage</h4>";
} else {
content += String(data.waterleakageSensor) + "</h4>";
}
content += "<h4>Temperature, water inlet: " + String(data.temperature_water_inlet) + " &deg;C</h4>";
content += "<h4>Temperature, power relay: " + String(data.powerRelayTemperature) + " &deg;C</h4>";
content += "<h4>Batterymanagement mode: " + String(data.batteryManagementMode) + "</h4>"; content += "<h4>Batterymanagement mode: " + String(data.batteryManagementMode) + "</h4>";
content += "<h4>BMS ignition: " + String(data.BMS_ign) + "</h4>"; content += "<h4>BMS ignition: " + String(data.BMS_ign) + "</h4>";
content += "<h4>Battery relay: " + String(data.batteryRelay) + "</h4>"; content += "<h4>Battery relay: " + String(data.batteryRelay) + "</h4>";
content += "<h4>Inverter voltage: " + String(data.inverterVoltage) + " V</h4>";
content += "<h4>Isolation resistance: " + String(data.isolation_resistance_kOhm) + " kOhm</h4>";
content += "<h4>Power on total time: " + String(data.powered_on_total_time) + " s</h4>";
content += "<h4>Fastcharging sessions: " + String(data.number_of_fastcharging_sessions) + " x</h4>";
content += "<h4>Slowcharging sessions: " + String(data.number_of_standard_charging_sessions) + " x</h4>";
content +=
"<h4>Normal charged energy amount: " + String(data.accumulated_normal_charging_energy_kWh) + " kWh</h4>";
content += "<h4>Fastcharged energy amount: " + String(data.accumulated_fastcharging_energy_kWh) + " kWh</h4>";
content += "<h4>Total amount charged energy: " + String(data.cumulative_energy_charged_kWh / 10.0) + " kWh</h4>";
content +=
"<h4>Total amount discharged energy: " + String(data.cumulative_energy_discharged_kWh / 10.0) + " kWh</h4>";
content += "<h4>Cumulative charge current: " + String(data.cumulative_charge_current_ah / 10.0) + " Ah</h4>";
content +=
"<h4>Cumulative discharge current: " + String(data.cumulative_discharge_current_ah / 10.0) + " Ah</h4>";
}; };
print_hyundai(*kia_datalayer); print_hyundai(*kia_datalayer);

View file

@ -0,0 +1,102 @@
#include "SAMSUNG-SDI-LV-BATTERY.h"
#include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
/*
- Baud rate: 500kbps
- Format: CAN2.0A 11 bit identifier
- Data Length: 8byte
- CAN data is transmitted with encoding in little endian low byte first unless stated otherwise.
- Broadcasting period: 500ms
TODO: Implement the error bit handling for easier visualization if the battery stops operating
- alarms_frame0
- alarms_frame1
- protection_frame2
- protection_frame3
*/
void SamsungSdiLVBattery::update_values() {
datalayer.battery.status.real_soc = system_SOC * 100;
datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>(
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
datalayer.battery.status.soh_pptt = system_SOH * 100;
datalayer.battery.status.voltage_dV = system_voltage / 10;
datalayer.battery.status.current_dA = system_current * 10;
datalayer.battery.status.max_charge_power_W = system_voltage * charge_current_limit;
datalayer.battery.status.max_discharge_power_W = system_voltage * discharge_current_limit;
datalayer.battery.status.temperature_min_dC = (int16_t)(minimum_cell_temperature * 10);
datalayer.battery.status.temperature_max_dC = (int16_t)(maximum_cell_temperature * 10);
datalayer.battery.status.cell_max_voltage_mV = maximum_cell_voltage;
datalayer.battery.status.cell_min_voltage_mV = minimum_cell_voltage;
datalayer.battery.info.max_design_voltage_dV = battery_charge_voltage;
datalayer.battery.info.min_design_voltage_dV = battery_discharge_voltage;
}
void SamsungSdiLVBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x500: //Voltage, current, SOC, SOH
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
system_voltage = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
system_current = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
system_SOC = rx_frame.data.u8[4];
system_SOH = rx_frame.data.u8[5];
break;
case 0x501:
alarms_frame0 = rx_frame.data.u8[0];
alarms_frame1 = rx_frame.data.u8[1];
protection_frame2 = rx_frame.data.u8[2];
protection_frame3 = rx_frame.data.u8[3];
break;
case 0x502:
battery_charge_voltage = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
charge_current_limit = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
discharge_current_limit = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
battery_discharge_voltage = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
break;
case 0x503:
maximum_cell_voltage = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
minimum_cell_voltage = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
break;
case 0x504:
maximum_cell_temperature = rx_frame.data.u8[5];
minimum_cell_temperature = rx_frame.data.u8[6];
break;
case 0x505:
system_permanent_failure_status_dry_contact = rx_frame.data.u8[2];
system_permanent_failure_status_fuse_open = rx_frame.data.u8[3];
break;
default:
break;
}
}
void SamsungSdiLVBattery::transmit_can(unsigned long currentMillis) {
//No periodic sending required
}
void SamsungSdiLVBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.system.status.battery_allows_contactor_closing = true;
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
datalayer.battery.info.total_capacity_Wh = 5000;
}

View file

@ -0,0 +1,46 @@
#ifndef SAMSUNG_SDI_LV_BATTERY_H
#define SAMSUNG_SDI_LV_BATTERY_H
#include <Arduino.h>
#include "../datalayer/datalayer.h"
#include "CanBattery.h"
#ifdef SAMSUNG_SDI_LV_BATTERY
#define SELECTED_BATTERY_CLASS SamsungSdiLVBattery
#endif
class SamsungSdiLVBattery : public CanBattery {
public:
virtual void setup(void);
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr const char* Name = "Samsung SDI LV Battery";
private:
static const int MAX_PACK_VOLTAGE_DV = 600; //5000 = 500.0V
static const int MIN_PACK_VOLTAGE_DV = 300;
static const int MAX_CELL_DEVIATION_MV = 250;
static const int MAX_CELL_VOLTAGE_MV = 4200;
static const int MIN_CELL_VOLTAGE_MV = 3000;
uint16_t system_voltage = 50000;
int16_t system_current = 0;
uint8_t system_SOC = 50;
uint8_t system_SOH = 99;
uint16_t battery_charge_voltage = 9999;
uint16_t charge_current_limit = 0;
uint16_t discharge_current_limit = 0;
uint16_t battery_discharge_voltage = 0;
uint8_t alarms_frame0 = 0;
uint8_t alarms_frame1 = 0;
uint8_t protection_frame2 = 0;
uint8_t protection_frame3 = 0;
uint16_t maximum_cell_voltage = 3700;
uint16_t minimum_cell_voltage = 3700;
int8_t maximum_cell_temperature = 0;
int8_t minimum_cell_temperature = 0;
uint8_t system_permanent_failure_status_dry_contact = 0;
uint8_t system_permanent_failure_status_fuse_open = 0;
};
#endif

View file

@ -75,7 +75,6 @@ unsigned long currentTime = 0;
unsigned long lastPowerRemovalTime = 0; unsigned long lastPowerRemovalTime = 0;
unsigned long bmsPowerOnTime = 0; unsigned long bmsPowerOnTime = 0;
const unsigned long powerRemovalInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds const unsigned long powerRemovalInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const unsigned long powerRemovalDuration = 30000; // 30 seconds in milliseconds
const unsigned long bmsWarmupDuration = 3000; const unsigned long bmsWarmupDuration = 3000;
void set(uint8_t pin, bool direction, uint32_t pwm_freq = 0xFFFF) { void set(uint8_t pin, bool direction, uint32_t pwm_freq = 0xFFFF) {
@ -306,8 +305,9 @@ void handle_BMSpower() {
} }
} }
// If power has been removed for 30 seconds, restore the power // If power has been removed for user configured interval (1-59 seconds), restore the power
if (datalayer.system.status.BMS_reset_in_progress && currentTime - lastPowerRemovalTime >= powerRemovalDuration) { if (datalayer.system.status.BMS_reset_in_progress &&
currentTime - lastPowerRemovalTime >= datalayer.battery.settings.user_set_bms_reset_duration_ms) {
// Reapply power to the BMS // Reapply power to the BMS
digitalWrite(bms_power_pin, HIGH); digitalWrite(bms_power_pin, HIGH);
bmsPowerOnTime = currentTime; bmsPowerOnTime = currentTime;

View file

@ -79,6 +79,10 @@ void init_stored_settings() {
if (temp < 16) { if (temp < 16) {
datalayer.battery.settings.sofar_user_specified_battery_id = temp; datalayer.battery.settings.sofar_user_specified_battery_id = temp;
} }
temp = settings.getUInt("BMSRESETDUR", false);
if (temp != 0) {
datalayer.battery.settings.user_set_bms_reset_duration_ms = temp;
}
#ifdef COMMON_IMAGE #ifdef COMMON_IMAGE
user_selected_battery_type = (BatteryType)settings.getUInt("BATTTYPE", (int)BatteryType::None); user_selected_battery_type = (BatteryType)settings.getUInt("BATTTYPE", (int)BatteryType::None);
@ -187,6 +191,9 @@ void store_settings() {
if (!settings.putUInt("SOFAR_ID", datalayer.battery.settings.sofar_user_specified_battery_id)) { if (!settings.putUInt("SOFAR_ID", datalayer.battery.settings.sofar_user_specified_battery_id)) {
set_event(EVENT_PERSISTENT_SAVE_INFO, 12); set_event(EVENT_PERSISTENT_SAVE_INFO, 12);
} }
if (!settings.putUInt("BMSRESETDUR", datalayer.battery.settings.sofar_user_specified_battery_id)) {
set_event(EVENT_PERSISTENT_SAVE_INFO, 13);
}
settings.end(); // Close preferences handle settings.end(); // Close preferences handle
} }

View file

@ -138,6 +138,9 @@ struct DATALAYER_BATTERY_SETTINGS_TYPE {
/** The user specified maximum allowed discharge voltage, in deciVolt. 3000 = 300.0 V */ /** The user specified maximum allowed discharge voltage, in deciVolt. 3000 = 300.0 V */
uint16_t max_user_set_discharge_voltage_dV = BATTERY_MAX_DISCHARGE_VOLTAGE; uint16_t max_user_set_discharge_voltage_dV = BATTERY_MAX_DISCHARGE_VOLTAGE;
/** The user specified BMS reset period. Keeps track on how many milliseconds should we keep power off during daily BMS reset */
uint16_t user_set_bms_reset_duration_ms = 30000;
/** Parameters for keeping track of the limiting factor in the system */ /** Parameters for keeping track of the limiting factor in the system */
bool user_settings_limit_discharge = false; bool user_settings_limit_discharge = false;
bool user_settings_limit_charge = false; bool user_settings_limit_charge = false;

View file

@ -310,6 +310,7 @@ struct DATALAYER_INFO_ECMP {
uint32_t pid_time_spent_over_55c = 0; uint32_t pid_time_spent_over_55c = 0;
uint32_t pid_contactor_closing_counter = 0; uint32_t pid_contactor_closing_counter = 0;
uint32_t pid_date_of_manufacture = 0; uint32_t pid_date_of_manufacture = 0;
uint16_t pid_SOH_cell_1 = 0;
}; };
struct DATALAYER_INFO_GEELY_GEOMETRY_C { struct DATALAYER_INFO_GEELY_GEOMETRY_C {
@ -348,6 +349,19 @@ struct DATALAYER_INFO_KIAHYUNDAI64 {
uint8_t batteryManagementMode = 0; uint8_t batteryManagementMode = 0;
uint8_t BMS_ign = 0; uint8_t BMS_ign = 0;
uint8_t batteryRelay = 0; uint8_t batteryRelay = 0;
uint16_t inverterVoltage = 0;
uint8_t ecu_serial_number[16] = {0};
uint8_t ecu_version_number[16] = {0};
uint32_t cumulative_charge_current_ah = 0;
uint32_t cumulative_discharge_current_ah = 0;
uint32_t cumulative_energy_charged_kWh = 0;
uint32_t cumulative_energy_discharged_kWh = 0;
uint32_t powered_on_total_time = 0;
uint16_t isolation_resistance_kOhm = 0;
uint16_t number_of_standard_charging_sessions = 0;
uint16_t number_of_fastcharging_sessions = 0;
uint16_t accumulated_normal_charging_energy_kWh = 0;
uint16_t accumulated_fastcharging_energy_kWh = 0;
}; };
struct DATALAYER_INFO_TESLA { struct DATALAYER_INFO_TESLA {
@ -747,7 +761,7 @@ struct DATALAYER_INFO_VOLVO_POLESTAR {
uint16_t BECMsupplyVoltage = 0; uint16_t BECMsupplyVoltage = 0;
uint16_t BECMBatteryVoltage = 0; uint16_t BECMBatteryVoltage = 0;
uint16_t BECMBatteryCurrent = 0; int16_t BECMBatteryCurrent = 0;
uint16_t BECMUDynMaxLim = 0; uint16_t BECMUDynMaxLim = 0;
uint16_t BECMUDynMinLim = 0; uint16_t BECMUDynMinLim = 0;

View file

@ -59,16 +59,31 @@ class Esp32Hal {
return alloc_pins(name, pins[Is]...); return alloc_pins(name, pins[Is]...);
} }
template <typename... Pins> // Base case: no more pins
bool alloc_pins_ignore_unused(const char* name, Pins... pins) { inline bool alloc_pins_ignore_unused_impl(const char* name) {
std::vector<gpio_num_t> valid_pins; return alloc_pins(name); // Call with 0 pins
for (gpio_num_t pin : std::vector<gpio_num_t>{static_cast<gpio_num_t>(pins)...}) { }
if (pin != GPIO_NUM_NC) {
valid_pins.push_back(pin); // Recursive case: process one pin at a time
template <typename... Rest>
bool alloc_pins_ignore_unused_impl(const char* name, gpio_num_t first, Rest... rest) {
if (first == GPIO_NUM_NC) {
return alloc_pins_ignore_unused_impl(name, rest...);
} else {
return call_alloc_pins_filtered(name, first, rest...);
} }
} }
return alloc_pins_from_vector(name, valid_pins, std::make_index_sequence<sizeof...(pins)>{}); // This helper just forwards pins after filtering is done
template <typename... Pins>
bool call_alloc_pins_filtered(const char* name, Pins... pins) {
return alloc_pins(name, pins...);
}
// Entry point
template <typename... Pins>
bool alloc_pins_ignore_unused(const char* name, Pins... pins) {
return alloc_pins_ignore_unused_impl(name, static_cast<gpio_num_t>(pins)...);
} }
virtual bool always_enable_bms_power() { return false; } virtual bool always_enable_bms_power() { return false; }

View file

@ -130,7 +130,8 @@ void update_machineryprotection() {
// Battery is fully charged. Dont allow any more power into it // Battery is fully charged. Dont allow any more power into it
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety // Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.reported_soc == 10000) //Scaled SOC% value is 100.00% if (datalayer.battery.status.reported_soc == 10000 ||
datalayer.battery.status.real_soc == 10000) //Either Scaled OR Real SOC% value is 100.00%
{ {
if (!battery_full_event_fired) { if (!battery_full_event_fired) {
set_event(EVENT_BATTERY_FULL, 0); set_event(EVENT_BATTERY_FULL, 0);
@ -145,7 +146,8 @@ void update_machineryprotection() {
// Battery is empty. Do not allow further discharge. // Battery is empty. Do not allow further discharge.
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety // Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.bms_status == ACTIVE) { if (datalayer.battery.status.bms_status == ACTIVE) {
if (datalayer.battery.status.reported_soc == 0) { //Scaled SOC% value is 0.00% if (datalayer.battery.status.reported_soc == 0 ||
datalayer.battery.status.real_soc == 0) { //Either Scaled OR Real SOC% value is 0.00%, time to stop
if (!battery_empty_event_fired) { if (!battery_empty_event_fired) {
set_event(EVENT_BATTERY_EMPTY, 0); set_event(EVENT_BATTERY_EMPTY, 0);
battery_empty_event_fired = true; battery_empty_event_fired = true;

View file

@ -41,6 +41,7 @@ void init_events(void) {
events.entries[EVENT_CAN_CHARGER_MISSING].level = EVENT_LEVEL_INFO; events.entries[EVENT_CAN_CHARGER_MISSING].level = EVENT_LEVEL_INFO;
events.entries[EVENT_CAN_INVERTER_MISSING].level = EVENT_LEVEL_WARNING; events.entries[EVENT_CAN_INVERTER_MISSING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CONTACTOR_WELDED].level = EVENT_LEVEL_WARNING; events.entries[EVENT_CONTACTOR_WELDED].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CONTACTOR_OPEN].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CPU_OVERHEATING].level = EVENT_LEVEL_WARNING; events.entries[EVENT_CPU_OVERHEATING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CPU_OVERHEATED].level = EVENT_LEVEL_ERROR; events.entries[EVENT_CPU_OVERHEATED].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_WATER_INGRESS].level = EVENT_LEVEL_ERROR; events.entries[EVENT_WATER_INGRESS].level = EVENT_LEVEL_ERROR;
@ -192,6 +193,8 @@ String get_event_message_string(EVENTS_ENUM_TYPE event) {
return "Inverter not sending messages via CAN for the last 60 seconds. Check wiring!"; return "Inverter not sending messages via CAN for the last 60 seconds. Check wiring!";
case EVENT_CONTACTOR_WELDED: case EVENT_CONTACTOR_WELDED:
return "Contactors sticking/welded. Inspect battery with caution!"; return "Contactors sticking/welded. Inspect battery with caution!";
case EVENT_CONTACTOR_OPEN:
return "Battery decided to open contactors. Inspect battery!";
case EVENT_CPU_OVERHEATING: case EVENT_CPU_OVERHEATING:
return "Battery-Emulator CPU overheating! Increase airflow/cooling to increase hardware lifespan!"; return "Battery-Emulator CPU overheating! Increase airflow/cooling to increase hardware lifespan!";
case EVENT_CPU_OVERHEATED: case EVENT_CPU_OVERHEATED:

View file

@ -22,6 +22,7 @@
XX(EVENT_CAN_NATIVE_TX_FAILURE) \ XX(EVENT_CAN_NATIVE_TX_FAILURE) \
XX(EVENT_CHARGE_LIMIT_EXCEEDED) \ XX(EVENT_CHARGE_LIMIT_EXCEEDED) \
XX(EVENT_CONTACTOR_WELDED) \ XX(EVENT_CONTACTOR_WELDED) \
XX(EVENT_CONTACTOR_OPEN) \
XX(EVENT_CPU_OVERHEATING) \ XX(EVENT_CPU_OVERHEATING) \
XX(EVENT_CPU_OVERHEATED) \ XX(EVENT_CPU_OVERHEATED) \
XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \ XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \

View file

@ -3,6 +3,7 @@
#include <Print.h> #include <Print.h>
#include <inttypes.h> #include <inttypes.h>
#include "../../../USER_SETTINGS.h"
#include "types.h" #include "types.h"
class Logging : public Print { class Logging : public Print {

View file

@ -4,8 +4,45 @@
#include "index_html.h" #include "index_html.h"
#if defined(DEBUG_VIA_WEB) || defined(LOG_TO_SD) #if defined(DEBUG_VIA_WEB) || defined(LOG_TO_SD)
char* strnchr(const char* s, int c, size_t n) {
// Like strchr, but only searches the first 'n' bytes of the string.
if (s == NULL) {
return NULL;
}
// Iterate through the string up to 'n' characters or until a null terminator is found.
for (size_t i = 0; i < n && s[i] != '\0'; ++i) {
if (s[i] == c) {
// Character found, return a pointer to it.
return (char*)&s[i];
}
}
// If the character to be found is the null terminator, and we haven't exceeded
// 'n' bytes, check if the null terminator is at the current position.
if (c == '\0') {
for (size_t i = 0; i < n; ++i) {
if (s[i] == '\0') {
return (char*)&s[i];
}
}
}
// Character not found within the first 'n' bytes.
return NULL;
}
String debug_logger_processor(void) { String debug_logger_processor(void) {
String content = String(index_html_header); String content = String();
// Reserve enough space for the content to avoid reallocations.
if (!content.reserve(1000 + sizeof(datalayer.system.info.logged_can_messages))) {
if (content.reserve(15)) {
content += "Out of memory.";
}
return content;
}
content += index_html_header;
// Page format // Page format
content += "<style>"; content += "<style>";
content += "body { background-color: black; color: white; font-family: Arial, sans-serif; }"; content += "body { background-color: black; color: white; font-family: Arial, sans-serif; }";
@ -28,7 +65,34 @@ String debug_logger_processor(void) {
// Start a new block for the debug log messages // Start a new block for the debug log messages
content += "<PRE style='text-align: left'>"; content += "<PRE style='text-align: left'>";
content += String(datalayer.system.info.logged_can_messages); size_t offset = datalayer.system.info.logged_can_messages_offset;
// If we're mid-buffer, print the older part first.
if (offset > 0 && offset < (sizeof(datalayer.system.info.logged_can_messages) - 1)) {
// Find the next newline after the current offset. The offset will always be
// before the penultimate character, so (offset + 1) will be the final '\0'
// or earlier.
char* next_newline = strnchr(&datalayer.system.info.logged_can_messages[offset + 1], '\n',
sizeof(datalayer.system.info.logged_can_messages) - offset - 1);
if (next_newline != NULL) {
// We found a newline, so append from the character after that. We check
// the string length to ensure we don't add any intermediate '\0'
// characters.
content.concat(next_newline + 1,
strnlen(next_newline + 1, sizeof(datalayer.system.info.logged_can_messages) - offset - 2));
} else {
// No newline found, so append from the next character after the offset to
// the end of the buffer. We check the string length to ensure we don't
// add any intermediate '\0' characters.
content.concat(&datalayer.system.info.logged_can_messages[offset + 1],
strnlen(&datalayer.system.info.logged_can_messages[offset + 1],
sizeof(datalayer.system.info.logged_can_messages) - offset - 1));
}
}
// Append the first part of the buffer up to the current write offset (which
// points to the first \0).
content.concat(datalayer.system.info.logged_can_messages, offset);
content += "</PRE>"; content += "</PRE>";
// Add JavaScript for navigation // Add JavaScript for navigation

View file

@ -402,6 +402,10 @@ String settings_processor(const String& var, BatteryEmulatorSettingsStore& setti
return String(datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV / 1.0, 0); return String(datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV / 1.0, 0);
} }
if (var == "BMS_RESET_DURATION") {
return String(datalayer.battery.settings.user_set_bms_reset_duration_ms / 1000.0, 0);
}
if (var == "CHARGER_CLASS") { if (var == "CHARGER_CLASS") {
if (!charger) { if (!charger) {
return "hidden"; return "hidden";
@ -544,6 +548,9 @@ const char* getCANInterfaceName(CAN_Interface interface) {
xhr=new xhr=new
XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/updateMaxDischargeVoltage?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value between 0 and 1000.0');}}} XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/updateMaxDischargeVoltage?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value between 0 and 1000.0');}}}
function editBMSresetDuration(){var value=prompt('Amount of seconds BMS power should be off during periodic daily resets. Requires "Periodic BMS reset" to be enabled. Enter value in seconds (1-59):');if(value!==null){if(value>=1&&value<=59){var
xhr=new XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/updateBMSresetDuration?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value between 1 and 59');}}}
function editTeslaBalAct(){var value=prompt('Enable or disable forced LFP balancing. Makes the battery charge to 101percent. This should be performed once every month, to keep LFP batteries balanced. Ensure battery is fully charged before enabling, and also that you have enough sun or grid power to feed power into the battery while balancing is active. Enter 1 for enabled, 0 for disabled');if(value!==null){if(value==0||value==1){var xhr=new function editTeslaBalAct(){var value=prompt('Enable or disable forced LFP balancing. Makes the battery charge to 101percent. This should be performed once every month, to keep LFP batteries balanced. Ensure battery is fully charged before enabling, and also that you have enough sun or grid power to feed power into the battery while balancing is active. Enter 1 for enabled, 0 for disabled');if(value!==null){if(value==0||value==1){var xhr=new
XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/TeslaBalAct?value='+value,true);xhr.send();}}else{alert('Invalid value. Please enter 1 or 0');}} XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/TeslaBalAct?value='+value,true);xhr.send();}}else{alert('Invalid value. Please enter 1 or 0');}}
@ -792,6 +799,8 @@ const char* getCANInterfaceName(CAN_Interface interface) {
<h4 class='%VOLTAGE_LIMITS_ACTIVE_CLASS%'>Target discharge voltage: %DISCHARGE_VOLTAGE% V </span> <button onclick='editMaxDischargeVoltage()'>Edit</button></h4> <h4 class='%VOLTAGE_LIMITS_ACTIVE_CLASS%'>Target discharge voltage: %DISCHARGE_VOLTAGE% V </span> <button onclick='editMaxDischargeVoltage()'>Edit</button></h4>
<h4 style='color: white;'>Periodic BMS reset off time: %BMS_RESET_DURATION% s </span><button onclick='editBMSresetDuration()'>Edit</button></h4>
</div> </div>
<div style='background-color: #2E37AD; padding: 10px; margin-bottom: 10px;border-radius: 50px' class="%FAKE_VOLTAGE_CLASS%"> <div style='background-color: #2E37AD; padding: 10px; margin-bottom: 10px;border-radius: 50px' class="%FAKE_VOLTAGE_CLASS%">

View file

@ -594,14 +594,6 @@ void init_webserver() {
} }
}); });
update_string("/equipmentStop", [](String value) {
if (value == "true" || value == "1") {
setBatteryPause(true, false, true); //Pause battery, do not pause CAN, equipment stop on (store to flash)
} else {
setBatteryPause(false, false, false);
}
});
// Route for editing SOCMin // Route for editing SOCMin
update_string_setting("/updateSocMin", [](String value) { update_string_setting("/updateSocMin", [](String value) {
datalayer.battery.settings.min_percentage = static_cast<uint16_t>(value.toFloat() * 100); datalayer.battery.settings.min_percentage = static_cast<uint16_t>(value.toFloat() * 100);
@ -657,6 +649,11 @@ void init_webserver() {
datalayer.battery.settings.max_user_set_discharge_voltage_dV = static_cast<uint16_t>(value.toFloat() * 10); datalayer.battery.settings.max_user_set_discharge_voltage_dV = static_cast<uint16_t>(value.toFloat() * 10);
}); });
// Route for editing BMSresetDuration
update_string_setting("/updateBMSresetDuration", [](String value) {
datalayer.battery.settings.user_set_bms_reset_duration_ms = static_cast<uint16_t>(value.toFloat() * 1000);
});
// Route for editing FakeBatteryVoltage // Route for editing FakeBatteryVoltage
update_string_setting("/updateFakeBatteryVoltage", [](String value) { battery->set_fake_voltage(value.toFloat()); }); update_string_setting("/updateFakeBatteryVoltage", [](String value) { battery->set_fake_voltage(value.toFloat()); });
@ -1410,7 +1407,7 @@ String processor(const String& var) {
content += content +=
"var xhr=new " "var xhr=new "
"XMLHttpRequest();xhr.onload=function() { " "XMLHttpRequest();xhr.onload=function() { "
"window.location.reload();};xhr.open('GET','/equipmentStop?stop='+stop,true);xhr.send();"; "window.location.reload();};xhr.open('GET','/equipmentStop?value='+stop,true);xhr.send();";
content += "}"; content += "}";
content += "</script>"; content += "</script>";

View file

@ -113,16 +113,20 @@ void wifi_monitor() {
if ((hasConnectedBefore && (currentMillis - lastWiFiCheck > current_check_interval)) || if ((hasConnectedBefore && (currentMillis - lastWiFiCheck > current_check_interval)) ||
(!hasConnectedBefore && (currentMillis - lastWiFiCheck > INIT_WIFI_FULL_RECONNECT_INTERVAL))) { (!hasConnectedBefore && (currentMillis - lastWiFiCheck > INIT_WIFI_FULL_RECONNECT_INTERVAL))) {
DEBUG_PRINTF("Time to monitor Wi-Fi status: %d, %d, %d, %d, %d\n", hasConnectedBefore, currentMillis, lastWiFiCheck,
current_check_interval, INIT_WIFI_FULL_RECONNECT_INTERVAL);
lastWiFiCheck = currentMillis; lastWiFiCheck = currentMillis;
wl_status_t status = WiFi.status(); wl_status_t status = WiFi.status();
if (status != WL_CONNECTED) { // WL_IDLE_STATUS can mean we're connected but haven't yet gotten the IP.
if (status != WL_CONNECTED && status != WL_IDLE_STATUS) {
// Increase the current check interval if it's not at the maximum // Increase the current check interval if it's not at the maximum
if (current_check_interval + STEP_WIFI_CHECK_INTERVAL <= MAX_STEP_WIFI_CHECK_INTERVAL) if (current_check_interval + STEP_WIFI_CHECK_INTERVAL <= MAX_STEP_WIFI_CHECK_INTERVAL) {
current_check_interval += STEP_WIFI_CHECK_INTERVAL; current_check_interval += STEP_WIFI_CHECK_INTERVAL;
#ifdef DEBUG_LOG }
logging.println("Wi-Fi not connected, attempting to reconnect..."); DEBUG_PRINTF("Wi-Fi not connected (status=%d), attempting to reconnect\n", status);
#endif
// Try WiFi.reconnect() if it was successfully connected at least once // Try WiFi.reconnect() if it was successfully connected at least once
if (hasConnectedBefore) { if (hasConnectedBefore) {
lastReconnectAttempt = currentMillis; // Reset reconnection attempt timer lastReconnectAttempt = currentMillis; // Reset reconnection attempt timer
@ -203,7 +207,7 @@ void onWifiConnect(WiFiEvent_t event, WiFiEventInfo_t info) {
clear_event(EVENT_WIFI_DISCONNECT); clear_event(EVENT_WIFI_DISCONNECT);
set_event(EVENT_WIFI_CONNECT, 0); set_event(EVENT_WIFI_CONNECT, 0);
connected_once = true; connected_once = true;
DEBUG_PRINTF("Wi-Fi connected. RSSI: %d dBm, IP address: %s, SSID: %s\n", -WiFi.RSSI(), DEBUG_PRINTF("Wi-Fi connected. status: %d, RSSI: %d dBm, IP address: %s, SSID: %s\n", WiFi.status(), -WiFi.RSSI(),
WiFi.localIP().toString().c_str(), WiFi.SSID().c_str()); WiFi.localIP().toString().c_str(), WiFi.SSID().c_str());
hasConnectedBefore = true; // Mark as successfully connected at least once hasConnectedBefore = true; // Mark as successfully connected at least once
reconnectAttempts = 0; // Reset the attempt counter reconnectAttempts = 0; // Reset the attempt counter

View file

@ -0,0 +1,154 @@
#include "GROWATT-WIT-CAN.h"
#include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h"
/* TODO:
This protocol has not been tested with any inverter. Proceed with extreme caution.
Search the file for "TODO" to see all the places that might require work
Largest TODO: Map all CAN message .ID properly
Example, how should 1AC6XXXX be transmitted? .ID = 0x1AC6, at the time being, but we need to add the targegt address and source address.
GROWATT BATTERY BMS CAN COMMUNICATION PROTOCOL V1.1 2024.7.19
29-bit identifier
500kBit/sec
Big-endian
Internal CAN: The extended frame CAN_ID is 29 bits, and the 29-bit CAN_ID is divided into five parts:
PRI, PG, DA, SA, and FSN.
- PRI: indicates priority.
- PG: indicates the page number. If the FSN is not enough, you can turn the page to increase the range of the FSN.
- TA: indicates the target address.
- SA: indicates the source address.
- FSN: represents functional domain.
(See the Wiki for full documentation explained)
Example: 0x1AC0FFF3: PRI is 6, PG is 2, FSN is 0xC0, TA is 0xFF, SA is 0xF3
*/
void GrowattWitInverter::update_values() {
//Maximum allowable charging current of the battery system, 0-10000 dA
GROWATT_1AC3XXXX.data.u8[0] = (datalayer.battery.status.max_charge_current_dA >> 8);
GROWATT_1AC3XXXX.data.u8[1] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
//Maximum allowable discharge current of the battery system, 0-10000 dA
GROWATT_1AC3XXXX.data.u8[2] = (datalayer.battery.status.max_discharge_current_dA >> 8);
GROWATT_1AC3XXXX.data.u8[3] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
//Maximum charging voltage of the battery system 0-15000 dV
//Stop charging when the current battery output voltage is greater than or equal to this value
if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage
//User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC3XXXX.data.u8[4] = (datalayer.battery.settings.max_user_set_charge_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[5] = (datalayer.battery.settings.max_user_set_charge_voltage_dV & 0x00FF);
GROWATT_1AC3XXXX.data.u8[6] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[7] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV & 0x00FF);
} else {
//Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC3XXXX.data.u8[4] = (datalayer.battery.info.max_design_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[5] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
GROWATT_1AC3XXXX.data.u8[6] = (datalayer.battery.info.min_design_voltage_dV >> 8);
GROWATT_1AC3XXXX.data.u8[7] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
}
//CV voltage value
//Total charge voltage of constant voltage (set to 10.0V below max voltage for now)
//When the battery output voltage is greater than or equal to this value, enter constant voltage charging state.
if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage
//User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC4XXXX.data.u8[4] = ((datalayer.battery.settings.max_user_set_charge_voltage_dV - 100) >> 8);
GROWATT_1AC4XXXX.data.u8[5] = ((datalayer.battery.settings.max_user_set_charge_voltage_dV - 100) & 0x00FF);
} else {
//Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V)
GROWATT_1AC4XXXX.data.u8[4] = ((datalayer.battery.info.max_design_voltage_dV - 100) >> 8);
GROWATT_1AC4XXXX.data.u8[5] = ((datalayer.battery.info.max_design_voltage_dV - 100) & 0x00FF);
}
//System BMS working status
if (datalayer.battery.status.current_dA == 0) {
GROWATT_1AC5XXXX.data.u8[0] = 1; //Standby
} else if (datalayer.battery.status.current_dA < 0) { //Negative value = Discharging
GROWATT_1AC5XXXX.data.u8[0] = 3; //Discharging
} else { //Positive value = Charging
GROWATT_1AC5XXXX.data.u8[0] = 2; //Charging
}
if (datalayer.battery.status.bms_status == FAULT) {
GROWATT_1AC5XXXX.data.u8[0] = 5; //FAULT, Stop using battery!
}
//SOC 0-100 %
GROWATT_1AC6XXXX.data.u8[0] = (datalayer.battery.status.reported_soc / 100);
//SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 low SOH flag (Indicates that battery is in unsafe use)
GROWATT_1AC6XXXX.data.u8[1] = (datalayer.battery.status.soh_pptt / 100);
//Rated battery capacity when new (0-50000) dAH
GROWATT_1AC6XXXX.data.u8[2]; //TODO
GROWATT_1AC6XXXX.data.u8[3]; //TODO
//Battery voltage, 0-15000 dV
GROWATT_1AC7XXXX.data.u8[2] = (datalayer.battery.status.voltage_dV >> 8);
GROWATT_1AC7XXXX.data.u8[3] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Battery current, 0-20000dA (offset -1000A)
// Apply the -1000 offset (add 1000 to the dA value)
uint32_t current_value = (int32_t)datalayer.battery.status.current_dA + 1000;
// Clamp to uint16_t range (0 to 65535)
if (current_value < 0) {
current_value = 0;
} else if (current_value > 65535) {
current_value = 65535;
}
GROWATT_1AC7XXXX.data.u8[4] = (current_value >> 8);
GROWATT_1AC7XXXX.data.u8[5] = (current_value & 0x00FF);
}
void GrowattWitInverter::map_can_frame_to_variable(CAN_frame rx_frame) {
uint32_t first4bytes = ((rx_frame.ID & 0xFFFF0000) >> 4);
//1AB5XXXX becomes 1AB5. Most likely not needed if all PCS messages come from XXXXDFF1
switch (first4bytes) {
case 0x1AB5: // Heartbeat command, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1AB6: // Time and date, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1AB7: // PCS status information, 1000ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1AB8: // Bus voltage setting 1, 50ms
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
case 0x1ABE: // PCS product information, Non-periodic
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
transmit_can_frame(&GROWATT_1AC2XXXX);
transmit_can_frame(&GROWATT_1A80XXXX);
transmit_can_frame(&GROWATT_1A82XXXX);
break;
default:
break;
}
}
void GrowattWitInverter::transmit_can(unsigned long currentMillis) {
//Send 100ms message
if (currentMillis - previousMillis100ms >= INTERVAL_100_MS) {
previousMillis100ms = currentMillis;
transmit_can_frame(&GROWATT_1AC3XXXX);
transmit_can_frame(&GROWATT_1AC4XXXX);
transmit_can_frame(&GROWATT_1AC5XXXX);
transmit_can_frame(&GROWATT_1AC7XXXX);
}
//Send 500ms message
if (currentMillis - previousMillis500ms >= INTERVAL_500_MS) {
previousMillis500ms = currentMillis;
transmit_can_frame(&GROWATT_1AC6XXXX);
}
}

View file

@ -0,0 +1,126 @@
#ifndef GROWATT_WIT_CAN_H
#define GROWATT_WIT_CAN_H
#include "CanInverterProtocol.h"
#ifdef GROWATT_WIT_CAN
#define SELECTED_INVERTER_CLASS GrowattWitInverter
#endif
class GrowattWitInverter : public CanInverterProtocol {
public:
const char* name() override { return Name; }
void update_values();
void transmit_can(unsigned long currentMillis);
void map_can_frame_to_variable(CAN_frame rx_frame);
static constexpr const char* Name = "Growatt WIT compatible battery via CAN";
private:
/* Do not change code below unless you are sure what you are doing */
//Actual content messages
CAN_frame GROWATT_1AC3XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC3, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC4XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC4, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC5XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC5, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC6XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC6, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC7XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC7, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC0XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC0, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC2XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC2, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC8XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC8, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AC9XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AC9, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACAXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACA, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACCXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACC, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACDXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACD, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACEXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACE, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1ACFXXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1ACF, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD0XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD0, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD1XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD1, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD8XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD8, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1AD9XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1AD9, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1A80XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1A80, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame GROWATT_1A82XXXX = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x1A82, //TODO
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
unsigned long previousMillis100ms = 0;
unsigned long previousMillis500ms = 0;
};
#endif

View file

@ -40,6 +40,9 @@ extern const char* name_for_inverter_type(InverterProtocolType type) {
case InverterProtocolType::GrowattLv: case InverterProtocolType::GrowattLv:
return GrowattLvInverter::Name; return GrowattLvInverter::Name;
case InverterProtocolType::GrowattWit:
return GrowattWitInverter::Name;
case InverterProtocolType::Kostal: case InverterProtocolType::Kostal:
return KostalInverterProtocol::Name; return KostalInverterProtocol::Name;
@ -118,6 +121,10 @@ bool setup_inverter() {
inverter = new GrowattLvInverter(); inverter = new GrowattLvInverter();
break; break;
case InverterProtocolType::GrowattWit:
inverter = new GrowattWitInverter();
break;
case InverterProtocolType::Kostal: case InverterProtocolType::Kostal:
inverter = new KostalInverterProtocol(); inverter = new KostalInverterProtocol();
break; break;

View file

@ -18,6 +18,7 @@ extern InverterProtocol* inverter;
#include "FOXESS-CAN.h" #include "FOXESS-CAN.h"
#include "GROWATT-HV-CAN.h" #include "GROWATT-HV-CAN.h"
#include "GROWATT-LV-CAN.h" #include "GROWATT-LV-CAN.h"
#include "GROWATT-WIT-CAN.h"
#include "KOSTAL-RS485.h" #include "KOSTAL-RS485.h"
#include "PYLON-CAN.h" #include "PYLON-CAN.h"
#include "PYLON-LV-CAN.h" #include "PYLON-LV-CAN.h"

View file

@ -12,6 +12,7 @@ enum class InverterProtocolType {
Foxess, Foxess,
GrowattHv, GrowattHv,
GrowattLv, GrowattLv,
GrowattWit,
Kostal, Kostal,
Pylon, Pylon,
PylonLv, PylonLv,

View file

@ -154,6 +154,12 @@ void KostalInverterProtocol::update_values() {
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 18); // Last current float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 18); // Last current
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 22); // Should be Avg current(1s) float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 22); // Should be Avg current(1s)
// Close contactors after 7 battery info frames requested
if (f2_startup_count > 7) {
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
}
// On startup, byte 56 seems to be always 0x00 couple of frames,. // On startup, byte 56 seems to be always 0x00 couple of frames,.
if (f2_startup_count < 9) { if (f2_startup_count < 9) {
CYCLIC_DATA[56] = 0x00; CYCLIC_DATA[56] = 0x00;
@ -226,7 +232,7 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
if (check_kostal_frame_crc(rx_index)) { if (check_kostal_frame_crc(rx_index)) {
incoming_message_counter = RS485_HEALTHY; incoming_message_counter = RS485_HEALTHY;
if (RS485_RXFRAME[1] == 'c') { if (RS485_RXFRAME[1] == 'c' && info_sent) {
if (RS485_RXFRAME[6] == 0x47) { if (RS485_RXFRAME[6] == 0x47) {
// Set time function - Do nothing. // Set time function - Do nothing.
send_kostal(ACK_FRAME, 8); // ACK send_kostal(ACK_FRAME, 8); // ACK
@ -234,12 +240,11 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
if (RS485_RXFRAME[6] == 0x5E) { if (RS485_RXFRAME[6] == 0x5E) {
// Set State function // Set State function
if (RS485_RXFRAME[7] == 0x00) { if (RS485_RXFRAME[7] == 0x00) {
// Allow contactor closing
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
send_kostal(ACK_FRAME, 8); // ACK send_kostal(ACK_FRAME, 8); // ACK
} else if (RS485_RXFRAME[7] == 0x04) { } else if (RS485_RXFRAME[7] == 0x04) {
// INVALID STATE, no ACK sent send_kostal(ACK_FRAME, 8); // ACK
} else if (RS485_RXFRAME[7] == 0xFF) {
// no ACK sent
} else { } else {
// Battery deep sleep? // Battery deep sleep?
send_kostal(ACK_FRAME, 8); // ACK send_kostal(ACK_FRAME, 8); // ACK
@ -250,7 +255,7 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
//Reverse polarity, do nothing //Reverse polarity, do nothing
} else { } else {
int code = RS485_RXFRAME[6] + RS485_RXFRAME[7] * 0x100; int code = RS485_RXFRAME[6] + RS485_RXFRAME[7] * 0x100;
if (code == 0x44a) { if (code == 0x44a && info_sent) {
//Send cyclic data //Send cyclic data
// TODO: Probably not a good idea to use the battery object here like this. // TODO: Probably not a good idea to use the battery object here like this.
if (battery) { if (battery) {
@ -274,13 +279,12 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
tmpframe[38] = calculate_kostal_crc(tmpframe, 38); tmpframe[38] = calculate_kostal_crc(tmpframe, 38);
null_stuffer(tmpframe, 40); null_stuffer(tmpframe, 40);
send_kostal(tmpframe, 40); send_kostal(tmpframe, 40);
datalayer.system.status.inverter_allows_contactor_closing = true; info_sent = true;
dbg_message("inverter_allows_contactor_closing (battery_info) -> true");
if (!startupMillis) { if (!startupMillis) {
startupMillis = currentMillis; startupMillis = currentMillis;
} }
} }
if (code == 0x353) { if (code == 0x353 && info_sent) {
//Send battery error/status //Send battery error/status
uint8_t tmpframe[9]; //copy values to prevent data manipulation during rewrite/crc calculation uint8_t tmpframe[9]; //copy values to prevent data manipulation during rewrite/crc calculation
memcpy(tmpframe, STATUS_FRAME, 9); memcpy(tmpframe, STATUS_FRAME, 9);

View file

@ -39,8 +39,7 @@ class KostalInverterProtocol : public Rs485InverterProtocol {
uint8_t incoming_message_counter = RS485_HEALTHY; uint8_t incoming_message_counter = RS485_HEALTHY;
int8_t f2_startup_count = 0; int8_t f2_startup_count = 0;
bool B1_delay = false; bool info_sent = false;
unsigned long B1_last_millis = 0;
unsigned long currentMillis; unsigned long currentMillis;
unsigned long startupMillis = 0; unsigned long startupMillis = 0;
unsigned long contactorMillis = 0; unsigned long contactorMillis = 0;

View file

@ -61,12 +61,35 @@ void SmaBydHInverter::
SMA_4D8.data.u8[6] = STOP_STATE; SMA_4D8.data.u8[6] = STOP_STATE;
} }
//Lifetime charged energy amount
SMA_458.data.u8[0] = (datalayer.battery.status.total_charged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[1] = (datalayer.battery.status.total_charged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[2] = (datalayer.battery.status.total_charged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[3] = (datalayer.battery.status.total_charged_battery_Wh & 0x000000FF);
//Lifetime discharged energy amount
SMA_458.data.u8[4] = (datalayer.battery.status.total_discharged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[5] = (datalayer.battery.status.total_discharged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[6] = (datalayer.battery.status.total_discharged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[7] = (datalayer.battery.status.total_discharged_battery_Wh & 0x000000FF);
//Error bits //Error bits
if (datalayer.system.status.battery_allows_contactor_closing) { if (datalayer.system.status.battery_allows_contactor_closing) {
SMA_158.data.u8[2] = 0xAA; SMA_158.data.u8[2] = 0xAA;
} else { } else {
SMA_158.data.u8[2] = 0x6A; SMA_158.data.u8[2] = 0x6A;
} }
//Highest battery temperature
SMA_518.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SMA_518.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Lowest battery temperature
SMA_518.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SMA_518.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Sum of all cellvoltages
SMA_518.data.u8[4] = (datalayer.battery.status.voltage_dV >> 8);
SMA_518.data.u8[5] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Cell min/max voltage (mV / 25)
SMA_518.data.u8[6] = (datalayer.battery.status.cell_min_voltage_mV / 25);
SMA_518.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV / 25);
control_contactor_led(); control_contactor_led();

View file

@ -60,6 +60,30 @@ void SmaBydHvsInverter::
SMA_4D8.data.u8[6] = STOP_STATE; SMA_4D8.data.u8[6] = STOP_STATE;
} }
//Highest battery temperature
SMA_518.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SMA_518.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Lowest battery temperature
SMA_518.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SMA_518.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Sum of all cellvoltages
SMA_518.data.u8[4] = (datalayer.battery.status.voltage_dV >> 8);
SMA_518.data.u8[5] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Cell min/max voltage (mV / 25)
SMA_518.data.u8[6] = (datalayer.battery.status.cell_min_voltage_mV / 25);
SMA_518.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV / 25);
//Lifetime charged energy amount
SMA_458.data.u8[0] = (datalayer.battery.status.total_charged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[1] = (datalayer.battery.status.total_charged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[2] = (datalayer.battery.status.total_charged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[3] = (datalayer.battery.status.total_charged_battery_Wh & 0x000000FF);
//Lifetime discharged energy amount
SMA_458.data.u8[4] = (datalayer.battery.status.total_discharged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[5] = (datalayer.battery.status.total_discharged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[6] = (datalayer.battery.status.total_discharged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[7] = (datalayer.battery.status.total_discharged_battery_Wh & 0x000000FF);
//Error bits //Error bits
if (datalayer.system.status.battery_allows_contactor_closing) { if (datalayer.system.status.battery_allows_contactor_closing) {
SMA_158.data.u8[2] = 0xAA; SMA_158.data.u8[2] = 0xAA;
@ -247,7 +271,7 @@ void SmaBydHvsInverter::transmit_can(unsigned long currentMillis) {
// Increment message index and wrap around if needed // Increment message index and wrap around if needed
batch_send_index++; batch_send_index++;
if (transmit_can_init == false) { if (transmit_can_init == false) { //We completed sending the batches
batch_send_index = 0; batch_send_index = 0;
} }
} }

View file

@ -66,7 +66,7 @@ class SmaBydHvsInverter : public SmaInverterBase {
.ext_ID = false, .ext_ID = false,
.DLC = 8, .DLC = 8,
.ID = 0x518, .ID = 0x518,
.data = {0x01, 0x4A, 0x01, 0x25, 0xFF, 0xFF, 0xFF, 0xFF}}; .data = {0x01, 0x4A, 0x01, 0x25, 0x10, 0x10, 0xFF, 0xFF}};
// Pairing/Battery setup information // Pairing/Battery setup information
@ -79,7 +79,7 @@ class SmaBydHvsInverter : public SmaInverterBase {
.ext_ID = false, .ext_ID = false,
.DLC = 8, .DLC = 8,
.ID = 0x598, .ID = 0x598,
.data = {0x00, 0x01, 0x0F, 0x2C, 0x5C, 0x98, 0xB6, 0xEE}}; .data = {0x00, 0x01, 0x0F, 0x2C, 0x5C, 0x98, 0xB6, 0xEE}}; //B0-4 Serial, rest unknown
CAN_frame SMA_5D8 = {.FD = false, CAN_frame SMA_5D8 = {.FD = false,
.ext_ID = false, .ext_ID = false,
.DLC = 8, .DLC = 8,

View file

@ -64,6 +64,30 @@ void SmaTripowerInverter::
SMA_4D8.data.u8[6] = READY_STATE; SMA_4D8.data.u8[6] = READY_STATE;
} }
//Highest battery temperature
SMA_518.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SMA_518.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Lowest battery temperature
SMA_518.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SMA_518.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Sum of all cellvoltages
SMA_518.data.u8[4] = (datalayer.battery.status.voltage_dV >> 8);
SMA_518.data.u8[5] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Cell min/max voltage (mV / 25)
SMA_518.data.u8[6] = (datalayer.battery.status.cell_min_voltage_mV / 25);
SMA_518.data.u8[7] = (datalayer.battery.status.cell_max_voltage_mV / 25);
//Lifetime charged energy amount
SMA_458.data.u8[0] = (datalayer.battery.status.total_charged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[1] = (datalayer.battery.status.total_charged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[2] = (datalayer.battery.status.total_charged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[3] = (datalayer.battery.status.total_charged_battery_Wh & 0x000000FF);
//Lifetime discharged energy amount
SMA_458.data.u8[4] = (datalayer.battery.status.total_discharged_battery_Wh & 0xFF000000) >> 24;
SMA_458.data.u8[5] = (datalayer.battery.status.total_discharged_battery_Wh & 0x00FF0000) >> 16;
SMA_458.data.u8[6] = (datalayer.battery.status.total_discharged_battery_Wh & 0x0000FF00) >> 8;
SMA_458.data.u8[7] = (datalayer.battery.status.total_discharged_battery_Wh & 0x000000FF);
control_contactor_led(); control_contactor_led();
// Check if Enable line is working. If we go too long without any input, raise an event // Check if Enable line is working. If we go too long without any input, raise an event

View file

@ -38,7 +38,7 @@ It is also deployed in these registries:
- Arduino Library Registry: [https://github.com/arduino/library-registry](https://github.com/arduino/library-registry) - Arduino Library Registry: [https://github.com/arduino/library-registry](https://github.com/arduino/library-registry)
- ESP Component Registry [https://components.espressif.com/components/esp32async/espasyncbebserver/](https://components.espressif.com/components/esp32async/espasyncbebserver/) - ESP Component Registry [https://components.espressif.com/components/esp32async/espasyncwebserver](https://components.espressif.com/components/esp32async/espasyncwebserver)
- PlatformIO Registry: [https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer](https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer) - PlatformIO Registry: [https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer](https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer)
@ -72,6 +72,19 @@ lib_deps =
ESP32Async/ESPAsyncWebServer ESP32Async/ESPAsyncWebServer
``` ```
### LibreTiny (BK7231N/T, RTL8710B, etc.)
Version 1.9.1 or newer is required.
```ini
[env:stable]
platform = libretiny @ ^1.9.1
lib_ldf_mode = chain
lib_deps =
ESP32Async/AsyncTCP
ESP32Async/ESPAsyncWebServer
```
### Unofficial dependencies ### Unofficial dependencies
**AsyncTCPSock** **AsyncTCPSock**
@ -100,7 +113,7 @@ platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = rpipicow board = rpipicow
board_build.core = earlephilhower board_build.core = earlephilhower
lib_deps = lib_deps =
ayushsharma82/RPAsyncTCP@^1.3.1 ayushsharma82/RPAsyncTCP@^1.3.2
ESP32Async/ESPAsyncWebServer ESP32Async/ESPAsyncWebServer
lib_ignore = lib_ignore =
lwIP_ESPHost lwIP_ESPHost

View file

@ -1,6 +1,6 @@
{ {
"name": "ESPAsyncWebServer", "name": "ESPAsyncWebServer",
"version": "3.7.2", "version": "3.7.10",
"description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.", "description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.",
"keywords": "http,async,websocket,webserver", "keywords": "http,async,websocket,webserver",
"homepage": "https://github.com/ESP32Async/ESPAsyncWebServer", "homepage": "https://github.com/ESP32Async/ESPAsyncWebServer",
@ -18,14 +18,18 @@
"platforms": [ "platforms": [
"espressif32", "espressif32",
"espressif8266", "espressif8266",
"raspberrypi" "raspberrypi",
"libretiny"
], ],
"dependencies": [ "dependencies": [
{ {
"owner": "ESP32Async", "owner": "ESP32Async",
"name": "AsyncTCP", "name": "AsyncTCP",
"version": "^3.3.6", "version": "^3.4.5",
"platforms": "espressif32" "platforms": [
"espressif32",
"libretiny"
]
}, },
{ {
"owner": "ESP32Async", "owner": "ESP32Async",
@ -40,7 +44,7 @@
{ {
"owner": "ayushsharma82", "owner": "ayushsharma82",
"name": "RPAsyncTCP", "name": "RPAsyncTCP",
"version": "^1.3.1", "version": "^1.3.2",
"platforms": "raspberrypi" "platforms": "raspberrypi"
} }
], ],

View file

@ -1,6 +1,6 @@
name=ESP Async WebServer name=ESP Async WebServer
includes=ESPAsyncWebServer.h includes=ESPAsyncWebServer.h
version=3.7.2 version=3.7.10
author=ESP32Async author=ESP32Async
maintainer=ESP32Async maintainer=ESP32Async
sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040

View file

@ -193,7 +193,7 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A
AsyncEventSourceClient::~AsyncEventSourceClient() { AsyncEventSourceClient::~AsyncEventSourceClient() {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_messageQueue.clear(); _messageQueue.clear();
close(); close();
@ -211,7 +211,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
#ifdef ESP32 #ifdef ESP32
// length() is not thread-safe, thus acquiring the lock before this call.. // length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_messageQueue.emplace_back(message, len); _messageQueue.emplace_back(message, len);
@ -241,7 +241,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
#ifdef ESP32 #ifdef ESP32
// length() is not thread-safe, thus acquiring the lock before this call.. // length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_messageQueue.emplace_back(std::move(msg)); _messageQueue.emplace_back(std::move(msg));
@ -261,7 +261,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) { void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) {
#ifdef ESP32 #ifdef ESP32
// Same here, acquiring the lock early // Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
// adjust in-flight len // adjust in-flight len
@ -290,7 +290,7 @@ void AsyncEventSourceClient::_onPoll() {
if (_messageQueue.size()) { if (_messageQueue.size()) {
#ifdef ESP32 #ifdef ESP32
// Same here, acquiring the lock early // Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_runQueue(); _runQueue();
} }
@ -367,7 +367,7 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient *client) {
return; return;
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
_clients.emplace_back(client); _clients.emplace_back(client);
if (_connectcb) { if (_connectcb) {
@ -382,7 +382,7 @@ void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient *client) {
_disconnectcb(client); _disconnectcb(client);
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
for (auto i = _clients.begin(); i != _clients.end(); ++i) { for (auto i = _clients.begin(); i != _clients.end(); ++i) {
if (i->get() == client) { if (i->get() == client) {
@ -398,10 +398,15 @@ void AsyncEventSource::close() {
// iterator should remain valid even when AsyncEventSource::_handleDisconnect() // iterator should remain valid even when AsyncEventSource::_handleDisconnect()
// is called very early // is called very early
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
for (const auto &c : _clients) { for (const auto &c : _clients) {
if (c->connected()) { if (c->connected()) {
/**
* @brief: Fix self-deadlock by using recursive_mutex instead.
* Due to c->close() shall call the callback function _onDisconnect()
* The calling flow _onDisconnect() --> _handleDisconnect() --> deadlock
*/
c->close(); c->close();
} }
} }
@ -412,7 +417,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
size_t aql = 0; size_t aql = 0;
uint32_t nConnectedClients = 0; uint32_t nConnectedClients = 0;
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
if (!_clients.size()) { if (!_clients.size()) {
return 0; return 0;
@ -430,7 +435,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
AsyncEvent_SharedData_t shared_msg = std::make_shared<String>(generateEventMessage(message, event, id, reconnect)); AsyncEvent_SharedData_t shared_msg = std::make_shared<String>(generateEventMessage(message, event, id, reconnect));
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
size_t hits = 0; size_t hits = 0;
size_t miss = 0; size_t miss = 0;
@ -446,7 +451,7 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c
size_t AsyncEventSource::count() const { size_t AsyncEventSource::count() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
size_t n_clients{0}; size_t n_clients{0};
for (const auto &i : _clients) { for (const auto &i : _clients) {

View file

@ -6,8 +6,13 @@
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#ifdef LIBRETINY
#ifdef round
#undef round
#endif
#endif
#include <mutex> #include <mutex>
#ifndef SSE_MAX_QUEUED_MESSAGES #ifndef SSE_MAX_QUEUED_MESSAGES
#define SSE_MAX_QUEUED_MESSAGES 32 #define SSE_MAX_QUEUED_MESSAGES 32
@ -129,7 +134,7 @@ private:
size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer
std::list<AsyncEventSourceMessage> _messageQueue; std::list<AsyncEventSourceMessage> _messageQueue;
#ifdef ESP32 #ifdef ESP32
mutable std::mutex _lockmq; mutable std::recursive_mutex _lockmq;
#endif #endif
bool _queueMessage(const char *message, size_t len); bool _queueMessage(const char *message, size_t len);
bool _queueMessage(AsyncEvent_SharedData_t &&msg); bool _queueMessage(AsyncEvent_SharedData_t &&msg);
@ -230,7 +235,7 @@ private:
#ifdef ESP32 #ifdef ESP32
// Same as for individual messages, protect mutations of _clients list // Same as for individual messages, protect mutations of _clients list
// since simultaneous access from different tasks is possible // since simultaneous access from different tasks is possible
mutable std::mutex _client_queue_lock; mutable std::recursive_mutex _client_queue_lock;
#endif #endif
ArEventHandlerFunction _connectcb = nullptr; ArEventHandlerFunction _connectcb = nullptr;
ArEventHandlerFunction _disconnectcb = nullptr; ArEventHandlerFunction _disconnectcb = nullptr;

View file

@ -113,43 +113,64 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons
void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) { void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) {
if (_onRequest) { if (_onRequest) {
// GET request:
if (request->method() == HTTP_GET) { if (request->method() == HTTP_GET) {
JsonVariant json; JsonVariant json;
_onRequest(request, json); _onRequest(request, json);
return; return;
} else if (request->_tempObject != NULL) { }
// POST / PUT / ... requests:
// check if JSON body is too large, if it is, don't deserialize
if (request->contentLength() > _maxContentLength) {
#ifdef ESP32
log_e("Content length exceeds maximum allowed");
#endif
request->send(413);
return;
}
if (request->_tempObject == NULL) {
// there is no body
request->send(400);
return;
}
#if ARDUINOJSON_VERSION_MAJOR == 5 #if ARDUINOJSON_VERSION_MAJOR == 5
DynamicJsonBuffer jsonBuffer; DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((uint8_t *)(request->_tempObject)); JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject);
if (json.success()) { if (json.success()) {
#elif ARDUINOJSON_VERSION_MAJOR == 6 #elif ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) { if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>(); JsonVariant json = jsonBuffer.as<JsonVariant>();
#else #else
JsonDocument jsonBuffer; JsonDocument jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) { if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>(); JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif #endif
_onRequest(request, json); _onRequest(request, json);
return;
}
}
request->send(_contentLength > _maxContentLength ? 413 : 400);
} else { } else {
request->send(500); // error parsing the body
request->send(400);
}
} }
} }
void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (_onRequest) { if (_onRequest) {
_contentLength = total; // ignore callback if size is larger than maxContentLength
if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { if (total > _maxContentLength) {
request->_tempObject = malloc(total); return;
}
if (index == 0) {
// this check allows request->_tempObject to be initialized from a middleware
if (request->_tempObject == NULL) {
request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string
if (request->_tempObject == NULL) { if (request->_tempObject == NULL) {
#ifdef ESP32 #ifdef ESP32
log_e("Failed to allocate"); log_e("Failed to allocate");
@ -158,8 +179,11 @@ void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uin
return; return;
} }
} }
}
if (request->_tempObject != NULL) { if (request->_tempObject != NULL) {
memcpy((uint8_t *)(request->_tempObject) + index, data, len); uint8_t *buffer = (uint8_t *)request->_tempObject;
memcpy(buffer + index, data, len);
} }
} }
} }

View file

@ -79,7 +79,6 @@ protected:
String _uri; String _uri;
WebRequestMethodComposite _method; WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest; ArJsonRequestHandlerFunction _onRequest;
size_t _contentLength;
#if ARDUINOJSON_VERSION_MAJOR == 6 #if ARDUINOJSON_VERSION_MAJOR == 6
size_t maxJsonBufferSize; size_t maxJsonBufferSize;
#endif #endif

View file

@ -3,30 +3,32 @@
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
AsyncWebHeader::AsyncWebHeader(const String &data) { const AsyncWebHeader AsyncWebHeader::parse(const char *data) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers
// In HTTP/1.X, a header is a case-insensitive name followed by a colon, then optional whitespace which will be ignored, and finally by its value
if (!data) { if (!data) {
return; return AsyncWebHeader(); // nullptr
} }
int index = data.indexOf(':'); if (data[0] == '\0') {
if (index < 0) { return AsyncWebHeader(); // empty string
return;
} }
_name = data.substring(0, index); if (strchr(data, '\n') || strchr(data, '\r')) {
_value = data.substring(index + 2); return AsyncWebHeader(); // Invalid header format
} }
char *colon = strchr(data, ':');
String AsyncWebHeader::toString() const { if (!colon) {
String str; return AsyncWebHeader(); // separator not found
if (str.reserve(_name.length() + _value.length() + 2)) { }
str.concat(_name); if (colon == data) {
str.concat((char)0x3a); return AsyncWebHeader(); // Header name cannot be empty
str.concat((char)0x20); }
str.concat(_value); char *startOfValue = colon + 1; // Skip the colon
str.concat(asyncsrv::T_rn); // skip one optional whitespace after the colon
} else { if (*startOfValue == ' ') {
#ifdef ESP32 startOfValue++;
log_e("Failed to allocate"); }
#endif String name;
} name.reserve(colon - data);
return str; name.concat(data, colon - data);
return AsyncWebHeader(name, String(startOfValue));
} }

View file

@ -0,0 +1,85 @@
#include "ESPAsyncWebServer.h"
/**
* @brief Sends a file from the filesystem to the client, with optional gzip compression and ETag-based caching.
*
* This method serves files over HTTP from the provided filesystem. If a compressed version of the file
* (with a `.gz` extension) exists and uncompressed version does not exist, it serves the compressed file.
* It also handles ETag caching using the CRC32 value from the gzip trailer, responding with `304 Not Modified`
* if the client's `If-None-Match` header matches the generated ETag.
*
* @param fs Reference to the filesystem (SPIFFS, LittleFS, etc.).
* @param path Path to the file to be served.
* @param contentType Optional MIME type of the file to be sent.
* If contentType is "" it will be obtained from the file extension
* @param download If true, forces the file to be sent as a download.
* @param callback Optional template processor for dynamic content generation.
* Templates will not be processed in compressed files.
*
* @note If neither the file nor its compressed version exists, responds with `404 Not Found`.
*/
void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) {
// Check uncompressed file first
if (fs.exists(path)) {
send(beginResponse(fs, path, contentType, download, callback));
return;
}
// Handle compressed version
const String gzPath = path + asyncsrv::T__gz;
File gzFile = fs.open(gzPath, "r");
// Compressed file not found or invalid
if (!gzFile.seek(gzFile.size() - 8)) {
send(404);
gzFile.close();
return;
}
// ETag validation
if (this->hasHeader(asyncsrv::T_INM)) {
// Generate server ETag from CRC in gzip trailer
uint8_t crcInTrailer[4];
gzFile.read(crcInTrailer, 4);
char serverETag[9];
_getEtag(crcInTrailer, serverETag);
// Compare with client's ETag
const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM);
if (inmHeader && inmHeader->value() == serverETag) {
gzFile.close();
this->send(304); // Not Modified
return;
}
}
// Send compressed file response
gzFile.close();
send(beginResponse(fs, path, contentType, download, callback));
}
/**
* @brief Generates an ETag string from a 4-byte trailer
*
* This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes.
*
* @param trailer[4] Input array of 4 bytes to convert to hexadecimal
* @param serverETag Output buffer to store the ETag
* Must be pre-allocated with minimum 9 bytes (8 hex + 1 null terminator)
*/
void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) {
static constexpr char hexChars[] = "0123456789ABCDEF";
uint32_t data;
memcpy(&data, trailer, 4);
serverETag[0] = hexChars[(data >> 4) & 0x0F];
serverETag[1] = hexChars[data & 0x0F];
serverETag[2] = hexChars[(data >> 12) & 0x0F];
serverETag[3] = hexChars[(data >> 8) & 0x0F];
serverETag[4] = hexChars[(data >> 20) & 0x0F];
serverETag[5] = hexChars[(data >> 16) & 0x0F];
serverETag[6] = hexChars[(data >> 28)];
serverETag[7] = hexChars[(data >> 24) & 0x0F];
serverETag[8] = '\0';
}

View file

@ -12,7 +12,7 @@ extern "C" {
/** Minor version number (x.X.x) */ /** Minor version number (x.X.x) */
#define ASYNCWEBSERVER_VERSION_MINOR 7 #define ASYNCWEBSERVER_VERSION_MINOR 7
/** Patch version number (x.x.X) */ /** Patch version number (x.x.X) */
#define ASYNCWEBSERVER_VERSION_PATCH 2 #define ASYNCWEBSERVER_VERSION_PATCH 10
/** /**
* Macro to convert version number into an integer * Macro to convert version number into an integer

View file

@ -17,6 +17,8 @@
#include <rom/ets_sys.h> #include <rom/ets_sys.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266) #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266)
#include <Hash.h> #include <Hash.h>
#elif defined(LIBRETINY)
#include <mbedtls/sha1.h>
#endif #endif
using namespace asyncsrv; using namespace asyncsrv;
@ -333,7 +335,7 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async
AsyncWebSocketClient::~AsyncWebSocketClient() { AsyncWebSocketClient::~AsyncWebSocketClient() {
{ {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
_messageQueue.clear(); _messageQueue.clear();
_controlQueue.clear(); _controlQueue.clear();
@ -351,7 +353,7 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
_lastMessageTime = millis(); _lastMessageTime = millis();
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::unique_lock<std::recursive_mutex> lock(_lock);
#endif #endif
if (!_controlQueue.empty()) { if (!_controlQueue.empty()) {
@ -362,6 +364,14 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
_controlQueue.pop_front(); _controlQueue.pop_front();
_status = WS_DISCONNECTED; _status = WS_DISCONNECTED;
if (_client) { if (_client) {
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true); _client->close(true);
} }
return; return;
@ -385,7 +395,7 @@ void AsyncWebSocketClient::_onPoll() {
} }
#ifdef ESP32 #ifdef ESP32
std::unique_lock<std::mutex> lock(_lock); std::unique_lock<std::recursive_mutex> lock(_lock);
#endif #endif
if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) { if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) {
_runQueue(); _runQueue();
@ -415,21 +425,21 @@ void AsyncWebSocketClient::_runQueue() {
bool AsyncWebSocketClient::queueIsFull() const { bool AsyncWebSocketClient::queueIsFull() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED); return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED);
} }
size_t AsyncWebSocketClient::queueLen() const { size_t AsyncWebSocketClient::queueLen() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
return _messageQueue.size(); return _messageQueue.size();
} }
bool AsyncWebSocketClient::canSend() const { bool AsyncWebSocketClient::canSend() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES; return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES;
} }
@ -440,7 +450,7 @@ bool AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t *data, si
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
_controlQueue.emplace_back(opcode, data, len, mask); _controlQueue.emplace_back(opcode, data, len, mask);
@ -458,7 +468,7 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::unique_lock<std::recursive_mutex> lock(_lock);
#endif #endif
if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) { if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) {
@ -466,6 +476,14 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
_status = WS_DISCONNECTED; _status = WS_DISCONNECTED;
if (_client) { if (_client) {
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true); _client->close(true);
} }
@ -551,6 +569,7 @@ void AsyncWebSocketClient::_onTimeout(uint32_t time) {
void AsyncWebSocketClient::_onDisconnect() { void AsyncWebSocketClient::_onDisconnect() {
// Serial.println("onDis"); // Serial.println("onDis");
_client = nullptr; _client = nullptr;
_server->_handleDisconnect(this);
} }
void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
@ -857,6 +876,16 @@ AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request)
return &_clients.back(); return &_clients.back();
} }
void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) {
const auto client_id = client->id();
const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [client_id](const AsyncWebSocketClient &c) {
return c.id() == client_id;
});
if (iter != std::end(_clients)) {
_clients.erase(iter);
}
}
bool AsyncWebSocket::availableForWriteAll() { bool AsyncWebSocket::availableForWriteAll() {
return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) {
return c.queueIsFull(); return c.queueIsFull();
@ -1300,11 +1329,20 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket
} }
k.concat(key); k.concat(key);
k.concat(WS_STR_UUID); k.concat(WS_STR_UUID);
#ifdef LIBRETINY
mbedtls_sha1_context ctx;
mbedtls_sha1_init(&ctx);
mbedtls_sha1_starts(&ctx);
mbedtls_sha1_update(&ctx, (const uint8_t *)k.c_str(), k.length());
mbedtls_sha1_finish(&ctx, hash);
mbedtls_sha1_free(&ctx);
#else
SHA1Builder sha1; SHA1Builder sha1;
sha1.begin(); sha1.begin();
sha1.add((const uint8_t *)k.c_str(), k.length()); sha1.add((const uint8_t *)k.c_str(), k.length());
sha1.calculate(); sha1.calculate();
sha1.getBytes(hash); sha1.getBytes(hash);
#endif
#endif #endif
base64_encodestate _state; base64_encodestate _state;
base64_init_encodestate(&_state); base64_init_encodestate(&_state);

View file

@ -5,8 +5,14 @@
#define ASYNCWEBSOCKET_H_ #define ASYNCWEBSOCKET_H_
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32
#if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#ifdef LIBRETINY
#ifdef round
#undef round
#endif
#endif
#include <mutex> #include <mutex>
#ifndef WS_MAX_QUEUED_MESSAGES #ifndef WS_MAX_QUEUED_MESSAGES
#define WS_MAX_QUEUED_MESSAGES 32 #define WS_MAX_QUEUED_MESSAGES 32
@ -152,7 +158,7 @@ private:
uint32_t _clientId; uint32_t _clientId;
AwsClientStatus _status; AwsClientStatus _status;
#ifdef ESP32 #ifdef ESP32
mutable std::mutex _lock; mutable std::recursive_mutex _lock;
#endif #endif
std::deque<AsyncWebSocketControl> _controlQueue; std::deque<AsyncWebSocketControl> _controlQueue;
std::deque<AsyncWebSocketMessage> _messageQueue; std::deque<AsyncWebSocketMessage> _messageQueue;
@ -291,7 +297,7 @@ private:
String _url; String _url;
std::list<AsyncWebSocketClient> _clients; std::list<AsyncWebSocketClient> _clients;
uint32_t _cNextId; uint32_t _cNextId;
AwsEventHandler _eventHandler{nullptr}; AwsEventHandler _eventHandler;
AwsHandshakeHandler _handshakeHandler; AwsHandshakeHandler _handshakeHandler;
bool _enabled; bool _enabled;
#ifdef ESP32 #ifdef ESP32
@ -305,8 +311,8 @@ public:
PARTIALLY_ENQUEUED = 2, PARTIALLY_ENQUEUED = 2,
} SendStatus; } SendStatus;
explicit AsyncWebSocket(const char *url) : _url(url), _cNextId(1), _enabled(true) {} explicit AsyncWebSocket(const char *url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {}
AsyncWebSocket(const String &url) : _url(url), _cNextId(1), _enabled(true) {} AsyncWebSocket(const String &url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {}
~AsyncWebSocket(){}; ~AsyncWebSocket(){};
const char *url() const { const char *url() const {
return _url.c_str(); return _url.c_str();
@ -385,6 +391,7 @@ public:
return _cNextId++; return _cNextId++;
} }
AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request); AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request);
void _handleDisconnect(AsyncWebSocketClient *client);
void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
bool canHandle(AsyncWebServerRequest *request) const override final; bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final; void handleRequest(AsyncWebServerRequest *request) override final;
@ -413,4 +420,86 @@ public:
} }
}; };
class AsyncWebSocketMessageHandler {
public:
AwsEventHandler eventHandler() const {
return _handler;
}
void onConnect(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> onConnect) {
_onConnect = onConnect;
}
void onDisconnect(std::function<void(AsyncWebSocket *server, uint32_t clientId)> onDisconnect) {
_onDisconnect = onDisconnect;
}
/**
* Error callback
* @param reason null-terminated string
* @param len length of the string
*/
void onError(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> onError) {
_onError = onError;
}
/**
* Complete message callback
* @param data pointer to the data (binary or null-terminated string). This handler expects the user to know which data type he uses.
*/
void onMessage(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> onMessage) {
_onMessage = onMessage;
}
/**
* Fragmented message callback
* @param data pointer to the data (binary or null-terminated string), will be null-terminated. This handler expects the user to know which data type he uses.
*/
// clang-format off
void onFragment(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> onFragment) {
_onFragment = onFragment;
}
// clang-format on
private:
// clang-format off
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> _onConnect;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> _onError;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> _onMessage;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> _onFragment;
std::function<void(AsyncWebSocket *server, uint32_t clientId)> _onDisconnect;
// clang-format on
// this handler is meant to only support 1-frame messages (== unfragmented messages)
AwsEventHandler _handler = [this](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_CONNECT) {
if (_onConnect) {
_onConnect(server, client);
}
} else if (type == WS_EVT_DISCONNECT) {
if (_onDisconnect) {
_onDisconnect(server, client->id());
}
} else if (type == WS_EVT_ERROR) {
if (_onError) {
_onError(server, client, *((uint16_t *)arg), (const char *)data, len);
}
} else if (type == WS_EVT_DATA) {
AwsFrameInfo *info = (AwsFrameInfo *)arg;
if (info->opcode == WS_TEXT) {
data[len] = 0;
}
if (info->final && info->index == 0 && info->len == len) {
if (_onMessage) {
_onMessage(server, client, data, len);
}
} else {
if (_onFragment) {
_onFragment(server, client, info, data, len);
}
}
}
};
};
#endif /* ASYNCWEBSOCKET_H_ */ #endif /* ASYNCWEBSOCKET_H_ */

View file

@ -4,9 +4,10 @@
#ifndef _ESPAsyncWebServer_H_ #ifndef _ESPAsyncWebServer_H_
#define _ESPAsyncWebServer_H_ #define _ESPAsyncWebServer_H_
#include "Arduino.h" #include <Arduino.h>
#include <FS.h>
#include <lwip/tcpbase.h>
#include "FS.h"
#include <algorithm> #include <algorithm>
#include <deque> #include <deque>
#include <functional> #include <functional>
@ -14,16 +15,13 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#ifdef ESP32 #if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include <WiFi.h>
#elif defined(ESP8266) #elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h> #include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h> #include <RPAsyncTCP.h>
#include <HTTP_Method.h> #include <HTTP_Method.h>
#include <WiFi.h>
#include <http_parser.h> #include <http_parser.h>
#else #else
#error Platform not supported #error Platform not supported
@ -131,12 +129,20 @@ private:
String _value; String _value;
public: public:
AsyncWebHeader() {}
AsyncWebHeader(const AsyncWebHeader &) = default; AsyncWebHeader(const AsyncWebHeader &) = default;
AsyncWebHeader(AsyncWebHeader &&) = default;
AsyncWebHeader(const char *name, const char *value) : _name(name), _value(value) {} AsyncWebHeader(const char *name, const char *value) : _name(name), _value(value) {}
AsyncWebHeader(const String &name, const String &value) : _name(name), _value(value) {} AsyncWebHeader(const String &name, const String &value) : _name(name), _value(value) {}
AsyncWebHeader(const String &data);
#ifndef ESP8266
[[deprecated("Use AsyncWebHeader::parse(data) instead")]]
#endif
AsyncWebHeader(const String &data)
: AsyncWebHeader(parse(data)){};
AsyncWebHeader &operator=(const AsyncWebHeader &) = default; AsyncWebHeader &operator=(const AsyncWebHeader &) = default;
AsyncWebHeader &operator=(AsyncWebHeader &&other) = default;
const String &name() const { const String &name() const {
return _name; return _name;
@ -144,7 +150,18 @@ public:
const String &value() const { const String &value() const {
return _value; return _value;
} }
String toString() const; String toString() const;
// returns true if the header is valid
operator bool() const {
return _name.length();
}
static const AsyncWebHeader parse(const String &data) {
return parse(data.c_str());
}
static const AsyncWebHeader parse(const char *data);
}; };
/* /*
@ -180,6 +197,7 @@ class AsyncWebServerRequest {
using FS = fs::FS; using FS = fs::FS;
friend class AsyncWebServer; friend class AsyncWebServer;
friend class AsyncCallbackWebHandler; friend class AsyncCallbackWebHandler;
friend class AsyncFileResponse;
private: private:
AsyncClient *_client; AsyncClient *_client;
@ -251,6 +269,8 @@ private:
void _send(); void _send();
void _runMiddlewareChain(); void _runMiddlewareChain();
static void _getEtag(uint8_t trailer[4], char *serverETag);
public: public:
File _tempFile; File _tempFile;
void *_tempObject; void *_tempObject;
@ -363,13 +383,7 @@ public:
send(beginResponse(code, contentType, content, len, callback)); send(beginResponse(code, contentType, content, len, callback));
} }
void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) { void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr);
if (fs.exists(path) || (!download && fs.exists(path + asyncsrv::T__gz))) {
send(beginResponse(fs, path, contentType, download, callback));
} else {
send(404);
}
}
void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) { void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) {
send(fs, path, contentType.c_str(), download, callback); send(fs, path, contentType.c_str(), download, callback);
} }
@ -462,7 +476,9 @@ public:
} }
AsyncWebServerResponse *beginChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); AsyncWebServerResponse *beginChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncWebServerResponse *beginChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); AsyncWebServerResponse *beginChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) {
return beginChunkedResponse(contentType.c_str(), callback, templateCallback);
}
AsyncResponseStream *beginResponseStream(const char *contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE); AsyncResponseStream *beginResponseStream(const char *contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE);
AsyncResponseStream *beginResponseStream(const String &contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) { AsyncResponseStream *beginResponseStream(const String &contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) {
@ -531,6 +547,9 @@ public:
* @return const AsyncWebParameter* * @return const AsyncWebParameter*
*/ */
const AsyncWebParameter *getParam(size_t num) const; const AsyncWebParameter *getParam(size_t num) const;
const AsyncWebParameter *getParam(int num) const {
return num < 0 ? nullptr : getParam((size_t)num);
}
size_t args() const { size_t args() const {
return params(); return params();
@ -546,7 +565,13 @@ public:
const String &arg(const __FlashStringHelper *data) const; // get request argument value by F(name) const String &arg(const __FlashStringHelper *data) const; // get request argument value by F(name)
#endif #endif
const String &arg(size_t i) const; // get request argument value by number const String &arg(size_t i) const; // get request argument value by number
const String &arg(int i) const {
return i < 0 ? emptyString : arg((size_t)i);
};
const String &argName(size_t i) const; // get request argument name by number const String &argName(size_t i) const; // get request argument name by number
const String &argName(int i) const {
return i < 0 ? emptyString : argName((size_t)i);
};
bool hasArg(const char *name) const; // check if argument exists bool hasArg(const char *name) const; // check if argument exists
bool hasArg(const String &name) const { bool hasArg(const String &name) const {
return hasArg(name.c_str()); return hasArg(name.c_str());
@ -556,6 +581,9 @@ public:
#endif #endif
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const; const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const;
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(int i) const {
return i < 0 ? emptyString : pathArg((size_t)i);
}
// get request header value by name // get request header value by name
const String &header(const char *name) const; const String &header(const char *name) const;
@ -568,7 +596,13 @@ public:
#endif #endif
const String &header(size_t i) const; // get request header value by number const String &header(size_t i) const; // get request header value by number
const String &header(int i) const {
return i < 0 ? emptyString : header((size_t)i);
};
const String &headerName(size_t i) const; // get request header name by number const String &headerName(size_t i) const; // get request header name by number
const String &headerName(int i) const {
return i < 0 ? emptyString : headerName((size_t)i);
};
size_t headers() const; // get header count size_t headers() const; // get header count
@ -590,6 +624,9 @@ public:
#endif #endif
const AsyncWebHeader *getHeader(size_t num) const; const AsyncWebHeader *getHeader(size_t num) const;
const AsyncWebHeader *getHeader(int num) const {
return num < 0 ? nullptr : getHeader((size_t)num);
};
const std::list<AsyncWebHeader> &getHeaders() const { const std::list<AsyncWebHeader> &getHeaders() const {
return _headers; return _headers;
@ -1011,6 +1048,10 @@ public:
setContentType(type.c_str()); setContentType(type.c_str());
} }
void setContentType(const char *type); void setContentType(const char *type);
bool addHeader(AsyncWebHeader &&header, bool replaceExisting = true);
bool addHeader(const AsyncWebHeader &header, bool replaceExisting = true) {
return header && addHeader(header.name(), header.value(), replaceExisting);
}
bool addHeader(const char *name, const char *value, bool replaceExisting = true); bool addHeader(const char *name, const char *value, bool replaceExisting = true);
bool addHeader(const String &name, const String &value, bool replaceExisting = true) { bool addHeader(const String &name, const String &value, bool replaceExisting = true) {
return addHeader(name.c_str(), value.c_str(), replaceExisting); return addHeader(name.c_str(), value.c_str(), replaceExisting);
@ -1069,6 +1110,15 @@ public:
void begin(); void begin();
void end(); void end();
tcp_state state() const {
#ifdef ESP8266
// ESPAsyncTCP and RPAsyncTCP methods are not corrected declared with const for immutable ones.
return static_cast<tcp_state>(const_cast<AsyncWebServer *>(this)->_server.status());
#else
return static_cast<tcp_state>(_server.status());
#endif
}
#if ASYNC_TCP_SSL_ENABLED #if ASYNC_TCP_SSL_ENABLED
void onSslFileRequest(AcSSlFileHandler cb, void *arg); void onSslFileRequest(AcSSlFileHandler cb, void *arg);
void beginSecure(const char *cert, const char *private_key_file, const char *password); void beginSecure(const char *cert, const char *private_key_file, const char *password);

View file

@ -172,7 +172,11 @@ void AsyncLoggingMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNex
return; return;
} }
_out->print(F("* Connection from ")); _out->print(F("* Connection from "));
#ifndef LIBRETINY
_out->print(request->client()->remoteIP().toString()); _out->print(request->client()->remoteIP().toString());
#else
_out->print(request->client()->remoteIP());
#endif
_out->print(':'); _out->print(':');
_out->println(request->client()->remotePort()); _out->println(request->client()->remotePort());
_out->print('>'); _out->print('>');

View file

@ -19,7 +19,6 @@ class AsyncStaticWebHandler : public AsyncWebHandler {
private: private:
bool _getFile(AsyncWebServerRequest *request) const; bool _getFile(AsyncWebServerRequest *request) const;
bool _searchFile(AsyncWebServerRequest *request, const String &path); bool _searchFile(AsyncWebServerRequest *request, const String &path);
uint8_t _countBits(const uint8_t value) const;
protected: protected:
FS _fs; FS _fs;

View file

@ -187,15 +187,6 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
return found; return found;
} }
uint8_t AsyncStaticWebHandler::_countBits(const uint8_t value) const {
uint8_t w = value;
uint8_t n;
for (n = 0; w != 0; n++) {
w &= w - 1;
}
return n;
}
void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
// Get the filename from request->_tempObject and free it // Get the filename from request->_tempObject and free it
String filename((char *)request->_tempObject); String filename((char *)request->_tempObject);
@ -218,11 +209,14 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
char buf[len]; char buf[len];
char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10); char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10);
etag = ret ? String(ret) : String(request->_tempFile.size()); etag = ret ? String(ret) : String(request->_tempFile.size());
#elif defined(LIBRETINY)
long val = lw ^ request->_tempFile.size();
etag = String(val);
#else #else
etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp
#endif #endif
} else { } else {
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
etag = String(request->_tempFile.size()); etag = String(request->_tempFile.size());
#else #else
etag = request->_tempFile.size(); etag = request->_tempFile.size();

View file

@ -22,10 +22,10 @@ enum {
}; };
AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c) AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c)
: _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), _url(), _host(), : _client(c), _server(s), _handler(NULL), _response(NULL), _onDisconnectfn(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY),
_contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false),
_expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0),
_itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) {
c->onError( c->onError(
[](void *r, AsyncClient *c, int8_t error) { [](void *r, AsyncClient *c, int8_t error) {
(void)c; (void)c;
@ -341,10 +341,10 @@ bool AsyncWebServerRequest::_parseReqHead() {
} }
bool AsyncWebServerRequest::_parseReqHeader() { bool AsyncWebServerRequest::_parseReqHeader() {
int index = _temp.indexOf(':'); AsyncWebHeader header = AsyncWebHeader::parse(_temp);
if (index) { if (header) {
String name(_temp.substring(0, index)); const String &name = header.name();
String value(_temp.substring(index + 2)); const String &value = header.value();
if (name.equalsIgnoreCase(T_Host)) { if (name.equalsIgnoreCase(T_Host)) {
_host = value; _host = value;
} else if (name.equalsIgnoreCase(T_Content_Type)) { } else if (name.equalsIgnoreCase(T_Content_Type)) {
@ -392,9 +392,9 @@ bool AsyncWebServerRequest::_parseReqHeader() {
_reqconntype = RCT_EVENT; _reqconntype = RCT_EVENT;
} }
} }
_headers.emplace_back(name, value); _headers.emplace_back(std::move(header));
} }
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
// Ancient PRI core does not have String::clear() method 8-() // Ancient PRI core does not have String::clear() method 8-()
_temp = emptyString; _temp = emptyString;
#else #else
@ -419,7 +419,7 @@ void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) {
_params.emplace_back(name, urlDecode(value), true); _params.emplace_back(name, urlDecode(value), true);
} }
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
// Ancient PRI core does not have String::clear() method 8-() // Ancient PRI core does not have String::clear() method 8-()
_temp = emptyString; _temp = emptyString;
#else #else

View file

@ -10,7 +10,7 @@
#undef max #undef max
#endif #endif
#include "literals.h" #include "literals.h"
#include <StreamString.h> #include <cbuf.h>
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -157,7 +157,7 @@ public:
class AsyncResponseStream : public AsyncAbstractResponse, public Print { class AsyncResponseStream : public AsyncAbstractResponse, public Print {
private: private:
StreamString _content; std::unique_ptr<cbuf> _content;
public: public:
AsyncResponseStream(const char *contentType, size_t bufferSize); AsyncResponseStream(const char *contentType, size_t bufferSize);
@ -168,6 +168,12 @@ public:
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t write(const uint8_t *data, size_t len); size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data); size_t write(uint8_t data);
/**
* @brief Returns the number of bytes available in the stream.
*/
size_t available() const {
return _content->available();
}
using Print::write; using Print::write;
}; };

View file

@ -134,6 +134,30 @@ bool AsyncWebServerResponse::headerMustBePresentOnce(const String &name) {
return false; return false;
} }
bool AsyncWebServerResponse::addHeader(AsyncWebHeader &&header, bool replaceExisting) {
if (!header) {
return false; // invalid header
}
for (auto i = _headers.begin(); i != _headers.end(); ++i) {
if (i->name().equalsIgnoreCase(header.name())) {
// header already set
if (replaceExisting) {
// remove, break and add the new one
_headers.erase(i);
break;
} else if (headerMustBePresentOnce(i->name())) { // we can have only one header with that name
// do not update
return false;
} else {
break; // accept multiple headers with the same name
}
}
}
// header was not found found, or existing one was removed
_headers.emplace_back(std::move(header));
return true;
}
bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) { bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) {
for (auto i = _headers.begin(); i != _headers.end(); ++i) { for (auto i = _headers.begin(); i != _headers.end(); ++i) {
if (i->name().equalsIgnoreCase(name)) { if (i->name().equalsIgnoreCase(name)) {
@ -595,6 +619,16 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size
* File Response * File Response
* */ * */
/**
* @brief Sets the content type based on the file path extension
*
* This method determines the appropriate MIME content type for a file based on its
* file extension. It supports both external content type functions (if available)
* and an internal mapping of common file extensions to their corresponding MIME types.
*
* @param path The file path string from which to extract the extension
* @note The method modifies the internal _contentType member variable
*/
void AsyncFileResponse::_setContentTypeFromPath(const String &path) { void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#if HAVE_EXTERN_GET_Content_Type_FUNCTION #if HAVE_EXTERN_GET_Content_Type_FUNCTION
#ifndef ESP8266 #ifndef ESP8266
@ -604,41 +638,47 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#endif #endif
_contentType = getContentType(path); _contentType = getContentType(path);
#else #else
if (path.endsWith(T__html)) { const char *cpath = path.c_str();
const char *dot = strrchr(cpath, '.');
if (!dot) {
_contentType = T_text_plain;
return;
}
if (strcmp(dot, T__html) == 0 || strcmp(dot, T__htm) == 0) {
_contentType = T_text_html; _contentType = T_text_html;
} else if (path.endsWith(T__htm)) { } else if (strcmp(dot, T__css) == 0) {
_contentType = T_text_html;
} else if (path.endsWith(T__css)) {
_contentType = T_text_css; _contentType = T_text_css;
} else if (path.endsWith(T__json)) { } else if (strcmp(dot, T__js) == 0) {
_contentType = T_application_json;
} else if (path.endsWith(T__js)) {
_contentType = T_application_javascript; _contentType = T_application_javascript;
} else if (path.endsWith(T__png)) { } else if (strcmp(dot, T__json) == 0) {
_contentType = T_application_json;
} else if (strcmp(dot, T__png) == 0) {
_contentType = T_image_png; _contentType = T_image_png;
} else if (path.endsWith(T__gif)) { } else if (strcmp(dot, T__ico) == 0) {
_contentType = T_image_gif;
} else if (path.endsWith(T__jpg)) {
_contentType = T_image_jpeg;
} else if (path.endsWith(T__ico)) {
_contentType = T_image_x_icon; _contentType = T_image_x_icon;
} else if (path.endsWith(T__svg)) { } else if (strcmp(dot, T__svg) == 0) {
_contentType = T_image_svg_xml; _contentType = T_image_svg_xml;
} else if (path.endsWith(T__eot)) { } else if (strcmp(dot, T__jpg) == 0) {
_contentType = T_font_eot; _contentType = T_image_jpeg;
} else if (path.endsWith(T__woff)) { } else if (strcmp(dot, T__gif) == 0) {
_contentType = T_font_woff; _contentType = T_image_gif;
} else if (path.endsWith(T__woff2)) { } else if (strcmp(dot, T__woff2) == 0) {
_contentType = T_font_woff2; _contentType = T_font_woff2;
} else if (path.endsWith(T__ttf)) { } else if (strcmp(dot, T__woff) == 0) {
_contentType = T_font_woff;
} else if (strcmp(dot, T__ttf) == 0) {
_contentType = T_font_ttf; _contentType = T_font_ttf;
} else if (path.endsWith(T__xml)) { } else if (strcmp(dot, T__eot) == 0) {
_contentType = T_font_eot;
} else if (strcmp(dot, T__xml) == 0) {
_contentType = T_text_xml; _contentType = T_text_xml;
} else if (path.endsWith(T__pdf)) { } else if (strcmp(dot, T__pdf) == 0) {
_contentType = T_application_pdf; _contentType = T_application_pdf;
} else if (path.endsWith(T__zip)) { } else if (strcmp(dot, T__zip) == 0) {
_contentType = T_application_zip; _contentType = T_application_zip;
} else if (path.endsWith(T__gz)) { } else if (strcmp(dot, T__gz) == 0) {
_contentType = T_application_x_gzip; _contentType = T_application_x_gzip;
} else { } else {
_contentType = T_text_plain; _contentType = T_text_plain;
@ -646,40 +686,73 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#endif #endif
} }
/**
* @brief Constructor for AsyncFileResponse that handles file serving with compression support
*
* This constructor creates an AsyncFileResponse object that can serve files from a filesystem,
* with automatic fallback to gzip-compressed versions if the original file is not found.
* It also handles ETag generation for caching and supports both inline and download modes.
*
* @param fs Reference to the filesystem object used to open files
* @param path Path to the file to be served (without compression extension)
* @param contentType MIME type of the file content (empty string for auto-detection)
* @param download If true, file will be served as download attachment; if false, as inline content
* @param callback Template processor callback for dynamic content processing
*/
AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) { : AsyncAbstractResponse(callback) {
_code = 200; // Try to open the uncompressed version first
_content = fs.open(path, fs::FileOpenMode::read);
if (_content.available()) {
_path = path; _path = path;
_contentLength = _content.size();
} else {
// Try to open the compressed version (.gz)
_path = path + asyncsrv::T__gz;
_content = fs.open(_path, fs::FileOpenMode::read);
_contentLength = _content.size();
if (!download && !fs.exists(_path) && fs.exists(_path + T__gz)) { if (_content.seek(_contentLength - 8)) {
_path = _path + T__gz;
addHeader(T_Content_Encoding, T_gzip, false); addHeader(T_Content_Encoding, T_gzip, false);
_callback = nullptr; // Unable to process zipped templates _callback = nullptr; // Unable to process zipped templates
_sendContentLength = true; _sendContentLength = true;
_chunked = false; _chunked = false;
// Add ETag and cache headers
uint8_t crcInTrailer[4];
_content.read(crcInTrailer, sizeof(crcInTrailer));
char serverETag[9];
AsyncWebServerRequest::_getEtag(crcInTrailer, serverETag);
addHeader(T_ETag, serverETag, true);
addHeader(T_Cache_Control, T_no_cache, true);
_content.seek(0);
} else {
// File is corrupted or invalid
_code = 404;
return;
}
} }
_content = fs.open(_path, fs::FileOpenMode::read); if (*contentType != '\0') {
_contentLength = _content.size();
if (strlen(contentType) == 0) {
_setContentTypeFromPath(path); _setContentTypeFromPath(path);
} else { } else {
_contentType = contentType; _contentType = contentType;
} }
if (download) {
// Extract filename from path and set as download attachment
int filenameStart = path.lastIndexOf('/') + 1; int filenameStart = path.lastIndexOf('/') + 1;
char buf[26 + path.length() - filenameStart]; char buf[26 + path.length() - filenameStart];
char *filename = (char *)path.c_str() + filenameStart; char *filename = (char *)path.c_str() + filenameStart;
if (download) {
// set filename and force download
snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename);
} else {
// set filename and force rendering
snprintf_P(buf, sizeof(buf), PSTR("inline"));
}
addHeader(T_Content_Disposition, buf, false); addHeader(T_Content_Disposition, buf, false);
} else {
// Serve file inline (display in browser)
addHeader(T_Content_Disposition, PSTR("inline"), false);
}
_code = 200;
} }
AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
@ -820,7 +893,9 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
_code = 200; _code = 200;
_contentLength = 0; _contentLength = 0;
_contentType = contentType; _contentType = contentType;
if (!_content.reserve(bufferSize)) { // internal buffer will be null on allocation failure
_content = std::unique_ptr<cbuf>(new cbuf(bufferSize));
if (bufferSize && _content->size() < bufferSize) {
#ifdef ESP32 #ifdef ESP32
log_e("Failed to allocate"); log_e("Failed to allocate");
#endif #endif
@ -828,14 +903,26 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
} }
size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) { size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) {
return _content.readBytes((char *)buf, maxLen); return _content->read((char *)buf, maxLen);
} }
size_t AsyncResponseStream::write(const uint8_t *data, size_t len) { size_t AsyncResponseStream::write(const uint8_t *data, size_t len) {
if (_started()) { if (_started()) {
return 0; return 0;
} }
size_t written = _content.write(data, len); if (len > _content->room()) {
size_t needed = len - _content->room();
_content->resizeAdd(needed);
// log a warning if allocation failed, but do not return: keep writing the bytes we can
// with _content->write: if len is more than the available size in the buffer, only
// the available size will be written
if (len > _content->room()) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
}
}
size_t written = _content->write((const char *)data, len);
_contentLength += written; _contentLength += written;
return written; return written;
} }

View file

@ -4,10 +4,18 @@
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#include "WebHandlerImpl.h" #include "WebHandlerImpl.h"
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#else
#error Platform not supported
#endif
using namespace asyncsrv; using namespace asyncsrv;
bool ON_STA_FILTER(AsyncWebServerRequest *request) { bool ON_STA_FILTER(AsyncWebServerRequest *request) {
#ifndef CONFIG_IDF_TARGET_ESP32H2 #if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
return WiFi.localIP() == request->client()->localIP(); return WiFi.localIP() == request->client()->localIP();
#else #else
return false; return false;
@ -15,7 +23,7 @@ bool ON_STA_FILTER(AsyncWebServerRequest *request) {
} }
bool ON_AP_FILTER(AsyncWebServerRequest *request) { bool ON_AP_FILTER(AsyncWebServerRequest *request) {
#ifndef CONFIG_IDF_TARGET_ESP32H2 #if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
return WiFi.localIP() != request->client()->localIP(); return WiFi.localIP() != request->client()->localIP();
#else #else
return false; return false;

View file

@ -213,6 +213,12 @@
# pragma GCC system_header # pragma GCC system_header
# endif # endif
#endif #endif
#ifdef true
# undef true
#endif
#ifdef false
# undef false
#endif
#define ARDUINOJSON_CONCAT_(A, B) A##B #define ARDUINOJSON_CONCAT_(A, B) A##B
#define ARDUINOJSON_CONCAT2(A, B) ARDUINOJSON_CONCAT_(A, B) #define ARDUINOJSON_CONCAT2(A, B) ARDUINOJSON_CONCAT_(A, B)
#define ARDUINOJSON_CONCAT3(A, B, C) \ #define ARDUINOJSON_CONCAT3(A, B, C) \
@ -239,11 +245,11 @@
#define ARDUINOJSON_BIN2ALPHA_1111() P #define ARDUINOJSON_BIN2ALPHA_1111() P
#define ARDUINOJSON_BIN2ALPHA_(A, B, C, D) ARDUINOJSON_BIN2ALPHA_##A##B##C##D() #define ARDUINOJSON_BIN2ALPHA_(A, B, C, D) ARDUINOJSON_BIN2ALPHA_##A##B##C##D()
#define ARDUINOJSON_BIN2ALPHA(A, B, C, D) ARDUINOJSON_BIN2ALPHA_(A, B, C, D) #define ARDUINOJSON_BIN2ALPHA(A, B, C, D) ARDUINOJSON_BIN2ALPHA_(A, B, C, D)
#define ARDUINOJSON_VERSION "7.4.1" #define ARDUINOJSON_VERSION "7.4.2"
#define ARDUINOJSON_VERSION_MAJOR 7 #define ARDUINOJSON_VERSION_MAJOR 7
#define ARDUINOJSON_VERSION_MINOR 4 #define ARDUINOJSON_VERSION_MINOR 4
#define ARDUINOJSON_VERSION_REVISION 1 #define ARDUINOJSON_VERSION_REVISION 2
#define ARDUINOJSON_VERSION_MACRO V741 #define ARDUINOJSON_VERSION_MACRO V742
#ifndef ARDUINOJSON_VERSION_NAMESPACE #ifndef ARDUINOJSON_VERSION_NAMESPACE
# define ARDUINOJSON_VERSION_NAMESPACE \ # define ARDUINOJSON_VERSION_NAMESPACE \
ARDUINOJSON_CONCAT5( \ ARDUINOJSON_CONCAT5( \

View file

@ -300,7 +300,7 @@ class AsyncServer : public AsyncSocketBase
void setNoDelay(bool nodelay) { _noDelay = nodelay; } void setNoDelay(bool nodelay) { _noDelay = nodelay; }
bool getNoDelay() { return _noDelay; } bool getNoDelay() { return _noDelay; }
uint8_t status(); uint8_t status() const;
protected: protected:
uint16_t _port; uint16_t _port;

View file

@ -12,7 +12,7 @@
src_dir = ./Software src_dir = ./Software
[env:esp32dev] [env:esp32dev]
platform = https://github.com/pioarduino/platform-espressif32/archive/refs/tags/54.03.21-new.zip ; Arduino 3.2.1 IDF 5.4.1 platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30/platform-espressif32.zip
board = esp32dev board = esp32dev
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = default, time, log2file monitor_filters = default, time, log2file
@ -20,3 +20,33 @@ board_build.partitions = min_spiffs.csv
framework = arduino framework = arduino
build_flags = -I include build_flags = -I include
lib_deps = lib_deps =
[env:lilygo_330]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30/platform-espressif32.zip
board = esp32dev
monitor_speed = 115200
monitor_filters = default, time, log2file
board_build.partitions = min_spiffs.csv
framework = arduino
build_flags = -I include -DHW_LILYGO -DCOMMON_IMAGE
lib_deps =
[env:stark_330]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30/platform-espressif32.zip
board = esp32dev
monitor_speed = 115200
monitor_filters = default, time, log2file, esp32_exception_decoder
board_build.partitions = min_spiffs.csv
framework = arduino
build_flags = -I include -DHW_STARK -DCOMMON_IMAGE
lib_deps =
[env:stark_330_debuglog]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30/platform-espressif32.zip
board = esp32dev
monitor_speed = 115200
monitor_filters = default, time, log2file, esp32_exception_decoder
board_build.partitions = min_spiffs.csv
framework = arduino
build_flags = -I include -DHW_STARK -DCOMMON_IMAGE -DDEBUG_VIA_USB
lib_deps =