Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

42
tools/openocpp/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# CLion
.idea/
# CMakeLists
cmake-build*/
build/
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Other
metrixpp.db

201
tools/openocpp/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

149
tools/openocpp/README.md Normal file
View File

@@ -0,0 +1,149 @@
# OpenOCPP
Multi-platform OCPP 1.6/2.0.1 embedded software for charging stations.
Website: [https://openocpp.com](https://openocpp.com)
## Certifications
This project has been tested and passed core test cases as part of the OCTT 2.0.1 Vendor Declaration of Conformance:
![Screenshot of Vendor Declaration of Conformance summary](docs/assets/vdoc-summary.png)
For more information on specific capabilities please contact support@openocpp.com
## ESP32 Demo
### Getting started
The ESP32 demo in this project depends on the **ESP-IDF** toolchain version **v5.2.3**. Setup instructions can be found
here:
- https://docs.espressif.com/projects/esp-idf/en/v5.2.5/esp32/get-started/index.html
An example setup process on an **Ubuntu VM** is provided below:
```shell
# Install Prerequisites
sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
# Get ESP-IDF
mkdir -p ~/esp
cd ~/esp
git clone -b v5.2.5 --recursive https://github.com/espressif/esp-idf.git
# Set up the Tools
cd ~/esp/esp-idf
./install.sh esp32
# Activate ESP-IDF
. $HOME/esp/esp-idf/export.sh
```
Note: the activation step above must be run every time a new terminal is opened before running any of the `idf.py`
commands.
### Building the project
Open a terminal in the OpenOCPP project directory and run the following commands:
```shell
# Navigate to the demo-esp32 folder (if necessary)
cd demo-esp32
# Active ESP-IDF (if necessary)
. $HOME/esp/esp-idf/export.sh
# Build the project - note: this step will take a few minutes
idf.py build
```
### Running the project
From a terminal in the OpenOCPP project run the following commands:
```shell
# Navigate to the demo-esp32 folder (if necessary)
cd demo-esp32
# Active ESP-IDF (if necessary)
. $HOME/esp/esp-idf/export.sh
# Flash and run the project
idf.py -p /dev/ttyUSB0 flash && idf.py -p /dev/ttyUSB0 monitor
```
Note that the above assumes that the ESP32 module/dev kit is connected to the `/dev/ttyUSB0` interface in the system/VM.
_For example:_ an ESP32 dev kit plugged into the host machine can generally be exposed to a KVM VM as follows:
- Press: Add Hardware
- Select: USB Host Device
- Select: Silicon Labs CP210x UART Bridge
- Press: Finish
Assuming the firmware is flashed successfully onto the ESP32 module the `monitor` command will restart the firmware and
begin trailing the log messages. This should look something like the following:
```text
I (31) boot: ESP-IDF v5.2.3 2nd stage bootloader
I (31) boot: compile time Feb 24 2025 10:14:06
I (31) boot: Multicore bootloader
I (35) boot: chip revision: v3.0
I (39) boot.esp32: SPI Speed : 80MHz
I (44) boot.esp32: SPI Mode : DIO
I (48) boot.esp32: SPI Flash Size : 4MB
I (53) boot: Enabling RNG early entropy source...
I (58) boot: Partition Table:
I (62) boot: ## Label Usage Type ST Offset Length
I (69) boot: 0 nvs WiFi data 01 02 00009000 00003000
I (77) boot: 1 pmjournal Unknown data 01 06 0000c000 00002000
I (84) boot: 2 otadata OTA data 01 00 0000e000 00002000
I (92) boot: 3 app1 OTA app 00 11 00010000 001e0000
I (99) boot: 4 app0 OTA app 00 10 001f0000 001e0000
I (106) boot: 5 spiffs Unknown data 01 82 003d0000 0002f000
I (114) boot: 6 sernr NVS keys 01 04 003ff000 00001000
I (122) boot: End of partition table
...
```
### Provisioning to the simulator
When the firmware finishes initializing it begins broadcasting an open wifi access point for provisioning purposes named
**Charger Simulator**. Connect to the access point and perform the following steps to connect the device to an OCPP
back-end:
- Open https://192.168.4.1 in your browser
- Note that both an HTTP and HTTPS server (with a self-signed certificate) is run for the purpose of the demo
- Accept the self-signed certificate
- Submit the provisioning information:
- Select the Wifi network (may take a moment to load)
- Enter/confirm the Wifi password
- Enter the OCPP network URL - for example:
- ws://0123456789abcdef.octt.openchargealliance.org:25316
- wss://my-ocpp-backend.com
- Enter the OCPP ID to use
- The value here is appended to the network URL, for example with an OCPP ID of TEST the simulator will connect to
the following URLs in the examples above:
- ws://0123456789abcdef.octt.openchargealliance.org:25316/TEST
- wss://my-ocpp-backend.com/TEST
- Select the OCPP protocol to use
- Press: Submit
After the above is completed the simulator will restart and connect to the provided OCPP back-end.
### Connecting to the running simulator
The running simulator can be controlled either by connecting to the simulator's IP address on the Wifi network it's
connected to, or by re-connecting to the **Charger Simulator** Wifi network. Note that the **Charger Simulator** network
will require the same password used to connect to the Wifi network after provisioning is complete.
When connected to the simulator the state of the device can be controlled via a web interface:
- Connect to the simulator
- Connect to the simulator's IP address on the local Wifi network: https://192.168.1.X
- Connect directly to the simulator's access point:
- Open https://192.168.4.1 in your browser (if your local network does not use the 192.168 prefix), otherwise
- Open https://10.0.0.1 in your browser (if your local network does use the 192.168 prefix)
- Select the **Configuration** tab to alter the charger's configuration, such as:
- Altering the default networking profile slot/configuration
- Select the **Control** tab to control the simulator, such as:
- Simulate connecting/disconnecting a vehicle
- Simulate charging suspended by EV/EVSE
- Simulate an RFID or other local authorization

3
tools/openocpp/demo-esp32/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
build/
sdkconfig
managed_components/

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.5)
set(CMAKE_CXX_STANDARD 17)
if (ESP_PLATFORM)
add_compile_definitions(JSON_NO_IO)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(TestFirmware LANGUAGES C CXX)
include_directories($ENV{IDF_PATH}/components/freertos/include/esp_additions/freertos/)
endif()

View File

@@ -0,0 +1,32 @@
dependencies:
espressif/esp_websocket_client:
component_hash: f77326f0e1c38da4e9c97e17c5d649b0dd13027f2645e720e48db269638fd622
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.4.0
espressif/zlib:
component_hash: d901723af51f13fc8e5824f39f32239c847956e8fd951a05266588dc5cfbb9ae
dependencies:
- name: idf
require: private
version: '>=4.4'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.3.1
idf:
source:
type: idf
version: 5.2.5
direct_dependencies:
- espressif/esp_websocket_client
- espressif/zlib
- idf
manifest_hash: 115b87000fdb1fc149cf270df267a604f2c5a46b6d9a0c7b042451668945afa3
target: esp32
version: 2.0.0

View File

@@ -0,0 +1,48 @@
# Custom function to gzip files
function(gzip_files)
file(GLOB WEB_FILES "${CMAKE_CURRENT_SOURCE_DIR}/web/*.html" "${CMAKE_CURRENT_SOURCE_DIR}/web/*.ico")
foreach(FILE ${WEB_FILES})
get_filename_component(FILENAME ${FILE} NAME)
set(GZIPPED_FILE "${CMAKE_CURRENT_SOURCE_DIR}/web/${FILENAME}.gz")
add_custom_command(
OUTPUT ${GZIPPED_FILE}
COMMAND ${CMAKE_COMMAND} -E echo "Gzipping ${FILENAME}"
COMMAND gzip -c ${FILE} > ${GZIPPED_FILE}
DEPENDS ${FILE}
VERBATIM
)
list(APPEND GZIPPED_FILES ${GZIPPED_FILE})
endforeach()
set(GZIPPED_FILES ${GZIPPED_FILES} PARENT_SCOPE)
endfunction()
# Call the function to generate gzipped files
gzip_files()
list(APPEND SOURCES "main.cc")
list(APPEND SOURCES "../../include/openocpp/implementation/logging_esp.cc")
idf_component_register(
SRCS ${SOURCES}
INCLUDE_DIRS "."
REQUIRES app_update esp_websocket_client esp_wifi nvs_flash spiffs driver esp_eth esp_https_server esp_http_client
EMBED_TXTFILES "certs/servercert.pem" "certs/prvtkey.pem" "certs/manufacturer.pem"
EMBED_FILES
"web/web_favicon.ico.gz"
"web/web_index.html.gz"
"web/web_setup.html.gz"
"web/web_config.html.gz"
"web/web_control.html.gz"
)
target_compile_options(${COMPONENT_LIB} PRIVATE -mtext-section-literals)
# add_compile_definitions(LOG_WITH_FILE_AND_LINE)
add_compile_definitions(JSON_NO_IO)
include_directories(../../include)
include_directories(../../rapidjson/include)
include_directories(${CMAKE_BINARY_DIR})

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIUEgpLccj+t8/xH452M0XijiShXkQwDQYJKoZIhvcNAQEL
BQAwZzELMAkGA1UEBhMCQ0ExEDAOBgNVBAgMB09udGFyaW8xEDAOBgNVBAcMB1Rv
cm9udG8xEjAQBgNVBAoMCUNoYXJnZUxhYjEgMB4GA1UEAwwXVGVzdEZpcm13YXJl
U2lnbmluZ0NlcnQwHhcNMjUwMjE0MTYwMjM1WhcNMzUwMjEyMTYwMjM1WjBnMQsw
CQYDVQQGEwJDQTEQMA4GA1UECAwHT250YXJpbzEQMA4GA1UEBwwHVG9yb250bzES
MBAGA1UECgwJQ2hhcmdlTGFiMSAwHgYDVQQDDBdUZXN0RmlybXdhcmVTaWduaW5n
Q2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJi2dU3Jba0V2X9o
JMcep3L+2ZqNiKt7fgM9Ey71Ih06wFdAA+hflIwlUJm8WeBTd2HhdezfRrWlhyrE
Dcm57BQfUNuI42HtYMhrpgkUtdBfL/DHKJKUCBIpgLDPYPSOEXo8tVxEKZSDiy9Z
eTQqHuCUc5pC4nWzWnvRnogOas+evn3RnCoEdo8mIdyDXiicHpuJbqoNa7EEs/VC
4NBIj9ErFTmwjJwmbPoKsodjRQHYy36x4rZNlZzyxeWj3wTw5umxMDWFaobWlJ+5
ZT5Us+eEnXdfITGHnutKIRe7sm2zw+pObpP7EShuZpnhYsTjCOtDHkhSgOww1UrY
QAHVmJMCAwEAAaNTMFEwHQYDVR0OBBYEFC5enSgIvaycENnmHgPnv5fjX/QlMB8G
A1UdIwQYMBaAFC5enSgIvaycENnmHgPnv5fjX/QlMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBACLB/MEN9gxfaaut52sFQx1fwdsDmyMm2+j3Sjng
lEoeRYxvP4zCNXgV49ZBlrWLP/jf5Hl7IbBBEOfk45b+D2O+cbdXREs9xwT+MV+5
atAEvyieRj+7bdqmIrdFnSBETdH7dgy48P0K2ORwdZaFHpn+Ihs13hGXQJGAgBUk
1W9zZSblLvOe9NHHIzhyeDY6oS0EKkr0Qhx1sTV/qSsmVr/lBG521NfmsZX5qm/u
OgAC/Pj96Hp9R50PwZUvDMcEzx65RjiNZ/+IPgk7Vd3cYNHyzDDElRqVFAgtkJjJ
Y+xlHj2La6hL9TnGOkA9DtQdyhjET6qfIh7d5wLsDVk3ilI=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwYp7epz++0QkH
JioMD7U7BitLgpcYPi8Cid1l7snt6Kp546iQsDBJ3l8xnRtPU7ANEsjT8KxIHmyw
h/NGp94FlOKRw3ahh3yUGtowS9vdHv+S+TAfuj07NjSnKIyv5KnGZJ+fDFl4Q1tT
aQJybY1Z4itirL6/2CGEm8g/iYhLNDBsRMfpDpfXe4URyWiM3Rhf7ztqZdveb9al
3pAJZIDTLWCFQI1MvQjKamkAQkES/gZj0iUZFwbGJPBj54nkuLFLKedw7DbwgrVg
0+n3fQ9b/gQepw5PxQjyobY2DsDgGZV+MFjUmaUTa+XX68SrG4wJ+DwrkdmpHReB
vFi1Hg1hAgMBAAECggEAaTCnZkl/7qBjLexIryC/CBBJyaJ70W1kQ7NMYfniWwui
f0aRxJgOdD81rjTvkINsPp+xPRQO6oOadjzdjImYEuQTqrJTEUnntbu924eh+2D9
Mf2CAanj0mglRnscS9mmljZ0KzoGMX6Z/EhnuS40WiJTlWlH6MlQU/FDnwC6U34y
JKy6/jGryfsx+kGU/NRvKSru6JYJWt5v7sOrymHWD62IT59h3blOiP8GMtYKeQlX
49om9Mo1VTIFASY3lrxmexbY+6FG8YO+tfIe0tTAiGrkb9Pz6tYbaj9FjEWOv4Vc
+3VMBUVdGJjgqvE8fx+/+mHo4Rg69BUPfPSrpEg7sQKBgQDlL85G04VZgrNZgOx6
pTlCCl/NkfNb1OYa0BELqWINoWaWQHnm6lX8YjrUjwRpBF5s7mFhguFjUjp/NW6D
0EEg5BmO0ePJ3dLKSeOA7gMo7y7kAcD/YGToqAaGljkBI+IAWK5Su5yldrECTQKG
YnMKyQ1MWUfCYEwHtPvFvE5aPwKBgQDFBWXekpxHIvt/B41Cl/TftAzE7/f58JjV
MFo/JCh9TDcH6N5TMTRS1/iQrv5M6kJSSrHnq8pqDXOwfHLwxetpk9tr937VRzoL
CuG1Ar7c1AO6ujNnAEmUVC2DppL/ck5mRPWK/kgLwZSaNcZf8sydRgphsW1ogJin
7g0nGbFwXwKBgQCPoZY07Pr1TeP4g8OwWTu5F6dSvdU2CAbtZthH5q98u1n/cAj1
noak1Srpa3foGMTUn9CHu+5kwHPIpUPNeAZZBpq91uxa5pnkDMp3UrLIRJ2uZyr8
4PxcknEEh8DR5hsM/IbDcrCJQglM19ZtQeW3LKkY4BsIxjDf45ymH407IQKBgE/g
Ul6cPfOxQRlNLH4VMVgInSyyxWx1mODFy7DRrgCuh5kTVh+QUVBM8x9lcwAn8V9/
nQT55wR8E603pznqY/jX0xvAqZE6YVPcw4kpZcwNwL1RhEl8GliikBlRzUL3SsW3
q30AfqEViHPE3XpE66PPo6Hb1ymJCVr77iUuC3wtAoGBAIBrOGunv1qZMfqmwAY2
lxlzRgxgSiaev0lTNxDzZkmU/u3dgdTwJ5DDANqPwJc6b8SGYTp9rQ0mbgVHnhIB
jcJQBQkTfq6Z0H6OoTVi7dPs3ibQJFrtkoyvYAbyk36quBmNRjVh6rc8468bhXYr
v/t+MeGJP/0Zw8v/X2CFll96
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDKzCCAhOgAwIBAgIUBxM3WJf2bP12kAfqhmhhjZWv0ukwDQYJKoZIhvcNAQEL
BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMTgx
MDE3MTEzMjU3WhcNMjgxMDE0MTEzMjU3WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ
UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T
sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k
qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd
GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4
sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb
jAn4PCuR2akdF4G8WLUeDWECAwEAAaNTMFEwHQYDVR0OBBYEFMnmdJKOEepXrHI/
ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADiXIGEkSsN0SLSfCF1VNWO3
emBurfOcDq4EGEaxRKAU0814VEmU87btIDx80+z5Dbf+GGHCPrY7odIkxGNn0DJY
W1WcF+DOcbiWoUN6DTkAML0SMnp8aGj9ffx3x+qoggT+vGdWVVA4pgwqZT7Ybntx
bkzcNFW0sqmCv4IN1t4w6L0A87ZwsNwVpre/j6uyBw7s8YoJHDLRFT6g7qgn0tcN
ZufhNISvgWCVJQy/SZjNBHSpnIdCUSJAeTY2mkM4sGxY0Widk8LnjydxZUSxC3Nl
hb6pnMh3jRq4h0+5CZielA4/a+TdrNPv/qok67ot/XJdY3qHCCd8O2b14OVq9jo=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,18 @@
## IDF Component Manager Manifest File
dependencies:
espressif/zlib: "^1.3.0"
espressif/esp_websocket_client: "^1.3.0"
## Required IDF version
idf:
version: ">=4.1.0"
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true

View File

@@ -0,0 +1,87 @@
#include "portal_demo.h"
#include "openocpp/implementation/platform_esp.h"
#include "openocpp/implementation/standard_charger.h"
#include "openocpp/implementation/station_test_esp32.h"
#include "openocpp/common/logging.h"
#include "driver/gpio.h"
extern "C" void app_main(void) {
chargelab::logging::SetLogLevel(chargelab::logging::LogLevel::debug);
auto firstBootAfterFactoryReset = std::make_shared<chargelab::SettingBool> (
[]() {
return chargelab::SettingMetadata {
"FirstBootAfterFactoryReset",
chargelab::SettingConfig::rwPolicy(),
std::nullopt,
std::nullopt,
chargelab::SettingBool::kTextTrue
};
},
[](auto const&) {return true;}
);
auto platform = std::make_shared<chargelab::PlatformESP>();
auto settings = platform->getSettings();
settings->registerCustomSetting(firstBootAfterFactoryReset);
if (firstBootAfterFactoryReset->getValue()) {
// Clear certificates and install default certificates
platform->removeCertificatesIf([](auto const&) {return true;});
extern const unsigned char _binary_manufacturer_pem_start[] asm("_binary_manufacturer_pem_start");
extern const unsigned char _binary_manufacturer_pem_end[] asm("_binary_manufacturer_pem_end");
std::string pem {_binary_manufacturer_pem_start, _binary_manufacturer_pem_end};
if (!platform->addCertificate(pem, chargelab::ocpp2_0::GetCertificateIdUseEnumType::kManufacturerRootCertificate)) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed adding default manufacturer root CA: " << pem;
} else {
CHARGELAB_LOG_MESSAGE(info) << "Added default manufacturer root CA: " << pem;
}
firstBootAfterFactoryReset->setValue(false);
}
CHARGELAB_LOG_MESSAGE(info) << "Current setting";
settings->visitSettings([](auto const& x) {
CHARGELAB_LOG_MESSAGE(info) << x.getId() << ": " << x.getValueAsString();
});
auto station_test = std::make_shared<chargelab::StationTestEsp32>(platform);
auto charger = std::make_unique<chargelab::StandardCharger>(platform, station_test);
charger->addAfter(std::make_shared<chargelab::PortalDemo>(platform, charger->reset_module,
charger->connector_status_module,
charger->configuration_module,
station_test));
std::optional<chargelab::SteadyPointMillis> held_since;
while (true) {
station_test->updateMeasurements();
charger->runStep();
auto const now = platform->steadyClockNow();
if (gpio_get_level(GPIO_NUM_0) == 0) {
if (!held_since.has_value())
held_since = platform->steadyClockNow();
if (now - held_since.value() > 5000) {
CHARGELAB_LOG_MESSAGE(info) << "Factory reset requested - clearing all saved state";
for (auto const& name : std::vector<std::string> {"pmjournal", "spiffs"}) {
auto partition = platform->getPartition(name);
if (partition != nullptr) {
if (!partition->erase()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed erasing partition: " << name;
}
}
}
platform->resetHard();
}
} else {
held_since = std::nullopt;
}
using namespace std::chrono_literals;
std::this_thread::sleep_for(100ms);
}
}

View File

@@ -0,0 +1,725 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_PORTAL_DEMO_H
#define CHARGELAB_OPEN_FIRMWARE_PORTAL_DEMO_H
#include "openocpp/module/reset_module.h"
#include "openocpp/module/common_templates.h"
#include "openocpp/common/settings.h"
#include "openocpp/implementation/platform_esp.h"
#include "openocpp/implementation/station_test_esp32.h"
#include "openocpp/module/connector_status_module.h"
#include "openocpp/module/configuration_module.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_eth.h"
#include "esp_https_server.h"
#include "esp_tls.h"
#include "sdkconfig.h"
#include "esp_http_client.h"
#include "openocpp/protocol/ocpp1_6/messages/get_configuration.h"
namespace chargelab {
namespace detail {
struct DeviceConfigInfo {
std::string centralSystemUrl;
std::string chargePointId;
std::string ocppProtocol; // OCPP16 or OCPP20
int securityProfile;
bool clearCertificates;
std::string serialNumber;
std::string maxCurrentAmps;
std::string vendor;
std::string model;
int numberOfConnectors;
std::string firmwareVersion;
CHARGELAB_JSON_INTRUSIVE(
DeviceConfigInfo,
centralSystemUrl,
chargePointId,
ocppProtocol,
securityProfile,
clearCertificates,
serialNumber,
maxCurrentAmps,
vendor,
model,
numberOfConnectors,
firmwareVersion
)
};
struct ConnectorSettings {
std::optional<int> connectorId;
std::optional<bool> vehicleConnected;
std::optional<bool> suspendedByVehicle;
std::optional<bool> suspendedByCharger;
std::optional<bool> allowWebsocketConnection;
CHARGELAB_JSON_INTRUSIVE(ConnectorSettings, connectorId, vehicleConnected, suspendedByVehicle, suspendedByCharger, allowWebsocketConnection)
};
struct ConnectorStatusInfo {
chargelab::charger::ConnectorStatus connectorStatus;
std::vector<ocpp1_6::SampledValue> meterValues1_6;
std::vector<ocpp2_0::SampledValueType> meterValues2_0;
ConnectorSettings connectorSettings;
ocpp1_6::ChargePointStatus chargePointStatus1_6;
CHARGELAB_JSON_INTRUSIVE(ConnectorStatusInfo, connectorStatus, meterValues1_6, meterValues2_0, connectorSettings, chargePointStatus1_6)
};
struct ChargerInfo {
std::string serialNumber;
std::string firmwareVersion;
CHARGELAB_JSON_INTRUSIVE(ChargerInfo, serialNumber, firmwareVersion);
};
struct ChargerInfoResponse {
ChargerInfo info;
CHARGELAB_JSON_INTRUSIVE(ChargerInfoResponse, info);
};
struct SimulateRfidInteraction {
std::string idToken;
ocpp2_0::IdTokenEnumType tokenType;
CHARGELAB_JSON_INTRUSIVE(SimulateRfidInteraction, idToken, tokenType);
};
struct WifiNetwork {
std::string ssid;
std::string rssi;
std::string encryption;
CHARGELAB_JSON_INTRUSIVE(WifiNetwork, ssid, rssi, encryption);
};
struct SetupPayload {
std::string ssid;
std::string password;
std::string networkUrl;
std::string ocppId;
ocpp2_0::OCPPVersionEnumType ocppProtocol;
CHARGELAB_JSON_INTRUSIVE(SetupPayload, ssid, password, networkUrl, ocppId, ocppProtocol);
};
struct OcppParametersInfo {
ocpp1_6::GetConfigurationRsp rsp;
CHARGELAB_JSON_INTRUSIVE(OcppParametersInfo, rsp)
};
struct OcppParameterSetting {
std::string key;
std::string value;
CHARGELAB_JSON_INTRUSIVE(OcppParameterSetting, key, value)
};
}
/**
* Notes:
* Increasing the HTTPD_MAX_REQ_HDR_LEN from 512 (default) to 1024 is recommended. The limit was hit on Chrome
* browsers.
*/
class PortalDemo : public PureService {
private:
static constexpr int kStartupDelayMillis = 10*1000;// 500;
static constexpr int kMaxPostContentLengthBytes = 5*1024;
public:
PortalDemo(
std::shared_ptr<PlatformESP> platform,
std::shared_ptr<ResetModule> reset,
std::shared_ptr<ConnectorStatusModule> connector_status_module,
std::shared_ptr<ConfigurationModule> configuration_module,
std::shared_ptr<StationTestEsp32> station
)
: platform_(std::move(platform)),
reset_(std::move(reset)),
connector_status_module_(std::move(connector_status_module)),
configuration_module_(std::move(configuration_module)),
station_(std::move(station))
{
CHARGELAB_LOG_MESSAGE(debug) << "Setting up portal";
settings_ = platform_->getSettings();
for (auto const& method : std::vector<httpd_method_t>{HTTP_GET, HTTP_POST}) {
handlers_.resize(handlers_.size()+1);
auto& x = handlers_.back();
x.uri = "*";
x.method = method;
x.handler = onRequest;
x.user_ctx = (void *)this;
}
}
~PortalDemo() {
stopServers();
}
private:
void runUnconditionally() override {
if (!initialized_) {
if (!start_ts_.has_value())
start_ts_ = platform_->steadyClockNow();
auto const elapsed = platform_->steadyClockNow() - start_ts_.value();
if (elapsed < kStartupDelayMillis)
return;
startServerHttps();
startServerHttp();
initialized_ = true;
}
}
private:
void startServerHttps() {
extern const unsigned char _binary_servercert_pem_start[] asm("_binary_servercert_pem_start");
extern const unsigned char _binary_servercert_pem_end[] asm("_binary_servercert_pem_end");
extern const unsigned char _binary_prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");
extern const unsigned char _binary_prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");
if (server_https_ != nullptr)
return;
CHARGELAB_LOG_MESSAGE(info) << "Starting HTTPS server...";
httpd_ssl_config_t config = HTTPD_SSL_CONFIG_DEFAULT();
{
config.servercert = _binary_servercert_pem_start;
config.servercert_len = _binary_servercert_pem_end - _binary_servercert_pem_start;
}
{
config.prvtkey_pem = _binary_prvtkey_pem_start;
config.prvtkey_len = _binary_prvtkey_pem_end - _binary_prvtkey_pem_start;
}
config.httpd.uri_match_fn = httpd_uri_match_wildcard;
config.httpd.lru_purge_enable = true;
config.httpd.stack_size += 1024;
CHARGELAB_LOG_MESSAGE(info) << "Using stack size: " << config.httpd.stack_size;
ESP_ERROR_CHECK(httpd_ssl_start(&server_https_, &config));
for (auto& x : handlers_)
httpd_register_uri_handler(server_https_, &x);
}
void startServerHttp() {
if (server_http_ != nullptr)
return;
CHARGELAB_LOG_MESSAGE(info) << "Starting HTTP server...";
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.uri_match_fn = httpd_uri_match_wildcard;
config.lru_purge_enable = true;
config.stack_size += 1024;
CHARGELAB_LOG_MESSAGE(info) << "Using stack size: " << config.stack_size;
ESP_ERROR_CHECK(httpd_start(&server_http_, &config));
for (auto& x : handlers_)
httpd_register_uri_handler(server_http_, &x);
}
void stopServers() {
if (server_http_ != nullptr) {
ESP_ERROR_CHECK(httpd_stop(server_http_));
server_http_ = nullptr;
}
if (server_https_ != nullptr) {
ESP_ERROR_CHECK(httpd_stop(server_https_));
server_https_ = nullptr;
}
}
std::optional<std::string> readQueryString(httpd_req_t *req) {
std::string query(512, '\0');
if (httpd_req_get_url_query_str(req, query.data(), query.size()) != ESP_OK)
return std::nullopt;
query.resize(strlen(query.c_str()));
return query;
}
std::optional<std::string> readQueryParameter(std::optional<std::string> const& query, std::string const& key) {
if (!query.has_value())
return std::nullopt;
std::string value(query->size(), '\0');
if (httpd_query_key_value(query->data(), key.c_str(), value.data(), value.size()) != ESP_OK)
return std::nullopt;
value.resize(strlen(value.c_str()));
return uri::decodeUriComponent(value);
}
void onGet(httpd_req_t *req) {
// Note: as extern variables the uniqueness of the name/combination is important. These can't be moved into
// local variable definitions within the subsequent if blocks with shared names (start/end for
// example); leaving these are verbose explicit names here to limit the risk of conflicting
// definitions.
extern const char _binary_web_favicon_ico_gz_start[] asm("_binary_web_favicon_ico_gz_start");
extern const char _binary_web_favicon_ico_gz_end[] asm("_binary_web_favicon_ico_gz_end");
extern const char _binary_web_index_html_gz_start[] asm("_binary_web_index_html_gz_start");
extern const char _binary_web_index_html_gz_end[] asm("_binary_web_index_html_gz_end");
extern const char _binary_web_setup_html_gz_start[] asm("_binary_web_setup_html_gz_start");
extern const char _binary_web_setup_html_gz_end[] asm("_binary_web_setup_html_gz_end");
extern const char _binary_web_config_html_gz_start[] asm("_binary_web_config_html_gz_start");
extern const char _binary_web_config_html_gz_end[] asm("_binary_web_config_html_gz_end");
extern const char _binary_web_control_html_gz_start[] asm("_binary_web_control_html_gz_start");
extern const char _binary_web_control_html_gz_end[] asm("_binary_web_control_html_gz_end");
std::string const uri = req->uri;
CHARGELAB_LOG_MESSAGE(info) << "Requested URI was: " << uri;
if (uri == "" || uri == "/" || uri == "/index.html" || uri == "/home") {
serveGzipHtml(req, _binary_web_index_html_gz_start, _binary_web_index_html_gz_end);
} else if (uri == "/setup") {
serveGzipHtml(req, _binary_web_setup_html_gz_start, _binary_web_setup_html_gz_end);
} else if (uri == "/config") {
serveGzipHtml(req, _binary_web_config_html_gz_start, _binary_web_config_html_gz_end);
} else if (uri == "/control") {
serveGzipHtml(req, _binary_web_control_html_gz_start, _binary_web_control_html_gz_end);
} else if (uri == "/networks") {
std::optional<std::vector<detail::WifiNetwork>> networks;
for (int i=0; i < 10; i++) {
networks = scanNetworks();
if (networks.has_value())
break;
using namespace std::chrono_literals;
std::this_thread::sleep_for(10ms);
}
if (networks.has_value()) {
auto const content = write_json_to_string(networks);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, content.data(), content.size());
} else {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, nullptr);
}
} else if (uri == "/req.html") {
} else if (uri == "/chargerInfo") {
detail::ChargerInfoResponse response = {
detail::ChargerInfo {
settings_->ChargerSerialNumber.getValue(),
settings_->ChargerFirmwareVersion.getValue()
}
};
auto const text = write_json_to_string(response);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, text.data(), text.size());
} else if (uri == "/configure") {
} else if (uri == "/logs") {
} else if (uri == "/getDeviceConfiguration") {
detail::DeviceConfigInfo response {};
response.securityProfile = settings_->SecurityProfile.getValue();
response.clearCertificates = false;
auto const& slot = settings_->NetworkConnectionProfiles.getValue(0);
if (slot.has_value())
response.centralSystemUrl = slot->ocppCsmsUrl.value();
response.chargePointId = settings_->ChargePointId.getValue();
response.ocppProtocol = settings_->OcppProtocol.getValue();
response.serialNumber = settings_->ChargerSerialNumber.getValue();
response.maxCurrentAmps = "40";
response.vendor = settings_->ChargerVendor.getValue();
response.model = settings_->ChargerModel.getValue();
response.firmwareVersion = settings_->ChargerFirmwareVersion.getValue();
response.numberOfConnectors = station_->getConnectorMetadata().size();
auto const text = write_json_to_string(response);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, text.data(), text.size());
} else if (uri == "/logs") {
} else if (uri.find("/getConnectorStatus") == 0) {
auto const query = readQueryString(req);
int connectorId = 1;
{
auto const text = readQueryParameter(query, "connectorId");
if (text.has_value()) {
auto const parsed = string::ToInteger(text.value());
if (parsed.has_value())
connectorId = parsed.value();
}
}
CHARGELAB_LOG_MESSAGE(info) << "Connector ID was: " << connectorId;
auto const evse = station_->lookupConnectorId1_6(connectorId);
if (!evse.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Connector ID doesn't exist";
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
auto const status = station_->pollConnectorStatus(evse.value());
if (!status.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Connector status was empty";
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
detail::ConnectorSettings settings {};
settings.vehicleConnected = status->vehicle_connected;
settings.suspendedByCharger = status->suspended_by_charger;
settings.suspendedByVehicle = status->suspended_by_vehicle;
auto status1_6_map = connector_status_module_->getChargePointStatus1_6();
auto it = status1_6_map.find(1); // use default connector 1 for now
detail::ConnectorStatusInfo response {
status.value(),
station_->pollMeterValues1_6(evse.value()),
station_->pollMeterValues2_0(evse.value()),
settings,
it != status1_6_map.end() ? it->second : ocpp1_6::ChargePointStatus{ocpp1_6::ChargePointStatus::kValueNotFoundInEnum}
};
auto const text = write_json_to_string(response);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, text.data(), text.size());
} else if (uri == "/getOcppParameters") { // ocpp 1.6 configuration parameter
detail::OcppParametersInfo response {};
auto getConfigurationRsp = configuration_module_->getConfiguration(ocpp1_6::GetConfigurationReq{});
if (getConfigurationRsp) {
if (std::holds_alternative<ocpp1_6::GetConfigurationRsp>(getConfigurationRsp.value())) {
response.rsp = std::move(std::get<ocpp1_6::GetConfigurationRsp>(std::move(getConfigurationRsp.value())));
}
}
auto const text = write_json_to_string(response);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, text.data(), text.size());
} else if (uri == "/favicon.ico") {
serveGzipContent(req, "image/vnd.microsoft.icon", _binary_web_favicon_ico_gz_start, _binary_web_favicon_ico_gz_end);
} else {
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, "<h1>Not found</h1>", HTTPD_RESP_USE_STRLEN);
}
}
void onPost(httpd_req_t *req) {
CHARGELAB_LOG_MESSAGE(info) << "Received POST request on: " << req->uri;
std::string payload;
if (req->content_len > 0) {
if (req->content_len > kMaxPostContentLengthBytes) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
payload.resize(req->content_len);
auto ret = httpd_req_recv(req, payload.data(), payload.size());
if (ret < 0 || ret != payload.size()) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
return;
} else {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
}
}
CHARGELAB_LOG_MESSAGE(info) << "Payload was: " << payload;
std::string const uri = req->uri;
if (uri == "/submitSetup") {
onPostSetupSubmit(payload, req);
} else if (uri == "/updateDeviceConfiguration") {
onPostUpdateDeviceConfiguration(payload, req);
} else if (uri == "/updateConnectorSettings") {
onPostUpdateConnectorSettings(payload, req);
} else if (uri == "/simulateRfidInteraction") {
onPostSimulateRfidInteraction(payload, req);
} else if (uri == "/updateOcppParameter") {
onPostUpdateOcppParameter(payload, req);
} else if (uri == "/reboot") {
onPostReboot(payload, req);
}
else {
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, "<h1>Hello Secure World - Post!</h1>", HTTPD_RESP_USE_STRLEN);
}
}
void onPostSetupSubmit(std::string const& payload, httpd_req_t *req) {
auto const parsed = read_json_from_string<detail::SetupPayload>(payload);
if (!parsed.has_value()) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
auto const& settings = parsed.value();
settings_->WifiSSID.forceValue(settings.ssid);
settings_->WifiPassword.forceValue(settings.password);
settings_->SecurityProfile.setValue(0);
settings_->NetworkConnectionProfiles.forceValue(0, ocpp2_0::NetworkConnectionProfileType {
// TODO: Need to set OCPP version from a field
settings.ocppProtocol,
ocpp2_0::OCPPTransportEnumType::kJSON,
settings.networkUrl,
settings_->DefaultMessageTimeout.getValue(),
0,
chargelab::ocpp2_0::OCPPInterfaceEnumType::kWireless0
});
settings_->NetworkConfigurationPriority.forceValue("0");
settings_->ChargePointId.forceValue(settings.ocppId);
reset_->resetImmediately(ocpp2_0::BootReasonEnumType::kLocalReset);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, "{}", HTTPD_RESP_USE_STRLEN);
}
void onPostUpdateDeviceConfiguration(std::string const& payload, httpd_req_t *req) {
auto const parsed = read_json_from_string<detail::DeviceConfigInfo>(payload);
if (!parsed.has_value()) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
// Clear existing networking profiles
for (int i=0; i < settings_->NetworkConnectionProfiles.size(); i++)
settings_->NetworkConnectionProfiles.forceValue(i, std::nullopt);
auto const& settings = parsed.value();
settings_->SecurityProfile.setValue(settings.securityProfile);
settings_->NetworkConnectionProfiles.forceValue(0, ocpp2_0::NetworkConnectionProfileType {
// TODO: Need to set OCPP version from a field
settings.ocppProtocol == "OCPP20" ? ocpp2_0::OCPPVersionEnumType::kOCPP20 :
ocpp2_0::OCPPVersionEnumType::kOCPP16,
ocpp2_0::OCPPTransportEnumType::kJSON,
settings.centralSystemUrl,
settings_->DefaultMessageTimeout.getValue(),
settings.securityProfile,
chargelab::ocpp2_0::OCPPInterfaceEnumType::kWireless0
});
settings_->NetworkConfigurationPriority.forceValue("0");
settings_->ChargePointId.forceValue(settings.chargePointId);
settings_->ChargerSerialNumber.setValue(settings.serialNumber);
settings_->ChargerVendor.setValue(settings.vendor);
settings_->ChargerModel.setValue(settings.model);
settings_->ChargerFirmwareVersion.setValue(settings.firmwareVersion);
if (settings.numberOfConnectors != (int)station_->getConnectorMetadata().size()) {
station_->updateState([&](detail::SimulatorState& state) {
state.evse_metadata.clear();
state.connector_metadata.clear();
state.connector_state.clear();
state.station_metadata = charger::StationMetadata {
1
};
for (int i=1; i <= settings.numberOfConnectors; i++) {
state.evse_metadata[ocpp2_0::EVSEType {i}] = charger::EvseMetadata {
1,
10560.0
};
state.connector_metadata[ocpp2_0::EVSEType {i, 1}] = charger::ConnectorMetadata {
1,
"cType1",
1,
10560.0,
48.0
};
}
});
}
if (settings.clearCertificates)
platform_->removeCertificatesIf([](detail::SavedCertificate const&) {return true;});
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, "{\"successful\":true}", HTTPD_RESP_USE_STRLEN);
}
void onPostUpdateConnectorSettings(std::string const& payload, httpd_req_t *req) {
auto const parsed = read_json_from_string<detail::ConnectorSettings>(payload);
if (!parsed.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed parsing ConnectorSettings from: " << payload;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
auto const& settings = parsed.value();
if (settings.allowWebsocketConnection.has_value())
platform_->setDisableWebsocketConnection(!settings.allowWebsocketConnection.value());
station_->updateState([&](detail::SimulatorState& state) {
for (auto const& entry : state.connector_metadata) {
if (settings.connectorId.has_value() && entry.second.connector_id1_6 != settings.connectorId.value())
continue;
auto& connector_state = state.connector_state[entry.first];
if (settings.vehicleConnected.has_value())
connector_state.vehicle_connected = settings.vehicleConnected.value();
if (settings.suspendedByVehicle.has_value())
connector_state.suspended_by_vehicle = settings.suspendedByVehicle.value();
if (settings.suspendedByCharger.has_value())
connector_state.suspended_by_charger = settings.suspendedByCharger.value();
}
});
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, "{}", HTTPD_RESP_USE_STRLEN);
}
void onPostSimulateRfidInteraction(std::string const& payload, httpd_req_t *req) {
auto const parsed = read_json_from_string<detail::SimulateRfidInteraction>(payload);
if (!parsed.has_value()) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
auto const data = parsed.value();
station_->simulateTap(
ocpp1_6::IdToken {data.idToken},
ocpp2_0::IdTokenType {data.idToken, data.tokenType},
1000
);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, "{}", HTTPD_RESP_USE_STRLEN);
}
void onPostUpdateOcppParameter(std::string const& payload, httpd_req_t *req) {
auto const parsed = read_json_from_string<detail::OcppParameterSetting>(payload);
if (!parsed.has_value()) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
return;
}
auto const setting = parsed.value();
auto changeConfigRsp = configuration_module_->changeConfiguration(ocpp1_6::ChangeConfigurationReq {
setting.key, setting.value }, true);
if (changeConfigRsp) {
if (std::holds_alternative<ocpp1_6::ChangeConfigurationRsp>(changeConfigRsp.value())) {
auto status = std::get<ocpp1_6::ChangeConfigurationRsp>(std::move(changeConfigRsp.value()));
CHARGELAB_LOG_MESSAGE(info) << "change configuration, key: " << setting.key << ", return: " << status;
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, write_json_to_string(status).data(), HTTPD_RESP_USE_STRLEN);
return;
}
}
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, nullptr);
}
void onPostReboot(std::string const& payload, httpd_req_t *req) {
reset_->resetImmediately(ocpp2_0::BootReasonEnumType::kLocalReset);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, "{}", HTTPD_RESP_USE_STRLEN);
}
void serveGzipHtml(httpd_req_t *req, char const* start, char const* end) {
auto length = end - start;
httpd_resp_set_type(req, "text/html");
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
httpd_resp_send(req, start, length);
}
void serveGzipContent(httpd_req_t *req, char const* content_type, char const* start, char const* end) {
auto length = end - start;
httpd_resp_set_type(req, content_type);
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
httpd_resp_send(req, start, length);
}
private:
static esp_err_t onRequest(httpd_req_t *req) {
if (req == nullptr) {
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected null req";
return ESP_FAIL;
}
auto const this_ptr = (PortalDemo*)req->user_ctx;
if (this_ptr == nullptr) {
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected null user_ctx";
return ESP_FAIL;
}
switch (req->method) {
default:
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected request method: " << req->method;
return ESP_FAIL;
case HTTP_GET:
this_ptr->onGet(req);
break;
case HTTP_POST:
this_ptr->onPost(req);
break;
}
return ESP_OK;
}
std::optional<std::vector<detail::WifiNetwork>> scanNetworks()
{
std::vector<detail::WifiNetwork> result;
std::vector<wifi_ap_record_t> ap_info;
ap_info.resize(20);
uint16_t number = ap_info.size();
auto scan_start_result = esp_wifi_scan_start(nullptr, true);
if (scan_start_result != ESP_OK) {
CHARGELAB_LOG_MESSAGE(warning) << "wifi scan start failed, error code:" << scan_start_result;
CHARGELAB_LOG_MESSAGE(warning) << "ESP_ERR_WIFI_NOT_INIT = " << ESP_ERR_WIFI_NOT_INIT;
CHARGELAB_LOG_MESSAGE(warning) << "ESP_ERR_WIFI_NOT_STARTED = " << ESP_ERR_WIFI_NOT_STARTED;
CHARGELAB_LOG_MESSAGE(warning) << "ESP_ERR_WIFI_TIMEOUT = " << ESP_ERR_WIFI_TIMEOUT;
CHARGELAB_LOG_MESSAGE(warning) << "ESP_ERR_WIFI_STATE = " << ESP_ERR_WIFI_STATE;
return std::nullopt;
}
auto get_ap_result = esp_wifi_scan_get_ap_records(&number, ap_info.data());
if (get_ap_result != ESP_OK) {
CHARGELAB_LOG_MESSAGE(warning) << "wifi scan get ap records failed, error code: " << get_ap_result;
return std::nullopt;
}
for (int i = 0; i < ap_info.size() && i < number; i++) {
detail::WifiNetwork network {};
network.ssid = (char const*)ap_info[i].ssid;
network.rssi = std::to_string(ap_info[i].rssi);
network.encryption = getAuthModeText(ap_info[i].authmode);
result.push_back(std::move(network));
}
return result;
}
std::string getAuthModeText(int authmode)
{
switch (authmode) {
case WIFI_AUTH_OPEN: return "WIFI_AUTH_OPEN";
case WIFI_AUTH_WEP: return "WIFI_AUTH_WEP";
case WIFI_AUTH_WPA_PSK: return "WIFI_AUTH_WPA_PSK";
case WIFI_AUTH_WPA2_PSK: return "WIFI_AUTH_WPA2_PSK";
case WIFI_AUTH_WPA_WPA2_PSK: return "WIFI_AUTH_WPA_WPA2_PSK";
case WIFI_AUTH_WPA2_ENTERPRISE: return "WIFI_AUTH_WPA2_ENTERPRISE";
case WIFI_AUTH_WPA3_PSK: return "WIFI_AUTH_WPA3_PSK";
case WIFI_AUTH_WPA2_WPA3_PSK: return "WIFI_AUTH_WPA2_WPA3_PSK";
default: return "WIFI_AUTH_UNKNOWN";
}
}
private:
std::shared_ptr<PlatformESP> platform_;
std::shared_ptr<ResetModule> reset_;
std::shared_ptr<ConnectorStatusModule> connector_status_module_;
std::shared_ptr<ConfigurationModule> configuration_module_;
std::shared_ptr<StationTestEsp32> station_;
std::shared_ptr<Settings> settings_;
std::vector<httpd_uri_t> handlers_;
std::optional<SteadyPointMillis> start_ts_ = std::nullopt;
bool initialized_ = false;
httpd_handle_t server_https_ = nullptr;
httpd_handle_t server_http_ = nullptr;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_PORTAL_DEMO_H

View File

@@ -0,0 +1 @@
*.gz

View File

@@ -0,0 +1,247 @@
<!DOCTYPE html>
<html>
<style>
.form-group {
margin-bottom: 10px;
}
.form-group label {
display: inline-block;
width: 280px; /* Adjust width as needed */
}
.form-group input {
width: 300px; /* Adjust width as needed */
}
.form-group input[type="checkbox"] {
width: auto; /* Let checkbox have natural width */
}
</style>
<body>
<h2 id="myTitle">Charger Configuration</h2>
<div>
<h3>Network Settings</h3>
<div class="form-group">
<label for="networkUrl">Central System URL (Slot 0):</label>
<input type="text" id="networkUrl" name="networkUrl" size="60">
</div>
<div class="form-group">
<label for="chargePointId">Charge Point ID:</label>
<input type="text" id="chargePointId" name="chargePointId" size="60">
</div>
<div class="form-group">
<label for="ocpp_protocol">OCPP Protocol:</label>
<select id="ocpp_protocol" name="ocpp_protocol">
<option value="OCPP 1.6" selected>OCPP 1.6</option>
<option value="OCPP 2.0.1">OCPP 2.0.1</option>
</select>
</div>
<div class="form-group">
<label for="securityProfile">Security Profile:</label>
<input type="number" id="securityProfile" name="securityProfile" size="60">
</div>
<div class="form-group">
<label for="clearCertificates">Clear certificates:</label>
<input type="checkbox" id="clearCertificates" name="clearCertificates">
</div>
</div>
<div>
<h3>Device Settings</h3>
<div class="form-group">
<label for="serialNumber">Serial Number:</label>
<input type="text" id="serialNumber" name="serialNumber" size="40">
</div>
<div class="form-group">
<label for="maxCurrent">Device Maximum Current (amps):</label>
<input type="number" id="maxCurrent" name="maxCurrent" size="40">
</div>
<div class="form-group">
<label for="vendor">Vendor:</label>
<input type="text" id="vendor" name="vendor" size="40">
</div>
<div class="form-group">
<label for="model">Model:</label>
<input type="text" id="model" name="model" size="40">
</div>
<div class="form-group">
<label for="numberOfConnectors">Number of Connectors:</label>
<input type="number" id="numberOfConnectors" name="numberOfConnectors" size="40">
</div>
</div>
<br>
<div>
<button onclick="submit()">Submit</button><br>
<p id="result" style="color:blue"> </p>
</div>
<hr>
<div>
<h3>OCPP 1.6 Configuration</h3>
<div class="form-group">
<label for="ocpp1_6_parameter">Select an OCPP Parameter:</label>
<select id="ocpp1_6_parameter" name="ocpp1_6_parameter" onchange="onOcppParameterChanged()"></select>
</div>
<div class="form-group">
<label for="ocpp1_6_parameter_input">Value:</label>
<input type="text" id="ocpp1_6_parameter_input">
</div>
<div class="form-group">
<label>OCPP Parameter Read-Only:</label>
<span id="ocpp-parameter-readonly-status">-</span>
</div>
<br>
<div>
<button id="ocpp1_6_submit_button" onclick="submitOcpp1_6_ParameterChange()">Submit Parameter Change</button>
<p id="ocpp1_6_submit_result" style="color:blue"> </p>
</div>
</div>
<script>
window.ocppParametersMap = window.ocppParametersMap || new Map();
fetchAndFillSettings();
fetchAndFillOcpp1_6_Parameter();
function fetchAndFillSettings() {
fetch("getDeviceConfiguration")
.then(response => response.json())
.then(payload => {
document.getElementById("networkUrl").value = payload.centralSystemUrl;
document.getElementById("chargePointId").value = payload.chargePointId;
let normalizedOcppProtocol = null;
if (payload.ocppProtocol != null) {
normalizedOcppProtocol = payload.ocppProtocol.replace(/^"(.*)"$/, "$1");
}
if (normalizedOcppProtocol == null || normalizedOcppProtocol === "OCPP20") {
document.getElementById("ocpp_protocol").value = "OCPP 2.0.1";
} else {
document.getElementById("ocpp_protocol").value = "OCPP 1.6";
}
document.getElementById("securityProfile").value = payload.securityProfile;
document.getElementById("serialNumber").value = payload.serialNumber;
document.getElementById("maxCurrent").value = payload.maxCurrentAmps;
document.getElementById("vendor").value = payload.vendor;
document.getElementById("model").value = payload.model;
document.getElementById("numberOfConnectors").value = payload.numberOfConnectors;
document.getElementById("myTitle").innerHTML = "Charger Configuration " + "(" + payload.firmwareVersion + ")";
});
}
function submit() {
var data = new Object();
data.centralSystemUrl = document.getElementById("networkUrl").value;
data.chargePointId = document.getElementById("chargePointId").value;
data.ocppProtocol = document.getElementById("ocpp_protocol").value === "OCPP 2.0.1" ? "OCPP20" : "OCPP16";
data.securityProfile = parseInt(document.getElementById("securityProfile").value);
data.clearCertificates = document.getElementById("clearCertificates").checked;
data.serialNumber = document.getElementById("serialNumber").value;
data.maxCurrentAmps = document.getElementById("maxCurrent").value;
data.vendor = document.getElementById("vendor").value;
data.model = document.getElementById("model").value;
data.numberOfConnectors = parseInt(document.getElementById("numberOfConnectors").value, 10);
data.firmwareVersion = "";
const jsonString = JSON.stringify(data);
console.log(jsonString);
fetch("updateDeviceConfiguration", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then(response => response.json())
.then(json => {
if (json.successful === true)
document.getElementById("result").innerHTML = "Configuration updated successfully";
else
document.getElementById("result").innerHTML = "Configuration update failed";
})
.catch((error) => {
document.getElementById("result").innerHTML = "Configuration update failed:" + error;
});
}
function fetchAndFillOcpp1_6_Parameter() {
fetch("getOcppParameters")
.then(response => response.json())
.then(payload => {
const configKeys = payload?.rsp?.configurationKey;
if (!configKeys) {
console.warn("No configuration keys found in response.");
return;
}
configKeys.forEach(item => {
window.ocppParametersMap.set(item.key, { value: item.value, readonly: item.readonly });
});
const select = document.getElementById("ocpp1_6_parameter");
select.innerHTML = Array.from(window.ocppParametersMap.keys())
.map(key => `<option value="${key}">${key}</option>`)
.join('');
onOcppParameterChanged();
})
.catch(e => {
console.error(e);
});
}
function onOcppParameterChanged() {
const selectedKey = document.getElementById("ocpp1_6_parameter").value;
const param = window.ocppParametersMap.get(selectedKey);
document.getElementById("ocpp1_6_parameter_input").value = param.value || "";
document.getElementById("ocpp-parameter-readonly-status").textContent = param.readonly ? "Yes (Read-Only)" : "No (Editable)";
}
async function submitOcpp1_6_ParameterChange() {
const selectedKey = document.getElementById("ocpp1_6_parameter").value;
const newValue = document.getElementById("ocpp1_6_parameter_input").value;
// Send data to server
try {
const response = await fetch('updateOcppParameter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: selectedKey, value: newValue })
});
if (response.ok) {
const payload = await response.json();
console.log(`change ocpp parameter response: ${payload}`);
if (payload.status === "Accepted" || payload.status === "RebootRequired") {
if (ocppParametersMap.has(selectedKey)) {
window.ocppParametersMap.get(selectedKey).value = newValue;
} else {
console.warn(`Key ${selectedKey} not found in ocppParametersMap.`);
}
}
document.getElementById("ocpp1_6_submit_result").innerHTML = `Get response with status: ${payload.status} for ${selectedKey} = ${newValue}`;
} else {
document.getElementById("ocpp1_6_submit_result").innerHTML = "Failed to update the ocpp variable value on server.";
}
} catch (error) {
console.error("Error submitting value:", error);
document.getElementById("ocpp1_6_submit_result").innerHTML = "An error occurred while updating the ocpp variable.";
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html>
<head>
<style>
.left {
position: relative;
top: 10px;
left: 40px;
width: 800px;
border: 0px solid #73AD21;
padding: 0px;
}
.eighteen-point {line-height: 18pt;}
.li-position
{
left: 30px;
position: relative;
}
.flex-container {
display: flex;
}
.flex-child {
flex: 0.5;
}
</style>
</head>
<body>
<div>
<h2>
<label style="color:green" for="connector_id">Connector Id:</label>
<select id="connector_id" name="connector_id" onchange="onConnectorIdChanged()">
</select>
</h2>
</div>
<div>
<h2>EV connection</h2>
<input onchange="updateChanges()" type="radio" id="ev_connected_false" name="ev_connected" checked>EV disconnected</input><br>
<input onchange="updateChanges()" type="radio" id="ev_connected_true" name="ev_connected">EV connected</input><br>
<h2>Suspended by EV</h2>
<input onchange="updateChanges()" type="radio" id="suspended_ev_true" name="suspended_ev" checked>Suspended by EV</input><br>
<input onchange="updateChanges()" type="radio" id="suspended_ev_false" name="suspended_ev">EV drawing power</input><br>
<h2>Suspended by charger</h2>
<input onchange="updateChanges()" type="radio" id="suspended_charger_true" name="suspended_charger" checked>Suspended by charger</input><br>
<input onchange="updateChanges()" type="radio" id="suspended_charger_false" name="suspended_charger">Charger delivering power</input><br>
<h2>RFID tap</h2>
<label for="rfid_token">Token:</label>
<input type="text" id="rfid_token" name="rfid_token" size="40"><br>
<label for="rfid_type">Type:</label>
<select name="rfid_type" id="rfid_type" style="margin-top: 10px">
<option value="Central">Central</option>
<option value="eMAID">eMAID</option>
<option value="ISO14443">ISO14443</option>
<option value="ISO15693">ISO15693</option>
<option value="KeyCode">KeyCode</option>
<option value="Local">Local</option>
<option value="MacAddress">MacAddress</option>
<option value="kNoAuthorization">kNoAuthorization</option>
</select>
<button onclick="onSendRFID()">Simulate RFID interaction</button><br>
<h2>Websocket connection</h2>
<input onchange="updateChanges()" type="radio" id="allow_websocket_connection_true" name="allow_websocket_connection" checked>Allow connection</input><br>
<input onchange="updateChanges()" type="radio" id="allow_websocket_connection_false" name="allow_websocket_connection">Disallow connection</input><br>
</div>
<br>
<hr>
<div class="flex-container">
<div class="flex-child">
<h2>Charging status</h2>
<label for="charging_status">Charging state:</label>
<strong id="charging_status" name="charging_status"></strong>
</div>
</div>
<br>
<div>
<br>
<p id="message_result" style="color:blue"></p>
</div>
<script>
var simulateRfidRequest = null;
var updateConnectorSettingsRequest = null;
var lastStatusUpdate = null;
// document.body.addEventListener('change', function(e) {
// if (e.target.id === "") {
//
// }
// if (e.target.type === "select-one") {
// document.getElementById("message_result").innerHTML = "";
// return;
// }
// updateChanges();
// });
setTimeout(() => {
fetch("getDeviceConfiguration", {signal: AbortSignal.timeout(1000)})
.then(response => response.text())
.then(text => {
let obj = JSON.parse(text);
let number_of_connectors = (obj || {})['numberOfConnectors'] || 0;
let connector_id_select = document.getElementById("connector_id");
for (let i = 0; i < number_of_connectors; i++) {
connector_id_select.options[i] = new Option(i+1, i+1);
}
})
.then(() => {
setTimeout(fetchAndFill, 100);
})
}, 100);
function onConnectorIdChanged() {
lastStatusUpdate = null;
}
function updateBoolInput(name, value) {
if (value === true) {
if (!document.getElementById(name + "_true").checked) {
console.log("Setting: " + name + "_true");
document.getElementById(name + "_true").checked = true;
}
} else {
if (!document.getElementById(name + "_false").checked) {
console.log("Setting: " + name + "_false");
document.getElementById(name + "_false").checked = true;
}
}
}
function fetchAndFill() {
let future;
if (updateConnectorSettingsRequest != null) {
future = fetch("updateConnectorSettings", {
signal: AbortSignal.timeout(1000),
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(updateConnectorSettingsRequest)
})
.then(()=>{
document.getElementById("message_result").innerHTML = "The charger status was updated successfully.";
})
.catch(error => {
document.getElementById("message_result").innerHTML = "The charger status was not updated due to error:" + error;
});
updateConnectorSettingsRequest = null;
} else if (simulateRfidRequest != null) {
future = fetch("simulateRfidInteraction", {
signal: AbortSignal.timeout(1000),
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(simulateRfidRequest)
})
.then(()=>{
document.getElementById("message_result").innerHTML = "RFID request accepted.";
})
.catch(error => {
document.getElementById("message_result").innerHTML = "RFID request failed with error: " + error;
});
simulateRfidRequest = null;
} else if (lastStatusUpdate == null || Math.abs(Date.now() - lastStatusUpdate) > 2000) {
lastStatusUpdate = Date.now();
connector_id = document.getElementById("connector_id").value;
if (connector_id === null) {
connector_id = 1;
}
future = fetch("getConnectorStatus" + "?connectorId=" + connector_id, {signal: AbortSignal.timeout(1000)})
.then(response => response.text())
.then(text => {
let obj = JSON.parse(text);
let settings = (obj || {})['connectorSettings'] || {};
updateBoolInput("ev_connected", settings['vehicleConnected']);
updateBoolInput("suspended_ev", settings['suspendedByVehicle']);
updateBoolInput("suspended_charger", settings['suspendedByCharger']);
let charging_status = (obj || {})['chargePointStatus1_6'] || {};
const chargingElement = document.getElementById('charging_status');
if (charging_status === "ValueNotFoundInEnum") {
chargingElement.innerHTML = "";
} else {
chargingElement.innerHTML = charging_status;
// Set color based on charging_status
switch (charging_status) {
case "Charging":
chargingElement.style.color = "green";
break;
case "Faulted":
chargingElement.style.color = "red";
break;
case "Unavailable":
chargingElement.style.color = "gray";
break;
default:
chargingElement.style.color = "blue"; // Default color
break;
}
}
});
} else {
future = Promise.resolve();
}
future.then(() => setTimeout(fetchAndFill, 100))
.catch(() => setTimeout(fetchAndFill, 100));
}
function updateChanges() {
updateConnectorSettingsRequest = {
connectorId: parseInt(document.getElementById("connector_id").value, 10),
vehicleConnected: document.getElementById("ev_connected_true").checked,
suspendedByVehicle: document.getElementById("suspended_ev_true").checked,
suspendedByCharger: document.getElementById("suspended_charger_true").checked,
allowWebsocketConnection: document.getElementById("allow_websocket_connection_true").checked
};
}
function onSendRFID() {
simulateRfidRequest = {
idToken: document.getElementById("rfid_token").value,
tokenType: document.getElementById("rfid_type").value
};
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
font-family: Trebuchet MS, Tahoma, Arial, Helvetica, sans-serif;
}
.topnav {
overflow: hidden;
background-color: #333;
}
.topnav a {
float: left;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
}
.topnav a:hover {
background-color: #ddd;
color: black;
}
.topnav a.active {
background-color: #04AA6D;
color: white;
}
</style>
</head>
<body>
<div class="topnav">
<a id="href_setup" class="mylink" href="#" onclick=loadSetup()>Setup</a>
<a id="href_config" class="mylink" href="#" onclick=loadConfig()>Configuration</a>
<a id="href_control" class="mylink" href="#" onclick=loadControl()>Control</a>
</div>
<div id="content" style="padding-left:16px">
</div>
<script>
addSetLinkActiveEvents();
document.getElementById('href_setup').click();
function addSetLinkActiveEvents() {
const links = document.querySelectorAll(".mylink");
if (links.length) {
links.forEach((link) => {
link.addEventListener('click', (e) => {
links.forEach((link) => {
link.classList.remove('active');
});
e.preventDefault();
link.classList.add('active');
});
});
}
}
function loadConfig() {
fetch("/config")
.then(response => response.text())
.then(text => {
document.getElementById("content").innerHTML = text;
executeScriptElements(document.getElementById("content"));
document.title = "Charger Simulator - Configuration";
})
.catch(error => {
document.getElementById("content").innerHTML = error;
});
}
function loadControl() {
fetch("/control")
.then(response => response.text())
.then(text => {
document.getElementById("content").innerHTML = text;
executeScriptElements(document.getElementById("content"));
document.title = "Charger Simulator - Control";
})
.catch(error => {
document.getElementById("content").innerHTML = error;
});
}
function loadSetup() {
fetch("/setup")
.then(response => response.text())
.then(text => {
document.getElementById("content").innerHTML = text;
executeScriptElements(document.getElementById("content"));
document.title = "Charger Simulator - Setup";
})
.catch(error => {
document.getElementById("content").innerHTML = error;
});
}
function executeScriptElements(containerElement) {
const scriptElements = containerElement.querySelectorAll("script");
Array.from(scriptElements).forEach((scriptElement) => {
const clonedElement = document.createElement("script");
Array.from(scriptElement.attributes).forEach((attribute) => {
clonedElement.setAttribute(attribute.name, attribute.value);
});
clonedElement.text = scriptElement.text;
scriptElement.parentNode.replaceChild(clonedElement, scriptElement);
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,495 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Charger</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'; style-src 'unsafe-inline'">
<style type="text/css">
body {
font-family: Trebuchet MS, Tahoma, Arial, Helvetica, sans-serif;
}
input[type=text],
input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 100%;
}
button:hover {
opacity: 0.8;
}
.cancelbtn {
width: auto;
padding: 10px 18px;
background-color: #f44336;
}
.imgcontainer {
text-align: center;
margin: 24px 0 12px 0;
position: relative;
}
img.avatar {
width: 40%;
border-radius: 50%;
}
.container {
padding: 16px;
}
span.psw {
float: right;
padding-top: 16px;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto 15% auto;
border: 1px solid #888;
width: 80%;
}
.close {
position: absolute;
right: 25px;
top: 0;
color: #000;
font-size: 35px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: red;
cursor: pointer;
}
.animate {
-webkit-animation: animatezoom 0.6s;
animation: animatezoom 0.6s
}
@-webkit-keyframes animatezoom {
from {
-webkit-transform: scale(0)
}
to {
-webkit-transform: scale(1)
}
}
@keyframes animatezoom {
from {
transform: scale(0)
}
to {
transform: scale(1)
}
}
@media screen and (max-width: 300px) {
span.psw {
display: block;
float: none;
}
.cancelbtn {
width: 100%;
}
}
h3 {
font-style: normal;
font-weight: normal;
font-size: 14px;
line-height: 20px;
}
.blue {
font-style: normal;
font-weight: bold;
font-size: 14px;
line-height: 20px;
color: #5DA2E1;
}
h2 {
font-style: normal;
font-weight: 600;
font-size: 28px;
line-height: 36px;
display: flex;
align-items: center;
color: #1F1F1F;
}
.form-group {
margin-top: 16px;
}
label {
font-style: normal;
font-weight: 600;
font-size: 16px;
line-height: 22px;
display: flex;
align-items: flex-end;
color: #1F1F1F;
}
select {
height: 48px;
width: 100%;
border: none;
background-color: #F5F6F8;
border-radius: 4px;
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #ADADAD;
padding: 12px 20px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSdibGFjaycgaGVpZ2h0PScyNCcgdmlld0JveD0nMCAwIDI0IDI0JyB3aWR0aD0nMjQnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PHBhdGggZD0nTTcgMTBsNSA1IDUtNXonLz48cGF0aCBkPSdNMCAwaDI0djI0SDB6JyBmaWxsPSdub25lJy8+PC9zdmc+);
background-repeat: no-repeat;
background-position-x: 97%;
background-position-y: 12px;
}
.black {
color: #1F1F1F;
}
.form-control {
margin-top: 4px;
}
.bolden {
font-family: "Arial Black"
}
input[type='password'],
input[type='text'] {
border: none;
background-color: #F5F6F8;
border-radius: 4px;
height: 48px;
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #ADADAD;
background-repeat: no-repeat;
background-position-x: 95%;
background-position-y: 12px;
background-size: 22px;
}
input:not(:placeholder-shown) {
color: black !important;
}
.eye {
width: 20px;
position: absolute;
right: 40px;
height: 60px;
background: no-repeat url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABDUlEQVQ4jd2SvW3DMBBGbwQVKlyo4BGC4FKFS4+TATKCNxAggkeoSpHSRQbwAB7AA7hQoUKFLH6E2qQQHfgHdpo0yQHX8T3exyPR/ytlQ8kOhgV7FvSx9+xglA3lM3DBgh0LPn/onbJhcQ0bv2SHlgVgQa/suFHVkCg7bm5gzB2OyvjlDFdDcoa19etZMN8Qp7oUDPEM2KFV1ZAQO2zPMBERO7Ra4JQNpRa4K4FDS0R0IdneCbQLb4/zh/c7QdH4NL40tPXrovFpjHQr6PJ6yr5hQV80PiUiIm1OKxZ0LICS8TWvpyyOf2DBQQtcXk8Zi3+JcKfNafVsjZ0WfGgJlZZQxZjdwzX+ykf6u/UF0Fwo5Apfcq8AAAAASUVORK5CYII=);
filter: brightness(0);
background-position: center;
cursor: pointer;
}
.eye-hide {
width: 20px;
position: absolute;
right: 40px;
height: 65px;
background: no-repeat url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpjYWVjOWZiYi00ZDU4LTQxMmYtODcwMi0zYWMxYzc4ZTU0MWQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjNDQTYwNTJFRjM3MTFFOEI1MDlFMjg4NDlCM0IyMjIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjNDQTYwNTFFRjM3MTFFOEI1MDlFMjg4NDlCM0IyMjIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpkMTgzYmMzOC1mMzlmLTQ2OTQtYjI3ZC0xOWVhZDBjN2RhZWMiIHN0UmVmOmRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDpjNzAwNzcyYi0yYzg4LWQzNDMtOTMwNi03ZjAyZDE2ZTBjOTgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz53I3B2AAABVklEQVR42sTTyytFURTH8XOFzMRA3RQDlCITQqSk/AMmkomBx+QWE8XAUIqZZEJedVMkAymPMkCuN3kUEckjUoqJCY7v0u/oZHoGdn3q7NNea6+99jkh13WdICPOCThCf+ZJKEAtqhHGJ26xjllsIgMN2PcH28shBdi5jtGFHc1dJWnGFZ7Q6QXn4QBHuMQ2itCGGzxjHGP4wioqveCwgueRq7LqMapd99CrRHdoRbdiMq0Hw9rNMqZiDVtKZuUnoA6LqszWx2Nax3Q+EFE1aVjBKfpxjQcMKOkJ0rW2SbE/Z7Mys3QrNZhT+TMYwYvm7QpOQQxRr/uH6EGLdr3HIM58N9CHRCRjAefWA6+RZVhSh62iHOSjQ00r0bpynfsChV6wlfWojyWi0mJ6rlJzG7GMV0wh2/8l2plLManmVegai1WujTfsYgIbeP9N8O8/U+AE3wIMAOAeYTJHg1SWAAAAAElFTkSuQmCC);
filter: brightness(0);
background-position: center;
cursor: pointer;
}
select::-ms-expand {
display: none;
}
input:focus,
select:focus {
border: solid 1px #000 !important;
outline: none;
}
input[type='submit'] {
width: 100%;
height: 56px;
background: #5DA2E1;
border-radius: 4px;
padding: 16px 20px;
border: none;
font-style: normal;
font-weight: 600;
font-size: 18px;
line-height: 24px;
color: white !important;
}
.error {
color: red;
font-size: 11px;
display: none;
}
.mb-12 {
margin-bottom: 12px;
}
.mb-8 {
margin-bottom: 8px;
}
.mt {
margin-top: 30px;
}
</style>
<script>
function _el(s) { return document.getElementById(s); }
window.onclick = function (event) {
var el = _el('id01');
if (event.target == el) {
el.style.display = "none";
}
}
var u = 'http://', z = "********", z1 = "", prev_el = null, u1 = 'acharger.ca';
function sendReq(r, cb) {
var x = new XMLHttpRequest();
x.onreadystatechange = function () {
if (this.readyState == 4 && (this.status == 200 || this.status == 0)) {
if (cb) cb(this.responseText);
}
};
x.open("GET", r, true);
x.send();
}
function liclk(e) {
if (e && e.innerText) {
var q = e.innerText.split(" "), t = e.style;
if (q.length >= 3) {
z1 = q[0];
var el = _el("uname");
el.value = z1;
el = _el("m_si");
el.value = z;
if (t) {
t.backgroundColor = "#00FFBF";
if (prev_el)
prev_el.backgroundColor = "#FFFFFF";
prev_el = t;
}
}
}
}
function checkFormValidation() {
document.getElementById("networkUrlError").style.display = "none";
document.getElementById("ocppIdError").style.display = "none";
var valid = true;
let password = document.getElementById("Password").value;
let confirmPassword = document.getElementById("Confirmpassword").value;
if (password !== "" && password !== confirmPassword) {
document.getElementById("PasswordMatchError").style.display = "block";
valid = false;
} else {
document.getElementById("PasswordMatchError").style.display = "none";
}
if (document.getElementById("networkUrl").value == "") {
document.getElementById("networkUrlError").style.display = "block";
valid = false;
}
if (document.getElementById("ocppId").value == "") {
document.getElementById("ocppIdError").style.display = "block";
valid = false;
}
return valid;
}
function submitData() {
if (!checkFormValidation()) {
return;
}
fetch("submitSetup", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
ssid: document.getElementById("wifi").value,
password: document.getElementById("Password").value,
networkUrl: document.getElementById("networkUrl").value,
ocppId: document.getElementById("ocppId").value,
ocppProtocol: document.getElementById("ocppProtocol").value
})
})
.then(()=>{
document.getElementById("step1").style.display = "none";
document.getElementById("step2").style.display = "block";
})
.catch(error => {
console.log("Submit failed with error: ", error);
});
}
function toggleShowPassword() {
if (document.getElementById('PasswordEye').className === "eye-hide") {
document.getElementById('Password').type = "text";
document.getElementById('Confirmpassword').type = "text";
document.getElementById('PasswordEye').className = "eye";
document.getElementById('ConfirmpasswordEye').className = "eye";
} else {
document.getElementById('Password').type = "password";
document.getElementById('Confirmpassword').type = "password";
document.getElementById('PasswordEye').className = "eye-hide"
document.getElementById('ConfirmpasswordEye').className = "eye-hide";
}
}
sendReq("networks", function (s) {
const networks = JSON.parse(s);
console.log(networks);
if (networks) {
var select = document.getElementById('wifi');
var opt = document.createElement('option');
opt.value = "";
opt.innerHTML = "WiFi name";
select.appendChild(opt);
for (let i = 0; i < networks.length; i++) {
const entry = networks[i];
var opt = document.createElement('option');
opt.value = entry.ssid;
opt.innerHTML = entry.ssid;
select.appendChild(opt);
}
}
});
</script>
</head>
<body>
<div id="step2" style="display: none;">
<h2>Charger successfully updated </h2>
<h3>Your charger's settings have been updated. </h3>
<h3>You may now close this web page.</h3>
</div>
<div id="step1">
<h2 class="mb-12">Test Charger setup</h2>
<div class="blue">*Case sensitive</div>
<div class="form-group">
<label>WiFi name</label>
<div class="form-control">
<select onchange="javascript:wifiChanged(this);checkFormValidation('wifi')" id="wifi"
name="wifi"></select>
<span id="wifiError" class="error">Wifi name is required!</span>
</div>
</div>
<div class="form-group">
<label>Password</label>
<div class="form-control">
<input id="Password" onblur="javascript:checkFormValidation()" name="Password" type="password"
placeholder="Password">
<span class="eye-hide" id="PasswordEye" onclick="javascript:toggleShowPassword()"></span>
<span id="PasswordError" class="error">Password is required!</span>
</div>
</div>
<div class="form-group">
<label>Confirm password</label>
<div class="form-control">
<input id="Confirmpassword" onblur="javascript:checkFormValidation()"
name="Confirmpassword" type="password" placeholder="Confirm password">
<span class="eye-hide" id="ConfirmpasswordEye"
onclick="javascript:toggleShowPassword()"></span>
<span id="ConfirmpasswordError" class="error">Confirm password is required!</span>
<span id="PasswordMatchError" class="error">Password and Confirm password does not match!</span>
</div>
</div>
<div class="form-group" id="divNetworkUrl" style="display: block;">
<label>Enter network URL</label>
<div class="form-control">
<input type="text" onblur="javascript:checkFormValidation()" placeholder="Enter network URL" id="networkUrl" name="networkUrl">
<span id="networkUrlError" class="error">Network URL is required!</span>
</div>
</div>
<div class="form-group" id="divOcppId" style="display: block;">
<label>Enter OCPP ID</label>
<div class="form-control">
<input type="text" onblur="javascript:checkFormValidation()" placeholder="Enter OCPP ID" id="ocppId" name="ocppId">
<span id="ocppIdError" class="error">OCPP ID is required!</span>
</div>
</div>
<div class="form-group" id="divOcppProtocol" style="display: block;">
<label>Select OCPP protocol</label>
<div class="form-control">
<select id="ocppProtocol" name="ocppProtocol">
<option value="OCPP16">OCPP 1.6</option>
<option value="OCPP20" selected>OCPP 2.0.1</option>
</select>
</div>
</div>
<div class="form-group mt">
<input type="submit" onclick="javascript:submitData()" value="Submit" />
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,9 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x3000,
pmjournal,data, undefined, , 0x2000
otadata, data, ota, 0xE000, 0x2000,
app1, app, ota_1, , 0x1E0000,
app0, app, ota_0, , 0x1E0000,
spiffs, data, spiffs, , 0x2F000,
sernr, data, nvs_keys, ,0x1000,
1 # Name, Type, SubType, Offset, Size, Flags
2 nvs, data, nvs, 0x9000, 0x3000,
3 pmjournal,data, undefined, , 0x2000
4 otadata, data, ota, 0xE000, 0x2000,
5 app1, app, ota_1, , 0x1E0000,
6 app0, app, ota_0, , 0x1E0000,
7 spiffs, data, spiffs, , 0x2F000,
8 sernr, data, nvs_keys, ,0x1000,

View File

@@ -0,0 +1,53 @@
CONFIG_COMPILER_CXX_EXCEPTIONS=n
CONFIG_ESP_MAIN_TASK_STACK_SIZE=10240
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=10240
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=2304
CONFIG_FREERTOS_MAX_TASK_NAME_LEN=32
CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=y
CONFIG_MB_TIMER_PORT_ENABLED=y
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_HEAP_USE_HOOKS=y
CONFIG_ESP_BROWNOUT_DET=n
CONFIG_ESP_SYSTEM_BROWNOUT_INTR=n
CONFIG_SPI_FLASH_BROWNOUT_RESET_XMC=n
CONFIG_SPI_FLASH_BROWNOUT_RESET=n
CONFIG_ESP_HTTPS_SERVER_ENABLE=y
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
# Minimizing wifi buffer usage - using "Minimum" profile:
# https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/wifi.html#how-to-configure-parameters
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=4
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=8
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=8
CONFIG_ESP_WIFI_RX_BA_WIN=Disable
CONFIG_LWIP_TCP_WND_DEFAULT=8
TCP_WND_DEFAULT=8
CONFIG_ESP_WIFI_IRAM_OPT=ENABLE
CONFIG_ESP_WIFI_RX_IRAM_OPT=ENABLE
CONFIG_LWIP_IRAM_OPTIMIZATION=ENABLE
# Minimizing TLS heap usage:
# https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/protocols/mbedtls.html#reducing-heap-usage
# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH=y
CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=n
# CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y
CONFIG_MBEDTLS_DYNAMIC_FREE_CA_CERT=y
# Note: disabling dynamic buffers - failing in esp_http_client_read during firmware update
CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH=n
CONFIG_MBEDTLS_DYNAMIC_BUFFER=n
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=1024

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -0,0 +1,50 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_CHARGING_STATION_H
#define CHARGELAB_OPEN_FIRMWARE_CHARGING_STATION_H
#include "openocpp/interface/element/websocket_raw_interface.h"
#include "openocpp/interface/message_handler.h"
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/interface/element/storage_interface.h"
#include "openocpp/interface/connector_interface.h"
#include "openocpp/helpers/optional.h"
#include "openocpp/helpers/string.h"
#include "openocpp/common/settings.h"
#include "openocpp/common/logging.h"
#include <memory>
#include <utility>
namespace chargelab {
class ChargingStation {
public:
ChargingStation(std::shared_ptr<WebsocketInterface> websocket, std::shared_ptr<MessageHandlerInterface> handler)
: websocket_(std::move(websocket)), handler_(std::move(handler))
{
}
~ChargingStation() {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting ChargingStation";
}
public:
void runStep() {
auto connected = websocket_->isConnected();
if (connected) {
optional::IfPresent(websocket_->pollTextMessages(), [&](auto&& x) { handler_->onTextMessage(x); });
optional::IfPresent(websocket_->pollBinaryMessages(), [&](auto&& x) { handler_->onBinaryMessage(x); });
}
handler_->runStep();
if (connected) {
optional::IfPresent(handler_->pollTextMessages(), [&](auto&& x) { websocket_->sendTextMessage(x); });
optional::IfPresent(handler_->pollBinaryMessages(), [&](auto&& x) { websocket_->sendBinaryMessage(x); });
}
}
private:
std::shared_ptr<SystemInterface> system_;
std::shared_ptr<WebsocketInterface> websocket_;
std::shared_ptr<MessageHandlerInterface> handler_;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_CHARGING_STATION_H

View File

@@ -0,0 +1,149 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_COMPRESSED_JOURNAL_H
#define CHARGELAB_OPEN_FIRMWARE_COMPRESSED_JOURNAL_H
#include "openocpp/common/compressed_queue.h"
#include "openocpp/interface/element/flash_block_interface.h"
namespace chargelab {
template <typename T, typename Serializer>
class CompressedJournalCustom {
public:
explicit CompressedJournalCustom(std::unique_ptr<FlashBlockInterface> storage, std::string protocol_version)
: storage_(std::move(storage)),
protocol_version_(std::move(protocol_version))
{
}
bool addUpdate(std::vector<T> const& final_state, T const& delta) {
// First try to add the delta to the stream
if (addToStream(std::vector<T>{delta}))
return true;
// Otherwise erase flash storage and write the final state
CHARGELAB_LOG_MESSAGE(info) << "Available journal storage exhausted - clearing";
storage_->erase();
writeHeader();
return addToStream(final_state);
}
template <typename Visitor>
void visit(Visitor&& visitor) {
if (!checkHeader())
return;
std::vector<uint8_t> raw_data;
raw_data.resize(storage_->size() - protocol_version_.size());
storage_->read(protocol_version_.size(), raw_data.data(), raw_data.size());
std::vector<uint8_t> input_buffer;
CompressedInputStreamZLib input(input_buffer, raw_data.data(), raw_data.size(), true);
while (true) {
auto record = input.nextRecord();
if (!record.has_value())
break;
auto result = Serializer::read(record.value());
if (result.has_value()) {
visitor(record.value(), result.value());
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Failed deserializing payload: " << record.value();
}
}
}
[[nodiscard]] std::size_t totalBytesWritten() const {
return bytes_written_;
}
[[nodiscard]] std::size_t storageSize() const {
return storage_->size();
}
private:
bool checkHeader() {
std::string test;
test.resize(protocol_version_.size());
storage_->read(0, test.data(), test.size());
return test == protocol_version_;
}
bool writeHeader() {
return storage_->write(0, protocol_version_.data(), protocol_version_.size());
}
bool addToStream(std::vector<T> const& records) {
if (!checkHeader())
return false;
std::vector<uint8_t> raw_data;
raw_data.resize(storage_->size() - protocol_version_.size());
storage_->read(protocol_version_.size(), raw_data.data(), raw_data.size());
std::vector<uint8_t> input_buffer;
std::vector<uint8_t> output_buffer;
CompressedInputStreamZLib input(input_buffer, raw_data.data(), raw_data.size(), true);
CompressedOutputStreamZLib output(output_buffer, Z_SYNC_FLUSH);
while (true) {
auto record = input.nextRecord();
if (!record.has_value())
break;
output.addRecord(record.value());
}
for (auto const& x : records)
output.addRecord(Serializer::write(x));
if (!output.close(false) || output_buffer.size() > raw_data.size())
return false;
total_bytes_ = output_buffer.size();
std::size_t first_index = 0;
while (first_index < output_buffer.size()) {
auto const old_byte = raw_data[first_index];
auto const new_byte = output_buffer[first_index];
// Note: assuming write operations can only set bits to zero in between erase operations
if ((old_byte & new_byte) != new_byte)
return false;
if (old_byte != new_byte)
break;
first_index++;
}
if (first_index >= output_buffer.size())
return true;
auto last_index = first_index;
for (auto i = first_index; i < output_buffer.size(); i++) {
auto const old_byte = raw_data[i];
auto const new_byte = output_buffer[i];
// Note: assuming write operations can only set bits to zero in between erase operations
if ((old_byte & new_byte) != new_byte)
return false;
if (old_byte != new_byte)
last_index = i;
}
auto const size = last_index - first_index + 1;
bytes_written_ += size;
return storage_->write(protocol_version_.size() + first_index, &output_buffer[first_index], size);
}
private:
std::unique_ptr<FlashBlockInterface> storage_;
std::string protocol_version_;
std::optional<std::size_t> total_bytes_;
std::atomic<std::size_t> bytes_written_ = 0;
};
template <typename T>
using CompressedJournalJson = CompressedJournalCustom<T, detail::JsonSerializer<T>>;
}
#endif //CHARGELAB_OPEN_FIRMWARE_COMPRESSED_JOURNAL_H

View File

@@ -0,0 +1,606 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_COMPRESSED_QUEUE_H
#define CHARGELAB_OPEN_FIRMWARE_COMPRESSED_QUEUE_H
#include "openocpp/common/logging.h"
#include <optional>
#include <vector>
#include <cstdint>
#include <cstring>
#include <variant>
#include <string>
#include "zlib.h"
namespace chargelab {
namespace detail {
class CompressedStreamZlibConstants {
public:
// zlib constants
static constexpr int kZlibLevel = Z_BEST_COMPRESSION;
// 2KiB history buffer
static constexpr int kZlibWindowBits = 11;
static constexpr int kZlibMemLevel = 1;
// Compressed block size threshold - when the size of the compressed records in a block exceeds this
// threshold a new block is created to store subsequent records
static constexpr std::size_t kCompressedBlockThreshold = 5 * 1024;
static constexpr std::size_t kInflateBufferInitialSize = 256;
static constexpr std::size_t kDeflateBufferStepSize = 256;
};
}
class CompressedOutputStreamZLib {
public:
CompressedOutputStreamZLib(std::vector<uint8_t>& buffer, int record_flush = Z_NO_FLUSH)
: buffer_(buffer), record_flush_(record_flush)
{
resetInternal();
}
~CompressedOutputStreamZLib() {
if (initialized_)
deflateEnd(&stream_);
}
CompressedOutputStreamZLib(CompressedOutputStreamZLib const&) = delete;
CompressedOutputStreamZLib(CompressedOutputStreamZLib&&) = delete;
CompressedOutputStreamZLib& operator=(CompressedOutputStreamZLib const&) = delete;
CompressedOutputStreamZLib& operator=(CompressedOutputStreamZLib&&) = delete;
void addRecord(std::string_view const& record) {
if (!initialized_)
return;
int size_prefix = (int)record.size();
writeBlock(&size_prefix, sizeof(size_prefix), Z_NO_FLUSH);
writeBlock(record.data(), record.size(), record_flush_);
}
void reset() {
resetInternal();
}
bool close(bool finish_stream = true) {
if (!initialized_)
return false;
if (finish_stream) {
while(true) {
if (stream_.avail_out <= 0) {
auto const index = buffer_.size();
buffer_.resize(buffer_.size() + detail::CompressedStreamZlibConstants::kDeflateBufferStepSize);
stream_.next_out = buffer_.data() + index;
stream_.avail_out = buffer_.size() - index;
}
auto const ret = deflate(&stream_, Z_FINISH);
if (ret == Z_STREAM_END)
break;
if (ret != Z_OK) {
CHARGELAB_LOG_MESSAGE(warning) << "deflate failed with error code: " << ret;
return false;
}
}
}
buffer_.resize(buffer_.size() - stream_.avail_out);
deflateEnd(&stream_);
std::memset(&stream_, 0, sizeof(stream_));
initialized_ = false;
return true;
}
[[nodiscard]] std::size_t approximateTotalBytes() const {
if (!initialized_)
return 0;
return buffer_.size() - stream_.avail_out;
}
private:
void writeBlock(void const* data, std::size_t size, int flush) {
stream_.next_in = (uint8_t*)data;
stream_.avail_in = size;
do {
if (stream_.avail_out <= 0) {
auto const index = buffer_.size() - stream_.avail_out;
buffer_.resize(buffer_.size() + detail::CompressedStreamZlibConstants::kDeflateBufferStepSize);
stream_.next_out = buffer_.data() + index;
stream_.avail_out = buffer_.size() - index;
}
auto const ret = deflate(&stream_, flush);
if (ret != Z_OK) {
CHARGELAB_LOG_MESSAGE(warning) << "deflate failed with error code: " << ret;
return;
}
} while(stream_.avail_out == 0);
stream_.next_in = Z_NULL;
stream_.avail_in = 0;
}
void resetInternal() {
buffer_.resize(detail::CompressedStreamZlibConstants::kDeflateBufferStepSize);
stream_.zalloc = Z_NULL;
stream_.zfree = Z_NULL;
stream_.opaque = Z_NULL;
stream_.next_in = Z_NULL;
stream_.avail_in = 0;
stream_.next_out = buffer_.data();
stream_.avail_out = buffer_.size();
auto const ret = deflateInit2(
&stream_,
detail::CompressedStreamZlibConstants::kZlibLevel,
Z_DEFLATED,
detail::CompressedStreamZlibConstants::kZlibWindowBits,
detail::CompressedStreamZlibConstants::kZlibMemLevel,
Z_DEFAULT_STRATEGY
);
if (ret == Z_OK) {
initialized_ = true;
} else {
CHARGELAB_LOG_MESSAGE(warning) << "deflateInit failed with error code: " << ret;
}
}
private:
z_stream stream_ {};
std::vector<uint8_t>& buffer_;
int record_flush_;
bool initialized_ = false;
};
class CompressedInputStreamZLib {
public:
CompressedInputStreamZLib(
std::vector<uint8_t>& buffer,
uint8_t* data,
std::size_t size,
bool ignore_read_errors = false
)
: buffer_(buffer),
ignore_read_errors_(ignore_read_errors)
{
buffer_.resize(detail::CompressedStreamZlibConstants::kInflateBufferInitialSize);
stream_.zalloc = Z_NULL;
stream_.zfree = Z_NULL;
stream_.opaque = Z_NULL;
stream_.next_in = data;
stream_.avail_in = size;
stream_.next_out = buffer_.data();
stream_.avail_out = buffer_.size();
auto const ret = inflateInit2(&stream_, detail::CompressedStreamZlibConstants::kZlibWindowBits);
if (ret == Z_OK) {
initialized_ = true;
} else {
CHARGELAB_LOG_MESSAGE(warning) << "inflateInit failed with error code: " << ret;
}
}
~CompressedInputStreamZLib() {
if (initialized_)
inflateEnd(&stream_);
}
CompressedInputStreamZLib(CompressedInputStreamZLib const&) = delete;
CompressedInputStreamZLib(CompressedInputStreamZLib&&) = delete;
CompressedInputStreamZLib& operator=(CompressedInputStreamZLib const&) = delete;
CompressedInputStreamZLib& operator=(CompressedInputStreamZLib&&) = delete;
std::optional<std::string_view> nextRecord() {
if (!initialized_)
return std::nullopt;
int size_value {};
auto size_ptr = readBlock(sizeof(size_value));
if (size_ptr == nullptr)
return std::nullopt;
std::memcpy(&size_value, size_ptr, sizeof(size_value));
auto data_ptr = readBlock(size_value);
if (data_ptr == nullptr) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed reading data block of size: " << size_value;
return std::nullopt;
}
return std::string_view {(char const*)data_ptr, (std::size_t)size_value};
}
bool close() {
if (!initialized_)
return false;
inflateEnd(&stream_);
initialized_ = false;
return true;
}
private:
void const* readBlock(std::size_t size) {
auto bytes_present = buffer_.size() - stream_.avail_out - index_;
if (bytes_present >= size) {
auto result = &buffer_[index_];
index_ += size;
return result;
}
if (buffer_.size() < size)
buffer_.resize(size);
std::memmove(buffer_.data(), buffer_.data()+index_, bytes_present);
stream_.next_out = buffer_.data() + bytes_present;
stream_.avail_out = buffer_.size() - bytes_present;
index_ = 0;
while (true) {
auto const avail_begin = stream_.avail_in;
auto const ret = inflate(&stream_, Z_BLOCK);
auto const avail_end = stream_.avail_in;
if (ret != Z_OK && ret != Z_STREAM_END) {
if (!ignore_read_errors_) {
CHARGELAB_LOG_MESSAGE(warning) << "inflate failed with error code " << ret << ": "
<< (char const *) stream_.msg;
}
return nullptr;
}
bytes_present = buffer_.size() - stream_.avail_out - index_;
if (bytes_present >= size)
break;
if (avail_begin == avail_end)
return nullptr;
}
index_ += size;
return buffer_.data();
}
private:
z_stream stream_ {};
std::vector<uint8_t>& buffer_;
bool ignore_read_errors_;
bool initialized_ = false;
std::size_t index_ = 0;
};
class CompressedQueueRawZLib {
public:
CompressedQueueRawZLib() {
}
// Use poll first and pop first instead, and just remove when the attempts time-out
std::optional<std::string> pollFront() {
if (blocks_.empty())
return std::nullopt;
CompressedInputStreamZLib input {inflate_buffer_, blocks_.front().data(), blocks_.front().size()};
auto const record = input.nextRecord();
if (!record.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected state - no records present in chunk";
blocks_.erase(blocks_.begin());
return std::nullopt;
}
return std::string {record.value()};
}
std::optional<std::string> popFront() {
if (blocks_.empty())
return std::nullopt;
CompressedInputStreamZLib input {inflate_buffer_, blocks_.front().data(), blocks_.front().size()};
auto const record = input.nextRecord();
if (!record.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected state - no records present in chunk";
blocks_.erase(blocks_.begin());
return std::nullopt;
}
auto result = std::string {record.value()};
bool empty = true;
CompressedOutputStreamZLib output {deflate_buffer_};
while (true) {
auto const& x = input.nextRecord();
if (!x.has_value())
break;
output.addRecord(x.value());
empty = false;
}
output.close();
if (empty) {
blocks_.erase(blocks_.begin());
} else {
blocks_.front() = deflate_buffer_;
}
return result;
}
void updateFront(std::string const& update) {
if (blocks_.empty())
return;
CompressedInputStreamZLib input {inflate_buffer_, blocks_.front().data(), blocks_.front().size()};
auto const record = input.nextRecord();
if (!record.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected state - no records present in chunk";
blocks_.erase(blocks_.begin());
return;
}
CompressedOutputStreamZLib output {deflate_buffer_};
output.addRecord(update);
while (true) {
auto const& x = input.nextRecord();
if (!x.has_value())
break;
output.addRecord(x.value());
}
if (!output.close()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed closing output stream - offline messages were dropped";
blocks_.erase(blocks_.begin());
} else {
blocks_.front() = deflate_buffer_;
}
}
void pushBack(std::string const& value) {
if (!blocks_.empty() && blocks_.back().size() < detail::CompressedStreamZlibConstants::kCompressedBlockThreshold) {
// Try to add the record to the final block
{
CompressedOutputStreamZLib writer{deflate_buffer_};
CompressedInputStreamZLib reader{inflate_buffer_, blocks_.back().data(), blocks_.back().size()};
while (true) {
auto record = reader.nextRecord();
if (!record.has_value())
break;
writer.addRecord(record.value());
}
if (!reader.close()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed reading historic data - dropping record";
clear();
return;
}
writer.addRecord(value);
if (!writer.close()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed closing compressed stream - dropping record" ;
clear();
return;
}
}
// Note: freeing the reader and writer before resizing buffer
blocks_.back() = deflate_buffer_;
} else {
// Otherwise add a new block
{
CompressedOutputStreamZLib writer{deflate_buffer_};
writer.addRecord(value);
if (!writer.close()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed closing compressed stream - dropping record";
clear();
return;
}
}
// Note: freeing the writer before adding new buffer
blocks_.push_back(deflate_buffer_);
}
}
template <typename Visitor>
void visit(Visitor&& visitor) {
for (auto& block : blocks_) {
CompressedInputStreamZLib reader{inflate_buffer_, block.data(), block.size()};
while (true) {
auto const& record = reader.nextRecord();
if (!record.has_value())
break;
visitor(record.value());
}
}
}
template <typename Predicate>
void removeIf(Predicate&& predicate) {
bool empty = true;
CompressedOutputStreamZLib writer{deflate_buffer_};
std::vector<std::vector<uint8_t>> original_blocks;
std::swap(blocks_, original_blocks);
for (auto& block : original_blocks) {
CompressedInputStreamZLib reader{inflate_buffer_, block.data(), block.size()};
while (true) {
auto const& record = reader.nextRecord();
if (!record.has_value())
break;
if (predicate(record.value()))
continue;
empty = false;
writer.addRecord(record.value());
if (writer.approximateTotalBytes() > detail::CompressedStreamZlibConstants::kCompressedBlockThreshold) {
if (!writer.close()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed closing compressed stream - blocks were dropped";
} else {
blocks_.push_back(deflate_buffer_);
}
empty = true;
writer.reset();
}
}
std::vector<uint8_t> empty_buffer{};
std::swap(empty_buffer, block);
// This may be a long-running operation, so sleep briefly between individual blocks to yield to other
// threads
using namespace std::chrono_literals;
std::this_thread::sleep_for(1ms);
}
if (!empty) {
if (!writer.close()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed closing compressed stream - blocks were dropped";
} else {
blocks_.push_back(deflate_buffer_);
}
}
}
void clear() {
blocks_.clear();
}
[[nodiscard]] std::size_t totalBytes() const {
std::size_t result = 0;
for (auto const& block : blocks_)
result += block.size();
return result;
}
[[nodiscard]] bool empty() const {
return blocks_.empty();
}
template<typename Visitor>
void write(Visitor&& visitor) {
for (auto const& block : blocks_)
visitor((void*)block.data(), block.size());
}
template<typename Supplier>
void read(Supplier&& supplier) {
blocks_.clear();
while (true) {
auto const next = supplier();
if (!next.has_value())
break;
blocks_.push_back(next.value());
}
}
private:
// Note: using a shared deflate and inflate buffer here to reduce memory fragmentation
std::vector<std::vector<uint8_t>> blocks_;
std::vector<uint8_t> deflate_buffer_;
std::vector<uint8_t> inflate_buffer_;
};
template <typename T, typename Serializer>
class CompressedQueueCustom {
public:
std::optional<T> pollFront() {
auto text = queue_.pollFront();
if (!text.has_value())
return std::nullopt;
return Serializer::read(text.value());
}
std::optional<T> popFront() {
auto text = queue_.popFront();
if (!text.has_value())
return std::nullopt;
return Serializer::read(text.value());
}
void updateFront(T const& update) {
queue_.updateFront(Serializer::write(update));
}
void pushBack(T const& value) {
queue_.pushBack(Serializer::write(value));
}
template<typename Visitor>
void visit(Visitor&& visitor) {
queue_.visit([&](auto const& text) {
auto result = Serializer::read(text);
if (result.has_value()) {
visitor(text, result.value());
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Failed deserializing payload: " << text;
}
});
}
template <typename Predicate>
void removeIf(Predicate&& predicate) {
queue_.template removeIf([&](auto const& text) {
auto result = Serializer::read(text);
if (result.has_value()) {
return predicate(text, result.value());
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Failed deserializing payload";
return true;
}
});
}
void clear() {
queue_.clear();
}
[[nodiscard]] std::size_t totalBytes() const {
return queue_.totalBytes();
}
[[nodiscard]] bool empty() const {
return queue_.empty();
}
template<typename Visitor>
void write(Visitor&& visitor) {
queue_.template write(std::forward<Visitor>(visitor));
}
template<typename Supplier>
void read(Supplier&& supplier) {
queue_.template read(std::forward<Supplier>(supplier));
}
private:
CompressedQueueRawZLib queue_;
};
namespace detail {
//payload_to_string(request)
template <typename T>
struct JsonSerializer {
static std::optional<T> read(std::string_view const& text) {
return read_json_from_string<T>(text);
}
static std::string write(T const& value) {
return write_json_to_string(value);
}
};
}
template <typename T>
using CompressedQueueJson = CompressedQueueCustom<T, detail::JsonSerializer<T>>;
}
#endif //CHARGELAB_OPEN_FIRMWARE_COMPRESSED_QUEUE_H

View File

@@ -0,0 +1,73 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_JSON_FILE_H
#define CHARGELAB_OPEN_FIRMWARE_JSON_FILE_H
#include <optional>
#include <string>
#include "openocpp/helpers/json.h"
#include "logging.h"
namespace chargelab {
class JsonFile {
static constexpr int kMaxFileDumpSize = 10*1000;
public:
template<class T>
static std::optional<T> loadFile(std::string const& file_name) {
auto file_content = readTextFile(file_name.c_str());
if (file_content) {
return read_json_from_string<T>(file_content);
}
return std::nullopt;
}
template<class T>
static bool saveProfiles(std::string const& file_name, T const & content) {
FILE* file = fopen(file_name.c_str(), "w");
if (file == nullptr) {
CHARGELAB_LOG_MESSAGE(error) << "Failed to open file for writing: " << file_name;
return false;
}
auto const json_string = write_json_to_string(content);
if (!json_string.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed serializing JSON content";
return false;
}
if (fwrite(json_string->data(), 1, json_string->size(), file) < json_string.size()) {
fclose(file);
CHARGELAB_LOG_MESSAGE(error) << "Failed to write file: " << file_name;
return false;
}
fclose(file);
return true;
}
private:
static std::optional<std::string> readTextFile(const char* file_name) {
FILE* file = fopen(file_name, "r");
if (file == nullptr) {
CHARGELAB_LOG_MESSAGE(debug) << "file doesn't exist:" << file_name;
return std::nullopt;
}
fseek(file, 0, SEEK_END);
size_t file_size = ftell(file);
fseek(file, 0, SEEK_SET);
std::string text;
text.resize(file_size);
if (fread(text.data(), 1, file_size, file) != file_size) {
fclose(file);
CHARGELAB_LOG_MESSAGE(error) << "Failed reading file:" << file_name << " file size:" << file_size;
return std::nullopt;
}
fclose(file);
return text;
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_JSON_FILE_H

View File

@@ -0,0 +1,195 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_LOGGING_H
#define CHARGELAB_OPEN_FIRMWARE_LOGGING_H
#include "openocpp/model/system_types.h"
#include <atomic>
#include <variant>
#include <optional>
#include <thread>
#include <memory>
#include <functional>
#include <string_view>
#include <string>
namespace chargelab::logging {
enum class LogLevel {
trace,
debug,
info,
warning,
error,
fatal
};
struct LogMetadata {
LogLevel level;
#if defined(LOG_WITH_FILE_AND_LINE)
std::string_view file;
int line;
#endif
std::string_view function;
};
void SetLogLevel(LogLevel level);
bool IsLoggingEnabled(LogLevel level);
void PrintLogMessage(LogMetadata const& metadata, std::string_view const& message);
void PrintLogMessage(LogMetadata const& metadata, std::string const& message);
using LoggingListenerFunction = std::function<void(LogMetadata const& metadata, std::string_view const& message)>;
void RegisterLoggingListener(std::shared_ptr<LoggingListenerFunction> const& callback);
void UnregisterLoggingListener(std::shared_ptr<LoggingListenerFunction> const& callback);
template <typename T, typename U=void>
struct LogWriter;
template <>
struct LogWriter<std::string, void> {
static void write(std::string& accumulator, std::string const& value) {
accumulator += value;
}
};
template <>
struct LogWriter<std::string_view, void> {
static void write(std::string& accumulator, std::string_view const& value) {
accumulator += value;
}
};
template <>
struct LogWriter<char const*, void> {
static void write(std::string& accumulator, char const* value) {
accumulator += value;
}
};
template <int N>
struct LogWriter<char const[N], void> {
static void write(std::string& accumulator, char const* value) {
accumulator += value;
}
};
template <int N>
struct LogWriter<char[N], void> {
static void write(std::string& accumulator, char const* value) {
accumulator += value;
}
};
template <typename T>
struct LogWriter<T, typename std::enable_if<std::is_arithmetic<T>::value>::type> {
static void write(std::string& accumulator, T const& value) {
accumulator += std::to_string(value);
}
};
template <>
struct LogWriter<SystemTimeMillis, void> {
static void write(std::string& accumulator, SystemTimeMillis value) {
accumulator += std::to_string(static_cast<int64_t>(value));
}
};
template <>
struct LogWriter<SteadyPointMillis, void> {
static void write(std::string& accumulator, SteadyPointMillis value) {
accumulator += std::to_string(static_cast<int64_t>(value));
}
};
template <typename T>
struct LogWriter<T, typename std::enable_if_t<std::is_enum<T>::value>> {
static void write(std::string& accumulator, T const& value) {
using IntType = typename std::underlying_type<T>::type;
accumulator += std::to_string(static_cast<IntType>(value));
}
};
class LogAccumulator {
public:
explicit LogAccumulator(LogLevel level,
#if defined(LOG_WITH_FILE_AND_LINE)
std::string_view file, int line,
#endif
std::string_view function)
: metadata_ {level,
#if defined(LOG_WITH_FILE_AND_LINE)
file, line,
#endif
function},
enabled_(IsLoggingEnabled(level))
{
}
~LogAccumulator() {
PrintLogMessage(metadata_, accumulator_);
}
[[nodiscard]] bool getDone() const {
return !enabled_ || done_;
}
void setDone(bool done) {
done_ = done;
}
template <typename T>
friend LogAccumulator& operator<<(LogAccumulator& os, T const& value) {
LogWriter<T>::write(os.accumulator_, value);
return os;
}
private:
LogMetadata metadata_;
bool enabled_;
bool done_ = false;
std::string accumulator_;
};
namespace detail {
constexpr char const* FileName(char const* path) {
char const* it = path;
char const* result = it;
while (true) {
auto ch = *(it++);
if (ch == '\0')
break;
if (ch == '/')
result = it;
}
return result;
}
}
class NoOpAccumulator {
template <typename T>
friend NoOpAccumulator& operator<<(NoOpAccumulator& os, T const&) {
return os;
}
};
}
#ifndef CHARGELAB_DISABLE_LOGGING
#if defined(LOG_WITH_FILE_AND_LINE)
#define CHARGELAB_LOG_MESSAGE(level) \
for (::chargelab::LogAccumulator accumulator{::chargelab::LogLevel::level, ::chargelab::detail::FileName(__FILE__), __LINE__, __func__}; !accumulator.getDone(); accumulator.setDone(true)) \
accumulator
#else
#define CHARGELAB_LOG_MESSAGE(level) \
for (::chargelab::logging::LogAccumulator accumulator{::chargelab::logging::LogLevel::level, __func__}; !accumulator.getDone(); accumulator.setDone(true)) \
accumulator
#endif
#else
#define CHARGELAB_LOG_MESSAGE(level) \
for (::chargelab::NoOpAccumulator accumulator{}; false;) \
accumulator
#endif
#endif //CHARGELAB_OPEN_FIRMWARE_LOGGING_H

View File

@@ -0,0 +1,94 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_MACRO_H
#define CHARGELAB_OPEN_FIRMWARE_MACRO_H
#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)
#define CHARGELAB_THROW(exception) throw exception
#define CHARGELAB_TRY try
#define CHARGELAB_CATCH catch(std::exception const& e)
#define CHARGELAB_RETHROW throw
#else
#include <cstdlib>
#define CHARGELAB_THROW(exception) std::abort()
#define CHARGELAB_TRY if(true)
#define CHARGELAB_CATCH for(std::exception e; false;)
#define CHARGELAB_RETHROW
#endif
#define CHARGELAB_STR(x) #x
#define CHARGELAB_XSTR(x) CHARGELAB_STR(x)
// echo -n "#define CHARGELAB_NUM_ARGS_IMPL(x1"; for x in `seq 2 64`; do echo -n ",x$x"; done; echo ",N,...) N"; echo -n "#define CHARGELAB_NUM_ARGS(...) CHARGELAB_NUM_ARGS_IMPL(__VA_ARGS__ __VA_OPT__(,)64"; for x in `seq 63 -1 0`; do echo -n ",$x"; done; echo ")";
#define CHARGELAB_NUM_ARGS_IMPL(x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59,x60,x61,x62,x63,x64,N,...) N
#define CHARGELAB_NUM_ARGS(...) CHARGELAB_NUM_ARGS_IMPL(__VA_ARGS__ __VA_OPT__(,)64,63,62,61,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,45,44,43,42,41,40,39,38,37,36,35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0)
// echo "#define CHARGELAB_EXPAND(x) x"; for x in `seq 0 64`; do echo -n "#define CHARGELAB_EXPAND_$x(f"; for y in `seq 1 $x`; do echo -n ",x$y"; done; echo -n ")"; for y in `seq 1 $x`; do echo -n " f(x$y)"; done; echo; done; echo -n "#define CHARGELAB_GET_MACRO(f"; for x in `seq 1 64`; do echo -n ",x$x"; done; echo -n ", TARGET, ...) TARGET"; echo; echo -n "#define CHARGELAB_PASTE(...) CHARGELAB_EXPAND(CHARGELAB_GET_MACRO(__VA_ARGS__"; for x in `seq 64 -1 0`; do echo -n ",CHARGELAB_EXPAND_$x"; done; echo "))";
#define CHARGELAB_EXPAND(x) x
#define CHARGELAB_EXPAND_0(f)
#define CHARGELAB_EXPAND_1(f,x1) f(x1)
#define CHARGELAB_EXPAND_2(f,x1,x2) f(x1) f(x2)
#define CHARGELAB_EXPAND_3(f,x1,x2,x3) f(x1) f(x2) f(x3)
#define CHARGELAB_EXPAND_4(f,x1,x2,x3,x4) f(x1) f(x2) f(x3) f(x4)
#define CHARGELAB_EXPAND_5(f,x1,x2,x3,x4,x5) f(x1) f(x2) f(x3) f(x4) f(x5)
#define CHARGELAB_EXPAND_6(f,x1,x2,x3,x4,x5,x6) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6)
#define CHARGELAB_EXPAND_7(f,x1,x2,x3,x4,x5,x6,x7) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7)
#define CHARGELAB_EXPAND_8(f,x1,x2,x3,x4,x5,x6,x7,x8) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8)
#define CHARGELAB_EXPAND_9(f,x1,x2,x3,x4,x5,x6,x7,x8,x9) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9)
#define CHARGELAB_EXPAND_10(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10)
#define CHARGELAB_EXPAND_11(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11)
#define CHARGELAB_EXPAND_12(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12)
#define CHARGELAB_EXPAND_13(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13)
#define CHARGELAB_EXPAND_14(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14)
#define CHARGELAB_EXPAND_15(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15)
#define CHARGELAB_EXPAND_16(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16)
#define CHARGELAB_EXPAND_17(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17)
#define CHARGELAB_EXPAND_18(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18)
#define CHARGELAB_EXPAND_19(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19)
#define CHARGELAB_EXPAND_20(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20)
#define CHARGELAB_EXPAND_21(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21)
#define CHARGELAB_EXPAND_22(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22)
#define CHARGELAB_EXPAND_23(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23)
#define CHARGELAB_EXPAND_24(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24)
#define CHARGELAB_EXPAND_25(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25)
#define CHARGELAB_EXPAND_26(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26)
#define CHARGELAB_EXPAND_27(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27)
#define CHARGELAB_EXPAND_28(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28)
#define CHARGELAB_EXPAND_29(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29)
#define CHARGELAB_EXPAND_30(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30)
#define CHARGELAB_EXPAND_31(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31)
#define CHARGELAB_EXPAND_32(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32)
#define CHARGELAB_EXPAND_33(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33)
#define CHARGELAB_EXPAND_34(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34)
#define CHARGELAB_EXPAND_35(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35)
#define CHARGELAB_EXPAND_36(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36)
#define CHARGELAB_EXPAND_37(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37)
#define CHARGELAB_EXPAND_38(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38)
#define CHARGELAB_EXPAND_39(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39)
#define CHARGELAB_EXPAND_40(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40)
#define CHARGELAB_EXPAND_41(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41)
#define CHARGELAB_EXPAND_42(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42)
#define CHARGELAB_EXPAND_43(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43)
#define CHARGELAB_EXPAND_44(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44)
#define CHARGELAB_EXPAND_45(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45)
#define CHARGELAB_EXPAND_46(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46)
#define CHARGELAB_EXPAND_47(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47)
#define CHARGELAB_EXPAND_48(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48)
#define CHARGELAB_EXPAND_49(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49)
#define CHARGELAB_EXPAND_50(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50)
#define CHARGELAB_EXPAND_51(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51)
#define CHARGELAB_EXPAND_52(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52)
#define CHARGELAB_EXPAND_53(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53)
#define CHARGELAB_EXPAND_54(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54)
#define CHARGELAB_EXPAND_55(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55)
#define CHARGELAB_EXPAND_56(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56)
#define CHARGELAB_EXPAND_57(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57)
#define CHARGELAB_EXPAND_58(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57) f(x58)
#define CHARGELAB_EXPAND_59(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57) f(x58) f(x59)
#define CHARGELAB_EXPAND_60(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59,x60) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57) f(x58) f(x59) f(x60)
#define CHARGELAB_EXPAND_61(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59,x60,x61) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57) f(x58) f(x59) f(x60) f(x61)
#define CHARGELAB_EXPAND_62(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59,x60,x61,x62) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57) f(x58) f(x59) f(x60) f(x61) f(x62)
#define CHARGELAB_EXPAND_63(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59,x60,x61,x62,x63) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57) f(x58) f(x59) f(x60) f(x61) f(x62) f(x63)
#define CHARGELAB_EXPAND_64(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59,x60,x61,x62,x63,x64) f(x1) f(x2) f(x3) f(x4) f(x5) f(x6) f(x7) f(x8) f(x9) f(x10) f(x11) f(x12) f(x13) f(x14) f(x15) f(x16) f(x17) f(x18) f(x19) f(x20) f(x21) f(x22) f(x23) f(x24) f(x25) f(x26) f(x27) f(x28) f(x29) f(x30) f(x31) f(x32) f(x33) f(x34) f(x35) f(x36) f(x37) f(x38) f(x39) f(x40) f(x41) f(x42) f(x43) f(x44) f(x45) f(x46) f(x47) f(x48) f(x49) f(x50) f(x51) f(x52) f(x53) f(x54) f(x55) f(x56) f(x57) f(x58) f(x59) f(x60) f(x61) f(x62) f(x63) f(x64)
#define CHARGELAB_GET_MACRO(f,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41,x42,x43,x44,x45,x46,x47,x48,x49,x50,x51,x52,x53,x54,x55,x56,x57,x58,x59,x60,x61,x62,x63,x64, TARGET, ...) TARGET
#define CHARGELAB_PASTE(...) CHARGELAB_EXPAND(CHARGELAB_GET_MACRO(__VA_ARGS__,CHARGELAB_EXPAND_64,CHARGELAB_EXPAND_63,CHARGELAB_EXPAND_62,CHARGELAB_EXPAND_61,CHARGELAB_EXPAND_60,CHARGELAB_EXPAND_59,CHARGELAB_EXPAND_58,CHARGELAB_EXPAND_57,CHARGELAB_EXPAND_56,CHARGELAB_EXPAND_55,CHARGELAB_EXPAND_54,CHARGELAB_EXPAND_53,CHARGELAB_EXPAND_52,CHARGELAB_EXPAND_51,CHARGELAB_EXPAND_50,CHARGELAB_EXPAND_49,CHARGELAB_EXPAND_48,CHARGELAB_EXPAND_47,CHARGELAB_EXPAND_46,CHARGELAB_EXPAND_45,CHARGELAB_EXPAND_44,CHARGELAB_EXPAND_43,CHARGELAB_EXPAND_42,CHARGELAB_EXPAND_41,CHARGELAB_EXPAND_40,CHARGELAB_EXPAND_39,CHARGELAB_EXPAND_38,CHARGELAB_EXPAND_37,CHARGELAB_EXPAND_36,CHARGELAB_EXPAND_35,CHARGELAB_EXPAND_34,CHARGELAB_EXPAND_33,CHARGELAB_EXPAND_32,CHARGELAB_EXPAND_31,CHARGELAB_EXPAND_30,CHARGELAB_EXPAND_29,CHARGELAB_EXPAND_28,CHARGELAB_EXPAND_27,CHARGELAB_EXPAND_26,CHARGELAB_EXPAND_25,CHARGELAB_EXPAND_24,CHARGELAB_EXPAND_23,CHARGELAB_EXPAND_22,CHARGELAB_EXPAND_21,CHARGELAB_EXPAND_20,CHARGELAB_EXPAND_19,CHARGELAB_EXPAND_18,CHARGELAB_EXPAND_17,CHARGELAB_EXPAND_16,CHARGELAB_EXPAND_15,CHARGELAB_EXPAND_14,CHARGELAB_EXPAND_13,CHARGELAB_EXPAND_12,CHARGELAB_EXPAND_11,CHARGELAB_EXPAND_10,CHARGELAB_EXPAND_9,CHARGELAB_EXPAND_8,CHARGELAB_EXPAND_7,CHARGELAB_EXPAND_6,CHARGELAB_EXPAND_5,CHARGELAB_EXPAND_4,CHARGELAB_EXPAND_3,CHARGELAB_EXPAND_2,CHARGELAB_EXPAND_1,CHARGELAB_EXPAND_0)(__VA_ARGS__))
#endif //CHARGELAB_OPEN_FIRMWARE_MACRO_H

View File

@@ -0,0 +1,129 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_OPERATION_HOLDER_H
#define CHARGELAB_OPEN_FIRMWARE_OPERATION_HOLDER_H
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/common/logging.h"
#include <optional>
#include <memory>
namespace chargelab {
struct NoOperation {};
inline constexpr NoOperation kNoOperation;
template <typename T>
class OperationHolder {
public:
explicit OperationHolder(std::shared_ptr<SystemInterface> system)
: system_(std::move(system))
{
assert(system_ != nullptr);
}
OperationHolder& operator=(NoOperation const&) {
state_ = std::nullopt;
consecutive_failures_ = std::nullopt;
idle_since_ = system_->steadyClockNow();
return *this;
}
bool operator==(NoOperation const&) const {
return !operationInProgress();
}
bool operator==(T const& rhs) const {
if (!state_.has_value())
return false;
return state_->first == rhs && operationInProgress();
}
bool operator!=(NoOperation const& rhs) const {
return !operator==(rhs);
}
bool operator!=(T const& rhs) const {
return !operator==(rhs);
}
[[nodiscard]] int consecutiveFailures() const {
int consecutive_failures = 0;
if (consecutive_failures_.has_value()) {
consecutive_failures = consecutive_failures_.value();
}
if (currentOperationTimedOut()) {
consecutive_failures++;
}
return consecutive_failures;
}
[[nodiscard]] bool currentOperationTimedOut() const {
return state_.has_value() && !operationInProgress();
}
[[nodiscard]] bool operationInProgress() const {
if (!state_.has_value())
return false;
auto const delta = system_->steadyClockNow() - state_->second;
if (delta < 0) {
CHARGELAB_LOG_MESSAGE(error) << "Invalid steady clock delta: " << delta;
return false;
} else if (delta >= timeout_seconds_*1000) {
return false;
}
return true;
}
[[nodiscard]] int getIdleDurationSeconds() const {
if (!state_.has_value()) {
if (idle_since_.has_value()) {
return (system_->steadyClockNow() - idle_since_.value())/1000;
} else {
return std::numeric_limits<int>::max();
}
}
auto const delta = (system_->steadyClockNow() - state_->second)/1000 - timeout_seconds_;
if (delta < 0)
return 0;
if (delta >= std::numeric_limits<int>::max())
return std::numeric_limits<int>::max();
return delta;
}
[[nodiscard]] bool wasIdleFor(int seconds) {
return !operationInProgress() && getIdleDurationSeconds() >= seconds;
}
void setWithTimeout(int timeout_seconds, std::optional<T> const& id) {
timeout_seconds_ = timeout_seconds;
if (id.has_value()) {
state_ = std::make_pair(id.value(), system_->steadyClockNow());
if (consecutive_failures_.has_value()) {
consecutive_failures_ = consecutive_failures_.value() + 1;
} else {
consecutive_failures_ = 0;
}
} else {
state_ = std::nullopt;
consecutive_failures_ = std::nullopt;
}
}
private:
std::shared_ptr<SystemInterface> system_;
int timeout_seconds_;
std::optional<std::pair<T, SteadyPointMillis>> state_ = std::nullopt;
std::optional<int> consecutive_failures_ = std::nullopt;
std::optional<SteadyPointMillis> idle_since_ = std::nullopt;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_OPERATION_HOLDER_H

View File

@@ -0,0 +1,81 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_RING_BUFFER_H
#define CHARGELAB_OPEN_FIRMWARE_RING_BUFFER_H
#include <array>
#include <optional>
#include <cassert>
namespace chargelab {
template <typename T, int N>
class RingBuffer {
public:
void pushBack(T value) {
int index;
if (size_ < (int)buffer_.size()) {
index = (begin_index_ + size_++) % buffer_.size();
} else {
index = begin_index_;
begin_index_ = (begin_index_ + 1) % buffer_.size();
}
buffer_[index] = std::move(value);
}
std::optional<T> popFront() {
if (size_ <= 0)
return std::nullopt;
T result = removeAt(begin_index_);
begin_index_ = (begin_index_ + 1) % buffer_.size();
size_--;
return result;
}
std::optional<T> popBack() {
if (size_ <= 0)
return std::nullopt;
T result = removeAt((begin_index_ + size_ - 1) % buffer_.size());
size_--;
return result;
}
T& front() {
return buffer_[begin_index_];
}
T& back() {
return buffer_[(begin_index_ + size_ - 1) % buffer_.size()];
}
T operator[] (int i) const {
assert(i >= 0 && i < size_);
return buffer_[(begin_index_ + i) % buffer_.size()];
}
T& operator[] (int i) {
assert(i >= 0 && i < size_);
return buffer_[(begin_index_ + i) % buffer_.size()];
}
[[nodiscard]] bool empty() const {
return size_ <= 0;
}
[[nodiscard]] int size() const {
return size_;
}
private:
T removeAt(int index) {
T result {};
std::swap(result, buffer_[index]);
return result;
}
private:
std::array<T, N> buffer_;
int begin_index_ = 0;
int size_ = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_RING_BUFFER_H

View File

@@ -0,0 +1,108 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_SERIALIZATION_H
#define CHARGELAB_OPEN_FIRMWARE_SERIALIZATION_H
#include <type_traits>
#include <optional>
#include <string>
#include <cstring>
namespace chargelab {
// Note: this file provides a number of very simple binary serialization wrappers that are used in certain contexts
// where JSON was too expensive (such as maintaining a large number of historic messages as a compressed stream).
template <typename T>
typename std::enable_if<std::is_trivial<T>::value, std::optional<std::size_t>>::type
readPrimitive(std::string_view const& text, std::optional<std::size_t> index, T& out) {
if (!index.has_value())
return std::nullopt;
if (index.value() + sizeof(out) > text.size())
return std::nullopt;
std::memcpy(&out, text.data() + index.value(), sizeof(out));
return index.value() + sizeof(out);
}
template <typename T>
typename std::enable_if<std::is_trivial<typename T::Value>::value, std::optional<std::size_t>>::type
readPrimitive(std::string_view const& text, std::optional<std::size_t> index, T& out) {
using raw_type = typename T::Value;
if (!index.has_value())
return std::nullopt;
if (index.value() + sizeof(raw_type{}) > text.size())
return std::nullopt;
raw_type raw;
std::memcpy(&raw, text.data() + index.value(), sizeof(raw));
out = raw;
return index.value() + sizeof(raw);
}
inline std::optional<std::size_t> readPrimitive(std::string_view const& text, std::optional<std::size_t> index, std::string& out) {
int32_t size = 0;
index = readPrimitive(text, index, size);
if (!index.has_value() || size < 0)
return std::nullopt;
if (index.value() + size > text.size())
return std::nullopt;
out.resize(size);
std::memcpy(out.data(), text.data() + index.value(), size);
return index.value() + size;
}
template <typename T>
std::optional<std::size_t> readPrimitive(std::string_view const& text, std::optional<std::size_t> index, std::optional<T>& out) {
bool present = false;
index = readPrimitive(text, index, present);
if (!index.has_value())
return std::nullopt;
if (present) {
T value;
auto result = readPrimitive(text, index, value);
out = std::move(value);
return result;
} else {
return index;
}
}
template <typename T>
typename std::enable_if<std::is_trivial<T>::value>::type
writePrimitive(std::string& text, T const& in) {
auto index = text.size();
text.resize(index + sizeof(in));
std::memcpy(text.data() + index, &in, sizeof(in));
}
template <typename T>
typename std::enable_if<std::is_trivial<typename T::Value>::value>::type
writePrimitive(std::string& text, T const& in) {
using raw_type = typename T::Value;
raw_type raw = in;
auto index = text.size();
text.resize(index + sizeof(raw));
std::memcpy(text.data() + index, &raw, sizeof(raw));
}
inline void writePrimitive(std::string& text, std::string const& in) {
writePrimitive(text, (int32_t)in.size());
auto index = text.size();
text.resize(index + in.size());
std::memcpy(text.data() + index, in.data(), in.size());
}
template <typename T>
void writePrimitive(std::string& text, std::optional<T> const& in) {
if (in.has_value()) {
writePrimitive(text, true);
writePrimitive(text, in.value());
} else {
writePrimitive(text, false);
}
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_SERIALIZATION_H

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STORAGE_H
#define CHARGELAB_OPEN_FIRMWARE_STORAGE_H
#include <string>
#include "openocpp/interface/element/storage_interface.h"
namespace chargelab {
namespace detail {
class CloseFileWrapper {
public:
CloseFileWrapper(std::FILE* file)
: file_(file)
{
}
~CloseFileWrapper() {
std::fclose(file_);
}
private:
std::FILE* file_;
};
}
class StorageFile : public StorageInterface {
public:
explicit StorageFile(std::string filename) : filename_(std::move(filename))
{
}
public:
bool read(std::function<bool(FILE *)> const& function) override {
auto file = std::fopen(filename_.c_str(), "r");
if (file == nullptr)
return false;
detail::CloseFileWrapper wrapper{file};
return function(file);
}
bool write(std::function<bool(FILE *)> const& function) override {
auto file = std::fopen(filename_.c_str(), "w");
if (file == nullptr)
return false;
detail::CloseFileWrapper wrapper{file};
return function(file);
}
private:
std::string filename_;
};
class StorageNull : public StorageInterface {
public:
bool read(std::function<bool(FILE *)> const&) override {
return false;
}
bool write(std::function<bool(FILE *)> const&) override {
return false;
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_STORAGE_H

View File

@@ -0,0 +1,150 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STREAM_H
#define CHARGELAB_OPEN_FIRMWARE_STREAM_H
#include <array>
#include <utility>
#include "openocpp/interface/element/byte_reader_interface.h"
#include "openocpp/interface/element/byte_writer_interface.h"
namespace chargelab::stream {
class BufferedByteWriter {
public:
BufferedByteWriter(ByteWriterInterface& output)
: output_(output)
{
}
void put(char ch) {
if (index_ >= buffer_.size())
flush();
buffer_[index_++] = ch;
}
void write(char const* s, std::size_t n) {
for (int i = 0; i < n; i++) {
put(s[i]);
}
}
void write(char const* s) {
write(s, std::strlen(s));
}
void write(std::string const &s) {
write(s.c_str(), s.size());
}
void flush() {
if (index_ == 0)
return;
output_.write(buffer_.data(), index_);
index_ = 0;
}
private:
ByteWriterInterface& output_;
std::array<char, 128> buffer_ {};
std::size_t index_ = 0;
};
class BufferedByteReader {
public:
BufferedByteReader(ByteReaderInterface& input)
: input_(input)
{
next();
}
int peek() const {
if (index_ >= count_)
return EOF;
return buffer_[index_];
}
int take() {
if (index_ >= count_)
return EOF;
auto const result = buffer_[index_++];
if (index_ >= count_ && count_ == buffer_.size())
next();
return result;
}
std::size_t tellg() const {
return offset_ + index_;
}
private:
void next() {
count_ = input_.read(buffer_.data(), buffer_.size());
index_ = 0;
}
private:
ByteReaderInterface& input_;
std::array<char, 128> buffer_ {};
std::size_t index_ = 0;
std::size_t count_ = 0;
std::size_t offset_ = 0;
};
class StringReader : public ByteReaderInterface {
public:
explicit StringReader(std::string text) : text_(std::move(text))
{
}
size_t read(char *s, std::size_t n) override {
if (index_ >= text_.size())
return 0;
std::size_t count = std::min(n, text_.size() - index_);
std::memcpy(s, &text_[index_], count);
index_ += count;
return count;
}
private:
std::string text_;
std::size_t index_ = 0;
};
class StringWriter : public ByteWriterInterface {
public:
void write(char const *s, std::size_t count) override {
auto const index = text_.size();
text_.resize(text_.size() + count);
std::memcpy(&text_[index], s, count);
}
std::string const& str() const {
return text_;
}
private:
std::string text_;
};
class SizeCalculator : public ByteWriterInterface {
public:
[[nodiscard]] std::size_t getTotalBytes() const {
return total_bytes_;
}
private:
void write(const char *, std::size_t count) override {
total_bytes_ += count;
}
private:
std::size_t total_bytes_ = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_STREAM_H

View File

@@ -0,0 +1,82 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_CHRONO_H
#define CHARGELAB_OPEN_FIRMWARE_CHRONO_H
#include "openocpp/model/system_types.h"
#include "openocpp/common/logging.h"
#include <optional>
#include <string>
#include <sstream>
#include "openocpp/protocol/ocpp1_6/types/date_time.h"
namespace chargelab::chrono {
namespace detail {
inline bool isLeapYear(unsigned int year) {
return (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0));
}
// month: 1-based
inline int getDaysOfMonth(unsigned int year, int month) {
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 4:
case 6:
case 9:
case 11:
return 30;
case 2:
return isLeapYear(year) ? 29 : 28;
default:
return -1;
}
}
inline int totalLeapYears(int year) {
return year/4 - year/100 + year/400;
}
}
inline std::optional<std::string> ToString(SystemTimeMillis const& timestamp) {
return ocpp1_6::DateTime::timestampToText(timestamp);
}
inline std::optional<SystemTimeMillis> FromString(std::string const& text) {
return ocpp1_6::DateTime::textToTimestamp(text);
}
inline std::time_t timegm2(std::tm const *tm) {
if (tm == nullptr) return -1;
if (tm->tm_mon >= 12 || tm->tm_hour >= 24 || tm->tm_min >= 60 || tm->tm_sec >= 61) {
return -1;
}
if (detail::getDaysOfMonth(tm->tm_year + 1900, tm->tm_mon + 1) == -1) {
return -1;
}
// get the day to the tm date since 1900-01-01 00:00:00 +0000, UTC instead of the Epoch (1970-01-01 00:00:00 +0000, UTC)
std::int64_t total_days = 0; //
int year = tm->tm_year + 1900;
auto const year_since_epoch = year - 1970;
int leap_years = detail::totalLeapYears(year - 1) - detail::totalLeapYears(1970);
total_days = year_since_epoch * 365 + leap_years;
for (int i = 0; i < tm->tm_mon; ++i) {
total_days += detail::getDaysOfMonth(year, i+1);
}
total_days += tm->tm_mday - 1; // tm_mday is 1-31
std::int64_t total_hours = total_days*24 + tm->tm_hour;
std::time_t total_seconds = (total_hours*60 + tm->tm_min)*60 + tm->tm_sec;
return total_seconds /*+ kSecondsFrom1990*/;
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_CHRONO_H

View File

@@ -0,0 +1,25 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_MAP_H
#define CHARGELAB_OPEN_FIRMWARE_MAP_H
#include <utility>
namespace chargelab {
namespace container {
template <typename T, typename V>
bool contains(T&& container, V const& value) {
return container.find(value) != container.end();
}
template <typename T>
bool containsAny(T&&) {
return true;
}
template <typename T, typename V, typename... Tail>
bool containsAny(T&& container, V const& head, Tail... tail) {
return contains(container, head) || containsAny(container, std::forward<Tail>(tail)...);
}
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_MAP_H

View File

@@ -0,0 +1,76 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_FILE_H
#define CHARGELAB_OPEN_FIRMWARE_FILE_H
#include "openocpp/common/logging.h"
#include "openocpp/helpers/json.h"
#include <cstdio>
#include <cctype>
namespace chargelab::file {
namespace detail {
template<typename CharType=char>
class FileByteWriter : public ByteWriterInterface {
public:
FileByteWriter(FILE* file) : file_(file) {
assert(file_ != nullptr);
}
~FileByteWriter() {
}
void write(const char *s, std::size_t length) override {
std::fwrite(s, sizeof(CharType), length, file_);
}
private:
FILE* file_;
std::array<CharType, 128> buffer_ {};
std::size_t index_ = 0;
};
}
inline bool is_eof_ignore_whitespace(FILE* file) {
while (true) {
auto ch = std::fgetc(file);
if (ch == EOF) {
return true;
}
if (!std::isspace(ch)) {
std::ungetc(ch, file);
return false;
}
}
}
template <typename T>
std::optional<T> json_read_object_from_file(FILE* file) {
if (is_eof_ignore_whitespace(file))
return std::nullopt;
int ch;
std::string line;
while ((ch = std::fgetc(file)) != EOF) {
if (ch == '\r' || ch == '\n')
break;
line += (char)ch;
}
auto const result = read_json_from_string<T>(line);
if (!result.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed parsing line as JSON: " << line;
return std::nullopt;
}
return result;
}
template<typename T>
void json_write_object_to_file(FILE* file, T&& value) {
write_json(detail::FileByteWriter<char> {file}, value);
std::fputc('\n', file);
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_FILE_H

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_OPTIONAL_H
#define CHARGELAB_OPEN_FIRMWARE_OPTIONAL_H
#include <optional>
namespace chargelab::optional {
template <typename T, typename C>
inline void IfPresent(std::optional<T> const& container, C&& consumer) {
if (container.has_value()) {
consumer(container.value());
}
}
template <typename T>
inline T GetOrDefault(std::optional<T> const& currentValue, T const& defaultValue) {
if (currentValue.has_value()) {
return currentValue.value();
} else {
return defaultValue;
}
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_OPTIONAL_H

View File

@@ -0,0 +1,40 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_SET_H
#define CHARGELAB_OPEN_FIRMWARE_SET_H
#include <set>
namespace chargelab {
namespace set {
template <typename T, typename V>
bool contains(std::vector<T> const& container, V const& value) {
return std::find(container.begin(), container.end(), value) != container.end();
}
template <typename T>
bool containsAny(std::vector<T> const&) {
return false;
}
template <typename T, typename V, typename... Tail>
bool containsAny(std::vector<T> const& container, V const& head, Tail... tail) {
return contains(container, head) || containsAny(container, std::forward<Tail>(tail)...);
}
template <typename T, typename V>
bool contains(std::set<T> const& container, V const& value) {
return container.find(value) != container.end();
}
template <typename T>
bool containsAny(std::set<T> const&) {
return false;
}
template <typename T, typename V, typename... Tail>
bool containsAny(std::set<T> const& container, V const& head, Tail... tail) {
return contains(container, head) || containsAny(container, std::forward<Tail>(tail)...);
}
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_SET_H

View File

@@ -0,0 +1,161 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STRING_H
#define CHARGELAB_OPEN_FIRMWARE_STRING_H
#include <string>
#include <array>
#include <optional>
#include <limits.h>
#include <cstdint>
namespace chargelab::string {
namespace detail {
char const kHexCharacters[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D' ,'E', 'F'};
}
inline bool BeginsWithIgnoreCaseAscii(std::string const& text, std::string const& prefix) {
if (text.length() < prefix.length())
return false;
auto text_it = text.begin();
auto prefix_it = prefix.begin();
while (prefix_it != prefix.end()) {
auto text_ch = *(text_it++);
auto prefix_ch = *(prefix_it++);
if (std::tolower(text_ch) != std::tolower(prefix_ch)) {
return false;
}
}
return true;
}
inline bool EqualsIgnoreCaseAscii(std::string const& lhs, std::string const& rhs) {
return lhs.size() == rhs.size() && BeginsWithIgnoreCaseAscii(lhs, rhs);
}
inline std::optional<int> ToInteger(std::string const& text) {
if (text.empty())
return std::nullopt;
errno = 0;
char* end = nullptr;
auto const result = strtol(text.c_str(), &end, 10);
auto const parse_error = errno;
if (end != text.c_str() + text.size()) {
return std::nullopt;
}
if (parse_error != 0) {
return std::nullopt;
}
return result;
}
inline std::optional<std::int64_t> ToInteger64(std::string const& text) {
if (text.empty())
return std::nullopt;
errno = 0;
char* end = nullptr;
auto const result = strtoll(text.c_str(), &end, 10);
auto const parse_error = errno;
if (end != text.c_str() + text.size()) {
return std::nullopt;
}
if (parse_error != 0) {
return std::nullopt;
}
return result;
}
inline std::optional<double> ToDouble(std::string const& text) {
if (text.empty())
return std::nullopt;
errno = 0;
char* end = nullptr;
auto const result = strtod(text.c_str(), &end);
auto const parse_error = errno;
if (end != text.c_str() + text.size()) {
return std::nullopt;
}
if (parse_error != 0) {
return std::nullopt;
}
return result;
}
template<typename T>
typename std::enable_if<
std::is_integral<T>::value,
std::string
>::type ToHexValue(T value) {
if (value == 0)
return "0x0";
std::string result;
if (value < 0) {
value = -value;
result = "-0x";
} else {
result = "0x";
}
for (int i=0; i < (int)sizeof(T); i++) {
auto const offset = (sizeof(T) - i - 1) * CHAR_BIT;
auto const byte = (value >> offset) & 0xFF;
result += detail::kHexCharacters[byte >> 4];
result += detail::kHexCharacters[byte & 0xF];
}
return result;
}
inline std::string ToHexString(std::uint8_t const* begin, std::uint8_t const* end, char const* separator = " ") {
std::string result;
char const* ifs = "";
for (auto it = begin; it != end; it++) {
auto const byte = *it;
result += ifs;
result += detail::kHexCharacters[byte >> 4];
result += detail::kHexCharacters[byte & 0xF];
ifs = separator;
}
return result;
}
inline std::string ToHexString(std::uint8_t const* begin, std::size_t length, char const* separator = " ") {
return ToHexString(begin, begin+length, separator);
}
template <std::size_t N>
inline std::string ToHexString(std::array<std::uint8_t, N> const& array, char const* separator = " ") {
return ToHexString(array.data(), array.size(), separator);
}
template <typename F>
inline void SplitVisitor(std::string const& text, std::string const& delimiter, F&& visitor) {
std::size_t begin = 0;
while (true) {
auto it = text.find(delimiter, begin);
if (it == std::string::npos) {
visitor(text.substr(begin));
break;
}
visitor(text.substr(begin, it-begin));
begin = it + delimiter.size();
}
}
inline std::string zeroPad(std::string const& text, int length) {
std::string result;
result.resize(std::max(length - text.size(), (std::size_t)0), '0');
return result + text;
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_STRING_H

View File

@@ -0,0 +1,251 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_URI_H
#define CHARGELAB_OPEN_FIRMWARE_URI_H
#include "openocpp/helpers/string.h"
#include "openocpp/helpers/json.h"
#include "openocpp/helpers/optional.h"
#include <cstdlib>
#include <regex>
namespace chargelab::uri {
namespace detail {
inline std::pair<std::string, std::optional<std::string>> SplitLeft(std::string const& text, std::string const& delimiter) {
auto it = text.find_first_of(delimiter);
if (it == std::string::npos)
return std::make_pair(text, std::nullopt);
return std::make_pair(
text.substr(0, it),
std::make_optional(text.substr(it + delimiter.size()))
);
}
inline std::pair<std::optional<std::string>, std::string> SplitRight(std::string const& text, std::string const& delimiter) {
auto result = SplitLeft(text, delimiter);
if (result.second.has_value()) {
return std::make_pair(std::make_optional(result.first), result.second.value());
} else {
return std::make_pair(std::nullopt, result.first);
}
}
int GetHexValue(char ch) {
if (ch >= '0' && ch <= '9')
return ch-'0';
if (ch >= 'a' && ch <= 'f')
return (ch-'a')+10;
if (ch >= 'A' && ch <= 'F')
return (ch-'A')+10;
return -1;
}
}
// TODO: Update to ws/wss
CHARGELAB_JSON_ENUM(WebsocketScheme,
WS,
WSS,
)
struct WebsocketParts {
WebsocketScheme scheme = WebsocketScheme::kValueNotFoundInEnum;
std::string host {};
int port = -1;
std::string path {};
CHARGELAB_JSON_INTRUSIVE(WebsocketParts, scheme, host, port, path)
};
inline std::optional<WebsocketParts> ParseWebsocketUri(std::string const& uri) {
WebsocketParts result {};
auto scheme_fragment = detail::SplitLeft(uri, "://");
if (!scheme_fragment.second.has_value())
return std::nullopt;
auto scheme = scheme_fragment.first;
if (string::EqualsIgnoreCaseAscii(scheme, "ws")) {
result.scheme = WebsocketScheme::kWS;
} else if (string::EqualsIgnoreCaseAscii(scheme, "wss")) {
result.scheme = WebsocketScheme::kWSS;
} else {
CHARGELAB_LOG_MESSAGE(error) << "Bad scheme in: " << uri;
return std::nullopt;
}
auto target_fragment = detail::SplitLeft(scheme_fragment.second.value(), "/");
auto host_port_fragment = detail::SplitLeft(target_fragment.first, ":");
if (host_port_fragment.second.has_value()) {
auto port = string::ToInteger(host_port_fragment.second.value());
if (!port.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed parsing port text in: " << uri;
return std::nullopt;
}
result.port = port.value();
} else {
switch (result.scheme) {
default:
assert(false && "Unexpected websocket protocol");
case WebsocketScheme::kWS:
result.port = 80;
break;
case WebsocketScheme::kWSS:
result.port = 443;
break;
}
}
result.host = host_port_fragment.first;
result.path = "/" + optional::GetOrDefault<std::string>(target_fragment.second, "");
CHARGELAB_LOG_MESSAGE(trace) << "Parsed websocket URI: " << result;
return result;
}
CHARGELAB_JSON_ENUM(FtpScheme,
FTP,
FTPS,
)
struct FtpParts {
FtpScheme scheme = FtpScheme::kValueNotFoundInEnum;
std::optional<std::string> username = std::nullopt;
std::optional<std::string> password = std::nullopt;
std::string host {};
int port = -1;
std::string path {};
CHARGELAB_JSON_INTRUSIVE(FtpParts, scheme, username, password, host, port, path)
};
inline std::optional<FtpParts> ParseFtpUri(std::string const& uri) {
FtpParts result {};
auto scheme_fragment = detail::SplitLeft(uri, "://");
if (!scheme_fragment.second.has_value())
return std::nullopt;
auto scheme = scheme_fragment.first;
if (string::EqualsIgnoreCaseAscii(scheme, "ftp")) {
result.scheme = FtpScheme::kFTP;
} else if (string::EqualsIgnoreCaseAscii(scheme, "ftps")) {
result.scheme = FtpScheme::kFTPS;
} else {
CHARGELAB_LOG_MESSAGE(error) << "Bad scheme in: " << uri;
return std::nullopt;
}
auto target_fragment = detail::SplitLeft(scheme_fragment.second.value(), "/");
auto credentials_fragment = detail::SplitRight(target_fragment.first, "@");
auto host_port_fragment = detail::SplitLeft(credentials_fragment.second, ":");
if (host_port_fragment.second.has_value()) {
auto port = string::ToInteger(host_port_fragment.second.value());
if (!port.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed parsing port text in: " << uri;
return std::nullopt;
}
result.port = port.value();
} else {
result.port = 21;
}
result.host = host_port_fragment.first;
result.path = "/" + optional::GetOrDefault<std::string>(target_fragment.second, "");
if (credentials_fragment.first.has_value()) {
auto username_password_fragment = detail::SplitLeft(credentials_fragment.first.value(), ":");
result.username = username_password_fragment.first;
if (username_password_fragment.second.has_value())
result.password = username_password_fragment.second.value();
}
CHARGELAB_LOG_MESSAGE(trace) << "Parsed FTP URI: " << result;
return result;
}
// TODO: Update to http/https
CHARGELAB_JSON_ENUM(HttpScheme,
Http,
Https,
)
struct HttpParts {
HttpScheme scheme = HttpScheme::kValueNotFoundInEnum;
std::string host {};
int port = -1;
std::string path {};
};
inline std::optional<HttpParts> parseHttpUri(std::string const& uri) {
HttpParts result {};
auto scheme_fragment = detail::SplitLeft(uri, "://");
if (!scheme_fragment.second.has_value())
return std::nullopt;
auto scheme = scheme_fragment.first;
if (string::EqualsIgnoreCaseAscii(scheme, "http")) {
result.scheme = HttpScheme::kHttp;
} else if (string::EqualsIgnoreCaseAscii(scheme, "https")) {
result.scheme = HttpScheme::kHttps;
} else {
CHARGELAB_LOG_MESSAGE(error) << "Bad scheme in: " << uri;
return std::nullopt;
}
auto target_fragment = detail::SplitLeft(scheme_fragment.second.value(), "/");
auto host_port_fragment = detail::SplitLeft(target_fragment.first, ":");
if (host_port_fragment.second.has_value()) {
auto port = string::ToInteger(host_port_fragment.second.value());
if (!port.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed parsing port text in: " << uri;
return std::nullopt;
}
result.port = port.value();
} else {
switch (result.scheme) {
default:
assert(false && "Unexpected websocket protocol");
case HttpScheme::kHttp:
result.port = 80;
break;
case HttpScheme::kHttps:
result.port = 443;
break;
}
}
result.host = host_port_fragment.first;
result.path = "/" + optional::GetOrDefault<std::string>(target_fragment.second, "");
return result;
}
std::string decodeUriComponent(std::string const& text) {
std::string result;
for (std::size_t i=0; i < text.size(); i++) {
// Note: allowing for deviations here; % prefixes not representing a valid octet and ignored
if (text[i] == '%' && i+2 < text.size()) {
auto const ch1 = detail::GetHexValue(text[i+1]);
auto const ch2 = detail::GetHexValue(text[i+2]);
if (ch1 >= 0 && ch2 >= 0) {
result += (char)((ch1 << 8) | ch2);
i += 2;
continue;
}
}
result += text[i];
}
return result;
}
}
#endif //CHARGELAB_OPEN_FIRMWARE_URI_H

View File

@@ -0,0 +1,171 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_HASH_METHODS_MBEDTLS_H
#define CHARGELAB_OPEN_FIRMWARE_HASH_METHODS_MBEDTLS_H
#include "openocpp/common/logging.h"
#include "openocpp/helpers/string.h"
#include <string>
#include <optional>
#include "mbedtls/ssl.h"
#include "mbedtls/base64.h"
namespace chargelab {
class HashMethodsMbedTLS;
namespace detail {
class HashCalculatorTypeMbedTLS {
private:
friend class ::chargelab::HashMethodsMbedTLS;
HashCalculatorTypeMbedTLS() {
mbedtls_md_init(&context_);
}
public:
~HashCalculatorTypeMbedTLS() {
mbedtls_md_free(&context_);
}
bool update(unsigned char const* input, std::size_t ilen) {
int err = mbedtls_md_update(&context_, input, ilen);
if (err != 0) {
CHARGELAB_LOG_MESSAGE(error) << "Update failed with error: " << err;
return false;
}
return true;
}
std::optional<std::vector<uint8_t>> finishBinary() {
auto md_info = mbedtls_md_info_from_ctx(&context_);
if (md_info == nullptr) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - missing md_info in context";
return std::nullopt;
}
std::vector<uint8_t> result;
result.resize(mbedtls_md_get_size(md_info));
int err = mbedtls_md_finish(&context_, (unsigned char*)result.data());
if (err != 0) {
CHARGELAB_LOG_MESSAGE(error) << "Finish failed with error: " << err;
return std::nullopt;
}
err = mbedtls_md_starts(&context_);
if (err != 0) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - Failed resetting MbedTLS MD context";
}
return result;
}
std::optional<std::string> finishHex() {
auto binary = finishBinary();
if (!binary.has_value())
return std::nullopt;
return string::ToHexString((unsigned char*)binary->data(), binary->size(), "");
}
void reset() {
finishBinary();
}
private:
mbedtls_md_context_t context_;
};
}
class HashMethodsMbedTLS {
private:
HashMethodsMbedTLS() {}
public:
using hash_enum_type = mbedtls_md_type_t;
using hash_calculator_type = detail::HashCalculatorTypeMbedTLS;
static constexpr hash_enum_type kHashTypeSHA256 = MBEDTLS_MD_SHA256;
static constexpr hash_enum_type kHashTypeSHA512 = MBEDTLS_MD_SHA512;
static std::unique_ptr<hash_calculator_type> createCalculator(hash_enum_type hash_type) {
auto md_info = mbedtls_md_info_from_type(hash_type);
if (md_info == nullptr) {
CHARGELAB_LOG_MESSAGE(error) << "Missing message digest info";
return nullptr;
}
auto result = std::unique_ptr<detail::HashCalculatorTypeMbedTLS> (new detail::HashCalculatorTypeMbedTLS());
int err = mbedtls_md_setup(&result->context_, md_info, 0);
if (err != 0) {
CHARGELAB_LOG_MESSAGE(error) << "Failed setting up context - error code: " << err;
return nullptr;
}
err = mbedtls_md_starts(&result->context_);
if (err != 0) {
CHARGELAB_LOG_MESSAGE(error) << "Failed starting context - error code: " << err;
return nullptr;
}
return result;
}
static std::optional<std::vector<uint8_t>> calculateHashBinary(
hash_enum_type hash_type,
unsigned char const* input,
std::size_t ilen
) {
auto md_info = mbedtls_md_info_from_type(hash_type);
if (md_info == nullptr) {
CHARGELAB_LOG_MESSAGE(warning) << "Missing message digest info";
return std::nullopt;
}
std::vector<uint8_t> digest_buffer;
digest_buffer.resize(mbedtls_md_get_size(md_info));
auto ret = mbedtls_md(md_info, input, ilen, (unsigned char*)digest_buffer.data());
if (ret != 0) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed calculating message digest - error: " << ret;
return std::nullopt;
}
return digest_buffer;
}
static std::optional<std::string> calculateHashHex(
hash_enum_type hash_type,
unsigned char const* input,
std::size_t ilen
) {
auto binary = calculateHashBinary(hash_type, input, ilen);
if (!binary.has_value())
return std::nullopt;
return string::ToHexString((unsigned char*)binary->data(), binary->size(), "");
}
static std::optional<std::vector<uint8_t>> decodeBase64(std::string const& base64_text) {
std::size_t olen = 0;
auto ret = mbedtls_base64_decode(nullptr, 0, &olen, (const unsigned char*)base64_text.data(), base64_text.size());
if (ret != 0 && ret != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed decoding base64 text/calculating size - error: " << ret;
return std::nullopt;
}
std::vector<uint8_t> result;
result.resize(olen);
ret = mbedtls_base64_decode(result.data(), result.size(), &olen, (const unsigned char*)base64_text.data(), base64_text.size());
if (ret != 0) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed decoding base64 text - error: " << ret;
return std::nullopt;
}
return result;
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_HASH_METHODS_MBEDTLS_H

View File

@@ -0,0 +1,111 @@
#include "openocpp/common/logging.h"
#include <atomic>
#include <mutex>
#define LOG_LOCAL_LEVEL ESP_LOG_VERBOSE
#include "esp_log.h"
namespace chargelab::logging {
namespace {
std::atomic<LogLevel> gLogLevel {LogLevel::trace};
constexpr char const* kTag = "main";
std::recursive_mutex gMutex {};
std::vector<std::shared_ptr<LoggingListenerFunction>> gListeners {};
}
void SetLogLevel(LogLevel level) {
gLogLevel = level;
switch (level) {
case LogLevel::trace: esp_log_level_set(kTag, ESP_LOG_VERBOSE); break;
case LogLevel::debug: esp_log_level_set(kTag, ESP_LOG_DEBUG); break;
default:
case LogLevel::info: esp_log_level_set(kTag, ESP_LOG_INFO); break;
case LogLevel::warning: esp_log_level_set(kTag, ESP_LOG_WARN); break;
case LogLevel::error: esp_log_level_set(kTag, ESP_LOG_ERROR); break;
case LogLevel::fatal: esp_log_level_set(kTag, ESP_LOG_ERROR); break;
}
}
bool IsLoggingEnabled(LogLevel level) {
LogLevel const limit = gLogLevel;
return static_cast<int>(level) >= static_cast<int>(limit);
}
#if defined(LOG_WITH_FILE_AND_LINE)
#define CHARGELAB_ESP_LOGGING_TEMPLATE(esp_macro) esp_macro(kTag, "[%s:%d(%s)] %s", metadata.file.data(), metadata.line, metadata.function.data(), message)
#else
#define CHARGELAB_ESP_LOGGING_TEMPLATE(esp_macro) esp_macro(kTag, "[%s] %s", metadata.function.data(), message)
#endif
namespace detail {
void PrintLogMessage(LogMetadata const& metadata, char const* message) {
switch (metadata.level) {
case LogLevel::trace: CHARGELAB_ESP_LOGGING_TEMPLATE(ESP_LOGV); break;
case LogLevel::debug: CHARGELAB_ESP_LOGGING_TEMPLATE(ESP_LOGD); break;
case LogLevel::info: CHARGELAB_ESP_LOGGING_TEMPLATE(ESP_LOGI); break;
case LogLevel::warning: CHARGELAB_ESP_LOGGING_TEMPLATE(ESP_LOGW); break;
case LogLevel::error: CHARGELAB_ESP_LOGGING_TEMPLATE(ESP_LOGE); break;
default:
case LogLevel::fatal: CHARGELAB_ESP_LOGGING_TEMPLATE(ESP_LOGE); break;
}
}
}
#undef CHARGELAB_ESP_LOGGING_TEMPLATE
void PrintLogMessage(LogMetadata const& metadata, std::string const& message) {
detail::PrintLogMessage(metadata, message.data());
std::lock_guard lock {gMutex};
if (!gListeners.empty()) {
for (auto const& x : gListeners) {
if (x != nullptr) {
(*x)(metadata, message);
}
}
}
}
void PrintLogMessage(LogMetadata const& metadata, std::string_view const& message) {
detail::PrintLogMessage(metadata, message.data());
std::lock_guard lock {gMutex};
if (!gListeners.empty()) {
for (auto const& x : gListeners) {
if (x != nullptr) {
(*x)(metadata, message);
}
}
}
}
void RegisterLoggingListener(std::shared_ptr<LoggingListenerFunction> const& callback) {
std::lock_guard lock {gMutex};
for (auto const& x : gListeners) {
if (x == callback) {
CHARGELAB_LOG_MESSAGE(warning) << "Callback registered multiple times";
return;
}
}
gListeners.push_back(callback);
}
void UnregisterLoggingListener(std::shared_ptr<LoggingListenerFunction> const& callback) {
std::lock_guard lock {gMutex};
auto initial_size = gListeners.size();
gListeners.erase(
std::remove_if(gListeners.begin(), gListeners.end(), [&](auto& x) {return x == callback;}),
gListeners.end()
);
if (initial_size - gListeners.size() > 1)
CHARGELAB_LOG_MESSAGE(warning) << "Callback registered multiple times";
}
}

View File

@@ -0,0 +1,138 @@
#include "openocpp/common/logging.h"
#include "openocpp/helpers/chrono.h"
#include "openocpp/helpers/optional.h"
#include <iostream>
#include <atomic>
#include <chrono>
#include <mutex>
namespace chargelab::logging {
namespace {
std::atomic<LogLevel> gLogLevel {LogLevel::trace};
std::atomic<int> gRecursiveCounter {};
std::recursive_mutex gMutex {};
std::vector<std::shared_ptr<LoggingListenerFunction>> gListeners {};
class RaiiCounter {
public:
explicit RaiiCounter(std::atomic<int>& counter) : counter_{counter} {
counter_++;
}
~RaiiCounter() {
counter_--;
}
private:
std::atomic<int>& counter_;
};
}
std::string_view ToString(LogLevel const& level) {
switch (level) {
case LogLevel::trace: return std::string_view {"trace"};
case LogLevel::debug: return std::string_view {"debug"};
case LogLevel::info: return std::string_view {"info"};
case LogLevel::warning: return std::string_view {"warning"};
case LogLevel::error: return std::string_view {"error"};
default:
case LogLevel::fatal: return std::string_view {"fatal"};
}
}
void SetLogLevel(LogLevel level) {
gLogLevel = level;
}
bool IsLoggingEnabled(LogLevel level) {
LogLevel const limit = gLogLevel;
return static_cast<int>(level) >= static_cast<int>(limit);
}
std::ostream& LogPrefix(LogMetadata const& metadata) {
auto const now = std::chrono::system_clock::now();
auto const millis = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
auto const ts = chrono::ToString(static_cast<chargelab::SystemTimeMillis>(millis));
std::ios_base::fmtflags original_flags(std::cout.flags());
std::cout << "[" << optional::GetOrDefault<std::string>(ts, "<bad timestamp>")
<< "] [" << std::hex << std::this_thread::get_id()
<< "] [" << ToString(metadata.level)
#if defined(LOG_WITH_FILE_AND_LINE)
<< "] [" << metadata.file << ":" << metadata.line << "(" << metadata.function << ")] ";
#else
<< "] [" << "(" << metadata.function << ")] ";
#endif
std::cout.flags(original_flags);
return std::cout;
}
void PrintLogMessage(LogMetadata const& metadata, std::string const& message) {
LogPrefix(metadata) << message << std::endl;
std::lock_guard lock {gMutex};
RaiiCounter counter {gRecursiveCounter};
if (gRecursiveCounter > 2) {
LogPrefix(metadata) << "Recursive PrintLogMessage call - suppressing messages" << std::endl;
return;
}
if (!gListeners.empty()) {
for (auto const& x : gListeners) {
if (x != nullptr) {
(*x)(metadata, message);
}
}
}
}
void PrintLogMessage(LogMetadata const& metadata, std::string_view const& message) {
LogPrefix(metadata) << message << std::endl;
std::lock_guard lock {gMutex};
RaiiCounter counter {gRecursiveCounter};
if (gRecursiveCounter > 2) {
LogPrefix(metadata) << "Recursive PrintLogMessage call - suppressing messages" << std::endl;
return;
}
if (!gListeners.empty()) {
for (auto const& x : gListeners) {
if (x != nullptr) {
(*x)(metadata, message);
}
}
}
}
void RegisterLoggingListener(std::shared_ptr<LoggingListenerFunction> const& callback) {
std::lock_guard lock {gMutex};
for (auto const& x : gListeners) {
if (x == callback) {
CHARGELAB_LOG_MESSAGE(warning) << "Callback registered multiple times";
return;
}
}
gListeners.push_back(callback);
}
void UnregisterLoggingListener(std::shared_ptr<LoggingListenerFunction> const& callback) {
std::lock_guard lock {gMutex};
auto initial_size = gListeners.size();
gListeners.erase(
std::remove_if(gListeners.begin(), gListeners.end(), [&](auto& x) {return x == callback;}),
gListeners.end()
);
if (initial_size - gListeners.size() > 1)
CHARGELAB_LOG_MESSAGE(warning) << "Callback registered multiple times";
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STANDARD_CHARGER_H
#define CHARGELAB_OPEN_FIRMWARE_STANDARD_CHARGER_H
#include "openocpp/interface/platform_interface.h"
#include "openocpp/interface/station_interface.h"
#include "openocpp/common/settings.h"
#include "openocpp/module/pending_messages_module.h"
#include "openocpp/module/connector_status_module.h"
#include "openocpp/module/get_logs_module.h"
#include "openocpp/module/boot_notification_module.h"
#include "openocpp/module/heartbeat_module.h"
#include "openocpp/module/configuration_module.h"
#include "openocpp/module/fallback_module.h"
#include "openocpp/module/reset_module.h"
#include "openocpp/module/firmware_update_module.h"
#include "openocpp/module/power_management_module1_6.h"
#include "openocpp/module/transaction_module1_6.h"
#include "openocpp/protocol/ocpp1_6/handlers/ocpp_message_handler.h"
#include "openocpp/module/power_management_module2_0.h"
#include "openocpp/module/transaction_module2_0.h"
#include "openocpp/protocol/ocpp2_0/handlers/ocpp_message_handler.h"
#include "openocpp/implementation/hash_methods_mbedtls.h"
namespace chargelab {
class StandardCharger {
public:
std::shared_ptr<PlatformInterface> platform;
std::shared_ptr<StationInterface> station;
std::shared_ptr<Settings> settings;
std::shared_ptr<PendingMessagesModule> pending_messages_module;
std::shared_ptr<ConnectorStatusModule> connector_status_module;
std::shared_ptr<GetLogsModule> get_logs_module;
std::shared_ptr<BootNotificationModule> boot_notification_module;
std::shared_ptr<HeartbeatModule> heartbeat_module;
std::shared_ptr<ConfigurationModule> configuration_module;
std::shared_ptr<FallbackModule> fallback_module;
std::shared_ptr<ResetModule> reset_module;
std::shared_ptr<FirmwareUpdateModule<HashMethodsMbedTLS>> firmware_update_module;
std::shared_ptr<PowerManagementModule1_6> power_management_module1_6;
std::shared_ptr<TransactionModule1_6> transaction_module1_6;
std::shared_ptr<ocpp1_6::OcppMessageHandler> message_handler1_6;
std::shared_ptr<PowerManagementModule2_0> power_management_module2_0;
std::shared_ptr<TransactionModule2_0> transaction_module2_0;
std::shared_ptr<ocpp2_0::OcppMessageHandler> message_handler2_0;
public:
template <typename T>
T notNull(T value) {
assert(value != nullptr);
return value;
}
StandardCharger(
std::shared_ptr<PlatformInterface> platform_interface,
std::shared_ptr<StationInterface> station_interface
)
: platform(std::move(platform_interface)),
station(std::move(station_interface))
{
settings = platform->getSettings();
CHARGELAB_LOG_MESSAGE(info) << "Initializing common modules...";
pending_messages_module = std::make_shared<PendingMessagesModule>(
notNull(settings),
notNull(platform),
platform->getStorage("transactions.dat")
);
connector_status_module = std::make_shared<ConnectorStatusModule>(
notNull(settings),
notNull(platform),
notNull(station)
);
get_logs_module = std::make_shared<GetLogsModule>(notNull(platform), notNull(pending_messages_module));
boot_notification_module = std::make_shared<BootNotificationModule>(notNull(settings), notNull(platform));
heartbeat_module = std::make_shared<HeartbeatModule>(notNull(settings), notNull(platform));
configuration_module = std::make_shared<ConfigurationModule>(notNull(settings), notNull(platform));
fallback_module = std::make_shared<FallbackModule>(notNull(platform));
reset_module = std::make_shared<ResetModule>(notNull(settings), notNull(platform), notNull(connector_status_module));
// TODO: Maybe hash methods should move into platform?
firmware_update_module = std::make_shared<FirmwareUpdateModule<HashMethodsMbedTLS>>(
notNull(platform),
notNull(reset_module),
notNull(pending_messages_module),
notNull(connector_status_module),
notNull(station)
);
CHARGELAB_LOG_MESSAGE(info) << "Initializing OCPP 1.6 modules...";
power_management_module1_6 = std::make_shared<PowerManagementModule1_6>(
notNull(settings),
notNull(platform),
notNull(station),
platform->getPartition("pmjournal")
);
transaction_module1_6 = std::make_shared<TransactionModule1_6>(
notNull(platform),
notNull(boot_notification_module),
notNull(power_management_module1_6),
notNull(pending_messages_module),
notNull(connector_status_module),
notNull(station)
);
message_handler1_6 = std::make_shared<ocpp1_6::OcppMessageHandler>(
notNull(settings),
notNull(platform),
std::vector<std::shared_ptr<AbstractModuleInterface>> {
notNull(platform),
notNull(boot_notification_module),
notNull(heartbeat_module),
notNull(configuration_module),
notNull(reset_module),
notNull(connector_status_module),
notNull(pending_messages_module),
notNull(firmware_update_module),
notNull(get_logs_module),
notNull(power_management_module1_6),
notNull(transaction_module1_6),
notNull(fallback_module)
},
// TODO: Get rid of this lambda; more expensive than a pointer? Also less clear what's being invoked at the call-site.
[this]() { return boot_notification_module->registrationComplete(); }
);
CHARGELAB_LOG_MESSAGE(info) << "Initializing OCPP 2.0 modules...";
power_management_module2_0 = std::make_shared<PowerManagementModule2_0>(
notNull(settings),
notNull(platform),
notNull(station),
platform->getPartition("pmjournal")
);
transaction_module2_0 = std::make_shared<TransactionModule2_0>(
notNull(platform),
notNull(boot_notification_module),
notNull(power_management_module2_0),
notNull(pending_messages_module),
notNull(connector_status_module),
notNull(station)
);
message_handler2_0 = std::make_shared<ocpp2_0::OcppMessageHandler>(
notNull(settings),
notNull(platform),
std::vector<std::shared_ptr<AbstractModuleInterface>> {
platform,
boot_notification_module,
heartbeat_module,
configuration_module,
reset_module,
connector_status_module,
pending_messages_module,
firmware_update_module,
get_logs_module,
power_management_module2_0,
transaction_module2_0,
fallback_module
},
// TODO: As above; remove this?
[this]() { return boot_notification_module->registrationComplete(); }
);
}
void runStep() {
message_handler1_6->runStep(platform->ocppConnection());
message_handler2_0->runStep(platform->ocppConnection());
}
void runLoop() {
while (true) {
message_handler1_6->runStep(platform->ocppConnection());
message_handler2_0->runStep(platform->ocppConnection());
using namespace std::chrono_literals;
std::this_thread::sleep_for(10ms);
}
}
template <typename T, typename... Args>
void addAfter(std::shared_ptr<T> module, Args&&... args) {
message_handler1_6->addAfter(module, std::forward<Args>(args)...);
message_handler2_0->addAfter(module, std::forward<Args>(args)...);
}
template <typename T, typename... Args>
void addBefore(std::shared_ptr<T> module, Args&&... args) {
message_handler1_6->addBefore(module, std::forward<Args>(args)...);
message_handler2_0->addBefore(module, std::forward<Args>(args)...);
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_STANDARD_CHARGER_H

View File

@@ -0,0 +1,109 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STATION_ESP32_H
#define CHARGELAB_OPEN_FIRMWARE_STATION_ESP32_H
#include "openocpp/interface/station_interface.h"
#include "openocpp/common/logging.h"
#include "openocpp/helpers/string.h"
#include "esp_ota_ops.h"
namespace chargelab {
class StationEsp32 : public StationInterface {
public:
StationEsp32()
: running_partition_(esp_ota_get_running_partition()),
update_partition_(esp_ota_get_next_update_partition(nullptr))
{
assert(running_partition_ != nullptr);
assert(update_partition_ != nullptr);
ESP_ERROR_CHECK(esp_ota_mark_app_valid_cancel_rollback());
CHARGELAB_LOG_MESSAGE(debug) << "Running partition: " << running_partition_->label;
CHARGELAB_LOG_MESSAGE(debug) << "Update partition: " << update_partition_->label;
}
std::string getActiveSlotId() override {
std::array<uint8_t, sizeof(uint32_t)> buffer {};
std::memcpy(buffer.data(), &running_partition_->address, buffer.size());
return string::ToHexString(buffer, "");
}
std::string getUpdateSlotId() override {
std::array<uint8_t, sizeof(uint32_t)> buffer {};
std::memcpy(buffer.data(), &update_partition_->address, buffer.size());
return string::ToHexString(buffer, "");
}
Result startUpdateProcess(std::size_t update_size) override {
if (update_handle_ != 0) {
CHARGELAB_LOG_MESSAGE(error) << "Another firmware update operation was in progress";
return Result::kFailed;
}
if (update_size > update_partition_->size) {
CHARGELAB_LOG_MESSAGE(warning) << "Update size exceeded partition size: " << update_size << " > " << update_partition_->size;
return Result::kFailed;
}
if (esp_err_t esp_status = esp_ota_begin(update_partition_, OTA_SIZE_UNKNOWN, &update_handle_) != ESP_OK) {
CHARGELAB_LOG_MESSAGE(error) << "Failed to start firmware update - error " << esp_status;
return Result::kFailed;
}
CHARGELAB_LOG_MESSAGE(info) << "Starting firmware update for partition: " << update_partition_->label;
update_size_ = update_size;
update_offset_ = 0;
return Result::kSucceeded;
}
Result processFirmwareChunk(const std::uint8_t *block, std::size_t size) override {
if (update_handle_ == 0) {
CHARGELAB_LOG_MESSAGE(error) << "Process chunk called outside of a firmware update operation";
return Result::kFailed;
}
if (update_offset_ + size > update_size_) {
CHARGELAB_LOG_MESSAGE(error) << "Firmware update chunks exceeded update size";
return Result::kFailed;
}
if (esp_err_t esp_status = esp_ota_write(update_handle_, (const void*)block, size) != ESP_OK) {
CHARGELAB_LOG_MESSAGE(error) << "Firmware update write failed - error " << esp_status;
esp_ota_abort(update_handle_);
update_handle_ = 0;
return Result::kFailed;
}
update_size_ += size;
return Result::kSucceeded;
}
Result finishUpdateProcess(bool succeeded) override {
if (update_handle_ == 0) {
CHARGELAB_LOG_MESSAGE(error) << "Finish update called outside of a firmware update operation";
return Result::kFailed;
}
if (succeeded) {
esp_ota_end(update_handle_);
esp_ota_set_boot_partition(update_partition_);
CHARGELAB_LOG_MESSAGE(info) << "Update applied - the next boot will attempt to boot into the updated partition";
} else {
esp_ota_abort(update_handle_);
}
update_handle_ = 0;
return Result::kSucceeded;
}
private:
esp_partition_t const* running_partition_;
esp_partition_t const* update_partition_;
esp_ota_handle_t update_handle_ = 0;
std::size_t update_size_ = 0;
std::size_t update_offset_ = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_STATION_ESP32_H

View File

@@ -0,0 +1,452 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STATION_TEST_ESP32_H
#define CHARGELAB_OPEN_FIRMWARE_STATION_TEST_ESP32_H
#include "openocpp/implementation/station_esp32.h"
#include "openocpp/interface/platform_interface.h"
#include "openocpp/helpers/string.h"
#include "openocpp/version.h"
#include <random>
namespace chargelab {
namespace detail {
struct SimulatorConnectorState {
bool connector_available {true};
bool vehicle_connected {false};
bool suspended_by_vehicle {false};
bool suspended_by_charger {false};
std::optional<SteadyPointMillis> last_update = std::nullopt;
double amps {48.0};
double volts {220.0};
double watt_hours {0.0};
// Note: not saved
std::optional<double> limit = std::nullopt;
bool charging_enabled {false};
CHARGELAB_JSON_INTRUSIVE(
SimulatorConnectorState,
connector_available,
vehicle_connected,
suspended_by_vehicle,
suspended_by_charger,
last_update,
amps,
volts,
watt_hours
)
};
struct SimulatorState {
charger::StationMetadata station_metadata {};
std::map<ocpp2_0::EVSEType, charger::EvseMetadata> evse_metadata {};
std::map<ocpp2_0::EVSEType, charger::ConnectorMetadata> connector_metadata {};
std::map<ocpp2_0::EVSEType, SimulatorConnectorState> connector_state {};
// Note: connector state is not saved intentionally
CHARGELAB_JSON_INTRUSIVE(SimulatorState, station_metadata, evse_metadata, connector_metadata, connector_state)
};
}
class StationTestEsp32 : public StationEsp32 {
public:
StationTestEsp32(std::shared_ptr<PlatformInterface> platform)
: platform_(std::move(platform)),
random_engine_ {std::random_device{}()}
{
assert(platform_ != nullptr);
settings_ = platform_->getSettings();
assert(settings_ != nullptr);
// TODO: Move these into SimulatorState so that they're persisted and can be configured?
settings_->ChargerVendor.setValue("ChargeLab");
settings_->ChargerModel.setValue("Simulator");
settings_->ChargerSerialNumber.setValue("0000");
settings_->ChargerFirmwareVersion.setValue(OPENOCPP_VERSION_TEXT);
settings_->ChargerAccessPointSSID.setValue("Charger Simulator");
settings_->registerCustomSetting(saved_state_ = std::make_shared<SettingString>(
[]() {
detail::SimulatorState default_state;
default_state.station_metadata = charger::StationMetadata {
1
};
default_state.evse_metadata[ocpp2_0::EVSEType {1}] = charger::EvseMetadata {
1,
10560.0
};
default_state.connector_metadata[ocpp2_0::EVSEType {1, 1}] = charger::ConnectorMetadata {
1,
"cType1",
1,
10560.0,
48
};
return SettingMetadata {
"SimulatorState",
SettingConfig::rwPolicy(),
std::nullopt,
std::nullopt,
write_json_to_string(default_state)
};
},
[](auto const&) {return true;}
));
}
public:
void simulateTap(ocpp1_6::IdToken token1_6, ocpp2_0::IdTokenType token2_0, int timeout_millis) {
std::lock_guard lock {mutex_};
rfid_action_ = std::make_pair(
static_cast<SteadyPointMillis>(platform_->steadyClockNow() + timeout_millis),
std::make_pair(token1_6, token2_0)
);
}
void updateMeasurements() {
std::lock_guard lock {mutex_};
loadIfModified();
std::uniform_real_distribution<double> distribution(0.95, 1.05);
for (auto& x : state_.connector_state) {
if (x.first.id == 0 || (x.first.connectorId && x.first.connectorId.value() == 0))
continue;
auto amps = 48.0;
if (x.second.limit.has_value())
amps = std::min(amps, x.second.limit.value());
if (station_limit_.has_value())
amps = std::min(amps, station_limit_.value());
x.second.amps = std::round((amps*distribution(random_engine_))*100.0)/100.0;
x.second.volts = std::round((220.0*distribution(random_engine_))*100.0)/100.0;
auto const now = platform_->steadyClockNow();
if (x.second.last_update.has_value()) {
auto const delta = now - x.second.last_update.value();
auto const delta_hours = delta/(1000.0 * 3600.0);
x.second.watt_hours += x.second.amps*x.second.volts*delta_hours;
}
x.second.last_update = now;
}
}
template<typename Visitor>
void updateState(Visitor&& visitor) {
std::lock_guard lock {mutex_};
loadIfModified();
visitor(state_);
saveIfModified();
}
public:
charger::StationMetadata getStationMetadata() override {
std::lock_guard lock {mutex_};
loadIfModified();
return state_.station_metadata;
}
[[nodiscard]] std::map<ocpp2_0::EVSEType, charger::EvseMetadata> getEvseMetadata() override {
std::lock_guard lock {mutex_};
loadIfModified();
return state_.evse_metadata;
}
[[nodiscard]] std::map<ocpp2_0::EVSEType, charger::ConnectorMetadata> getConnectorMetadata() override {
std::lock_guard lock {mutex_};
loadIfModified();
return state_.connector_metadata;
}
std::optional<charger::ConnectorStatus> pollConnectorStatus(const ocpp2_0::EVSEType &evse) override {
std::lock_guard lock {mutex_};
loadIfModified();
if (state_.connector_metadata.find(evse) == state_.connector_metadata.end())
return std::nullopt;
auto const& state = state_.connector_state[evse];
return charger::ConnectorStatus {
state.connector_available,
state.vehicle_connected,
state.charging_enabled,
state.suspended_by_vehicle,
state.suspended_by_charger,
state.watt_hours,
std::nullopt
};
}
std::vector<ocpp1_6::SampledValue> pollMeterValues1_6(const std::optional<ocpp2_0::EVSEType> &evse) override {
std::lock_guard lock {mutex_};
loadIfModified();
// Note: not supporting station level meter values here
if (!evse.has_value())
return {};
if (state_.connector_metadata.find(evse.value()) == state_.connector_metadata.end())
return {};
auto const& state = state_.connector_state[evse.value()];
return {
ocpp1_6::SampledValue {
std::to_string(roundTo(state.watt_hours, 1)),
std::nullopt,
std::nullopt,
ocpp1_6::Measurand::kEnergyActiveImportRegister,
std::nullopt,
std::nullopt,
ocpp1_6::UnitOfMeasure::kWattHours
},
ocpp1_6::SampledValue {
std::to_string(roundTo(state.amps, 1)),
std::nullopt,
std::nullopt,
ocpp1_6::Measurand::kCurrentImport,
std::nullopt,
std::nullopt,
ocpp1_6::UnitOfMeasure::kAmps
},
ocpp1_6::SampledValue {
std::to_string(roundTo(state.volts, 1)),
std::nullopt,
std::nullopt,
ocpp1_6::Measurand::kVoltage,
std::nullopt,
std::nullopt,
ocpp1_6::UnitOfMeasure::kVolts
}
};
}
std::vector<ocpp2_0::SampledValueType>
pollMeterValues2_0(const std::optional<ocpp2_0::EVSEType> &evse) override {
std::lock_guard lock {mutex_};
loadIfModified();
// Note: not supporting station level meter values here
if (!evse.has_value())
return {};
if (state_.connector_metadata.find(evse.value()) == state_.connector_metadata.end())
return {};
auto const& state = state_.connector_state[evse.value()];
return {
ocpp2_0::SampledValueType {
roundTo(state.watt_hours, 1),
std::nullopt,
ocpp2_0::MeasurandEnumType::kEnergy_Active_Import_Register,
std::nullopt,
std::nullopt,
std::nullopt,
ocpp2_0::UnitOfMeasureType {"Wh"}
},
ocpp2_0::SampledValueType {
roundTo(state.amps, 1),
std::nullopt,
ocpp2_0::MeasurandEnumType::kCurrent_Import,
std::nullopt,
std::nullopt,
std::nullopt,
ocpp2_0::UnitOfMeasureType {"A"}
},
ocpp2_0::SampledValueType {
roundTo(state.volts, 1),
std::nullopt,
ocpp2_0::MeasurandEnumType::kVoltage,
std::nullopt,
std::nullopt,
std::nullopt,
ocpp2_0::UnitOfMeasureType {"V"}
}
};
}
void setChargingEnabled(const ocpp2_0::EVSEType &evse, bool value) override {
std::lock_guard lock {mutex_};
loadIfModified();
if (state_.connector_metadata.find(evse) == state_.connector_metadata.end())
return;
state_.connector_state[evse].charging_enabled = value;
}
void setActiveChargePointMaxProfiles(std::vector<schedule_type1_6> const& active_schedules) override {
std::lock_guard lock {mutex_};
loadIfModified();
// Note: ignoring phases
std::optional<double> limit;
for (auto const& x : active_schedules) {
// Note: this should be handled here or the station should only accept a specific rate type
if (x.first.csChargingProfiles.chargingSchedule.chargingRateUnit != ocpp1_6::ChargingRateUnitType::kA)
continue;
if (!limit.has_value()) {
limit = x.second.limit;
} else {
limit = std::min(limit.value(), x.second.limit);
}
}
station_limit_ = limit;
}
void setActiveChargePointMaxProfiles(std::vector<schedule_type2_0> const& active_schedules) override {
std::lock_guard lock {mutex_};
loadIfModified();
// Note: ignoring phases
std::optional<double> limit;
for (auto const& x : active_schedules) {
// Note: profiles with multiple schedules are for ISO 15118 and aren't supported at the moment
if (x.first.chargingProfile.chargingSchedule.size() != 1)
continue;
auto const& schedule = x.first.chargingProfile.chargingSchedule.front();
if (schedule.chargingRateUnit != ocpp2_0::ChargingRateUnitEnumType::kA)
continue;
if (!limit.has_value()) {
limit = x.second.limit;
} else {
limit = std::min(limit.value(), x.second.limit);
}
}
station_limit_ = limit;
}
void setActiveEvseProfiles(int evse_id, std::vector<schedule_type1_6> const& active_schedules) override {
std::lock_guard lock {mutex_};
loadIfModified();
// Note: ignoring phases
std::optional<double> limit;
for (auto const& x : active_schedules) {
// Note: this should be handled here or the station should only accept a specific rate type
if (x.first.csChargingProfiles.chargingSchedule.chargingRateUnit != ocpp1_6::ChargingRateUnitType::kA)
continue;
if (!limit.has_value()) {
limit = x.second.limit;
} else {
limit = std::min(limit.value(), x.second.limit);
}
}
for (auto& x : state_.connector_state) {
if (x.first.id != evse_id)
continue;
x.second.limit = limit;
}
}
void setActiveEvseProfiles(int evse_id, std::vector<schedule_type2_0> const& active_schedules) override {
std::lock_guard lock {mutex_};
loadIfModified();
// Note: ignoring phases
std::optional<double> limit;
for (auto const& x : active_schedules) {
// Note: profiles with multiple schedules are for ISO 15118 and aren't supported at the moment
if (x.first.chargingProfile.chargingSchedule.size() != 1)
continue;
auto const& schedule = x.first.chargingProfile.chargingSchedule.front();
if (schedule.chargingRateUnit != ocpp2_0::ChargingRateUnitEnumType::kA)
continue;
if (!limit.has_value()) {
limit = x.second.limit;
} else {
limit = std::min(limit.value(), x.second.limit);
}
}
for (auto& x : state_.connector_state) {
if (x.first.id != evse_id)
continue;
x.second.limit = limit;
}
}
[[nodiscard]] std::optional<ocpp1_6::IdToken> readToken1_6() override {
std::lock_guard lock {mutex_};
if (!rfid_action_.has_value())
return std::nullopt;
if (platform_->steadyClockNow() - rfid_action_->first >= 0) {
rfid_action_ = std::nullopt;
return std::nullopt;
}
return rfid_action_->second.first;
}
[[nodiscard]] std::optional<ocpp2_0::IdTokenType> readToken2_0() override {
std::lock_guard lock {mutex_};
if (!rfid_action_.has_value())
return std::nullopt;
if (platform_->steadyClockNow() - rfid_action_->first >= 0) {
rfid_action_ = std::nullopt;
return std::nullopt;
}
return rfid_action_->second.second;
}
private:
void loadIfModified() {
auto const next_saved_state = saved_state_->getValue();
if (last_saved_state_ == next_saved_state)
return;
auto const data = read_json_from_string<detail::SimulatorState>(next_saved_state);
if (!data.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed reading saved state: " << next_saved_state;
} else {
state_ = data.value();
}
last_saved_state_ = next_saved_state;
}
void saveIfModified() {
auto const next_saved_state = write_json_to_string(state_);
if (last_saved_state_ == next_saved_state)
return;
saved_state_->setValue(next_saved_state);
last_saved_state_ = next_saved_state;
}
static double roundTo(double value, int decimal_places) {
auto const factor = (double)std::pow(10.0, decimal_places);
return std::round(value/factor) * factor;
}
private:
std::shared_ptr<PlatformInterface> platform_;
std::shared_ptr<Settings> settings_;
std::default_random_engine random_engine_;
std::mutex mutex_ {};
std::optional<double> station_limit_ = std::nullopt;
std::optional<std::pair<SteadyPointMillis, std::pair<ocpp1_6::IdToken, ocpp2_0::IdTokenType>>> rfid_action_;
detail::SimulatorState state_;
std::shared_ptr<SettingString> saved_state_;
std::string last_saved_state_;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_STATION_TEST_ESP32_H

View File

@@ -0,0 +1,53 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_CHARGING_PROFILE_PERSISTENCE_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_CHARGING_PROFILE_PERSISTENCE_INTERFACE_H
#include <string>
#include <vector>
#include <optional>
#include "openocpp/protocol/ocpp1_6/types/charging_profile.h"
#include "openocpp/helpers/json.h"
namespace chargelab {
struct ActiveProfileIdsPersistenceInfo {
std::unordered_set<int> activeRestartProtectionProfileIds;
std::optional<SystemTimeMillis> lastUpdated;
};
CHARGELAB_OPTIONAL_NULL_DEFINE_TYPE_NON_INTRUSIVE(
ActiveProfileIdsPersistenceInfo,
activeRestartProtectionProfileIds,
lastUpdated
)
struct ChargingProfilePersistenceInfo {
std::vector<pair<int, ocpp1_6::ChargingProfile>> profiles;
int criticalWriteCredits;
};
CHARGELAB_OPTIONAL_NULL_DEFINE_TYPE_NON_INTRUSIVE(
ChargingProfilePersistenceInfo,
profiles,
criticalWriteCredits
)
class ChargingProfilePersistenceInterface {
public:
virtual ~ChargingProfilePersistenceInterface() = default;
virtual std::optional<ChargingProfilePersistenceInfo> loadProfilePersistenceInfo() = 0;
virtual bool saveProfilePersistenceInfo(ChargingProfilePersistenceInfo const & profiles) = 0;
virtual std::optional<ActiveProfileIdsPersistenceInfo> loadActiveProfileIdsInfo() = 0;
virtual bool saveActiveProfileIdsInfo(ActiveProfileIdsPersistenceInfo const & persistence_info) = 0;
virtual bool saveWriteCredits(int write_credits) = 0;
virtual int getCreditIncreaseIntervalSeconds() = 0; // this is for making unit test easier
virtual int getActiveIdsUpdateIntervalSeconds() = 0; // for making unit test easier
virtual int getCheckProfileExpiryIntervalSeconds() = 0; // for making unit test easier
virtual int getFlashCreditInterval() = 0; // how many difference between the memory value and disk value, for making unit test easier
virtual int getFlashCreditMaximumLimit() = 0; // the biggest credit value allowed to write to disk, for making unit test easier
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_CHARGING_PROFILE_PERSISTENCE_INTERFACE_H

View File

@@ -0,0 +1,24 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_FETCH_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_FETCH_INTERFACE_H
#include <string>
#include <vector>
#include <cstdint>
#include <optional>
#include <functional>
namespace chargelab {
class FetchInterface {
public:
enum class Result {
kSucceeded,
kFailed
};
public:
virtual ~FetchInterface() = default;
virtual Result fetch(std::string const& uri, std::function<Result(std::uint8_t const*, std::size_t)> const& process_chunk) = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_FETCH_INTERFACE_H

View File

@@ -0,0 +1,30 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_SYSTEM_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_SYSTEM_INTERFACE_H
#include "openocpp/model/system_types.h"
#include "openocpp/interface/element/storage_interface.h"
#include "openocpp/interface/element/flash_block_interface.h"
#include <cstdint>
#include <memory>
namespace chargelab {
class SystemInterface {
public:
virtual ~SystemInterface() = default;
virtual SystemTimeMillis systemClockNow() = 0;
virtual SteadyPointMillis steadyClockNow() = 0;
virtual void setSystemClock(SystemTimeMillis now) = 0;
virtual void resetSoft() = 0;
virtual void resetHard() = 0;
virtual bool isClockOutOfSync() = 0;
virtual std::unique_ptr<chargelab::StorageInterface> getStorage(std::string const& file_name) = 0;
virtual std::unique_ptr<chargelab::FlashBlockInterface> getPartition(std::string const& label) = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_SYSTEM_INTERFACE_H

View File

@@ -0,0 +1,22 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_UPLOAD_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_UPLOAD_INTERFACE_H
#include <string>
#include <functional>
namespace chargelab {
class UploadInterface {
public:
enum class Result {
kSucceeded,
kFailed
};
public:
virtual ~UploadInterface() = default;
virtual Result upload(std::string const& uri, std::vector<uint8_t> const& content, bool append, std::function<void(std::size_t)> const& report_progress) = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_UPLOAD_INTERFACE_H

View File

@@ -0,0 +1,14 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_BYTE_READER_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_BYTE_READER_INTERFACE_H
#include <string>
#include <cstring>
namespace chargelab {
class ByteReaderInterface {
public:
virtual std::size_t read(char* s, std::size_t n) = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_BYTE_READER_INTERFACE_H

View File

@@ -0,0 +1,14 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_BYTE_WRITER_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_BYTE_WRITER_INTERFACE_H
#include <string>
#include <cstring>
namespace chargelab {
class ByteWriterInterface {
public:
virtual void write(char const *s, std::size_t count) = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_BYTE_WRITER_INTERFACE_H

View File

@@ -0,0 +1,18 @@
#ifndef ESP32_FIRMWARE_TEST_FLASH_BLOCK_INTERFACE_H
#define ESP32_FIRMWARE_TEST_FLASH_BLOCK_INTERFACE_H
#include <functional>
namespace chargelab {
class FlashBlockInterface {
public:
virtual ~FlashBlockInterface() = default;
virtual bool read(std::size_t src_offset, void *dst, std::size_t size) const = 0;
virtual bool write(std::size_t dst_offset, void *src, std::size_t size) = 0;
virtual bool erase() = 0;
[[nodiscard]] virtual std::size_t size() const = 0;
};
}
#endif //ESP32_FIRMWARE_TEST_FLASH_BLOCK_INTERFACE_H

View File

@@ -0,0 +1,47 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_REST_CONNECTION_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_REST_CONNECTION_INTERFACE_H
#include <optional>
namespace chargelab {
class RestConnectionInterface {
public:
virtual ~RestConnectionInterface() = default;
virtual int getStatusCode() = 0;
virtual std::size_t getContentLength() = 0;
/**
* Sets a custom header. Must be called before open().
* @param key
* @param value
*/
virtual void setHeader(std::string const& key, std::string const& value) = 0;
/**
* Opens the connection to the server. Must be called after setting any headers for this request.
* @param content_length length of data that will be written to the server
* @return true on success, otherwise false
*/
virtual bool open(std::size_t content_length) = 0;
/**
* Completes the REST request and reads the status code/content length from the server. This must be called
* after any POST payload and headers are written to the connection.
* @return true on success, otherwise false
*/
virtual bool send() = 0;
/**
* @return -1 on error, otherwise number bytes read
*/
virtual int read(char* buffer, int len) = 0;
/**
* @return a negative status code on error, otherwise number bytes read
*/
virtual int write(char const* buffer, int len) = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_REST_CONNECTION_INTERFACE_H

View File

@@ -0,0 +1,17 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STORAGE_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_STORAGE_INTERFACE_H
#include <cstdio>
#include <functional>
namespace chargelab {
class StorageInterface {
public:
virtual ~StorageInterface() = default;
virtual bool read(std::function<bool(FILE*)> const& function) = 0;
virtual bool write(std::function<bool(FILE*)> const& function) = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_STORAGE_INTERFACE_H

View File

@@ -0,0 +1,33 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_WEBSOCKET_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_WEBSOCKET_INTERFACE_H
#include <string>
#include <vector>
#include <cstdint>
#include <optional>
#include "openocpp/helpers/json.h"
#include "openocpp/interface/element/byte_writer_interface.h"
namespace chargelab {
class WebsocketInterface {
public:
virtual ~WebsocketInterface() = default;
virtual bool isConnected() = 0;
virtual std::optional<std::string> getSubprotocol() = 0;
virtual std::size_t pendingMessages() = 0;
virtual std::optional<std::string> pollMessages() = 0;
virtual void sendCustom(std::function<void(ByteWriterInterface&)> payload) = 0;
template <typename T>
void sendJson(T const& value) {
sendCustom([&](ByteWriterInterface& stream) {
write_json(stream, value);
});
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_WEBSOCKET_INTERFACE_H

View File

@@ -0,0 +1,58 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_PLATFORM_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_PLATFORM_INTERFACE_H
#include "openocpp/model/system_types.h"
#include "openocpp/interface/element/rest_connection_interface.h"
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/interface/component/upload_interface.h"
#include "openocpp/interface/element/websocket_interface.h"
#include "openocpp/common/settings.h"
#include "openocpp/module/common_templates.h"
#include <cstdint>
#include <memory>
namespace chargelab {
enum class RestMethod {
kGet,
kPost,
kPut,
kPatch,
kDelete,
kHead,
kOptions
};
struct SignatureAndHash {
const unsigned char *hash = nullptr;
std::size_t hash_len = 0;
const unsigned char *sig = nullptr;
std::size_t sig_len = 0;
};
class PlatformInterface :
public SystemInterface,
public ServiceStatefulGeneral
{
public:
virtual std::shared_ptr<Settings> getSettings() = 0;
virtual std::shared_ptr<WebsocketInterface> ocppConnection() = 0;
virtual std::shared_ptr<RestConnectionInterface> restRequest(RestMethod method, std::string const& uri) = 0;
virtual bool verifyManufacturerCertificate(
std::string const& pem,
std::optional<SignatureAndHash> const& check_sha256
) = 0;
virtual std::shared_ptr<RestConnectionInterface> getRequest(std::string const& uri) {
return restRequest(RestMethod::kGet, uri);
}
virtual std::shared_ptr<RestConnectionInterface> putRequest(std::string const& uri) {
return restRequest(RestMethod::kPut, uri);
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_PLATFORM_INTERFACE_H

View File

@@ -0,0 +1,274 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_STATION_INTERFACE_H
#define CHARGELAB_OPEN_FIRMWARE_STATION_INTERFACE_H
#include "openocpp/protocol/ocpp1_6/types/charge_point_error_code.h"
#include "openocpp/protocol/ocpp1_6/types/ci_string_type.h"
#include "openocpp/protocol/ocpp1_6/types/sampled_value.h"
#include "openocpp/protocol/ocpp1_6/types/charging_schedule_period.h"
#include "openocpp/protocol/ocpp1_6/messages/set_charging_profile.h"
#include "openocpp/protocol/ocpp2_0/types/sampled_value_type.h"
#include "openocpp/protocol/ocpp2_0/types/evse_type.h"
#include "openocpp/protocol/ocpp2_0/messages/set_charging_profile.h"
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/helpers/json.h"
#include <string>
#include <vector>
#include <optional>
namespace chargelab {
namespace charger {
// TODO: Move these into nested classes under StationInterface?
struct FaultedStatus1_6 {
ocpp1_6::ChargePointErrorCode errorCode = ocpp1_6::ChargePointErrorCode::kValueNotFoundInEnum;
std::optional<ocpp1_6::CiString50Type> info;
std::optional<ocpp1_6::CiString255Type> vendorId;
std::optional<ocpp1_6::CiString50Type> vendorErrorCode;
CHARGELAB_JSON_INTRUSIVE(FaultedStatus1_6, errorCode, info, vendorId, vendorErrorCode)
};
struct FaultedStatus2_0 {
// TODO
CHARGELAB_JSON_INTRUSIVE_EMPTY(FaultedStatus2_0)
};
struct FaultedStatus {
FaultedStatus1_6 status1_6;
FaultedStatus2_0 status2_0;
CHARGELAB_JSON_INTRUSIVE(FaultedStatus, status1_6, status2_0)
};
// TODO: Need to add type information and flags to enable an implementation to enable/disable connectors (if,
// for example, multiple physical connectors are present but only one can be used at a time).
struct ConnectorStatus {
bool connector_available;
bool vehicle_connected;
bool charging_enabled;
bool suspended_by_vehicle;
bool suspended_by_charger;
double meter_watt_hours;
std::optional<FaultedStatus> faulted_status = std::nullopt;
CHARGELAB_JSON_INTRUSIVE(ConnectorStatus, connector_available, vehicle_connected, charging_enabled, suspended_by_vehicle,
suspended_by_charger, faulted_status)
};
struct ConnectorMetadata {
/**
* OCPP 1.6 connector ID
*/
int connector_id1_6;
std::string connector_type;
int supply_phases;
double power_max_watts;
double power_max_amps;
CHARGELAB_JSON_INTRUSIVE(ConnectorMetadata, connector_id1_6, connector_type, supply_phases, power_max_watts, power_max_amps)
};
struct EvseMetadata {
int supply_phases;
double power_max_watts;
CHARGELAB_JSON_INTRUSIVE(EvseMetadata, supply_phases, power_max_watts)
};
struct StationMetadata {
int supply_phases;
CHARGELAB_JSON_INTRUSIVE(StationMetadata, supply_phases)
};
}
class StationInterface {
public:
using schedule_type1_6 = std::pair<ocpp1_6::SetChargingProfileReq, ocpp1_6::ChargingSchedulePeriod>;
using schedule_type2_0 = std::pair<ocpp2_0::SetChargingProfileRequest, ocpp2_0::ChargingSchedulePeriodType>;
enum class Result {
kSucceeded,
kFailed
};
public:
virtual ~StationInterface() = default;
/**
* Returns the station metadata required by OCPP 2.0.1.
*
* @return station metadata
*/
virtual charger::StationMetadata getStationMetadata() = 0;
/**
* Returns the EVSE metadata required by OCPP 2.0.1. The connectorId field should not be set for all entries in
* the returned map.
*
* @return EVSE metadata
*/
virtual std::map<ocpp2_0::EVSEType, charger::EvseMetadata> getEvseMetadata() = 0;
/**
* Returns the connector metadata containing the OCPP 1.6 connectorId mappings and the information required by
* OCPP 2.0.1. The connectorId field should be set for all entries in the returned map.
*
* @return connector metadata
*/
virtual std::map<ocpp2_0::EVSEType, charger::ConnectorMetadata> getConnectorMetadata() = 0;
/**
* Polls the status of a particular connector. Note that this call is expected to be provided with a connector
* ID - when absent this call should always return an empty optional.
*
* @param evse
* @return a non-empty optional with the connector status if the connector exists, otherwise an empty optional
*/
virtual std::optional<charger::ConnectorStatus> pollConnectorStatus(ocpp2_0::EVSEType const& evse) = 0;
/**
* Polls the meter values for the station, EVSE, or connector if they're available.
*
* @param evse an empty optional for station meter values, an EVSE with no connector for EVSE meter values, and
* an EVSE/connector for connector meter values.
* @return the meter values data if available, otherwise an empty vector
*/
virtual std::vector<ocpp1_6::SampledValue> pollMeterValues1_6(std::optional<ocpp2_0::EVSEType> const& evse) = 0;
/**
* Polls the meter values for the station, EVSE, or connector if they're available.
*
* @param evse an empty optional for station meter values, an EVSE with no connector for EVSE meter values, and
* an EVSE/connector for connector meter values.
* @return the meter values data if available, otherwise an empty vector
*/
virtual std::vector<ocpp2_0::SampledValueType> pollMeterValues2_0(std::optional<ocpp2_0::EVSEType> const& evse) = 0;
/**
* Enables/disables charging on a particular connector.
*
* @param evse
* @param value true to enable charging, otherwise false
*/
virtual void setChargingEnabled(ocpp2_0::EVSEType const& evse, bool value) = 0;
// Note: providing the charger with the active profile and period explicitly for the following reasons:
// - If a TxProfile limits based on Amps and a ChargePointMaxProfile limits based on Watts the interpretation
// will need to be left to the vendor. Do they apply the limit based on the maximum voltage or a live reading?
// If the reading fluctuates does the implementation switch from one limit to another?
// - A vendor *may* want to communicate more details to a user. For example, they may want to display the
// current profile limits (or other associated metadata) on screen.
// This method will be called whenever the active schedule changes.
/**
* Sets the active OCPP 1.6 charging schedules that were assigned to the charger.
*
* @param active_schedules the active schedule periods and their associated charging profiles
*/
virtual void setActiveChargePointMaxProfiles(std::vector<schedule_type1_6> const& active_schedules) = 0;
/**
* Sets the active OCPP 2.0 charging schedules that were assigned to the charger.
*
* @param active_schedules the active schedule periods and their associated charging profiles
*/
virtual void setActiveChargePointMaxProfiles(std::vector<schedule_type2_0> const& active_schedules) = 0;
/**
* Sets the active OCPP 1.6 charging schedules that were assigned to a specific EVSE ID.
*
* @param active_schedules the active schedule periods and their associated charging profiles
*/
virtual void setActiveEvseProfiles(int evse_id, std::vector<schedule_type1_6> const& active_schedules) = 0;
/**
* Sets the active OCPP 2.0 charging schedules that were assigned to a specific EVSE ID.
*
* @param active_schedules the active schedule periods and their associated charging profiles
*/
virtual void setActiveEvseProfiles(int evse_id, std::vector<schedule_type2_0> const& active_schedules) = 0;
/**
* Reads an OCPP 1.6 ID token that was presented to the station. This method may either return the token once
* every interaction, or it may return the IdToken for as long as the RFID tag is detected by the RFID reader.
* If the latter approach is used the running transaction module is expected to only respond to changes in
* output.
*
* @return the IdToken value associated with an RFID (or other local) interaction
*/
[[nodiscard]] virtual std::optional<ocpp1_6::IdToken> readToken1_6() = 0;
/**
* Returns an OCPP 2.0 ID token that was presented to the station. This method may either return the token once
* every interaction, or it may return the IdToken for as long as the RFID tag is detected by the RFID reader.
* If the latter approach is used the running transaction module is expected to only respond to changes in
* output.
*
* @return
*/
[[nodiscard]] virtual std::optional<ocpp2_0::IdTokenType> readToken2_0() = 0;
/**
* Gets the active firmware slot (generally some kind of partition ID). This is compared to the update slot to
* determine whether or not the firmware update succeeded.
*
* @return active slot ID
*/
virtual std::string getActiveSlotId() = 0;
/**
* Starts a firmware update operation. Must be paired with a terminating finishUpdateProcess call on success or
* failure.
*
* @param update_size
* @return kSucceeded on success, otherwise kFailed
*/
virtual Result startUpdateProcess(std::size_t update_size) = 0;
/**
* Process an incoming block of data as part of a firmware update, generally writing that block to the update
* partition. May only be called during a firmware update operation (after a call to startUpdateProcess but
* before the associated call to finishUpdateProcess).
*
* @param block
* @param size
* @return kSucceeded on success, otherwise kFailed
*/
virtual Result processFirmwareChunk(std::uint8_t const* block, std::size_t size) = 0;
/**
* Gets the update slot ID (generally some kind of partition ID). This is compared against the active slot ID
* after an update completes to determine whether or not the firmware update succeeded. May only be called
* during a firmware update operation (after a call to startUpdateProcess but before the associated call to
* finishUpdateProcess).
*
* @return update slot ID
*/
virtual std::string getUpdateSlotId() = 0;
/**
* Finishes a firmware update. If succeeded == true and this call returns kSucceeded then the next restart is
* expected to attempt to boot into the updated firmware. If succeeded == false or this call returns kFailed
* then the next restart is expected to boot into the active firmware slot. A return value of kFailed when
* provided with a succeeded == false flag is treated as an error state and the next restart is expected to boot
* into the active firmware slot;
*
* @param succeeded
* @return kSucceeded on success, otherwise kFailed
*/
virtual Result finishUpdateProcess(bool succeeded) = 0;
public:
/**
* Helper method to lookup an OCPP 1.6 connector ID based on the provided metadata.
*
* @param id an OCPP 1.6 connector ID
* @return the associated EVSE if the connector was found, otherwise std::nullopt
*/
virtual std::optional<ocpp2_0::EVSEType> lookupConnectorId1_6(int id) {
for (auto const& entry : getConnectorMetadata()) {
if (entry.second.connector_id1_6 == id)
return entry.first;
}
return std::nullopt;
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_STATION_INTERFACE_H

View File

@@ -0,0 +1,11 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_SYSTEM_TYPES_H
#define CHARGELAB_OPEN_FIRMWARE_SYSTEM_TYPES_H
#include <cstdint>
namespace chargelab {
enum SystemTimeMillis : std::int64_t {};
enum SteadyPointMillis : std::int64_t {};
}
#endif //CHARGELAB_OPEN_FIRMWARE_SYSTEM_TYPES_H

View File

@@ -0,0 +1,79 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_ABSTRACT_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_ABSTRACT_MODULE_H
#include "openocpp/protocol/ocpp1_6/handlers/abstract_request_handler.h"
#include "openocpp/protocol/ocpp1_6/handlers/abstract_response_handler.h"
#include "openocpp/protocol/ocpp1_6/handlers/abstract_service.h"
#include "openocpp/protocol/ocpp2_0/handlers/abstract_request_handler.h"
#include "openocpp/protocol/ocpp2_0/handlers/abstract_response_handler.h"
#include "openocpp/protocol/ocpp2_0/handlers/abstract_service.h"
namespace chargelab {
namespace detail {
class PureServiceInterface {
public:
virtual void runUnconditionally() {
};
};
struct ConcreteImplementationsOCPP1_6 {
ocpp1_6::AbstractService* service;
ocpp1_6::AbstractRequestHandler* request_handler;
ocpp1_6::AbstractResponseHandler* response_handler;
};
struct ConcreteImplementationsOCPP2_0 {
ocpp2_0::AbstractService* service;
ocpp2_0::AbstractRequestHandler* request_handler;
ocpp2_0::AbstractResponseHandler* response_handler;
};
struct ConcreteImplementations {
ConcreteImplementationsOCPP1_6 ocpp1_6;
ConcreteImplementationsOCPP2_0 ocpp2_0;
PureServiceInterface* pure_service;
};
}
class AbstractModuleInterface {
public:
virtual ~AbstractModuleInterface() = default;
virtual detail::ConcreteImplementations getImplementations() = 0;
};
template <typename T>
class AbstractModuleBase : public AbstractModuleInterface {
public:
detail::ConcreteImplementations getImplementations() override {
auto parent_this = static_cast<T*>(this);
detail::ConcreteImplementations result {};
PopulateImplementation(result.ocpp1_6.service, parent_this);
PopulateImplementation(result.ocpp1_6.request_handler, parent_this);
PopulateImplementation(result.ocpp1_6.response_handler, parent_this);
PopulateImplementation(result.ocpp2_0.service, parent_this);
PopulateImplementation(result.ocpp2_0.request_handler, parent_this);
PopulateImplementation(result.ocpp2_0.response_handler, parent_this);
PopulateImplementation(result.pure_service, parent_this);
return result;
}
private:
template <typename U>
static typename std::enable_if<std::is_base_of<U, T>::value, U*>::type ConvertToBase(T* ptr) {
return static_cast<U*>(ptr);
}
template <typename U>
static typename std::enable_if<!std::is_base_of<U, T>::value, U*>::type ConvertToBase(T*) {
return nullptr;
}
template <typename U>
static void PopulateImplementation(U*& implementation, T* ptr) {
implementation = ConvertToBase<U>(ptr);
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_ABSTRACT_MODULE_H

View File

@@ -0,0 +1,699 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_BOOT_NOTIFICATION_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_BOOT_NOTIFICATION_MODULE_H
#include <utility>
#include "openocpp/module/common_templates.h"
#include "openocpp/common/operation_holder.h"
#include "openocpp/common/settings.h"
#include "openocpp/common/logging.h"
namespace chargelab {
class BootNotificationModule : public ServiceStatefulGeneral
{
/* OCPP 1.6 Section 4.2 on Pending registration statuses:
*
* The Central System MAY also return a Pending registration status to indicate that it wants to retrieve or set
* certain information on the Charge Point before the Central System will accept the Charge Point. If the Central
* System returns the Pending status, the communication channel SHOULD NOT be closed by either the Charge
* Point or the Central System. The Central System MAY send request messages to retrieve information from the
* Charge Point or change its configuration. The Charge Point SHOULD respond to these messages. The Charge
* Point SHALL NOT send request messages to the Central System unless it has been instructed by the Central
* System to do so with a TriggerMessage.req request.
*/
private:
static constexpr const int kSecurityProfileMigrationResetGracePeriodMillis = 5*1000;
public:
BootNotificationModule(
std::shared_ptr<Settings> settings,
std::shared_ptr<SystemInterface> const& system_interface
)
: settings_(std::move(settings)),
system_interface_(system_interface),
pending_boot_notification_req_ {system_interface}
{
assert(system_interface_ != nullptr);
if (settings_->hasPendingTransition(SettingTransitionType::kConnection)) {
auto timeout = system_interface_->steadyClockNow() + settings_->ConnectionTransitionTimeout.getValue()*1000;
if (settings_->startTransition(SettingTransitionType::kConnection)) {
CHARGELAB_LOG_MESSAGE(info) << "Starting connection transition";
connection_transition_timeout_ = static_cast<SteadyPointMillis>(timeout);
}
}
auto const reason_text = settings_->CustomBootReason.getValue();
if (!reason_text.empty())
CHARGELAB_LOG_MESSAGE(info) << "Custom boot reason was: " << settings_->CustomBootReason.getValue();
}
~BootNotificationModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting BootNotificationModule";
}
public:
bool registrationComplete() {
return registration_complete_;
}
private:
// OCPP 1.6 implementation
void runStep(ocpp1_6::OcppRemote &remote) override {
// Clear this flag if present
settings_->CustomBootReason.setValue("");
runStepImpl([&]() {
// TODO: Improved validation/length safety (truncate?)
ocpp1_6::BootNotificationReq req {};
req.chargePointVendor = ocpp1_6::CiString20Type(settings_->ChargerVendor.getValue());
req.chargePointModel = ocpp1_6::CiString20Type(settings_->ChargerModel.getValue());
req.chargePointSerialNumber = ocpp1_6::CiString25Type(settings_->ChargerSerialNumber.getValue());
req.firmwareVersion = ocpp1_6::CiString50Type(settings_->ChargerFirmwareVersion.getValue());
if (auto iccid = settings_->ChargerICCID.getValue(); !iccid.empty())
req.iccid = ocpp1_6::CiString20Type(iccid);
if (auto imsi = settings_->ChargerIMSI.getValue(); !imsi.empty())
req.imsi = ocpp1_6::CiString20Type(imsi);
if (auto meter_serial_number = settings_->ChargerMeterSerialNumber.getValue(); !meter_serial_number.empty())
req.meterSerialNumber = ocpp1_6::CiString25Type(meter_serial_number);
if (auto meter_type = settings_->ChargerMeterType.getValue(); !meter_type.empty())
req.meterType = ocpp1_6::CiString25Type(meter_type);
return remote.sendBootNotificationReq(req);
});
}
void onBootNotificationRsp(
const std::string &unique_id,
const ocpp1_6::ResponseMessage<ocpp1_6::BootNotificationRsp> &rsp
) override {
if (pending_boot_notification_req_ == unique_id) {
pending_boot_notification_req_ = kNoOperation;
if (std::holds_alternative<ocpp1_6::BootNotificationRsp>(rsp)) {
auto const& value = std::get<ocpp1_6::BootNotificationRsp>(rsp);
auto const& ts = value.currentTime.getTimestamp();
if (ts.has_value()) {
system_interface_->setSystemClock(ts.value());
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Invalid BootNotification response timestamp: "
<< value.currentTime;
}
CHARGELAB_LOG_MESSAGE(info) << "BootNotification response was: " << value;
switch (value.status) {
case ocpp1_6::RegistrationStatus::kAccepted:
requested_next_boot_notification_req_ = std::nullopt;
settings_->HeartbeatInterval.setValue(value.interval);
break;
case ocpp1_6::RegistrationStatus::kValueNotFoundInEnum:
CHARGELAB_LOG_MESSAGE(error) << "Invalid BootNotification response status for request ID: " << unique_id;
[[fallthrough]];
case ocpp1_6::RegistrationStatus::kPending:
case ocpp1_6::RegistrationStatus::kRejected:
requested_next_boot_notification_req_ = static_cast<SteadyPointMillis> (
system_interface_->steadyClockNow() + value.interval*1000
);
break;
}
switch (value.status) {
case ocpp1_6::RegistrationStatus::kRejected:
case ocpp1_6::RegistrationStatus::kValueNotFoundInEnum:
registration_complete_ = false;
allow_ocpp_calls_ = false;
break;
case ocpp1_6::RegistrationStatus::kPending:
registration_complete_ = false;
allow_ocpp_calls_ = true;
break;
case ocpp1_6::RegistrationStatus::kAccepted:
registration_complete_ = true;
allow_ocpp_calls_ = true;
break;
}
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to BootNotification request: " << std::get<ocpp1_6::CallError> (rsp);
}
}
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::TriggerMessageRsp>>
onTriggerMessageReq(const ocpp1_6::TriggerMessageReq &req) override {
switch (req.requestedMessage) {
case ocpp1_6::MessageTrigger::kBootNotification:
// Note: deviating from the 2.0.1 specification and allowing boot notification trigger messages
// after registration completed in 1.6 here.
force_boot_notification_req_ = true;
return ocpp1_6::TriggerMessageRsp{ocpp1_6::TriggerMessageStatus::kAccepted};
default:
case ocpp1_6::MessageTrigger::kValueNotFoundInEnum:
case ocpp1_6::MessageTrigger::kDiagnosticsStatusNotification:
case ocpp1_6::MessageTrigger::kFirmwareStatusNotification:
case ocpp1_6::MessageTrigger::kHeartbeat:
case ocpp1_6::MessageTrigger::kMeterValues:
case ocpp1_6::MessageTrigger::kStatusNotification:
if (!registrationComplete()) {
return unauthorizedError1_6();
} else {
return std::nullopt;
}
}
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::RemoteStartTransactionRsp>>
onRemoteStartTransactionReq(const ocpp1_6::RemoteStartTransactionReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::RemoteStopTransactionRsp>>
onRemoteStopTransactionReq(const ocpp1_6::RemoteStopTransactionReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::CancelReservationRsp>>
onCancelReservationReq(const ocpp1_6::CancelReservationReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ChangeAvailabilityRsp>>
onChangeAvailabilityReq(const ocpp1_6::ChangeAvailabilityReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ChangeConfigurationRsp>>
onChangeConfigurationReq(const ocpp1_6::ChangeConfigurationReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ClearCacheRsp>>
onClearCacheReq(const ocpp1_6::ClearCacheReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ClearChargingProfileRsp>>
onClearChargingProfileReq(const ocpp1_6::ClearChargingProfileReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::DataTransferRsp>>
onDataTransferReq(const ocpp1_6::DataTransferReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::GetCompositeScheduleRsp>>
onGetCompositeScheduleReq(const ocpp1_6::GetCompositeScheduleReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::GetConfigurationRsp>>
onGetConfigurationReq(const ocpp1_6::GetConfigurationReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::GetDiagnosticsRsp>>
onGetDiagnosticsReq(const ocpp1_6::GetDiagnosticsReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::GetLocalListVersionRsp>>
onGetLocalListVersionReq(const ocpp1_6::GetLocalListVersionReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ReserveNowRsp>>
onReserveNowReq(const ocpp1_6::ReserveNowReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ResetRsp>>
onResetReq(const ocpp1_6::ResetReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::SendLocalListRsp>>
onSendLocalListReq(const ocpp1_6::SendLocalListReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::SetChargingProfileRsp>>
onSetChargingProfileReq(const ocpp1_6::SetChargingProfileReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::UnlockConnectorRsp>>
onUnlockConnectorReq(const ocpp1_6::UnlockConnectorReq&) override {
return unauthorizedCallHandler1_6();
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::UpdateFirmwareRsp>>
onUpdateFirmwareReq(const ocpp1_6::UpdateFirmwareReq&) override {
return unauthorizedCallHandler1_6();
}
// OCPP 2.0.1 implementation
void runStep(ocpp2_0::OcppRemote &remote) override {
auto reason = ocpp2_0::BootReasonEnumType::kPowerUp;
auto const reason_text = settings_->CustomBootReason.getValue();
if (!reason_text.empty()) {
reason = ocpp2_0::BootReasonEnumType::from_string(reason_text);
} else if (force_boot_notification_req_) {
reason = ocpp2_0::BootReasonEnumType::kTriggered;
}
runStepImpl([&]() {
ocpp2_0::BootNotificationRequest req {};
req.reason = reason;
req.chargingStation.serialNumber = settings_->ChargerSerialNumber.getValue();
req.chargingStation.model = settings_->ChargerModel.getValue();
req.chargingStation.vendorName = settings_->ChargerVendor.getValue();
req.chargingStation.firmwareVersion = settings_->ChargerFirmwareVersion.getValue();
auto modem = ocpp2_0::ModemType {};
if (auto iccid = settings_->ChargerICCID.getValue(); !iccid.empty())
modem.iccid = iccid;
if (auto imsi = settings_->ChargerIMSI.getValue(); !imsi.empty())
modem.imsi = imsi;
if (modem.iccid.has_value() || modem.imsi.has_value())
req.chargingStation.modem = std::move(modem);
return remote.sendBootNotificationReq(req);
});
}
void onBootNotificationRsp(
const std::string &unique_id,
const ocpp2_0::ResponseMessage<ocpp2_0::BootNotificationResponse> &rsp
) override {
if (pending_boot_notification_req_ == unique_id) {
pending_boot_notification_req_ = kNoOperation;
settings_->CustomBootReason.setValue("");
if (std::holds_alternative<ocpp2_0::BootNotificationResponse>(rsp)) {
auto const& value = std::get<ocpp2_0::BootNotificationResponse>(rsp);
auto const& ts = value.currentTime.getTimestamp();
if (ts.has_value()) {
system_interface_->setSystemClock(ts.value());
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Invalid BootNotification response timestamp: "
<< value.currentTime;
}
CHARGELAB_LOG_MESSAGE(info) << "BootNotification response was: " << value;
switch (value.status) {
case ocpp2_0::RegistrationStatusEnumType::kAccepted:
requested_next_boot_notification_req_ = std::nullopt;
settings_->HeartbeatInterval.setValue(value.interval);
break;
case ocpp2_0::RegistrationStatusEnumType::kValueNotFoundInEnum:
CHARGELAB_LOG_MESSAGE(error) << "Invalid BootNotification response status for request ID: " << unique_id;
[[fallthrough]];
case ocpp2_0::RegistrationStatusEnumType::kPending:
case ocpp2_0::RegistrationStatusEnumType::kRejected:
requested_next_boot_notification_req_ = static_cast<SteadyPointMillis> (
system_interface_->steadyClockNow() + value.interval*1000
);
break;
}
switch (value.status) {
case ocpp2_0::RegistrationStatusEnumType::kRejected:
case ocpp2_0::RegistrationStatusEnumType::kValueNotFoundInEnum:
registration_complete_ = false;
allow_ocpp_calls_ = false;
break;
case ocpp2_0::RegistrationStatusEnumType::kPending:
registration_complete_ = false;
allow_ocpp_calls_ = true;
break;
case ocpp2_0::RegistrationStatusEnumType::kAccepted:
registration_complete_ = true;
allow_ocpp_calls_ = true;
break;
}
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to BootNotification request: " << std::get<ocpp2_0::CallError> (rsp);
}
}
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::TriggerMessageResponse>>
onTriggerMessageReq(const ocpp2_0::TriggerMessageRequest& req) override {
switch (req.requestedMessage) {
case ocpp2_0::MessageTriggerEnumType::kBootNotification:
if (!registration_complete_) {
force_boot_notification_req_ = true;
return ocpp2_0::TriggerMessageResponse{ocpp2_0::TriggerMessageStatusEnumType::kAccepted};
} else {
// F06.FR.17
return ocpp2_0::TriggerMessageResponse{ocpp2_0::TriggerMessageStatusEnumType::kRejected};
}
default:
return unauthorizedCallHandler2_0();
}
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::CancelReservationResponse>>
onCancelReservationReq(const ocpp2_0::CancelReservationRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::CertificateSignedResponse>>
onCertificateSignedReq(const ocpp2_0::CertificateSignedRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ChangeAvailabilityResponse>>
onChangeAvailabilityReq(const ocpp2_0::ChangeAvailabilityRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ClearCacheResponse>>
onClearCacheReq(const ocpp2_0::ClearCacheRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ClearChargingProfileResponse>>
onClearChargingProfileReq(const ocpp2_0::ClearChargingProfileRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ClearDisplayMessageResponse>>
onClearDisplayMessageReq(const ocpp2_0::ClearDisplayMessageRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ClearVariableMonitoringResponse>>
onClearVariableMonitoringReq(const ocpp2_0::ClearVariableMonitoringRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::CostUpdatedResponse>>
onCostUpdatedReq(const ocpp2_0::CostUpdatedRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::CustomerInformationResponse>>
onCustomerInformationReq(const ocpp2_0::CustomerInformationRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::DataTransferResponse>>
onDataTransferReq(const ocpp2_0::DataTransferRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::DeleteCertificateResponse>>
onDeleteCertificateReq(const ocpp2_0::DeleteCertificateRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetBaseReportResponse>>
onGetBaseReportReq(const ocpp2_0::GetBaseReportRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetChargingProfilesResponse>>
onGetChargingProfilesReq(const ocpp2_0::GetChargingProfilesRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetCompositeScheduleResponse>>
onGetCompositeScheduleReq(const ocpp2_0::GetCompositeScheduleRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetDisplayMessagesResponse>>
onGetDisplayMessagesReq(const ocpp2_0::GetDisplayMessagesRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetInstalledCertificateIdsResponse>>
onGetInstalledCertificateIdsReq(const ocpp2_0::GetInstalledCertificateIdsRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetLocalListVersionResponse>>
onGetLocalListVersionReq(const ocpp2_0::GetLocalListVersionRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetLogResponse>>
onGetLogReq(const ocpp2_0::GetLogRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetMonitoringReportResponse>>
onGetMonitoringReportReq(const ocpp2_0::GetMonitoringReportRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetReportResponse>>
onGetReportReq(const ocpp2_0::GetReportRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetTransactionStatusResponse>>
onGetTransactionStatusReq(const ocpp2_0::GetTransactionStatusRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetVariablesResponse>>
onGetVariablesReq(const ocpp2_0::GetVariablesRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::InstallCertificateResponse>>
onInstallCertificateReq(const ocpp2_0::InstallCertificateRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::PublishFirmwareResponse>>
onPublishFirmwareReq(const ocpp2_0::PublishFirmwareRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::RequestStartTransactionResponse>>
onRequestStartTransactionReq(const ocpp2_0::RequestStartTransactionRequest&) override {
// B02.FR.05
if (!registration_complete_) {
return ocpp2_0::RequestStartTransactionResponse {ocpp2_0::RequestStartStopStatusEnumType::kRejected};
}
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::RequestStopTransactionResponse>>
onRequestStopTransactionReq(const ocpp2_0::RequestStopTransactionRequest&) override {
// B02.FR.05
if (!registration_complete_) {
return ocpp2_0::RequestStopTransactionResponse {ocpp2_0::RequestStartStopStatusEnumType::kRejected};
}
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ReserveNowResponse>>
onReserveNowReq(const ocpp2_0::ReserveNowRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ResetResponse>>
onResetReq(const ocpp2_0::ResetRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SendLocalListResponse>>
onSendLocalListReq(const ocpp2_0::SendLocalListRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetChargingProfileResponse>>
onSetChargingProfileReq(const ocpp2_0::SetChargingProfileRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetDisplayMessageResponse>>
onSetDisplayMessageReq(const ocpp2_0::SetDisplayMessageRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetMonitoringBaseResponse>>
onSetMonitoringBaseReq(const ocpp2_0::SetMonitoringBaseRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetMonitoringLevelResponse>>
onSetMonitoringLevelReq(const ocpp2_0::SetMonitoringLevelRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetNetworkProfileResponse>>
onSetNetworkProfileReq(const ocpp2_0::SetNetworkProfileRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetVariableMonitoringResponse>>
onSetVariableMonitoringReq(const ocpp2_0::SetVariableMonitoringRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetVariablesResponse>>
onSetVariablesReq(const ocpp2_0::SetVariablesRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::UnlockConnectorResponse>>
onUnlockConnectorReq(const ocpp2_0::UnlockConnectorRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::UnpublishFirmwareResponse>>
onUnpublishFirmwareReq(const ocpp2_0::UnpublishFirmwareRequest&) override {
return unauthorizedCallHandler2_0();
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::UpdateFirmwareResponse>>
onUpdateFirmwareReq(const ocpp2_0::UpdateFirmwareRequest&) override {
return unauthorizedCallHandler2_0();
}
private:
template <typename T>
void runStepImpl(T&& sendBootNotification) {
auto const now = system_interface_->steadyClockNow();
if (connection_transition_timeout_.has_value()) {
if (allow_ocpp_calls_) {
CHARGELAB_LOG_MESSAGE(debug) << "Connection transition completed - committing";
connection_transition_timeout_ = std::nullopt;
settings_->commit(SettingTransitionType::kConnection);
} else if (now >= connection_transition_timeout_.value()) {
CHARGELAB_LOG_MESSAGE(debug) << "Connection transition failed - rolling back to previous settings";
settings_->rollback(SettingTransitionType::kConnection);
settings_->saveIfModified();
system_interface_->resetHard();
}
}
// Note: only updating the security profile when first connecting to the back-end
if (!security_profile_checked_ && allow_ocpp_calls_ && !settings_->hasRunningTransition(SettingTransitionType::kConnection)) {
auto const activeNetworkProfileSlot = settings_->ActiveNetworkProfile.getValue();
auto const activeNetworkProfile = settings_->NetworkConnectionProfiles.transitionCurrentValue(activeNetworkProfileSlot);
if (activeNetworkProfile.has_value() &&
activeNetworkProfile->securityProfile > settings_->SecurityProfile.getValue()) {
// A05.FR.06
for (std::size_t i = 0; i < settings_->NetworkConnectionProfiles.transitionSize(); i++) {
auto const value = settings_->NetworkConnectionProfiles.transitionCurrentValue(i);
if (!value.has_value())
continue;
if (value->securityProfile >= activeNetworkProfile->securityProfile)
continue;
CHARGELAB_LOG_MESSAGE(info) << "Removing profile " << i << ": " << value;
settings_->NetworkConnectionProfiles.forceCurrentValue(i, std::nullopt);
char const* ifs = "";
std::string filtered_priorities;
string::SplitVisitor(settings_->NetworkConfigurationPriority.transitionCurrentValue(), ",", [&](std::string const& value) {
if (string::ToInteger(value) != std::make_optional(i)) {
filtered_priorities += ifs + value;
ifs = ",";
}
});
CHARGELAB_LOG_MESSAGE(info) << "Priorities before: " << settings_->NetworkConfigurationPriority.transitionCurrentValue();
settings_->NetworkConfigurationPriority.forceValue(filtered_priorities);
CHARGELAB_LOG_MESSAGE(info) << "Priorities after: " << settings_->NetworkConfigurationPriority.transitionCurrentValue();
}
settings_->SecurityProfile.setValue(activeNetworkProfile->securityProfile);
CHARGELAB_LOG_MESSAGE(info) << "Security profile updated to: " << activeNetworkProfile->securityProfile;
}
security_profile_checked_ = true;
}
if (force_boot_notification_req_) {
force_boot_notification_req_ = false;
pending_boot_notification_req_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
sendBootNotification()
);
return;
}
if (registrationComplete())
return;
if (requested_next_boot_notification_req_.has_value()) {
if (now - requested_next_boot_notification_req_.value() < 0)
return;
}
if (pending_boot_notification_req_.wasIdleFor(settings_->HeartbeatInterval.getValue())) {
pending_boot_notification_req_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
sendBootNotification()
);
}
}
std::optional<ocpp1_6::CallError> unauthorizedCallHandler1_6() {
if (allow_ocpp_calls_) {
return std::nullopt;
} else {
return unauthorizedError1_6();
}
}
std::optional<ocpp2_0::CallError> unauthorizedCallHandler2_0() {
if (allow_ocpp_calls_) {
return std::nullopt;
} else {
return unauthorizedError2_0();
}
}
static ocpp1_6::CallError unauthorizedError1_6() {
return ocpp1_6::CallError {
ocpp1_6::ErrorCode::kSecurityError,
"Waiting for CSMS to accept boot notification",
common::RawJson::empty_object()
};
}
static ocpp2_0::CallError unauthorizedError2_0() {
return ocpp2_0::CallError {
ocpp2_0::ErrorCode::kSecurityError,
"Waiting for CSMS to accept boot notification",
common::RawJson::empty_object()
};
}
private:
std::shared_ptr<Settings> settings_;
std::shared_ptr<SystemInterface> system_interface_;
OperationHolder<std::string> pending_boot_notification_req_;
std::optional<SteadyPointMillis> connection_transition_timeout_ = std::nullopt;
std::optional<SteadyPointMillis> requested_next_boot_notification_req_ = std::nullopt;
std::atomic<bool> registration_complete_ = false;
std::atomic<bool> allow_ocpp_calls_ = false;
std::atomic<bool> force_boot_notification_req_ = false;
std::atomic<bool> security_profile_checked_ = false;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_BOOT_NOTIFICATION_MODULE_H

View File

@@ -0,0 +1,79 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_COMMON_TEMPLATES_H
#define CHARGELAB_OPEN_FIRMWARE_COMMON_TEMPLATES_H
#include "openocpp/protocol/ocpp1_6/handlers/abstract_request_handler.h"
#include "openocpp/protocol/ocpp1_6/handlers/abstract_response_handler.h"
#include "openocpp/protocol/ocpp1_6/handlers/abstract_service.h"
#include "openocpp/protocol/ocpp2_0/handlers/abstract_request_handler.h"
#include "openocpp/protocol/ocpp2_0/handlers/abstract_response_handler.h"
#include "openocpp/protocol/ocpp2_0/handlers/abstract_service.h"
#include "openocpp/module/abstract_module.h"
namespace chargelab {
class ServiceStateless1_6 :
public AbstractModuleBase<ServiceStateless1_6>,
private detail::PureServiceInterface,
private ocpp1_6::AbstractRequestHandler
{
friend class AbstractModuleBase<ServiceStateless1_6>;
};
class ServiceStateful1_6 :
public AbstractModuleBase<ServiceStateful1_6>,
private detail::PureServiceInterface,
private ocpp1_6::AbstractService,
private ocpp1_6::AbstractRequestHandler,
private ocpp1_6::AbstractResponseHandler
{
friend class AbstractModuleBase<ServiceStateful1_6>;
};
class ServiceStateless2_0 :
public AbstractModuleBase<ServiceStateless2_0>,
private detail::PureServiceInterface,
private ocpp2_0::AbstractRequestHandler
{
friend class AbstractModuleBase<ServiceStateless2_0>;
};
class ServiceStateful2_0 :
public AbstractModuleBase<ServiceStateful2_0>,
private detail::PureServiceInterface,
private ocpp2_0::AbstractService,
private ocpp2_0::AbstractRequestHandler,
private ocpp2_0::AbstractResponseHandler
{
friend class AbstractModuleBase<ServiceStateful2_0>;
};
class ServiceStatelessGeneral :
public AbstractModuleBase<ServiceStatelessGeneral>,
private detail::PureServiceInterface,
private ocpp1_6::AbstractRequestHandler,
private ocpp2_0::AbstractRequestHandler
{
friend class AbstractModuleBase<ServiceStatelessGeneral>;
};
class ServiceStatefulGeneral :
public AbstractModuleBase<ServiceStatefulGeneral>,
private detail::PureServiceInterface,
private ocpp1_6::AbstractService,
private ocpp1_6::AbstractRequestHandler,
private ocpp1_6::AbstractResponseHandler,
private ocpp2_0::AbstractService,
private ocpp2_0::AbstractRequestHandler,
private ocpp2_0::AbstractResponseHandler
{
friend class AbstractModuleBase<ServiceStatefulGeneral>;
};
class PureService :
public AbstractModuleBase<PureService>,
private detail::PureServiceInterface
{
friend class AbstractModuleBase<PureService>;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_COMMON_TEMPLATES_H

View File

@@ -0,0 +1,521 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_CONFIGURATION_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_CONFIGURATION_MODULE_H
#include "openocpp/protocol/common/protocol_constants.h"
#include "openocpp/module/common_templates.h"
#include "openocpp/common/settings.h"
#include "openocpp/helpers/string.h"
#include "openocpp/interface/component/system_interface.h"
#include <utility>
#include <functional>
namespace chargelab {
class ConfigurationModule : public ServiceStatefulGeneral {
private:
static constexpr char const* kMaskedValue = "****";
static constexpr int kOcpp20NotifyReportRequestOverheadBytes = 100;
public:
explicit ConfigurationModule(std::shared_ptr<Settings> settings, std::shared_ptr<SystemInterface> system)
: settings_(std::move(settings)),
system_(std::move(system))
{
assert(settings_ != nullptr);
assert(system_ != nullptr);
}
~ConfigurationModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting ConfigurationModule";
settings_->saveIfModified();
}
public:
void runUnconditionally() override {
settings_->saveIfModified();
}
void runStep(ocpp1_6::OcppRemote&) override {
}
void runStep(ocpp2_0::OcppRemote &remote) override {
if (ocpp2_0_pending_base_report_.has_value()) {
auto request = ocpp2_0_pending_base_report_.value();
auto settings = settings_;
ocpp2_0::NotifyReportRequest response {
request.requestId,
{system_->systemClockNow()},
false,
0,
{[&](std::function<void(ocpp2_0::ReportDataType const &)> const &visitor) {
settings->visitSettings([&](SettingBase& setting) {
auto const metadata = setting.getMetadata();
bool include_characteristics;
switch (request.reportBase) {
default:
CHARGELAB_LOG_MESSAGE(error) << "Unexpected report type in generator: " << request.reportBase;
return;
case ocpp2_0::ReportBaseEnumType::kConfigurationInventory:
include_characteristics = true;
if (!metadata.config.isAllowOcppWrite())
return;
break;
case ocpp2_0::ReportBaseEnumType::kFullInventory:
include_characteristics = true;
break;
}
if (!metadata.model2_0.has_value())
return;
auto element = ocpp2_0::ReportDataType {
metadata.model2_0->component_type,
metadata.model2_0->variable_type,
setting.getAttributes2_0()
};
if (include_characteristics) {
element.variableCharacteristics = metadata.model2_0->variable_characteristics;
}
visitor(element);
});
}}
};
if (remote.sendNotifyReportReq(response).has_value()) {
ocpp2_0_pending_base_report_ = std::nullopt;
}
}
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetVariablesResponse>>
onGetVariablesReq(const ocpp2_0::GetVariablesRequest &request) override {
if ((int)request.getVariableData.size() > settings_->ItemsPerMessageGetVariables.getValue()) {
return ocpp2_0::CallError {
ocpp2_0::ErrorCode::kOccurrenceConstraintViolation,
{"Exceeded ItemsPerMessageGetVariables limit"},
common::RawJson::empty_object()
};
}
auto settings = settings_;
return ocpp2_0::GetVariablesResponse {
{[=](std::function<void(ocpp2_0::GetVariableResultType const &)> const &visitor) {
for (auto const& get_variable : request.getVariableData) {
bool componentFound = false;
bool variableFound = false;
auto result = ocpp2_0::GetVariableResultType {
ocpp2_0::GetVariableStatusEnumType::kValueNotFoundInEnum,
get_variable.attributeType,
std::nullopt,
get_variable.component,
get_variable.variable
};
settings->visitSettings([&](SettingBase& setting) {
auto const metadata = setting.getMetadata();
if (variableFound)
return;
if (!metadata.model2_0.has_value())
return;
if (metadata.model2_0->component_type != get_variable.component) {
return;
} else {
componentFound = true;
}
if (metadata.model2_0->variable_type != get_variable.variable) {
return;
} else {
variableFound = true;
}
if (get_variable.attributeType.has_value()) {
// TODO - right now everything defined is "actual"
if (get_variable.attributeType.value() != ocpp2_0::AttributeEnumType::kActual) {
result.attributeStatus = ocpp2_0::GetVariableStatusEnumType::kNotSupportedAttributeType;
return;
}
}
if (!metadata.config.isAllowOcppRead()) {
result.attributeStatus = ocpp2_0::GetVariableStatusEnumType::kRejected;
return;
}
result.attributeStatus = ocpp2_0::GetVariableStatusEnumType::kAccepted;
result.attributeValue = setting.getValueAsString();
});
if (!componentFound) {
result.attributeStatus = ocpp2_0::GetVariableStatusEnumType::kUnknownComponent;
} else if (!variableFound) {
result.attributeStatus = ocpp2_0::GetVariableStatusEnumType::kUnknownVariable;
}
visitor(result);
}
}}
};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetVariablesResponse>>
onSetVariablesReq(const ocpp2_0::SetVariablesRequest &request) override {
std::vector<ocpp2_0::SetVariableResultType> results;
for (auto const& set_variable : request.setVariableData) {
bool componentFound = false;
bool variableFound = false;
auto result = ocpp2_0::SetVariableResultType {
set_variable.attributeType,
ocpp2_0::SetVariableStatusEnumType::kValueNotFoundInEnum,
set_variable.component,
set_variable.variable
};
auto const network_configuration_priority_id = settings_->NetworkConfigurationPriority.getId();
settings_->visitSettings([&](SettingBase& setting) {
auto const metadata = setting.getMetadata();
if (variableFound)
return;
if (!metadata.model2_0.has_value())
return;
if (metadata.model2_0->component_type != set_variable.component) {
return;
} else {
componentFound = true;
}
if (metadata.model2_0->variable_type != set_variable.variable) {
return;
} else {
variableFound = true;
}
if (set_variable.attributeType.has_value()) {
// TODO - right now everything defined is "actual"
if (set_variable.attributeType.value() != ocpp2_0::AttributeEnumType::kActual) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kNotSupportedAttributeType;
return;
}
}
if (!metadata.config.isAllowOcppWrite()) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kRejected;
return;
}
// A05.FR.02
if (metadata.id == network_configuration_priority_id) {
bool missing_profile = false;
int target_security_profile = 0;
string::SplitVisitor(set_variable.attributeValue.value(), ",", [&](std::string const& text) {
auto slot = string::ToInteger(text);
if (!slot.has_value())
return;
auto const& profile = settings_->NetworkConnectionProfiles.getValue(slot.value());
if (!profile.has_value()) {
missing_profile = true;
return;
}
target_security_profile = std::max(target_security_profile, profile->securityProfile);
});
// TODO: Is there a specific requirement for this?
if (target_security_profile < settings_->SecurityProfile.getValue()) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kRejected;
return;
}
if (target_security_profile > settings_->SecurityProfile.getValue()) {
// A05.FR.02
if (target_security_profile >= 2 && settings_->InstalledCSMSRootCertificateCount.getValue() <= 0) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kRejected;
return;
}
// A05.FR.03
// TODO: Not relevant until we have something that can store a charging station certificate
if (target_security_profile == 3) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kRejected;
return;
}
}
}
// TODO: move to update attribute?
if (!settings_->setSettingValue(metadata.id, set_variable.attributeValue.value())) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kRejected;
return;
}
if (metadata.config.isRebootRequired()) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kRebootRequired;
} else {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kAccepted;
}
});
if (!componentFound) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kUnknownComponent;
} else if (!variableFound) {
result.attributeStatus = ocpp2_0::SetVariableStatusEnumType::kUnknownVariable;
}
results.push_back(std::move(result));
}
return ocpp2_0::SetVariablesResponse {std::move(results)};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetBaseReportResponse>>
onGetBaseReportReq(const ocpp2_0::GetBaseReportRequest &request) override {
if (ocpp2_0_pending_base_report_.has_value()) {
return ocpp2_0::GetBaseReportResponse {
ocpp2_0::GenericDeviceModelStatusEnumType::kRejected
};
}
switch (request.reportBase) {
case ocpp2_0::ReportBaseEnumType::kValueNotFoundInEnum:
case ocpp2_0::ReportBaseEnumType::kSummaryInventory:
return ocpp2_0::GetBaseReportResponse {
ocpp2_0::GenericDeviceModelStatusEnumType::kNotSupported
};
case ocpp2_0::ReportBaseEnumType::kFullInventory:
case ocpp2_0::ReportBaseEnumType::kConfigurationInventory:
break;
}
ocpp2_0_pending_base_report_ = request;
return ocpp2_0::GetBaseReportResponse {
ocpp2_0::GenericDeviceModelStatusEnumType::kAccepted
};
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ChangeConfigurationRsp>>
onChangeConfigurationReq(const ocpp1_6::ChangeConfigurationReq& req) override {
return changeConfiguration(req, false);
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ChangeConfigurationRsp>>
changeConfiguration(const ocpp1_6::ChangeConfigurationReq& req, bool force_change) {
CHARGELAB_LOG_MESSAGE(info) << "changing configuration, key:" << req.key.value() << ", value:" << req.value.value();
auto const state = settings_->getSettingState(req.key.value());
if (!force_change) {
if (!state.has_value() || (!state->config.isAllowOcppWrite() && !state->config.isAllowOcppRead()))
return ocpp1_6::ChangeConfigurationRsp{ocpp1_6::ConfigurationStatus::kNotSupported};
if (!state->config.isAllowOcppWrite())
return ocpp1_6::ChangeConfigurationRsp{ocpp1_6::ConfigurationStatus::kRejected};
}
// TODO: Move to 1_6 device model rather than id
if (!settings_->setSettingValue(req.key.value(), req.value.value()))
return ocpp1_6::ChangeConfigurationRsp{ocpp1_6::ConfigurationStatus::kRejected};
if (force_change) {
settings_->saveIfModified();
}
if (state->config.isRebootRequired()) {
return ocpp1_6::ChangeConfigurationRsp{ocpp1_6::ConfigurationStatus::kRebootRequired};
} else {
return ocpp1_6::ChangeConfigurationRsp{ocpp1_6::ConfigurationStatus::kAccepted};
}
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::GetConfigurationRsp>>
onGetConfigurationReq(const ocpp1_6::GetConfigurationReq &req) override {
return getConfiguration(req);
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::GetConfigurationRsp>>
getConfiguration(const ocpp1_6::GetConfigurationReq &req) {
std::vector<std::string> include_keys;
std::vector<ocpp1_6::CiString50Type> unknown_keys = req.key.value();
// TODO: Move to 1_6 device model rather than id
bool include_all_keys = req.key.value().empty();
if (!unknown_keys.empty()) {
if ((int)req.key.value().size() > settings_->GetConfigurationMaxKeys.getValue()) {
return ocpp1_6::CallError {
ocpp1_6::ErrorCode::kPropertyConstraintViolation,
"ConnectorId is not same as the local connector Id - must be same",
common::RawJson::empty_object()
};
}
unknown_keys = req.key.value();
settings_->visitSettings([&](SettingBase const& setting) {
auto const metadata = setting.getMetadata();
if (!metadata.config.isAllowOcppRead() && !metadata.config.isAllowOcppWrite())
return;
auto it = std::remove_if(unknown_keys.begin(), unknown_keys.end(), [&] (auto const& x) {
return string::EqualsIgnoreCaseAscii(x.value(), metadata.id);
});
if (it != unknown_keys.end()) {
include_keys.push_back(metadata.id);
unknown_keys.erase(it, unknown_keys.end());
}
});
}
auto settings = settings_;
return ocpp1_6::GetConfigurationRsp {
{[=](std::function<void(ocpp1_6::KeyValue const &)> const &visitor) {
auto const& keys = include_keys;
settings->visitSettings([&](SettingBase const& setting) {
auto const metadata = setting.getMetadata();
if (!metadata.config.isAllowOcppRead() && !metadata.config.isAllowOcppWrite())
return;
std::string value;
if (metadata.config.isAllowOcppRead()) {
value = setting.getValueAsString();
} else {
value = kMaskedValue;
}
if (include_all_keys || std::find(keys.begin(), keys.end(), setting.getId()) != keys.end()) {
visitor(ocpp1_6::KeyValue{
{metadata.id},
!metadata.config.isAllowOcppWrite(),
std::move(value)
});
}
});
}},
std::move(unknown_keys)
};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::SetNetworkProfileResponse>>
onSetNetworkProfileReq(const ocpp2_0::SetNetworkProfileRequest &request) override {
if (request.configurationSlot < 0 || request.configurationSlot >= detail::SettingsConstants::kMaxProfileSlots) {
// B09.FR.02
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"BadConfigurationSlot"}}
};
}
// Note: SIM/VPN not supported
auto const& data = request.connectionData;
if (data.apn.has_value()) {
// B09.FR.02
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"ApnNotSupported"}}
};
}
if (data.vpn.has_value()) {
// B09.FR.02
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"VpnNotSupported"}}
};
}
// Note: only JSON is supported
if (data.ocppTransport != ocpp2_0::OCPPTransportEnumType::kJSON) {
// B09.FR.02
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"BadOcppTransport"}}
};
}
std::string protocol;
switch (data.ocppVersion) {
default:
case ocpp2_0::OCPPVersionEnumType::kOCPP12:
case ocpp2_0::OCPPVersionEnumType::kOCPP15:
// B09.FR.02
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"BadOcppVersion"}}
};
case ocpp2_0::OCPPVersionEnumType::kOCPP16:
protocol = ProtocolConstants::kProtocolOcpp1_6;
break;
case ocpp2_0::OCPPVersionEnumType::kOCPP20:
protocol = ProtocolConstants::kProtocolOcpp2_0_1;
break;
}
// Note: an assumed "wifi" interface is always used here
if (data.ocppInterface != ocpp2_0::OCPPInterfaceEnumType::kWireless0) {
// B09.FR.02
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"BadOcppInterface"}}
};
}
auto parsed = uri::ParseWebsocketUri(data.ocppCsmsUrl.value());
if (!parsed.has_value()) {
// B09.FR.02
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"BadOcppCsmsUrl"}}
};
}
if (request.connectionData.securityProfile < settings_->SecurityProfile.getValue()) {
// B09.FR.04
return ocpp2_0::SetNetworkProfileResponse {
ocpp2_0::SetNetworkProfileStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {{"BadSecurityProfile"}}
};
}
if (settings_->NetworkConnectionProfiles.setValue(request.configurationSlot, request.connectionData)) {
// B09.FR.01
return ocpp2_0::SetNetworkProfileResponse {ocpp2_0::SetNetworkProfileStatusEnumType::kAccepted};
} else {
// B09.FR.03
return ocpp2_0::SetNetworkProfileResponse {ocpp2_0::SetNetworkProfileStatusEnumType::kFailed};
}
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetMonitoringReportResponse>>
onGetMonitoringReportReq(const ocpp2_0::GetMonitoringReportRequest&) override {
return ocpp2_0::GetMonitoringReportResponse {ocpp2_0::GenericDeviceModelStatusEnumType::kEmptyResultSet};
}
private:
template<int N>
static bool ciEquals(ocpp2_0::IdentifierStringPrimitive<N> const& lhs, ocpp2_0::IdentifierStringPrimitive<N> const& rhs) {
return string::EqualsIgnoreCaseAscii(lhs.value(), rhs.value());
}
template<int N>
static bool ciEquals(std::optional<ocpp2_0::IdentifierStringPrimitive<N>> const& lhs, std::optional<ocpp2_0::IdentifierStringPrimitive<N>> const& rhs) {
if (lhs.has_value() != rhs.has_value())
return false;
if (!lhs.has_value())
return true;
return ciEquals(lhs.value(), rhs.value());
}
private:
std::shared_ptr<Settings> settings_;
std::shared_ptr<SystemInterface> system_;
std::optional<ocpp2_0::GetBaseReportRequest> ocpp2_0_pending_base_report_ = std::nullopt;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_CONFIGURATION_MODULE_H

View File

@@ -0,0 +1,849 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_CONNECTOR_STATUS_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_CONNECTOR_STATUS_MODULE_H
#include "openocpp/module/common_templates.h"
#include "openocpp/interface/platform_interface.h"
#include "openocpp/interface/station_interface.h"
#include "openocpp/common/settings.h"
#include "openocpp/common/logging.h"
#include "openocpp/common/operation_holder.h"
#include <utility>
#include <set>
namespace chargelab {
namespace detail {
struct ReportedConnectorStatus {
std::optional<SteadyPointMillis> last_status_sent = std::nullopt;
std::optional<ocpp2_0::StatusNotificationRequest> last_reported_status2_0 = std::nullopt;
std::optional<ocpp1_6::StatusNotificationReq> last_reported_status1_6 = std::nullopt;
bool current_plug_was_charging = false;
bool force_update = false;
};
}
class ConnectorStatusModule : public ServiceStatefulGeneral {
private:
static constexpr int kSettingUpdateIntervalMillis = 500;
public:
ConnectorStatusModule(
std::shared_ptr<Settings> settings,
std::shared_ptr<PlatformInterface> const& platform,
std::shared_ptr<StationInterface> station
)
: settings_(std::move(settings)),
platform_(platform),
station_(std::move(station)),
pending_connector_status_req_ {platform}
{
assert(platform_ != nullptr);
loadInoperativeConnectors();
}
~ConnectorStatusModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting ConnectorStatusModule";
}
public:
bool isChargingEnabled() {
for (auto const& entry : station_->getConnectorMetadata()) {
auto const current_state = station_->pollConnectorStatus(entry.first);
if (current_state.has_value() && current_state->charging_enabled)
return true;
}
return false;
}
void setPendingReset(bool value, bool hard_reset) {
pending_reset_ = value;
reset_reason_hard_ = hard_reset;
}
bool getPendingReset() const {
return pending_reset_;
}
bool getResetReasonHard() const {
return reset_reason_hard_;
}
void setPendingStartRequests(std::vector<std::optional<ocpp2_0::EVSEType>> const& pending) {
pending_start_.assign(pending.begin(), pending.end());
}
std::unordered_map<int, ocpp1_6::ChargePointStatus> getChargePointStatus1_6() {
std::lock_guard<std::mutex> lock(last_charge_point_status_map1_6_mutex_);
return last_charge_point_status_map1_6_;
}
bool setConnector0Inoperative(bool inoperative) {
if (inoperative_connectors_[std::nullopt] != inoperative) {
inoperative_connectors_[std::nullopt] = inoperative;
return true;
}
return false;
}
private:
void runUnconditionally() override {
addAndUpdateStateSettings();
auto const connection = platform_->ocppConnection();
if (connection != nullptr && connection->isConnected()) {
last_online_ = platform_->steadyClockNow();
} else if (last_online_.has_value()) {
// B04.FR.01
auto const elapsed = platform_->steadyClockNow() - last_online_.value();
if (elapsed/1000 >= settings_->OfflineThreshold.getValue()) {
bool forced_update = false;
for (auto& x : reported_status_) {
if (!x.second.force_update) {
x.second.force_update = true;
forced_update = true;
}
}
if (forced_update) {
CHARGELAB_LOG_MESSAGE(info) << "OfflineThreshold exceeded - forcing status notification updates on next connection";
}
}
}
}
void runStep(ocpp2_0::OcppRemote &remote) override {
for (auto& entry : station_->getConnectorMetadata()) {
auto const current_state = station_->pollConnectorStatus(entry.first);
if (!current_state.has_value())
continue;
auto& reported_status = reported_status_[entry.first];
advanceConnectorState(reported_status, current_state.value());
auto current_status = getStatus2_0(entry.first, current_state.value());
if (pending_connector_status_req_.operationInProgress())
continue;
if (!reported_status.force_update && reported_status.last_reported_status2_0.has_value()) {
if (current_status == reported_status.last_reported_status2_0->connectorStatus)
continue;
// Note: rate limiting charging status doesn't appear to be part of the 2.0.1 spec
}
reported_status.last_status_sent = platform_->steadyClockNow();
pending_connector_status_update2_0_ = std::make_pair(entry.first, ocpp2_0::StatusNotificationRequest {
ocpp2_0::DateTime{platform_->systemClockNow()},
current_status,
entry.first.id,
entry.first.connectorId.value()
});
pending_connector_status_req_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
remote.sendStatusNotificationReq(pending_connector_status_update2_0_->second)
);
}
}
void onStatusNotificationRsp(
const std::string &uniqueId,
const ocpp2_0::ResponseMessage<ocpp2_0::StatusNotificationResponse> &rsp
) override {
if (pending_connector_status_req_ == uniqueId) {
// TODO: Allowing failed operations to timeout instead of retrying immediately. This might need more
// thought.
//pending_connector_status_req_ = kNoOperation;
if (std::holds_alternative<ocpp2_0::StatusNotificationResponse>(rsp)) {
if (pending_connector_status_update2_0_.has_value()) {
auto const& evse = pending_connector_status_update2_0_->first;
auto const& request = pending_connector_status_update2_0_->second;
reported_status_[evse].last_reported_status2_0 = request;
reported_status_[evse].force_update = false;
}
pending_connector_status_req_ = kNoOperation;
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to StatusNotification request: " << std::get<ocpp2_0::CallError> (rsp);
}
pending_connector_status_update2_0_ = std::nullopt;
}
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::TriggerMessageResponse>>
onTriggerMessageReq(const ocpp2_0::TriggerMessageRequest &req) override {
if (req.requestedMessage != ocpp2_0::MessageTriggerEnumType::kStatusNotification)
return std::nullopt;
// Note: deviating from F06.FR.12 and treating req.evse as a filter instead. If evse isn't specified all
// connector statuses are reported, if connector ID isn't specified all statuses for that EVSE ID are
// reported.
bool found = false;
for (auto const& entry : station_->getConnectorMetadata()) {
if (req.evse.has_value()) {
if (req.evse->id != entry.first.id)
continue;
if (req.evse->connectorId.has_value() && req.evse->connectorId != entry.first.connectorId)
continue;
}
CHARGELAB_LOG_MESSAGE(debug) << "Got trigger message for EVSE: " << entry.first;
reported_status_[entry.first].force_update = true;
found = true;
}
if (!found) {
return ocpp2_0::TriggerMessageResponse {ocpp2_0::TriggerMessageStatusEnumType::kRejected};
}
return ocpp2_0::TriggerMessageResponse {ocpp2_0::TriggerMessageStatusEnumType::kAccepted};
}
void runStep(ocpp1_6::OcppRemote &remote) override {
for (auto& entry : station_->getConnectorMetadata()) {
auto const current_state = station_->pollConnectorStatus(entry.first);
if (!current_state.has_value())
continue;
auto& reported_status = reported_status_[entry.first];
advanceConnectorState(reported_status, current_state.value());
auto current_status = getStatus1_6(entry.first, current_state.value(), reported_status.current_plug_was_charging);
if (pending_connector_status_req_.operationInProgress())
continue;
{
std::lock_guard<std::mutex> lock(last_charge_point_status_map1_6_mutex_);
last_charge_point_status_map1_6_[entry.second.connector_id1_6] = current_status;
}
ocpp1_6::StatusNotificationReq request {
entry.second.connector_id1_6,
ocpp1_6::ChargePointErrorCode::kNoError,
std::nullopt,
current_status,
ocpp1_6::DateTime{platform_->systemClockNow()},
std::nullopt,
std::nullopt
};
if (current_state->faulted_status.has_value()) {
auto const& faulted = current_state->faulted_status->status1_6;
request.errorCode = faulted.errorCode;
request.info = faulted.info;
request.vendorId = faulted.vendorId;
request.vendorErrorCode = faulted.vendorErrorCode;
}
if (!reported_status.force_update && reported_status.last_reported_status1_6.has_value()) {
// Note: ignoring info parameter for updates; could potentially contain noisy information
auto const& last = reported_status.last_reported_status1_6.value();
if (request.status == last.status && request.errorCode == last.errorCode && request.vendorId == last.vendorId && request.vendorErrorCode == last.vendorErrorCode)
continue;
// TODO: rate limiting?
}
reported_status.last_status_sent = platform_->steadyClockNow();
pending_connector_status_update1_6_ = std::make_pair(entry.first, request);
pending_connector_status_req_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
remote.sendStatusNotificationReq(request)
);
}
}
void onStatusNotificationRsp(
const std::string &uniqueId,
const ocpp1_6::ResponseMessage<ocpp1_6::StatusNotificationRsp> &rsp
) override {
if (pending_connector_status_req_ == uniqueId) {
// TODO: Allowing failed operations to timeout instead of retrying immediately. This might need more
// thought.
//pending_connector_status_req_ = kNoOperation;
if (std::holds_alternative<ocpp1_6::StatusNotificationRsp>(rsp)) {
if (pending_connector_status_update1_6_.has_value()) {
auto const& evse = pending_connector_status_update1_6_->first;
auto const& request = pending_connector_status_update1_6_->second;
reported_status_[evse].last_reported_status1_6 = request;
reported_status_[evse].force_update = false;
}
pending_connector_status_req_ = kNoOperation;
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to StatusNotification request: " << std::get<ocpp1_6::CallError> (rsp);
}
pending_connector_status_update1_6_ = std::nullopt;
}
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::TriggerMessageRsp>>
onTriggerMessageReq(const ocpp1_6::TriggerMessageReq &req) override {
if (req.requestedMessage != ocpp1_6::MessageTrigger::kStatusNotification)
return std::nullopt;
bool found = false;
for (auto const& entry : station_->getConnectorMetadata()) {
if (req.connectorId.has_value()) {
if (entry.second.connector_id1_6 != req.connectorId.value())
continue;
}
CHARGELAB_LOG_MESSAGE(debug) << "Got trigger message for EVSE: " << entry.first;
reported_status_[entry.first].force_update = true;
found = true;
}
if (!found) {
return ocpp1_6::TriggerMessageRsp {ocpp1_6::TriggerMessageStatus::kRejected};
}
return ocpp1_6::TriggerMessageRsp {ocpp1_6::TriggerMessageStatus::kAccepted};
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ChangeAvailabilityRsp>>
onChangeAvailabilityReq(const ocpp1_6::ChangeAvailabilityReq &req) override {
std::vector<bool*> inoperative_connectors;
for (auto const& entry : station_->getConnectorMetadata()) {
if (req.connectorId == 0 || entry.second.connector_id1_6 == req.connectorId) {
inoperative_connectors.emplace_back(&inoperative_connectors_[entry.first]);
}
}
if (inoperative_connectors.empty())
return ocpp1_6::ChangeAvailabilityRsp {ocpp1_6::AvailabilityStatus::kRejected};
switch (req.type) {
default:
return ocpp1_6::CallError {
ocpp1_6::ErrorCode::kPropertyConstraintViolation,
"Bad ChangeAvailabilityReq type enum",
common::RawJson::empty_object()
};
case ocpp1_6::AvailabilityType::kOperative:
for (auto &connector : inoperative_connectors) {
*connector = false;
}
break;
case ocpp1_6::AvailabilityType::kInoperative:
for (auto &connector : inoperative_connectors) {
*connector = true;
}
break;
}
bool charging = false;
for (auto const& entry : station_->getConnectorMetadata()) {
if (req.connectorId != 0) {
if (req.connectorId != entry.second.connector_id1_6)
continue;
}
auto const status = station_->pollConnectorStatus(entry.first);
if (status.has_value() && status->charging_enabled) {
charging = true;
break;
}
}
saveInoperativeConnectors();
if (charging) {
return ocpp1_6::ChangeAvailabilityRsp {ocpp1_6::AvailabilityStatus::kScheduled};
} else {
return ocpp1_6::ChangeAvailabilityRsp {ocpp1_6::AvailabilityStatus::kAccepted};
}
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ChangeAvailabilityResponse>>
onChangeAvailabilityReq(const ocpp2_0::ChangeAvailabilityRequest &req) override {
std::map<ocpp2_0::EVSEType, charger::ConnectorMetadata> metadata;
for (auto const& entry : station_->getConnectorMetadata()) {
if (req.evse.has_value()) {
if (req.evse->id != entry.first.id)
continue;
if (req.evse->connectorId.has_value() && req.evse->connectorId != entry.first.connectorId)
continue;
}
metadata[entry.first] = entry.second;
}
if (metadata.size() == 0)
return ocpp2_0::ChangeAvailabilityResponse {ocpp2_0::ChangeAvailabilityStatusEnumType::kRejected};
switch (req.operationalStatus) {
default:
return ocpp2_0::CallError {
ocpp2_0::ErrorCode::kPropertyConstraintViolation,
"Bad ChangeAvailabilityRequest operationalStatus enum",
common::RawJson::empty_object()
};
case ocpp2_0::OperationalStatusEnumType::kOperative:
inoperative_connectors_[req.evse] = false;
break;
case ocpp2_0::OperationalStatusEnumType::kInoperative:
inoperative_connectors_[req.evse] = true;
break;
}
bool charging = false;
for (auto const& entry : metadata) {
auto const status = station_->pollConnectorStatus(entry.first);
if (status.has_value() && status->charging_enabled) {
charging = true;
break;
}
}
saveInoperativeConnectors();
if (charging) {
return ocpp2_0::ChangeAvailabilityResponse {ocpp2_0::ChangeAvailabilityStatusEnumType::kScheduled};
} else {
return ocpp2_0::ChangeAvailabilityResponse {ocpp2_0::ChangeAvailabilityStatusEnumType::kAccepted};
}
}
private:
void advanceConnectorState(detail::ReportedConnectorStatus& reported, charger::ConnectorStatus const& current) {
if (current.vehicle_connected && current.charging_enabled) {
reported.current_plug_was_charging = true;
} else if (!current.vehicle_connected) {
reported.current_plug_was_charging = false;
}
}
void saveInoperativeConnectors() {
std::string result;
char const* ifs = "";
for (auto const& x : inoperative_connectors_) {
if (!x.second)
continue;
result += ifs;
if (!x.first.has_value()) {
result += "0";
} else if (!x.first->connectorId.has_value()) {
result += std::to_string(x.first->id);
} else {
result += std::to_string(x.first->id) + "." + std::to_string(x.first->connectorId.value());
}
ifs = ",";
}
settings_->InoperativeConnectors.setValue(result);
CHARGELAB_LOG_MESSAGE(debug) << "Saved inoperative connectors: " << result;
}
void loadInoperativeConnectors() {
auto const value = settings_->InoperativeConnectors.getValue();
CHARGELAB_LOG_MESSAGE(debug) << "Loading inoperative connectors: " << value;
inoperative_connectors_.clear();
string::SplitVisitor(value, ",", [&](std::string const& text) {
if (text.empty())
return;
auto const it = text.find('.');
if (it != std::string::npos) {
auto const id = string::ToInteger(text.substr(0, it));
auto const connectorId = string::ToInteger(text.substr(it+1));
if (id && connectorId) {
inoperative_connectors_[ocpp2_0::EVSEType{id.value(), connectorId}] = true;
}
} else {
auto const id = string::ToInteger(text);
if (id) {
if (id.value() == 0) {
inoperative_connectors_[std::nullopt] = true;
} else {
inoperative_connectors_[ocpp2_0::EVSEType{id.value()}] = true;
}
}
}
});
CHARGELAB_LOG_MESSAGE(debug) << "Inoperative connectors: " << inoperative_connectors_;
}
ocpp2_0::ConnectorStatusEnumType getStatus2_0(ocpp2_0::EVSEType const& evse, charger::ConnectorStatus const& status) {
if (status.faulted_status.has_value())
return ocpp2_0::ConnectorStatusEnumType::kFaulted;
if (status.vehicle_connected && status.charging_enabled)
return ocpp2_0::ConnectorStatusEnumType::kOccupied;
// TODO: Need to explore this further. This is failing TC_B_22_CS in OCTT; is that because the transaction
// stop message wasn't received first or because the station must not send out an Unavailable status under
// these conditions?
// if (pending_reset_)
// return ocpp2_0::ConnectorStatusEnumType::kUnavailable;
if (!status.connector_available)
return ocpp2_0::ConnectorStatusEnumType::kUnavailable;
if (status.vehicle_connected)
return ocpp2_0::ConnectorStatusEnumType::kOccupied;
// TODO: What is the precedence of "Unavailable" in 2.0.1?
// Note: this fails test case TC_G_11_CS if this takes precedence over the "Occupied" state above.
if (inoperative_connectors_[std::nullopt])
return ocpp2_0::ConnectorStatusEnumType::kUnavailable;
if (inoperative_connectors_[ocpp2_0::EVSEType {evse.id}])
return ocpp2_0::ConnectorStatusEnumType::kUnavailable;
if (inoperative_connectors_[evse])
return ocpp2_0::ConnectorStatusEnumType::kUnavailable;
return ocpp2_0::ConnectorStatusEnumType::kAvailable;
}
ocpp1_6::ChargePointStatus getStatus1_6(ocpp2_0::EVSEType const& evse, charger::ConnectorStatus const& current, bool was_charging) {
if (current.faulted_status.has_value())
return ocpp1_6::ChargePointStatus::kFaulted;
if (current.vehicle_connected && current.charging_enabled) {
if (current.suspended_by_charger) {
return ocpp1_6::ChargePointStatus::kSuspendedEVSE;
} else if (current.suspended_by_vehicle) {
return ocpp1_6::ChargePointStatus::kSuspendedEV;
} else {
return ocpp1_6::ChargePointStatus::kCharging;
}
}
if (inoperative_connectors_[std::nullopt])
return ocpp1_6::ChargePointStatus::kUnavailable;
if (inoperative_connectors_[evse])
return ocpp1_6::ChargePointStatus::kUnavailable;
// if (pending_reset_)
// return ocpp1_6::ChargePointStatus::kUnavailable;
if (!current.connector_available)
return ocpp1_6::ChargePointStatus::kUnavailable;
if (current.vehicle_connected) {
if (was_charging) {
return ocpp1_6::ChargePointStatus::kFinishing;
} else {
return ocpp1_6::ChargePointStatus::kPreparing;
}
}
// For RFID, we have a pending start entry but with a nullopt value
auto it = std::find_if(pending_start_.begin(), pending_start_.end(), [=] (auto entry) {
return !entry.has_value() || entry->id == evse.id; } );
// Connector 0 can't be preparing
if (it != pending_start_.end() && evse.id != 0) {
return ocpp1_6::ChargePointStatus::kPreparing;
}
return ocpp1_6::ChargePointStatus::kAvailable;
}
template <typename Map, typename Key, typename Generator>
auto& getOrCreateSetting(Map& map, Key&& key, Generator&& generator) {
auto it = map.find(key);
if (it != map.end() && it->second != nullptr)
return *it->second;
auto setting = generator();
map[key] = setting;
settings_->registerCustomSetting(setting);
return *setting;
}
void addAndUpdateStateSettings() {
auto const now = platform_->steadyClockNow();
if (last_settings_update_.has_value()) {
auto const delta = now - last_settings_update_.value();
if (delta < kSettingUpdateIntervalMillis)
return;
}
last_settings_update_ = now;
// Update station level settings
{
auto const& metadata = station_->getStationMetadata();
settings_->ChargingStationAvailable.setValue(true);
settings_->ChargingStationAvailabilityState.setValue(
inoperative_connectors_[std::nullopt] ? "Unavailable" : "Available");
settings_->ChargingStationSupplyPhases.setValue(metadata.supply_phases);
}
// Update connector level settings
std::set<int> evse_ids {};
for (auto const& entry : station_->getConnectorMetadata()) {
if (!entry.first.connectorId.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Bad connector metadata - must specify a connector ID";
continue;
}
auto const& evse_id = entry.first.id;
auto const& connector_id = entry.first.connectorId.value();
auto const& connector_status = station_->pollConnectorStatus(entry.first);
evse_ids.insert(evse_id);
// 2.13.1
auto& available = getOrCreateSetting(settings_available_, entry.first, [&]() {
return std::make_shared<SettingBool>(
std::make_unique<SettingMetadata>(SettingMetadata {
"EVSE" + std::to_string(evse_id) + "Connector" + std::to_string(connector_id) + "Available",
SettingConfig::roNotSavedPolicy(),
std::nullopt,
DeviceModel2_0{
ocpp2_0::ComponentType{"Connector", std::nullopt, ocpp2_0::EVSEType{evse_id, connector_id}},
ocpp2_0::VariableType{"Available"},
ocpp2_0::VariableCharacteristicsType{std::nullopt, ocpp2_0::DataEnumType::kboolean}
},
SettingBool::kTextTrue
}),
[](auto const&) {return true;}
);
});
available.setValue(connector_status.has_value());
// 2.13.2
auto& availability_state = getOrCreateSetting(settings_availability_state_, entry.first, [&]() {
return std::make_shared<SettingString>(
std::make_unique<SettingMetadata>(SettingMetadata {
"EVSE" + std::to_string(evse_id) + "Connector" + std::to_string(connector_id) + "AvailabilityState",
SettingConfig::roNotSavedPolicy(),
std::nullopt,
DeviceModel2_0{
ocpp2_0::ComponentType{"Connector", std::nullopt, ocpp2_0::EVSEType{evse_id, connector_id}},
ocpp2_0::VariableType{"AvailabilityState"},
ocpp2_0::VariableCharacteristicsType{
std::nullopt,
ocpp2_0::DataEnumType::kOptionList,
std::nullopt,
std::nullopt,
"Available,Occupied,Reserved,Unavailable,Faulted"
}
},
"Unavailable"
}),
[](auto const&) {return true;}
);
});
if (connector_status.has_value()) {
auto const& status = getStatus2_0(entry.first, connector_status.value());
availability_state.setValue(status.to_string());
}
// 2.13.4
auto& connector_type = getOrCreateSetting(settings_connector_type_, entry.first, [&]() {
auto const& name = "EVSE" + std::to_string(evse_id) + "Connector" + std::to_string(connector_id) + "ConnectorType";
return std::make_shared<SettingString>(
std::make_unique<SettingMetadata>(SettingMetadata {
name,
SettingConfig::roNotSavedPolicy(),
DeviceModel1_6{name},
DeviceModel2_0{
ocpp2_0::ComponentType{"Connector", std::nullopt, ocpp2_0::EVSEType{evse_id, connector_id}},
ocpp2_0::VariableType{"ConnectorType"},
ocpp2_0::VariableCharacteristicsType{std::nullopt, ocpp2_0::DataEnumType::kstring}
},
""
}),
[](auto const&) {return true;}
);
});
connector_type.setValue(entry.second.connector_type);
// 2.13.6
auto& supply_phases = getOrCreateSetting(settings_supply_phases_, entry.first, [&]() {
auto const& name = "EVSE" + std::to_string(evse_id) + "Connector" + std::to_string(connector_id) + "SupplyPhases";
return std::make_shared<SettingInt>(
std::make_unique<SettingMetadata>(SettingMetadata {
name,
SettingConfig::roNotSavedPolicy(),
DeviceModel1_6{name},
DeviceModel2_0{
ocpp2_0::ComponentType{"Connector", std::nullopt, ocpp2_0::EVSEType{evse_id, connector_id}},
ocpp2_0::VariableType{"SupplyPhases"},
ocpp2_0::VariableCharacteristicsType{std::nullopt, ocpp2_0::DataEnumType::kinteger}
},
std::to_string(1)
}),
[](auto const&) {return true;}
);
});
supply_phases.setValue(entry.second.supply_phases);
// 2.13.7
// Note: max power won't be updated dynamically if the station information changes; this could possibly
// be improved in the future, but that may not be something that's ever needed in practice.
auto& power = getOrCreateSetting(settings_power_, entry.first, [&]() {
auto const& name = "EVSE" + std::to_string(evse_id) + "Connector" + std::to_string(connector_id) + "Power";
return std::make_shared<SettingDouble>(
std::make_unique<SettingMetadata>(SettingMetadata {
name,
SettingConfig::roNotSavedPolicy(),
DeviceModel1_6{name},
DeviceModel2_0{
ocpp2_0::ComponentType{"Connector", std::nullopt, ocpp2_0::EVSEType{evse_id, connector_id}},
ocpp2_0::VariableType{"Power"},
ocpp2_0::VariableCharacteristicsType{
"W",
ocpp2_0::DataEnumType::kdecimal,
std::nullopt,
entry.second.power_max_watts
}
},
std::to_string(0)
}),
[](auto const&) {return true;}
);
});
// TODO: Take this from the supplied meter values for the connector? Note that a wattage reading isn't
// mandatory, in which case we'd need to support not reporting the actual characteristic.
power.setValue(0);
}
// Update EVSE level settings
for (auto const& entry : station_->getEvseMetadata()) {
if (entry.first.connectorId.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Bad EVSE metadata - should not specify a connector ID";
continue;
}
// 2.13.1
getOrCreateSetting(settings_available_, ocpp2_0::EVSEType{entry.first.id}, [&]() {
return std::make_shared<SettingBool>(
std::make_unique<SettingMetadata>(SettingMetadata {
"EVSE" + std::to_string(entry.first.id) + "Available",
SettingConfig::roNotSavedPolicy(),
std::nullopt,
DeviceModel2_0{
ocpp2_0::ComponentType{"EVSE", std::nullopt, ocpp2_0::EVSEType{entry.first.id}},
ocpp2_0::VariableType{"Available"},
ocpp2_0::VariableCharacteristicsType{std::nullopt, ocpp2_0::DataEnumType::kboolean}
},
SettingBool::kTextTrue
}),
[](auto const&) {return true;}
);
});
// 2.13.2
auto& availability_state = getOrCreateSetting(settings_availability_state_, ocpp2_0::EVSEType{entry.first.id}, [&]() {
return std::make_shared<SettingString>(
std::make_unique<SettingMetadata>(SettingMetadata {
"EVSE" + std::to_string(entry.first.id) + "AvailabilityState",
SettingConfig::roNotSavedPolicy(),
std::nullopt,
DeviceModel2_0{
ocpp2_0::ComponentType{"EVSE", std::nullopt, ocpp2_0::EVSEType{entry.first.id}},
ocpp2_0::VariableType{"AvailabilityState"},
ocpp2_0::VariableCharacteristicsType{
std::nullopt,
ocpp2_0::DataEnumType::kOptionList,
std::nullopt,
std::nullopt,
"Available,Occupied,Reserved,Unavailable,Faulted"
}
},
"Unavailable"
}),
[](auto const&) {return true;}
);
});
// TODO: How should this be interpreted when there's more than one connector under an EVSE? Is it
// reasonable to only report "Available"/"Unavailable" here based on what was configured via
// ChangeAvailability?
availability_state.setValue(inoperative_connectors_[entry.first] ? "Unavailable" : "Available");
// 2.13.6
auto& supply_phases = getOrCreateSetting(settings_supply_phases_, entry.first, [&]() {
auto const& name = "EVSE" + std::to_string(entry.first.id) + "SupplyPhases";
return std::make_shared<SettingInt>(
std::make_unique<SettingMetadata>(SettingMetadata {
name,
SettingConfig::roNotSavedPolicy(),
DeviceModel1_6{name},
DeviceModel2_0{
ocpp2_0::ComponentType{"EVSE", std::nullopt, ocpp2_0::EVSEType{entry.first.id}},
ocpp2_0::VariableType{"SupplyPhases"},
ocpp2_0::VariableCharacteristicsType{std::nullopt, ocpp2_0::DataEnumType::kinteger}
},
std::to_string(1)
}),
[](auto const&) {return true;}
);
});
supply_phases.setValue(entry.second.supply_phases);
// 2.13.7
// Note: max power won't be updated dynamically if the station information changes; this could possibly
// be improved in the future, but that may not be something that's ever needed in practice.
auto& power = getOrCreateSetting(settings_power_, entry.first, [&]() {
auto const& name = "EVSE" + std::to_string(entry.first.id) + "Power";
return std::make_shared<SettingDouble>(
std::make_unique<SettingMetadata>(SettingMetadata {
name,
SettingConfig::roNotSavedPolicy(),
DeviceModel1_6{name},
DeviceModel2_0{
ocpp2_0::ComponentType{"EVSE", std::nullopt, ocpp2_0::EVSEType{entry.first.id}},
ocpp2_0::VariableType{"Power"},
ocpp2_0::VariableCharacteristicsType{
"W",
ocpp2_0::DataEnumType::kdecimal,
std::nullopt,
entry.second.power_max_watts
}
},
std::to_string(0)
}),
[](auto const&) {return true;}
);
});
// TODO: Take this from the supplied meter values for the connector? Note that a wattage reading isn't
// mandatory, in which case we'd need to support not reporting the actual characteristic.
power.setValue(0);
}
}
private:
std::shared_ptr<Settings> settings_;
std::shared_ptr<PlatformInterface> platform_;
std::shared_ptr<StationInterface> station_;
OperationHolder<std::string> pending_connector_status_req_;
std::optional<std::pair<chargelab::ocpp2_0::EVSEType, ocpp2_0::StatusNotificationRequest>> pending_connector_status_update2_0_ = std::nullopt;
std::optional<std::pair<chargelab::ocpp2_0::EVSEType, ocpp1_6::StatusNotificationReq>> pending_connector_status_update1_6_ = std::nullopt;
std::map<chargelab::ocpp2_0::EVSEType, detail::ReportedConnectorStatus> reported_status_;
std::map<std::optional<chargelab::ocpp2_0::EVSEType>, bool> inoperative_connectors_;
std::atomic<bool> pending_reset_ = false;
std::atomic<bool> reset_reason_hard_ = false; // hard reset or soft reset
std::optional<SteadyPointMillis> last_online_ = std::nullopt;
std::vector<std::optional<ocpp2_0::EVSEType>> pending_start_;
std::optional<SteadyPointMillis> last_settings_update_ = std::nullopt;
std::map<chargelab::ocpp2_0::EVSEType, std::shared_ptr<SettingBool>> settings_available_ {};
std::map<chargelab::ocpp2_0::EVSEType, std::shared_ptr<SettingString>> settings_availability_state_ {};
std::map<chargelab::ocpp2_0::EVSEType, std::shared_ptr<SettingString>> settings_connector_type_ {};
std::map<chargelab::ocpp2_0::EVSEType, std::shared_ptr<SettingInt>> settings_supply_phases_ {};
std::map<chargelab::ocpp2_0::EVSEType, std::shared_ptr<SettingDouble>> settings_power_ {};
// For portal module
std::unordered_map<int, ocpp1_6::ChargePointStatus> last_charge_point_status_map1_6_ {};
std::mutex last_charge_point_status_map1_6_mutex_;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_CONNECTOR_STATUS_MODULE_H

View File

@@ -0,0 +1,102 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_FALLBACK_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_FALLBACK_MODULE_H
#include "openocpp/module/common_templates.h"
namespace chargelab {
class FallbackModule : public ServiceStatefulGeneral {
public:
FallbackModule(std::shared_ptr<SystemInterface> system)
: system_(std::move(system))
{
}
private:
void runStep(ocpp1_6::OcppRemote&) override {
}
void runStep(ocpp2_0::OcppRemote &remote) override {
if (pending_customer_report_.has_value()) {
auto id = remote.sendNotifyCustomerInformationReq(ocpp2_0::NotifyCustomerInformationRequest {
"",
false,
0,
system_->systemClockNow(),
pending_customer_report_->requestId
});
if (id.has_value()) {
pending_customer_report_ = std::nullopt;
}
}
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::TriggerMessageRsp>>
onTriggerMessageReq(const ocpp1_6::TriggerMessageReq&) override {
return ocpp1_6::TriggerMessageRsp {ocpp1_6::TriggerMessageStatus::kNotImplemented};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::TriggerMessageResponse>>
onTriggerMessageReq(const ocpp2_0::TriggerMessageRequest&) override {
// F06.FR.08
return ocpp2_0::TriggerMessageResponse {ocpp2_0::TriggerMessageStatusEnumType::kNotImplemented};
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::DataTransferRsp>>
onDataTransferReq(const ocpp1_6::DataTransferReq&) override {
return ocpp1_6::DataTransferRsp {ocpp1_6::DataTransferStatus::kUnknownVendorId};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::DataTransferResponse>>
onDataTransferReq(const ocpp2_0::DataTransferRequest&) override {
// P01.FR.05
return ocpp2_0::DataTransferResponse {ocpp2_0::DataTransferStatusEnumType::kUnknownVendorId};
}
// TODO: This operation doesn't seem relevant if we're not caching customer identifiers like authorization
// tokens in any sort of local list or authorization cache. Leaving this here for now.
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::CustomerInformationResponse>>
onCustomerInformationReq(const ocpp2_0::CustomerInformationRequest& request) override {
// N10.FR.05
if (pending_customer_report_.has_value())
return ocpp2_0::CustomerInformationResponse {ocpp2_0::CustomerInformationStatusEnumType::kRejected};
// N10.FR.07
if (!request.clear && !request.report)
return ocpp2_0::CustomerInformationResponse {ocpp2_0::CustomerInformationStatusEnumType::kRejected};
// N10.FR.01
pending_customer_report_ = request;
return ocpp2_0::CustomerInformationResponse {ocpp2_0::CustomerInformationStatusEnumType::kAccepted};
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::UnlockConnectorRsp>>
onUnlockConnectorReq(const ocpp1_6::UnlockConnectorReq&) override {
return ocpp1_6::UnlockConnectorRsp {ocpp1_6::UnlockStatus::kNotSupported};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::UnlockConnectorResponse>>
onUnlockConnectorReq(const ocpp2_0::UnlockConnectorRequest&) override {
// Note - responding here to satisfy TC_E_16_CS, however a NotImplemented default response seems more
// appropriate.
return ocpp2_0::UnlockConnectorResponse {ocpp2_0::UnlockStatusEnumType::kUnlockFailed};
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::ClearCacheRsp>>
onClearCacheReq(const ocpp1_6::ClearCacheReq&) override {
return ocpp1_6::ClearCacheRsp {ocpp1_6::ClearCacheStatus::kAccepted};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::ClearCacheResponse>>
onClearCacheReq(const ocpp2_0::ClearCacheRequest&) override {
return ocpp2_0::ClearCacheResponse {ocpp2_0::ClearCacheStatusEnumType::kAccepted};
}
private:
std::shared_ptr<SystemInterface> system_;
std::optional<ocpp2_0::CustomerInformationRequest> pending_customer_report_ = std::nullopt;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_FALLBACK_MODULE_H

View File

@@ -0,0 +1,788 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_FIRMWARE_UPDATE_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_FIRMWARE_UPDATE_MODULE_H
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/interface/component/fetch_interface.h"
#include "openocpp/interface/platform_interface.h"
#include "openocpp/common/operation_holder.h"
#include "openocpp/module/common_templates.h"
#include "openocpp/module/pending_messages_module.h"
#include "openocpp/module/connector_status_module.h"
#include "openocpp/module/reset_module.h"
#include "openocpp/common/settings.h"
#include "openocpp/common/logging.h"
#include "openocpp/common/macro.h"
#include <thread>
#include <condition_variable>
#include <charconv>
namespace chargelab {
namespace detail {
template <typename HM>
struct FirmwareUpdateOperation {
ocpp2_0::UpdateFirmwareRequest request;
// for ocpp1.6 only
std::optional<ocpp1_6::FirmwareStatus> last_status1_6 = std::nullopt;
// L01.FR.04
std::unique_ptr<typename HM::hash_calculator_type> signature_hash = HM::createCalculator(HM::kHashTypeSHA256);
std::shared_ptr<RestConnectionInterface> connection = nullptr;
std::vector<uint8_t> buffer {};
std::size_t content_length = 0;
std::size_t total_bytes_read = 0;
std::size_t total_failures = 0;
std::vector<std::vector<uint8_t>> block_hashes {};
std::optional<std::vector<uint8_t>> expected_signature_hash = std::nullopt;
std::optional<ocpp2_0::FirmwareStatusEnumType> last_status2_0 = std::nullopt;
bool finished_signature_check = false;
bool finished_flashing_firmware = false;
bool running_firmware_update = false;
};
}
template <typename HM>
class FirmwareUpdateModule : public ServiceStatefulGeneral {
private:
static constexpr int kChunkSize = 20*1024;
static constexpr int kPriorityFirmwareStatusNotification = 100;
// Note: arbitrary random assigned ID
static constexpr std::uint64_t kOperationGroupId = 0x78106033AC8E780Aull;
public:
explicit FirmwareUpdateModule(
std::shared_ptr<PlatformInterface> platform,
std::shared_ptr<ResetModule> reset,
std::shared_ptr<PendingMessagesModule> pending_messages,
std::shared_ptr<ConnectorStatusModule> connector_status,
std::shared_ptr<StationInterface> station
)
: platform_(std::move(platform)),
reset_(std::move(reset)),
pending_messages_(std::move(pending_messages)),
connector_status_(std::move(connector_status)),
station_(std::move(station))
{
assert(platform_ != nullptr);
assert(station_ != nullptr);
settings_ = platform_->getSettings();
assert(settings_ != nullptr);
}
~FirmwareUpdateModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting FirmwareUpdateModule";
}
private:
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::UpdateFirmwareRsp>>
onUpdateFirmwareReq(const ocpp1_6::UpdateFirmwareReq &req) override {
if (operation_.has_value()) {
return ocpp1_6::CallError {
ocpp1_6::ErrorCode::kGenericError,
"InProgress",
common::RawJson::empty_object()
};
}
// Check if there is transaction in progress
bool found_charging_connector = false;
for (auto const& entry: station_->getConnectorMetadata()) {
if (entry.first.id == 0) continue;
auto const connector_status = station_->pollConnectorStatus(entry.first);
if (!connector_status.has_value())
continue;
if (connector_status->charging_enabled) {
found_charging_connector = true;
break;
}
}
if (found_charging_connector) {
return ocpp1_6::CallError {
ocpp1_6::ErrorCode::kGenericError,
"ActiveChargeSession",
common::RawJson::empty_object()
};
}
// Set all connectors to Unavailable
need_to_restore_connector_0_operative_ = connector_status_->setConnector0Inoperative(true);
operation_ = detail::FirmwareUpdateOperation<HM> {
ocpp2_0::UpdateFirmwareRequest {
req.retries,
req.retryInterval,
0,
ocpp2_0::FirmwareType {
req.location.value(),
optional::GetOrDefault(req.retrieveDate.getTimestamp(), platform_->systemClockNow())
}
}
};
return ocpp1_6::UpdateFirmwareRsp {};
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::UpdateFirmwareResponse>>
onUpdateFirmwareReq(const ocpp2_0::UpdateFirmwareRequest &request) override {
// Note: there doesn't appear to be any specific requirement here in the 2.0.1 specification and this is
// incompatible with a station simultaneously supporting L02...but this is required by TC_L_18_CS which is
// mandatory for core certification.
if (!request.firmware.signature.has_value()) {
return ocpp2_0::UpdateFirmwareResponse{
ocpp2_0::UpdateFirmwareStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {"MissingSignature"}
};
}
if (!request.firmware.signingCertificate.has_value()) {
return ocpp2_0::UpdateFirmwareResponse{
ocpp2_0::UpdateFirmwareStatusEnumType::kRejected,
ocpp2_0::StatusInfoType {"MissingSigningCert"}
};
}
// L01.FR.21
// L01.FR.22
if (request.firmware.signingCertificate.has_value()) {
if (!platform_->verifyManufacturerCertificate(request.firmware.signingCertificate->value(), std::nullopt)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad manufacturer certificate";
// L01.FR.02
pending_messages_->sendRequest2_0(
ocpp2_0::SecurityEventNotificationRequest {
"InvalidFirmwareSigningCertificate",
platform_->systemClockNow()
},
buildPendingMessagePolicy(PendingMessageType::kSecurityEvent)
);
return ocpp2_0::UpdateFirmwareResponse{ocpp2_0::UpdateFirmwareStatusEnumType::kInvalidCertificate};
}
}
// Note: this isn't strictly necessary, however passing a free-form text string to the platform URI parsing
// method seems potentially dangerous.
if (!uri::parseHttpUri(request.firmware.location.value()).has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad firmware location: " << request.firmware.location.value();
return ocpp2_0::UpdateFirmwareResponse{ocpp2_0::UpdateFirmwareStatusEnumType::kRejected};
}
auto status = ocpp2_0::UpdateFirmwareStatusEnumType::kAccepted;
if (operation_.has_value()) {
// L01.FR.24
operation_ = std::nullopt;
status = ocpp2_0::UpdateFirmwareStatusEnumType::kAcceptedCanceled;
}
operation_ = detail::FirmwareUpdateOperation<HM>{request};
return ocpp2_0::UpdateFirmwareResponse{status};
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::TriggerMessageRsp>>
onTriggerMessageReq(const ocpp1_6::TriggerMessageReq&) override {
return std::nullopt;
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::TriggerMessageResponse>>
onTriggerMessageReq(const ocpp2_0::TriggerMessageRequest &req) override {
if (req.requestedMessage != ocpp2_0::MessageTriggerEnumType::kFirmwareStatusNotification)
return std::nullopt;
if (req.evse.has_value()) {
force_update_ = false;
} else {
force_update_ = true;
}
return ocpp2_0::TriggerMessageResponse {ocpp2_0::TriggerMessageStatusEnumType::kAccepted};
}
void checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType status) {
if (operation_->last_status2_0 == std::make_optional(status))
return;
pending_messages_->sendRequest2_0(
ocpp2_0::FirmwareStatusNotificationRequest {
status,
operation_->request.requestId
},
buildPendingMessagePolicy(PendingMessageType::kNotificationEvent)
);
operation_->last_status2_0 = status;
}
void checkAndUpdateStatus(ocpp1_6::FirmwareStatus status) {
if (operation_->last_status1_6 == std::make_optional(status))
return;
pending_messages_->sendRequest1_6(
ocpp1_6::FirmwareStatusNotificationReq {
status
},
buildPendingMessagePolicy(PendingMessageType::kNotificationEvent)
);
operation_->last_status1_6 = status;
}
static bool checkOrRetryConnection(
std::shared_ptr<PlatformInterface> const& platform,
detail::FirmwareUpdateOperation<HM>& operation
) {
if (operation.connection != nullptr)
return true;
auto const uri = operation.request.firmware.location.value();
CHARGELAB_LOG_MESSAGE(info) << "Downloading firmware from: " << uri;
operation.connection = platform->getRequest(uri);
if (operation.connection == nullptr) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed establishing connection to: " << uri;
operation.total_failures++;
return false;
}
if (!operation.connection->open(0)) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed opening connection to server";
operation.total_failures++;
operation.connection = nullptr;
return false;
}
if (!operation.connection->send()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed sending request";
operation.total_failures++;
operation.connection = nullptr;
return false;
}
auto const status = operation.connection->getStatusCode();
if (!(status >= 200 && status < 300)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad status code - expecting 2xx response: " << status;
operation.total_failures++;
operation.connection = nullptr;
return false;
}
operation.content_length = operation.connection->getContentLength();
operation.total_bytes_read = 0;
return true;
}
void runStep(ocpp1_6::OcppRemote &) override {
performBootChecks1_6();
performFirmwareUpdate1_6();
}
void runStep(ocpp2_0::OcppRemote& remote) override {
performBootChecks2_0();
performFirmwareUpdate2_0();
if (force_update_.has_value()) {
if (!force_update_.value()) {
// Note - doesn't seem to be defined by 2.0.1 requirements, following TC_F_17_CS
auto const result = remote.sendCall(ocpp2_0::FirmwareStatusNotificationRequest {
ocpp2_0::FirmwareStatusEnumType::kIdle
});
if (result.has_value())
force_update_ = std::nullopt;
} else {
if (operation_.has_value() && operation_->last_status2_0.has_value()) {
auto const result = remote.sendCall(ocpp2_0::FirmwareStatusNotificationRequest {
operation_->last_status2_0.value(),
operation_->request.requestId
});
if (result.has_value())
force_update_ = std::nullopt;
} else {
// F06.FR.16
auto const result = remote.sendCall(ocpp2_0::FirmwareStatusNotificationRequest {
ocpp2_0::FirmwareStatusEnumType::kIdle
});
if (result.has_value())
force_update_ = std::nullopt;
}
}
}
}
void performBootChecks2_0() {
if (performed_boot_checks_)
return;
auto const active = station_->getActiveSlotId();
settings_->ActiveFirmwareSlotId.setValue(active);
auto const update = settings_->ExpectedUpdateFirmwareSlotId.getValue();
if (!update.empty()) {
int request_id;
std::string slot_id;
parseExpectedUpdateFirmwareSlotId(update, request_id, slot_id);
pending_messages_->sendRequest2_0(
ocpp2_0::FirmwareStatusNotificationRequest {
slot_id == active ?
ocpp2_0::FirmwareStatusEnumType::kInstalled :
ocpp2_0::FirmwareStatusEnumType::kInstallationFailed,
request_id
},
buildPendingMessagePolicy(PendingMessageType::kNotificationEvent)
);
pending_messages_->sendRequest2_0(
ocpp2_0::SecurityEventNotificationRequest{
"FirmwareUpdated",
platform_->systemClockNow()
},
buildPendingMessagePolicy(PendingMessageType::kSecurityEvent)
);
settings_->ExpectedUpdateFirmwareSlotId.setValue("");
}
performed_boot_checks_ = true;
}
void performFirmwareUpdate2_0() {
if (!operation_.has_value())
return;
auto const max_retries = optional::GetOrDefault(
operation_->request.retries,
settings_->FirmwareUpdateDefaultRetries.getValue()
);
if (operation_->request.firmware.retrieveDateTime.isAfter(platform_->systemClockNow(), false)) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kDownloadScheduled);
return;
}
if (!operation_->finished_signature_check) {
if ((int)operation_->total_failures > max_retries) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kDownloadFailed);
operation_ = std::nullopt;
return;
}
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kDownloading);
if (!checkOrRetryConnection(platform_, operation_.value()))
return;
auto const remaining = (int)operation_->content_length - (int)operation_->total_bytes_read;
if (remaining > 0) {
if (operation_->total_bytes_read == 0)
operation_->signature_hash->reset();
operation_->buffer.resize(kChunkSize);
auto const bytes_read = operation_->connection->read((char*)operation_->buffer.data(), (int)operation_->buffer.size());
// Note: treating zero bytes read as a failure condition here to prevent an infinite loop under
// those conditions.
if (bytes_read <= 0) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed reading data - retrying operation";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
// Block hash
auto hash = HM::calculateHashBinary(
HM::kHashTypeSHA256,
(unsigned char const*)operation_->buffer.data(),
bytes_read
);
if (!hash.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed hashing block";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
operation_->block_hashes.push_back(std::move(hash.value()));
operation_->signature_hash->update((unsigned char const*)operation_->buffer.data(), bytes_read);
operation_->total_bytes_read += bytes_read;
CHARGELAB_LOG_MESSAGE(debug) << "Hashing progress: " << operation_->total_bytes_read << " / " << operation_->content_length;
}
if (operation_->total_bytes_read >= operation_->content_length) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kDownloaded);
auto signature_hash = operation_->signature_hash->finishBinary();
if (!signature_hash.has_value()) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kInvalidSignature);
// L01.FR.03
pending_messages_->sendRequest2_0(
ocpp2_0::SecurityEventNotificationRequest {
"InvalidFirmwareSignature",
platform_->systemClockNow()
},
buildPendingMessagePolicy(PendingMessageType::kSecurityEvent)
);
operation_ = std::nullopt;
return;
}
auto const& firmware = operation_->request.firmware;
if (firmware.signingCertificate.has_value() && firmware.signature.has_value()) {
auto signature_binary = HM::decodeBase64(firmware.signature->value());
if (!signature_binary.has_value()) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kInvalidSignature);
// L01.FR.03
pending_messages_->sendRequest2_0(
ocpp2_0::SecurityEventNotificationRequest {
"InvalidFirmwareSignature",
platform_->systemClockNow()
},
buildPendingMessagePolicy(PendingMessageType::kSecurityEvent)
);
operation_ = std::nullopt;
return;
}
SignatureAndHash signature_and_hash {};
signature_and_hash.sig = signature_binary->data();
signature_and_hash.sig_len = signature_binary->size();
signature_and_hash.hash = signature_hash->data();
signature_and_hash.hash_len = signature_hash->size();
auto const valid = platform_->verifyManufacturerCertificate(
firmware.signingCertificate->value(),
signature_and_hash
);
if (!valid) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kInvalidSignature);
// L01.FR.03
pending_messages_->sendRequest2_0(
ocpp2_0::SecurityEventNotificationRequest {
"InvalidFirmwareSignature",
platform_->systemClockNow()
},
buildPendingMessagePolicy(PendingMessageType::kSecurityEvent)
);
operation_ = std::nullopt;
return;
}
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kSignatureVerified);
}
std::size_t total_block_hash_size = 0;
for (auto const& x : operation_->block_hashes)
total_block_hash_size += x.size();
CHARGELAB_LOG_MESSAGE(debug) << "Total block hash size (" << operation_->block_hashes.size() << " blocks): " << total_block_hash_size;
operation_->expected_signature_hash = std::move(signature_hash);
operation_->finished_signature_check = true;
operation_->connection = nullptr;
}
return;
}
if (!operation_->finished_flashing_firmware) {
// L01.FR.16
if (operation_->request.firmware.installDateTime.has_value()) {
if (operation_->request.firmware.installDateTime->isAfter(platform_->systemClockNow(), false)) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kInstallScheduled);
return;
}
}
if ((int)operation_->total_failures > max_retries) {
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kInstallationFailed);
operation_ = std::nullopt;
return;
}
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kInstalling);
if (!checkOrRetryConnection(platform_, operation_.value()))
return;
auto const remaining = (int)operation_->content_length - (int)operation_->total_bytes_read;
if (remaining > 0) {
operation_->buffer.resize(kChunkSize);
auto const bytes_read = operation_->connection->read((char*)operation_->buffer.data(), (int)operation_->buffer.size());
// Note: treating zero bytes read as a failure condition here to prevent an infinite loop under
// those conditions.
if (bytes_read <= 0) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed reading data - retrying operation";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
if (operation_->total_bytes_read == 0) {
if (operation_->running_firmware_update) {
station_->finishUpdateProcess(false);
operation_->running_firmware_update = false;
}
if (station_->startUpdateProcess(operation_->content_length) != StationInterface::Result::kSucceeded) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed starting firmware update process";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
operation_->running_firmware_update = true;
} else {
if (!operation_->running_firmware_update) {
CHARGELAB_LOG_MESSAGE(error)
<< "Unexpected state - expected running firmware update operation";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
}
if (station_->processFirmwareChunk(operation_->buffer.data(), bytes_read) != StationInterface::Result::kSucceeded) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed processing firmware chunk";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
operation_->signature_hash->update((unsigned char const*)operation_->buffer.data(), bytes_read);
operation_->total_bytes_read += bytes_read;
CHARGELAB_LOG_MESSAGE(debug) << "Flashing progress: " << operation_->total_bytes_read << " / " << operation_->content_length;
}
if (operation_->total_bytes_read >= operation_->content_length) {
// TODO: Check signature
auto const slot_id = station_->getUpdateSlotId();
if (station_->finishUpdateProcess(true) != StationInterface::Result::kSucceeded) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed finishing update process";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
operation_->finished_flashing_firmware = true;
operation_->connection = nullptr;
// settings_->ExpectedUpdateFirmwareSlotId.setValueFromString(std::to_string(operation_->request.requestId) + ":" + slot_id);
settings_->ExpectedUpdateFirmwareSlotId.setValueFromString(buildExpectedUpdateFirmwareSlotId(
operation_->request.requestId, slot_id));
checkAndUpdateStatus(ocpp2_0::FirmwareStatusEnumType::kInstallRebooting);
reset_->resetOnIdle(ocpp2_0::BootReasonEnumType::kFirmwareUpdate);
}
return;
}
}
void performBootChecks1_6() {
if (performed_boot_checks_)
return;
auto const active = station_->getActiveSlotId();
settings_->ActiveFirmwareSlotId.setValue(active);
auto const update = settings_->ExpectedUpdateFirmwareSlotId.getValue();
if (!update.empty()) {
int request_id;
std::string slot_id;
parseExpectedUpdateFirmwareSlotId(update, request_id, slot_id);
pending_messages_->sendRequest1_6(
ocpp1_6::FirmwareStatusNotificationReq {
slot_id == active ?
ocpp1_6::FirmwareStatus::kInstalled :
ocpp1_6::FirmwareStatus::kInstallationFailed
},
buildPendingMessagePolicy(PendingMessageType::kNotificationEvent)
);
settings_->ExpectedUpdateFirmwareSlotId.setValue("");
}
performed_boot_checks_ = true;
}
void performFirmwareUpdate1_6() {
if (!operation_.has_value())
return;
auto const max_retries = optional::GetOrDefault(
operation_->request.retries,
settings_->FirmwareUpdateDefaultRetries.getValue()
);
if ((int)operation_->total_failures > max_retries) {
if (operation_->content_length == 0 || operation_->content_length > operation_->total_bytes_read) {
checkAndUpdateStatus(ocpp1_6::FirmwareStatus::kDownloadFailed);
} else {
checkAndUpdateStatus(ocpp1_6::FirmwareStatus::kInstallationFailed);
}
operation_ = std::nullopt;
restoreConnector0OperativeIfNeeded();
return;
}
if (!operation_->last_status1_6.has_value()) {
checkAndUpdateStatus(ocpp1_6::FirmwareStatus::kDownloading);
} else {
if (operation_->last_status1_6 == ocpp1_6::FirmwareStatus::kDownloaded) {
checkAndUpdateStatus(ocpp1_6::FirmwareStatus::kInstalling);
}
}
if (!checkOrRetryConnection(platform_, operation_.value()))
return;
auto const remaining = (int)operation_->content_length - (int)operation_->total_bytes_read;
if (remaining > 0) {
operation_->buffer.resize(kChunkSize);
auto const bytes_read = operation_->connection->read((char*)operation_->buffer.data(), (int)operation_->buffer.size());
// Note: treating zero bytes read as a failure condition here to prevent an infinite loop under
// those conditions.
if (bytes_read <= 0) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed reading data - retrying operation";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
if (operation_->total_bytes_read == 0) {
if (operation_->running_firmware_update) {
station_->finishUpdateProcess(false);
operation_->running_firmware_update = false;
}
if (station_->startUpdateProcess(operation_->content_length) != StationInterface::Result::kSucceeded) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed starting firmware update process";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
operation_->running_firmware_update = true;
} else {
if (!operation_->running_firmware_update) {
CHARGELAB_LOG_MESSAGE(error)
<< "Unexpected state - expected running firmware update operation";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
}
if (station_->processFirmwareChunk(operation_->buffer.data(), bytes_read) != StationInterface::Result::kSucceeded) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed processing firmware chunk";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
// TODO: For ocpp1.6, the existing approach is to pre-set the hash value to the setting parameter and
// read the firmware hash value from the firmware itself. Then compare them to decide if the downloaded firmware is the right one.
// What approach is expected here?
operation_->signature_hash->update((unsigned char const*)operation_->buffer.data(), bytes_read);
operation_->total_bytes_read += bytes_read;
CHARGELAB_LOG_MESSAGE(debug) << "Flashing progress: " << operation_->total_bytes_read << " / " << operation_->content_length;
}
if (operation_->total_bytes_read >= operation_->content_length) {
// TODO: Check signature
if (operation_->last_status1_6 == ocpp1_6::FirmwareStatus::kDownloading) {
checkAndUpdateStatus(ocpp1_6::FirmwareStatus::kDownloaded);
return;
} else if (operation_->last_status1_6 == ocpp1_6::FirmwareStatus::kDownloaded) {
checkAndUpdateStatus(ocpp1_6::FirmwareStatus::kInstalling);
return;
}
auto const slot_id = station_->getUpdateSlotId();
if (station_->finishUpdateProcess(true) != StationInterface::Result::kSucceeded) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed finishing update process";
operation_->connection = nullptr;
operation_->total_failures++;
return;
}
//settings_->ExpectedUpdateFirmwareSlotId.setValueFromString(std::to_string(operation_->request.requestId) + ":" + slot_id);
settings_->ExpectedUpdateFirmwareSlotId.setValueFromString(buildExpectedUpdateFirmwareSlotId(
operation_->request.requestId, slot_id));
operation_ = std::nullopt;
restoreConnector0OperativeIfNeeded();
reset_->resetOnIdle(ocpp2_0::BootReasonEnumType::kFirmwareUpdate);
}
}
void restoreConnector0OperativeIfNeeded() {
if (need_to_restore_connector_0_operative_) {
connector_status_->setConnector0Inoperative(false);
need_to_restore_connector_0_operative_ = false;
}
}
static std::string buildExpectedUpdateFirmwareSlotId(int request_id, std::string const& slot_id) {
return std::to_string(request_id) + ":" + slot_id;
}
static void parseExpectedUpdateFirmwareSlotId(std::string const& expectedUpdateFirmwareSlotId,
int& request_id, std::string& slot_id) {
auto index = expectedUpdateFirmwareSlotId.find(':');
if (index != std::string::npos) {
request_id = optional::GetOrDefault(string::ToInteger(expectedUpdateFirmwareSlotId.substr(0, index)), 0);
slot_id = expectedUpdateFirmwareSlotId.substr(index+1);
} else {
request_id = 0;
slot_id = expectedUpdateFirmwareSlotId;
}
}
PendingMessagePolicy buildPendingMessagePolicy(PendingMessageType message_type) {
return PendingMessagePolicy {
message_type,
kOperationGroupId,
settings_->NotificationMessageDefaultRetries.getValue(),
settings_->NotificationMessageDefaultRetryInterval.getValue(),
kPriorityFirmwareStatusNotification,
false,
false
};
}
private:
std::shared_ptr<PlatformInterface> platform_;
std::shared_ptr<ResetModule> reset_;
std::shared_ptr<PendingMessagesModule> pending_messages_;
std::shared_ptr<ConnectorStatusModule> connector_status_; // TODO: Remove dependency on ConnectorStatusModule
std::shared_ptr<StationInterface> station_;
std::shared_ptr<Settings> settings_;
/**
* Set to true to force an OCPP 2.0.1 or 1.6 firmware status update and false to force an "idle" firmware status
* update (only applies when an associated trigger message is received with an EVSE filter).
*/
std::optional<bool> force_update_ = std::nullopt;
std::optional<detail::FirmwareUpdateOperation<HM>> operation_ = std::nullopt;
bool performed_boot_checks_ = false;
bool need_to_restore_connector_0_operative_ { false };
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_FIRMWARE_UPDATE_MODULE_H

View File

@@ -0,0 +1,288 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_GET_DIAGNOSTICS_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_GET_DIAGNOSTICS_MODULE_H
#include "openocpp/module/common_templates.h"
#include "openocpp/interface/component/system_interface.h"
#include "log_streaming_module.h"
#include "openocpp/common/ring_buffer.h"
#include "openocpp/common/operation_holder.h"
#include "openocpp/interface/component/upload_interface.h"
#include "openocpp/helpers/chrono.h"
#include "openocpp/common/macro.h"
#include <queue>
#include <thread>
#include <sstream>
#include <ctime>
#include <utility>
namespace chargelab {
namespace detail {
struct DiagnosticsUploadState {
explicit DiagnosticsUploadState(std::shared_ptr<SystemInterface> const& system)
: pending_notification {system} {
}
OperationHolder<std::string> pending_notification;
std::string location {};
int failed_attempts = 0;
int total_retries = 0;
int retry_interval_seconds = 0;
std::atomic<DiagnosticsStatus> operation_state = DiagnosticsStatus::kPending;
std::atomic<DiagnosticsStatus> messaging_state = DiagnosticsStatus::kPending;
std::optional<std::thread> operation = std::nullopt;
std::optional<std::vector<uint8_t>> current_upload = std::nullopt;
std::queue<detail::DiagnosticsLine> pending_upload;
};
}
class GetDiagnosticsModule : public ServiceStatefulGeneral {
private:
static constexpr int kBufferLimitBytes = 3*1024; // 10*1024;
static constexpr int kRingBufferMaxSize = 25; // 50;
// for diagnostic uploading
static constexpr int kDefaultUploadRetries = 1;
static constexpr int kDefaultUploadRetryIntervalSeconds = 10;
static constexpr int kMaxBytesPerUpload = 3*1024; // 5*1024;
public:
explicit GetDiagnosticsModule(
Settings& settings,
std::shared_ptr<SystemInterface> system_interface,
std::shared_ptr<UploadInterface> upload
) :
system_interface_(std::move(system_interface)),
upload_(std::move(upload)),
charge_point_id_ {settings.getSetting(settings::CommonKey::kChargePointId)}
{
listener_ = std::make_shared<LoggingListenerFunction>([&](LogMetadata const& metadata, std::string_view const& message) {
if (metadata.level != LogLevel::warning && metadata.level != LogLevel::error && metadata.level != LogLevel::fatal)
return;
detail::DiagnosticsLine line {};
line.level = metadata.level;
#if defined(LOG_WITH_FILE_AND_LINE)
line.file = metadata.file;
line.line = metadata.line;
#endif
line.function = metadata.function;
line.message = message;
line.timestamp = system_interface_->systemClockNow();
std::lock_guard lock {mutex_};
int total_size = history_byte_size_ + line.size();
while (total_size > kBufferLimitBytes && !history_.empty()) {
total_size -= history_.front().size();
history_.popFront();
}
if (total_size < kBufferLimitBytes) {
history_byte_size_ = total_size;
history_.pushBack(std::move(line));
} else {
history_byte_size_ = 0;
}
});
RegisterLoggingListener(listener_); // register this callback to the global listener
}
~GetDiagnosticsModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting GetDiagnosticsModule";
UnregisterLoggingListener(listener_);
}
private:
void runStep(ocpp1_6::ChargePointRemoteInterface &remote) override {
if (!in_progress_.has_value())
return;
auto &state = in_progress_.value();
detail::DiagnosticsStatus const operation_state = state.operation_state;
detail::DiagnosticsStatus const messaging_state = state.messaging_state;
if (operation_state == messaging_state)
return;
ocpp1_6::DiagnosticsStatus status;
switch (operation_state) {
default:
assert(false && "Unexpected diagnostics status");
case detail::DiagnosticsStatus::kPending:
case detail::DiagnosticsStatus::kUploading:
status = ocpp1_6::DiagnosticsStatus::kUploading;
break;
case detail::DiagnosticsStatus::kUploaded:
status = ocpp1_6::DiagnosticsStatus::kUploaded;
state.operation->join();
in_progress_ = std::nullopt;
break;
case detail::DiagnosticsStatus::kUploadFailed:
status = ocpp1_6::DiagnosticsStatus::kUploadFailed;
state.operation->join();
in_progress_ = std::nullopt;
break;
}
state.messaging_state = operation_state;
state.pending_notification.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
remote.sendDiagnosticsStatusNotificationReq({status})
);
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::GetDiagnosticsRsp>>
onGetDiagnosticsReq(
const ocpp1_6::GetDiagnosticsReq& req
) override {
if (in_progress_.has_value()) {
return ocpp1_6::CallError {
ocpp1_6::ErrorCode::kGenericError,
"InProgress",
"An uploading diagnostics operation is already in progress"
};
}
ocpp1_6::GetDiagnosticsRsp rsp;
std::queue<detail::DiagnosticsLine> pending_upload;
{
std::lock_guard lock{mutex_};
int size = history_.size();
for (int i = 0; i < size; ++i) {
if (req.startTime.has_value() && history_[i].timestamp < req.startTime->getTimestamp())
continue;
if (req.stopTime.has_value() && history_[i].timestamp > req.stopTime->getTimestamp())
continue;
pending_upload.push(history_[i]);
}
}
if (pending_upload.empty()) {
rsp.fileName = std::nullopt;
return rsp;
}
in_progress_.emplace(system_interface_);
in_progress_->pending_upload = std::move(pending_upload);
rsp.fileName = buildDiagnosticsFileName();
auto &state = in_progress_.value();
// Trim leading and trailing whitespaces
std::string location = req.location.value();
auto trim = [](const std::string &str, const std::string &whitespaces = " \t") -> std::string {
auto begin = str.find_first_not_of(whitespaces);
if (begin == std::string::npos) return "";
auto end = str.find_last_not_of(whitespaces);
return str.substr(begin, end - begin + 1);
};
location = trim(location);
if (!location.empty() && location.back() != '/')
state.location = location + "/" + rsp.fileName.value();
else
state.location = location + rsp.fileName.value();
state.total_retries = optional::GetOrDefault(req.retries, kDefaultUploadRetries);
state.retry_interval_seconds = optional::GetOrDefault(req.retryInterval,
kDefaultUploadRetryIntervalSeconds);
state.operation = std::thread([&]() {
using namespace std::chrono_literals;
CHARGELAB_TRY {
CHARGELAB_LOG_MESSAGE(info) << "Starting diagnostics upload: " << state.location;
state.operation_state = detail::DiagnosticsStatus::kUploading;
bool append = false;
while (!state.pending_upload.empty() || state.current_upload.has_value()) {
if (!state.current_upload.has_value()) {
std::string content;
size_t content_len = 0;
while (!state.pending_upload.empty()) {
std::string s = state.pending_upload.front().to_string();
if (content_len + s.size() > kMaxBytesPerUpload)
break;
content.append(s);
content_len += s.size();
state.pending_upload.pop();
}
state.current_upload.emplace(std::vector<uint8_t>(content.begin(), content.end()));
}
// uploading
std::vector<uint8_t> const &content = state.current_upload.value();
auto result = upload_->write(state.location, content, append, [&](std::size_t) {});
if (result == UploadInterface::Result::kFailed) {
if (++state.failed_attempts > state.total_retries) {
CHARGELAB_LOG_MESSAGE(warning) << "Diagnostics upload failed writing";
state.operation_state = detail::DiagnosticsStatus::kUploadFailed;
return;
}
std::this_thread::sleep_for(std::chrono::seconds(state.retry_interval_seconds));
} else {
state.current_upload = std::nullopt;
append = true; // appending for next block
}
}
CHARGELAB_LOG_MESSAGE(info) << "Diagnostics upload succeeded";
state.operation_state = detail::DiagnosticsStatus::kUploaded;
} CHARGELAB_CATCH {
CHARGELAB_LOG_MESSAGE(error) << "Diagnostics upload failed unexpectedly: " << e.what();
state.operation_state = detail::DiagnosticsStatus::kUploadFailed;
}
});
return rsp;
}
void onDiagnosticsStatusNotificationRsp(
std::string const& unique_id,
ocpp1_6::ResponseMessage<::chargelab::ocpp1_6::DiagnosticsStatusNotificationRsp> const& rsp
) override {
if (in_progress_.has_value()) {
auto& state = in_progress_.value();
if (state.pending_notification == unique_id) {
state.pending_notification = kNoOperation;
if (std::holds_alternative<ocpp1_6::CallError>(rsp)) {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to FirmwareStatusNotification request: " << std::get<ocpp1_6::CallError> (rsp);
}
}
}
}
private:
std::string buildDiagnosticsFileName() {
std::time_t t = system_interface_->systemClockNow()/1000;
std::tm *tm = gmtime(&t);
char buf[100];
sprintf(buf, "%d-%02d-%02dT%02d-%02d-%02dZ", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
std::string file_name = "diagn_" + charge_point_id_.getValue() + "_" + std::string(buf) + ".txt";
return file_name;
}
private:
std::shared_ptr<SystemInterface> system_interface_;
std::shared_ptr<LoggingListenerFunction> listener_ = nullptr;
std::mutex mutex_;
RingBuffer<detail::DiagnosticsLine, kRingBufferMaxSize> history_; // keep the original diagnostics lines
int history_byte_size_ = 0;
std::shared_ptr<UploadInterface> upload_;
std::optional<detail::DiagnosticsUploadState> in_progress_ = std::nullopt;
BasicTextSetting charge_point_id_;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_GET_DIAGNOSTICS_MODULE_H

View File

@@ -0,0 +1,492 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_GET_LOGS_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_GET_LOGS_MODULE_H
#include "openocpp/module/common_templates.h"
#include "openocpp/module/pending_messages_module.h"
#include "openocpp/protocol/common/small_string.h"
#include "openocpp/interface/element/rest_connection_interface.h"
#include "openocpp/interface/platform_interface.h"
#include "openocpp/common/ring_buffer.h"
#include "openocpp/common/serialization.h"
namespace chargelab {
namespace detail {
struct UploadState {
ocpp2_0::GetLogRequest request;
std::string filename;
std::shared_ptr<RestConnectionInterface> connection = nullptr;
int total_failures = 0;
std::size_t bytes_written = 0;
std::size_t content_length = 0;
int min_index = 0;
int max_index = 0;
int last_index = 0;
std::optional<ocpp2_0::UploadLogStatusEnumType> last_status2_0 = std::nullopt;
std::optional<SteadyPointMillis> first_attempt = std::nullopt;
};
struct LogLine {
int message_index;
SystemTimeMillis timestamp;
logging::LogLevel level;
std::string message;
};
class LogLineSerializer {
public:
static std::optional<LogLine> read(std::string_view const& text) {
LogLine result;
std::optional<int> index = 0;
index = readPrimitive(text, index, result.message_index);
index = readPrimitive(text, index, result.timestamp);
index = readPrimitive(text, index, result.level);
index = readPrimitive(text, index, result.message);
if (!index.has_value())
return std::nullopt;
return result;
}
static std::string write(LogLine const& wrapper) {
std::string result;
writePrimitive(result, wrapper.message_index);
writePrimitive(result, wrapper.timestamp);
writePrimitive(result, wrapper.level);
writePrimitive(result, wrapper.message);
return result;
}
};
}
class GetLogsModule : public ServiceStatefulGeneral {
private:
// Note: this is the decompressed size of the dropped records
static constexpr int kTargetDroppedLogMessageBytes = 1024;
static constexpr int kMaxRetainedLogMessagesBytes = 2*1024;
static constexpr int kUploadStepMaxTimeMillis = 100;
static constexpr int kPriorityLogStatusNotification = 100;
static constexpr int kQueueReportFrequencySeconds = 10;
static constexpr int kMaxRingBufferSize = 5;
// Note: arbitrary random assigned ID
static constexpr std::uint64_t kOperationGroupId = 0xDB0A571F9D6E2A10ull;
public:
explicit GetLogsModule(
std::shared_ptr<PlatformInterface> platform,
std::shared_ptr<PendingMessagesModule> pending_messages
)
: platform_(std::move(platform)),
pending_messages_(std::move(pending_messages))
{
assert(platform_ != nullptr);
settings_ = platform_->getSettings();
assert(settings_ != nullptr);
listener_ = std::make_shared<logging::LoggingListenerFunction>([&](logging::LogMetadata const& metadata, std::string_view const& message) {
std::string merged_message;
#if defined(LOG_WITH_FILE_AND_LINE)
merged_message += "[";
merged_message += metadata.file;
merged_message += ":";
merged_message += std::to_string(metadata.line);
merged_message += "]";
#endif
merged_message += message;
log_buffer_.pushBack(detail::LogLine{
index_++,
platform_->systemClockNow(),
metadata.level,
std::move(merged_message)
});
});
logging::RegisterLoggingListener(listener_); // register this callback to the global listener
CHARGELAB_LOG_MESSAGE(info) << "Size of platform pointer: " << sizeof(platform_);
CHARGELAB_LOG_MESSAGE(info) << "Size of operation: " << sizeof(operation_);
CHARGELAB_LOG_MESSAGE(info) << "Size of listener: " << sizeof(*listener_);
CHARGELAB_LOG_MESSAGE(info) << "Size of queue: " << sizeof(log_queue_);
CHARGELAB_LOG_MESSAGE(info) << "Size of buffer: " << sizeof(log_buffer_);
}
private:
void runUnconditionally() override {
reportQueueSize();
uploadLogs();
flushLogMessages();
}
void flushLogMessages() {
for (int i=0; i < 10; i++) {
auto next = log_buffer_.popFront();
if (!next.has_value())
break;
log_queue_.pushBack(std::move(next.value()));
}
while (log_queue_.totalBytes() > kMaxRetainedLogMessagesBytes) {
std::size_t total_removed = 0;
log_queue_.removeIf([&] (std::string_view const& text, detail::LogLine const&) {
if (total_removed > kTargetDroppedLogMessageBytes)
return false;
total_removed += text.size();
return true;
});
}
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::GetLogResponse>>
onGetLogReq(const ocpp2_0::GetLogRequest& request) override {
if (!uri::parseHttpUri(request.log.remoteLocation.value()).has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad remoteLocation: " << request.log.remoteLocation.value();
return ocpp2_0::GetLogResponse {ocpp2_0::LogStatusEnumType::kRejected};
}
std::string filename = settings_->ChargerSerialNumber.getValue();
switch (request.logType) {
case ocpp2_0::LogEnumType::kValueNotFoundInEnum:
CHARGELAB_LOG_MESSAGE(warning) << "Bad logType in request";
return ocpp2_0::GetLogResponse {ocpp2_0::LogStatusEnumType::kRejected};
case ocpp2_0::LogEnumType::kDiagnosticsLog:
filename += "-diagnostics-";
break;
case ocpp2_0::LogEnumType::kSecurityLog:
filename += "-security-";
break;
}
filename += std::to_string(platform_->systemClockNow()/1000) + ".txt";
ocpp2_0::LogStatusEnumType status;
if (!operation_.has_value()) {
// N01.FR.01
status = ocpp2_0::LogStatusEnumType::kAccepted;
} else {
// N01.FR.12
status = ocpp2_0::LogStatusEnumType::kAcceptedCanceled;
// N01.FR.20
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kAcceptedCanceled);
}
operation_ = detail::UploadState {request, filename};
return ocpp2_0::GetLogResponse {status, filename};
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::TriggerMessageRsp>>
onTriggerMessageReq(const ocpp1_6::TriggerMessageReq &req) override {
(void)req;
return std::nullopt;
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::TriggerMessageResponse>>
onTriggerMessageReq(const ocpp2_0::TriggerMessageRequest &req) override {
if (req.requestedMessage != ocpp2_0::MessageTriggerEnumType::kLogStatusNotification)
return std::nullopt;
if (req.evse.has_value() || !operation_.has_value()) {
pending_messages_->sendRequest2_0(
ocpp2_0::LogStatusNotificationRequest {
ocpp2_0::UploadLogStatusEnumType::kIdle
},
PendingMessagePolicy {
PendingMessageType::kNotificationEvent,
kOperationGroupId,
settings_->NotificationMessageDefaultRetries.getValue(),
settings_->NotificationMessageDefaultRetryInterval.getValue(),
kPriorityLogStatusNotification,
false,
false
}
);
} else {
operation_->last_status2_0 = std::nullopt;
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploading);
}
return ocpp2_0::TriggerMessageResponse {ocpp2_0::TriggerMessageStatusEnumType::kAccepted};
}
private:
void reportQueueSize() {
auto const now = platform_->steadyClockNow();
if (last_queue_size_report_.has_value()) {
auto const delta = now - last_queue_size_report_.value();
if (delta < kQueueReportFrequencySeconds*1000)
return;
}
last_queue_size_report_ = now;
CHARGELAB_LOG_MESSAGE(info) << "Total log queue size: " << log_queue_.totalBytes();
}
void uploadLogs() {
if (!operation_.has_value())
return;
if ((int)operation_->total_failures > optional::GetOrDefault(operation_->request.retries, 0)) {
// N01.FR.10
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploadFailure);
operation_ = std::nullopt;
return;
}
if (!checkOrRetryConnection())
return;
if (!operation_->first_attempt.has_value())
operation_->first_attempt = platform_->steadyClockNow();
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploading);
bool wrote_first = false;
bool ran_out_of_time = false;
auto const start_ts = platform_->steadyClockNow();
{
bool only_security_logs = operation_->request.logType == ocpp2_0::LogEnumType::kSecurityLog;
log_queue_.visit([&] (std::string_view const&, detail::LogLine const& line) {
if (ran_out_of_time)
return;
if (operation_->connection == nullptr)
return;
if (operation_->bytes_written >= operation_->content_length)
return;
if (wrote_first) {
if (platform_->steadyClockNow() - start_ts > kUploadStepMaxTimeMillis) {
ran_out_of_time = true;
return;
}
}
auto const& log = operation_->request.log;
if (log.oldestTimestamp.has_value() && log.oldestTimestamp->isAfter(line.timestamp, false))
return;
if (log.latestTimestamp.has_value() && log.latestTimestamp->isBefore(line.timestamp, false))
return;
if (only_security_logs && line.message.find("[security]") == std::string::npos)
return;
// Note: allowing for integer overflow; checking if message_index < min_index
if (line.message_index - operation_->min_index < 0)
return;
// Note: allowing for integer overflow; checking if message_index > max_index
if (line.message_index - operation_->max_index > 0)
return;
// Note: allowing for integer overflow; checking if message_index <= last_index
if (line.message_index - operation_->last_index <= 0)
return;
// Truncate the message to fit in the original content_length
auto message = renderLine(line);
if (operation_->bytes_written + message.size() > operation_->content_length) {
auto const truncated = operation_->bytes_written + message.size() - operation_->content_length;
message.resize(std::min(std::max((std::size_t)0, truncated), message.size()));
}
if (!operation_->connection->write(message.data(), message.size())) {
CHARGELAB_LOG_MESSAGE(info) << "Failed writing payload at " << operation_->bytes_written << " bytes";
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploadFailure);
operation_->total_failures++;
operation_->connection = nullptr;
return;
}
operation_->last_index = line.message_index;
operation_->bytes_written += message.size();
wrote_first = true;
});
}
if (ran_out_of_time)
return;
if (operation_->connection == nullptr)
return;
std::string buffer;
while (operation_->bytes_written < operation_->content_length) {
// Note: this can happen if log lines are removed while an upload operation is in progress
buffer.resize(std::min((std::size_t)1024, operation_->content_length - operation_->bytes_written), ' ');
if (!operation_->connection->write(buffer.data(), buffer.size())) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed writing padding to server";
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploadFailure);
operation_->total_failures++;
operation_->connection = nullptr;
return;
}
CHARGELAB_LOG_MESSAGE(warning) << "Added padding: " << buffer.size() << " bytes";
operation_->bytes_written += buffer.size();
}
if (!operation_->connection->send()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed finishing HTTP request";
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploadFailure);
operation_->total_failures++;
operation_->connection = nullptr;
return;
}
auto const status = operation_->connection->getStatusCode();
if (status == 403) {
CHARGELAB_LOG_MESSAGE(warning) << "Unauthorized response from server: " << status;
// N01.FR.10
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kPermissionDenied);
operation_->total_failures++;
operation_->connection = nullptr;
return;
}
if (status < 200 || status >= 300) {
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected status code: " << status;
// N01.FR.10
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kBadMessage);
operation_->total_failures++;
operation_->connection = nullptr;
return;
}
CHARGELAB_LOG_MESSAGE(warning) << "Upload succeeded - response was: " << status;
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploaded);
operation_ = std::nullopt;
}
static std::string renderLine(detail::LogLine const& line) {
std::string result;
result += "[" + write_json_to_string(ocpp2_0::DateTime{line.timestamp}) + "] ";
result += "[" + write_json_to_string(line.level) + "] ";
result += line.message;
result += "\n";
return result;
}
bool checkOrRetryConnection() {
if (!operation_.has_value())
return false;
if (operation_->connection != nullptr)
return true;
if (operation_->first_attempt.has_value()) {
auto const delta_seconds = (platform_->steadyClockNow() - operation_->first_attempt.value())/1000;
auto const interval = optional::GetOrDefault(operation_->request.retryInterval, 0);
if (delta_seconds < interval*operation_->total_failures)
return false;
}
auto const uri = operation_->request.log.remoteLocation.value();
CHARGELAB_LOG_MESSAGE(info) << "Uploading logs to: " << uri;
operation_->connection = platform_->putRequest(uri);
if (operation_->connection == nullptr) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed creating post request for: " << uri;
// N01.FR.10
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploadFailure);
operation_->total_failures++;
return false;
}
// Compute the size and index limits
std::optional<int> min_index = std::nullopt;
std::optional<int> max_index = std::nullopt;
std::size_t content_length = 0;
{
log_queue_.visit([&] (std::string_view const&, detail::LogLine const& line) {
auto const& log = operation_->request.log;
if (log.oldestTimestamp.has_value() && log.oldestTimestamp->isAfter(line.timestamp, false))
return;
if (log.latestTimestamp.has_value() && log.latestTimestamp->isBefore(line.timestamp, false))
return;
if (!min_index.has_value()) {
min_index = line.message_index;
} else {
// Note: allowing for integer overflow; checking if message_index < min_index
if (line.message_index - min_index.value() < 0)
min_index = line.message_index;
}
if (!max_index.has_value()) {
max_index = line.message_index;
} else {
// Note: allowing for integer overflow; checking if message_index > max_index
if (line.message_index - max_index.value() > 0)
max_index = line.message_index;
}
content_length += renderLine(line).size();
});
}
operation_->bytes_written = 0;
operation_->content_length = content_length;
operation_->min_index = optional::GetOrDefault(min_index, 0);
operation_->max_index = optional::GetOrDefault(max_index, 0);
operation_->last_index = optional::GetOrDefault(min_index, 0) - 1;
// N01.FR.19
operation_->connection->setHeader("Content-Type", "text/plain");
operation_->connection->setHeader("Content-Disposition", operation_->filename);
if (!operation_->connection->open(content_length)) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed opening connection to server";
checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType::kUploadFailure);
operation_->total_failures++;
operation_->connection = nullptr;
} else {
CHARGELAB_LOG_MESSAGE(info) << "Connection opened to server - uploading " << content_length << " bytes...";
}
return true;
}
void checkAndUpdateStatus(ocpp2_0::UploadLogStatusEnumType status) {
if (operation_->last_status2_0 == std::make_optional(status))
return;
pending_messages_->sendRequest2_0(
ocpp2_0::LogStatusNotificationRequest {
status,
operation_->request.requestId
},
PendingMessagePolicy {
PendingMessageType::kNotificationEvent,
kOperationGroupId,
settings_->NotificationMessageDefaultRetries.getValue(),
settings_->NotificationMessageDefaultRetryInterval.getValue(),
kPriorityLogStatusNotification,
false,
false
}
);
operation_->last_status2_0 = status;
}
private:
std::shared_ptr<PlatformInterface> platform_;
std::shared_ptr<PendingMessagesModule> pending_messages_;
std::shared_ptr<Settings> settings_;
std::shared_ptr<logging::LoggingListenerFunction> listener_;
std::optional<detail::UploadState> operation_ = std::nullopt;
std::optional<SteadyPointMillis> last_queue_size_report_ = std::nullopt;
RingBuffer<detail::LogLine, kMaxRingBufferSize> log_buffer_;
std::atomic<int> index_ = 0;
CompressedQueueCustom<detail::LogLine, detail::LogLineSerializer> log_queue_;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_GET_LOGS_MODULE_H

View File

@@ -0,0 +1,129 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_HEARTBEAT_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_HEARTBEAT_MODULE_H
#include "openocpp/module/common_templates.h"
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/common/operation_holder.h"
#include "openocpp/common/settings.h"
#include <utility>
namespace chargelab {
class HeartbeatModule : public ServiceStatefulGeneral {
public:
HeartbeatModule(
std::shared_ptr<Settings> settings,
std::shared_ptr<SystemInterface> const& system_interface
)
: settings_(std::move(settings)),
system_interface_(system_interface),
pending_heartbeat_req_ {system_interface}
{
}
~HeartbeatModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting HeartbeatModule";
}
public:
void runStep(ocpp1_6::OcppRemote &remote) override {
if (!pending_heartbeat_req_.wasIdleFor(settings_->HeartbeatInterval.getValue())) {
if (!force_heartbeat_ || pending_heartbeat_req_.operationInProgress())
return;
}
pending_heartbeat_req_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
remote.sendHeartbeatReq({})
);
force_heartbeat_ = false;
}
void onHeartbeatRsp(
const std::string &unique_id,
const std::variant<ocpp1_6::HeartbeatRsp, ocpp1_6::CallError> &rsp
) override {
if (pending_heartbeat_req_ == unique_id) {
pending_heartbeat_req_ = kNoOperation;
if (std::holds_alternative<ocpp1_6::HeartbeatRsp>(rsp)) {
auto const& value = std::get<ocpp1_6::HeartbeatRsp>(rsp);
auto const& ts = value.currentTime.getTimestamp();
if (ts.has_value()) {
system_interface_->setSystemClock(ts.value());
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Invalid Heartbeat response timestamp: "
<< value.currentTime;
}
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to Hearbeat request: " << std::get<ocpp1_6::CallError> (rsp);
}
}
}
void runStep(ocpp2_0::OcppRemote &remote) override {
if (!pending_heartbeat_req_.wasIdleFor(settings_->HeartbeatInterval.getValue())) {
if (!force_heartbeat_ || pending_heartbeat_req_.operationInProgress())
return;
}
pending_heartbeat_req_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
remote.sendHeartbeatReq({})
);
force_heartbeat_ = false;
}
void onHeartbeatRsp(
const std::string &unique_id,
const std::variant<ocpp2_0::HeartbeatResponse, ocpp2_0::CallError> &rsp
) override {
if (pending_heartbeat_req_ == unique_id) {
pending_heartbeat_req_ = kNoOperation;
if (std::holds_alternative<ocpp2_0::HeartbeatResponse>(rsp)) {
auto const& value = std::get<ocpp2_0::HeartbeatResponse>(rsp);
auto const& ts = value.currentTime.getTimestamp();
if (ts.has_value()) {
system_interface_->setSystemClock(ts.value());
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Invalid Heartbeat response timestamp: "
<< value.currentTime;
}
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to Hearbeat request: " << std::get<ocpp2_0::CallError> (rsp);
}
}
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::TriggerMessageRsp>>
onTriggerMessageReq(const ocpp1_6::TriggerMessageReq &req) override {
if (req.requestedMessage == ocpp1_6::MessageTrigger::kHeartbeat) {
force_heartbeat_ = true;
return ocpp1_6::TriggerMessageRsp {ocpp1_6::TriggerMessageStatus::kAccepted};
}
return std::nullopt;
}
std::optional<ocpp2_0::ResponseToRequest<ocpp2_0::TriggerMessageResponse>>
onTriggerMessageReq(const ocpp2_0::TriggerMessageRequest &request) override {
if (request.requestedMessage == ocpp2_0::MessageTriggerEnumType::kHeartbeat) {
force_heartbeat_ = true;
return ocpp2_0::TriggerMessageResponse {ocpp2_0::TriggerMessageStatusEnumType::kAccepted};
}
return std::nullopt;
}
private:
std::shared_ptr<Settings> settings_;
std::shared_ptr<SystemInterface> system_interface_;
OperationHolder<std::string> pending_heartbeat_req_;
bool force_heartbeat_ = false;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_HEARTBEAT_MODULE_H

View File

@@ -0,0 +1,218 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_LOCAL_LIMITS_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_LOCAL_LIMITS_MODULE_H
#include "openocpp/interface/component/system_interface.h"
#include <vector>
#include <string>
#include <optional>
#include <iostream>
namespace chargelab {
struct LeaseAmps {
int created_index;
double allocated_amps;
SteadyPointMillis expiry;
std::optional<int> completed_index = std::nullopt;
};
namespace detail {
struct LocalLimitState {
std::string charge_point_id_;
std::vector<LeaseAmps> active_leases_;
std::optional<double> requested_amps_;
std::optional<SteadyPointMillis> requested_amps_ts_;
// Informational
std::optional<double> last_reported_amps_;
std::optional<double> last_reported_watts_;
};
}
class LocalLimitModule {
private:
static constexpr int kRequestExpirySeconds = 3600;
static constexpr int kLeaseDurationSeconds = 3600;
static constexpr int kLeaseRefreshThresholdSeconds = 15*60;
public:
explicit LocalLimitModule(
std::shared_ptr<chargelab::SystemInterface> system,
double max_amps
)
: system_(std::move(system)),
max_amps_(max_amps)
{
}
public:
int getUniqueId(std::string const& charge_point_id) {
std::lock_guard lock {mutex_};
for (auto const& x : state_) {
if (x.second.charge_point_id_ == charge_point_id)
return x.first;
}
auto result = last_id_++;
state_[result].charge_point_id_ = charge_point_id;
return result;
}
void setRequestedAmps(int id, double amps) {
std::lock_guard lock {mutex_};
state_[id].requested_amps_ = amps;
state_[id].requested_amps_ts_ = system_->steadyClockNow();
}
void setCurrentAllocatedAmps(int id, double amps) {
std::lock_guard lock {mutex_};
state_[id].last_reported_amps_ = amps;
}
void setCurrentWatts(int id, double watts) {
std::lock_guard lock {mutex_};
state_[id].last_reported_watts_ = watts;
}
std::optional<LeaseAmps> leaseAmps(int id, std::optional<LeaseAmps> const& current) {
std::lock_guard lock {mutex_};
auto result = leaseAmpsImpl(id);
bool refresh = false;
if (result.allocated_amps != current->allocated_amps)
refresh = true;
if ((current->expiry - system_->steadyClockNow())/1000 < kLeaseRefreshThresholdSeconds)
refresh = true;
if (!refresh)
return std::nullopt;
state_[id].active_leases_.push_back(result);
return result;
}
void activateLease(int id, LeaseAmps& current) {
std::lock_guard lock {mutex_};
int completed_index = lease_index_++;
current.completed_index = completed_index;
auto& leases = state_[id].active_leases_;
for (auto& x : leases) {
if (x.created_index == current.created_index) {
x.completed_index = completed_index;
break;
}
}
leases.erase(
std::remove_if(
leases.begin(),
leases.end(),
[=](LeaseAmps const& x){return x.completed_index && current.created_index - x.completed_index.value() > 0;}
),
leases.end()
);
}
void printCurrentAmps() {
std::lock_guard lock {mutex_};
system("clear");
auto now = system_->steadyClockNow();
for (auto& x : state_) {
auto lease = removeDeadEntriesAndGetMaxLease(x.second.active_leases_);
if (!lease)
continue;
std::cout << "ID " << x.first << " {" << x.second.charge_point_id_ << "}: ";
std::cout << std::setw(4) << std::round(lease->allocated_amps*10)/10.0 << "A (" << std::setw(2) << static_cast<int>((lease->expiry - now)/1000) << "s)";
if (x.second.last_reported_amps_ && x.second.requested_amps_)
std::cout << " " << std::setw(4) << std::round(x.second.last_reported_amps_.value()*10)/10.0 << "A/" << std::setw(4) << std::round(x.second.requested_amps_.value()*10)/10.0 << "A";
if (x.second.last_reported_watts_)
std::cout << " " << std::setw(4) << std::round(x.second.last_reported_watts_.value()) << "W";
std::cout << std::endl;
}
}
private:
LeaseAmps leaseAmpsImpl(int id) {
auto now = system_->steadyClockNow();
auto expiry = static_cast<SteadyPointMillis>(now + kLeaseDurationSeconds*1000);
int index = lease_index_++;
auto const& state = state_[id];
if (!state.requested_amps_)
return {index, 0.0, expiry};
if (!state.requested_amps_ts_ || (now - state.requested_amps_ts_.value())/1000.0 >= kRequestExpirySeconds)
return {index, 0.0, expiry};
double total_requested = 0;
for (auto const& x : state_) {
if (!x.second.requested_amps_ || !x.second.requested_amps_ts_)
continue;
if ((now - x.second.requested_amps_ts_.value())/1000.0 >= kRequestExpirySeconds)
continue;
total_requested += x.second.requested_amps_.value();
}
double total_leased = 0;
for (auto& x : state_) {
if (x.first == id)
continue;
auto lease = removeDeadEntriesAndGetMaxLease(x.second.active_leases_);
if (lease)
total_leased += lease->allocated_amps;
}
auto requested = state.requested_amps_.value();
auto result = std::min(requested, requested*max_amps_/total_requested);
result = std::min(max_amps_ - total_leased, result);
result = std::max(0.0, result);
return {index, result, expiry};
}
std::optional<LeaseAmps> removeDeadEntriesAndGetMaxLease(std::vector<LeaseAmps>& leases) {
auto now = system_->steadyClockNow();
leases.erase(
std::remove_if(
leases.begin(),
leases.end(),
[&](auto const& x) {return now - x.expiry > 0;}
),
leases.end()
);
std::optional<LeaseAmps> max_lease;
for (auto const& x : leases) {
if (max_lease) {
if (x.allocated_amps < max_lease->allocated_amps)
continue;
if (x.allocated_amps == max_lease->allocated_amps && x.expiry - max_lease->expiry < 0)
continue;
}
max_lease = x;
}
return max_lease;
}
private:
std::shared_ptr<chargelab::SystemInterface> system_;
double max_amps_;
int last_id_ = 0;
int lease_index_ = 0;
std::mutex mutex_;
std::map<int, detail::LocalLimitState> state_;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_LOCAL_LIMITS_MODULE_H

View File

@@ -0,0 +1,231 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_LOG_STREAMING_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_LOG_STREAMING_MODULE_H
#include "openocpp/module/common_templates.h"
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/common/settings.h"
#include <utility>
#include <sstream>
namespace chargelab {
namespace detail {
struct LogLine {
LogLevel level = LogLevel::kValueNotFoundInEnum;
#if defined(LOG_WITH_FILE_AND_LINE)
std::string file {};
int line = -1;
#endif
std::string function {};
std::string message {};
int size() {
return sizeof(LogLine) +
#if defined(LOG_WITH_FILE_AND_LINE)
file.size() +
#endif
function.size() + message.size();
}
};
#if defined(LOG_WITH_FILE_AND_LINE)
CHARGELAB_OPTIONAL_NULL_DEFINE_TYPE_NON_INTRUSIVE(LogLine, level, file, line, function, message)
#else
CHARGELAB_OPTIONAL_NULL_DEFINE_TYPE_NON_INTRUSIVE(LogLine, level, function, message)
#endif
struct LogMessage {
std::vector<LogLine> messages;
};
CHARGELAB_OPTIONAL_NULL_DEFINE_TYPE_NON_INTRUSIVE(LogMessage, messages)
}
class LogStreamingModule : public ChargePointServiceStateful {
private:
static const int kBufferLimitBytes = 3*1024; // 10*1024;
static const int kRingBufferMaxSize = 25; // 50;
static const int kMaxLinesPerMessage = 15; // 20;
static const int kBufferLimitMillis = 30*1000; // 30*1000;
public:
LogStreamingModule(
Settings& settings,
std::shared_ptr<SystemInterface> const& system_interface
) : system_interface_(system_interface),
pending_dump_request_ {system_interface},
log_streaming_enabled_ {chargelab::settings::CreateBasicBooleanSetting<false>(
settings,
settings::CommonKey::kLogStreamingEnabled,
"LogStreamingEnabled"
)}
{
listener_ = std::make_shared<LoggingListenerFunction>([&](LogMetadata const& metadata, std::string_view const& message) {
// TODO: Find a better way to filter out specific messages related to the dump itself
if (message.find("Writing text message") != std::string::npos && message.find("StreamingLogMessage") != std::string::npos) {
return;
}
if (!log_streaming_enabled_.getValue())
return;
detail::LogLine line {};
line.level = metadata.level;
#if defined(LOG_WITH_FILE_AND_LINE)
line.file = metadata.file;
line.line = metadata.line;
line.file.shrink_to_fit();
#endif
line.function = metadata.function;
line.message = message;
line.function.shrink_to_fit();
line.message.shrink_to_fit();
std::lock_guard lock{mutex_};
history_.pushBack(std::move(line));
});
RegisterLoggingListener(listener_);
}
~LogStreamingModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting LogStreamingModule";
UnregisterLoggingListener(listener_);
}
private:
void runStep(ocpp1_6::ChargePointRemoteInterface &remote) override {
{
std::lock_guard lock{mutex_};
if (history_.empty())
return;
auto const now = system_interface_->steadyClockNow();
if (history_.size() < kMaxLinesPerMessage && !getHistoryLimitIndex().has_value()) {
if (!last_dump_.has_value())
last_dump_ = now;
auto const delta = now - last_dump_.value();
if (delta < kBufferLimitMillis) {
return;
}
}
}
if (!pending_dump_request_.operationInProgress()) {
CHARGELAB_LOG_MESSAGE(debug) << "Flushing log messages - total size was: " << getHistorySize() << " bytes" << ", total memory size: " << getHistoryMemorySize()
<< ",ring buffer size: " << history_.size();
detail::LogMessage payload {};
{
std::lock_guard lock{mutex_};
int total_size = 0;
for (int i=0; i < kMaxLinesPerMessage && !history_.empty(); i++) {
if (total_size + history_.front().size() > kBufferLimitBytes) break;
total_size += history_.front().size();
detail::LogLine value = std::move(history_.front());
history_.popFront();
payload.messages.push_back(std::move(value));
}
}
pending_dump_request_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
remote.sendDataTransferReq(ocpp1_6::DataTransferReq {
ocpp1_6::CiString255Type("ChargeLab"),
ocpp1_6::CiString50Type("StreamingLogMessage"),
write_json_to_string(payload)
})
);
}
std::lock_guard lock{mutex_};
auto limit = getHistoryLimitIndex();
if (limit.has_value()) {
for (int i=0; i <= limit.value(); i++) {
history_.front() = detail::LogLine {};
history_.popFront();
}
}
}
void onDataTransferRsp(
const std::string &unique_id,
const ocpp1_6::ResponseMessage <chargelab::ocpp1_6::DataTransferRsp> &rsp
) override {
if (pending_dump_request_ == unique_id) {
pending_dump_request_ = kNoOperation;
last_dump_ = system_interface_->steadyClockNow();
if (std::holds_alternative<ocpp1_6::CallError>(rsp)) {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to DataTransfer request: " << std::get<ocpp1_6::CallError> (rsp);
}
}
}
private:
std::optional<int> getHistoryLimitIndex() {
int index;
std::size_t total_bytes = 0;
for (index = history_.size()-1; index >= 0; index--) {
auto const& value = history_[index];
total_bytes += sizeof(value.level);
#if defined(LOG_WITH_FILE_AND_LINE)
total_bytes += sizeof(std::string) + value.file.size();
total_bytes += sizeof(value.line);
#endif
total_bytes += sizeof(std::string) + value.function.size();
total_bytes += sizeof(std::string) + value.message.size();
if (total_bytes >= kBufferLimitBytes)
return index;
}
return std::nullopt;
}
std::size_t getHistorySize() {
std::size_t total_bytes = 0;
for (int index=0; index < history_.size(); index++) {
auto const& value = history_[index];
total_bytes += sizeof(value.level);
#if defined(LOG_WITH_FILE_AND_LINE)
total_bytes += sizeof(std::string) + value.file.size();
total_bytes += sizeof(value.line);
#endif
total_bytes += sizeof(std::string) + value.function.size();
total_bytes += sizeof(std::string) + value.message.size();
}
return total_bytes;
}
std::size_t getHistoryMemorySize() {
std::size_t total_bytes = 0;
for (int index=0; index < history_.size(); index++) {
auto const& value = history_[index];
total_bytes += sizeof(value.level);
#if defined(LOG_WITH_FILE_AND_LINE)
total_bytes += sizeof(std::string) + value.file.capacity();
total_bytes += sizeof(value.line);
#endif
total_bytes += sizeof(std::string) + value.function.capacity();
total_bytes += sizeof(std::string) + value.message.capacity();
}
return total_bytes;
}
private:
std::shared_ptr<SystemInterface> system_interface_;
OperationHolder<std::string> pending_dump_request_;
BasicBooleanSetting<false> log_streaming_enabled_;
std::optional<SteadyPointMillis> last_dump_ = std::nullopt;
std::shared_ptr<LoggingListenerFunction> listener_ = nullptr;
std::mutex mutex_;
RingBuffer<detail::LogLine, kRingBufferMaxSize> history_;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_LOG_STREAMING_MODULE_H

View File

@@ -0,0 +1,967 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_PENDING_MESSAGES_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_PENDING_MESSAGES_MODULE_H
#include "openocpp/interface/platform_interface.h"
#include "openocpp/module/common_templates.h"
#include "openocpp/common/operation_holder.h"
#include "openocpp/common/compressed_queue.h"
#include "openocpp/common/logging.h"
#include "openocpp/common/serialization.h"
#include <string>
#include <optional>
#include <unordered_set>
namespace chargelab {
CHARGELAB_JSON_ENUM(PendingMessageType,
TransactionEvent,
SecurityEvent,
NotificationEvent,
Other
);
struct PendingMessagePolicy {
PendingMessageType message_type;
std::optional<uint64_t> group_id = std::nullopt;
int message_attempts = 1;
int retry_interval_seconds = 10;
// Determines which records are dropped first when available storage is exceeded - higher priority takes precedence
int priority = 0;
bool add_remote_transaction_id = false;
bool add_message_sequence_number = false;
bool must_flush_to_disk = false;
CHARGELAB_JSON_INTRUSIVE(
PendingMessagePolicy,
message_type,
group_id,
message_attempts,
retry_interval_seconds,
priority,
add_remote_transaction_id,
add_message_sequence_number,
must_flush_to_disk
)
};
namespace detail {
struct PendingMessageWrapper {
int64_t unique_id;
std::string payload;
PendingMessagePolicy policy;
std::optional<ocpp1_6::ActionId> action_id1_6;
std::optional<ocpp2_0::ActionId> action_id2_0;
int attempts = 0;
CHARGELAB_JSON_INTRUSIVE(PendingMessageWrapper, unique_id, payload, policy, action_id1_6, action_id2_0, attempts)
};
class PendingMessageSerializer {
public:
static std::optional<PendingMessageWrapper> read(std::string_view const& text) {
PendingMessageWrapper result;
std::optional<int> index = 0;
index = readPrimitive(text, index, result.unique_id);
index = readPrimitive(text, index, result.payload);
index = readPrimitive(text, index, result.policy.message_type);
index = readPrimitive(text, index, result.policy.group_id);
index = readPrimitive(text, index, result.policy.retry_interval_seconds);
index = readPrimitive(text, index, result.policy.message_attempts);
index = readPrimitive(text, index, result.policy.priority);
index = readPrimitive(text, index, result.policy.add_remote_transaction_id);
index = readPrimitive(text, index, result.policy.add_message_sequence_number);
index = readPrimitive(text, index, result.policy.must_flush_to_disk);
index = readPrimitive(text, index, result.action_id1_6);
index = readPrimitive(text, index, result.action_id2_0);
index = readPrimitive(text, index, result.attempts);
if (!index.has_value())
return std::nullopt;
return result;
}
static std::string write(PendingMessageWrapper const& wrapper) {
std::string result;
writePrimitive(result, wrapper.unique_id);
writePrimitive(result, wrapper.payload);
writePrimitive(result, wrapper.policy.message_type);
writePrimitive(result, wrapper.policy.group_id);
writePrimitive(result, wrapper.policy.retry_interval_seconds);
writePrimitive(result, wrapper.policy.message_attempts);
writePrimitive(result, wrapper.policy.priority);
writePrimitive(result, wrapper.policy.add_remote_transaction_id);
writePrimitive(result, wrapper.policy.add_message_sequence_number);
writePrimitive(result, wrapper.policy.must_flush_to_disk);
writePrimitive(result, wrapper.action_id1_6);
writePrimitive(result, wrapper.action_id2_0);
writePrimitive(result, wrapper.attempts);
return result;
}
};
}
class PendingMessagesModule : public ServiceStatefulGeneral {
private:
static constexpr int kUpdateCacheAndStatsFrequencySeconds = 15;
static constexpr int kMaxLiveMessages = 5;
static constexpr int kOfflineSizeLimitBytes = 10*1024;
static constexpr int kTargetDeleteRecordCount = 10;
static constexpr int kMaxStorageBlockSize = 50*1024;
static constexpr int kFlushToDiskFrequencyMillis = 30*60*1000; // half hour
public:
using saved_message_supplier = std::function<void(std::function<void(detail::PendingMessageWrapper const&)> const&)>;
public:
PendingMessagesModule(
std::shared_ptr<Settings> settings,
std::shared_ptr<SystemInterface> const& system,
std::shared_ptr<StorageInterface> storage
)
: settings_ {std::move(settings)},
system_ {system},
storage_ {std::move(storage)},
random_engine_ {std::random_device{}()},
// TODO: Review this; timeout will not change until reboot
request_operation_ {system}
{
std::uniform_int_distribution<int64_t> distribution(0, std::numeric_limits<int64_t>::max());
request_id_ = distribution(random_engine_);
loadFromStorage();
// Clear transaction_ids_ and sequence_ids_ if offline_queue_ and live_queue_ are both empty
if (offline_queue_.empty() && live_queue_.empty()) {
transaction_ids_.clear();
sequence_ids_.clear();
}
settings_->MessageFlushCounter.setValue(pending_messages_write_count_);
}
void registerOnSaveMessageSupplier(std::shared_ptr<saved_message_supplier> const& supplier) {
for (auto const& x : saved_message_suppliers_) {
if (x == supplier) {
CHARGELAB_LOG_MESSAGE(warning) << "Callback registered multiple times";
return;
}
}
saved_message_suppliers_.push_back(supplier);
}
void unregisterOnSaveMessageSupplier(std::shared_ptr<saved_message_supplier> const& supplier) {
auto initial_size = saved_message_suppliers_.size();
saved_message_suppliers_.erase(
std::remove_if(saved_message_suppliers_.begin(), saved_message_suppliers_.end(), [&](auto& x) {return x == supplier;}),
saved_message_suppliers_.end()
);
if (initial_size - saved_message_suppliers_.size() > 1)
CHARGELAB_LOG_MESSAGE(warning) << "Callback registered multiple times";
}
public:
template<typename T>
detail::PendingMessageWrapper generateRequest1_6(T const& request, PendingMessagePolicy policy = PendingMessagePolicy{}) {
return detail::PendingMessageWrapper {
request_id_++,
write_json_to_string(request),
policy,
T::kActionId,
std::nullopt
};
}
template<typename T>
detail::PendingMessageWrapper generateRequest2_0(T const& request, PendingMessagePolicy const& policy) {
return detail::PendingMessageWrapper {
request_id_++,
write_json_to_string(request),
policy,
std::nullopt,
T::kActionId
};
}
template<typename T>
std::string sendRequest1_6(T const& request, PendingMessagePolicy policy = PendingMessagePolicy{}) {
detail::PendingMessageWrapper wrapper = generateRequest1_6(request, policy);
if (activeGroupsContains(policy.group_id)) {
offline_queue_.pushBack(wrapper);
pending_messages_changed_ = true;
} else {
live_queue_.push_back(wrapper);
}
if (policy.must_flush_to_disk)
must_flush_to_disk_ = true;
return std::to_string(wrapper.unique_id);
}
template<typename T>
std::string sendRequest2_0(T const& request, PendingMessagePolicy const& policy) {
detail::PendingMessageWrapper wrapper = generateRequest2_0(request, policy);
if (activeGroupsContains(policy.group_id)) {
offline_queue_.pushBack(wrapper);
pending_messages_changed_ = true;
} else {
live_queue_.push_back(wrapper);
}
if (policy.must_flush_to_disk)
must_flush_to_disk_ = true;
return std::to_string(wrapper.unique_id);
}
template <typename Visitor>
void visitPending(Visitor&& visitor) {
for (auto const& x : live_queue_)
visitor(x.policy, x.payload);
offline_queue_.visit([&] (std::string_view const&, detail::PendingMessageWrapper const& wrapper) {
visitor(wrapper.policy, wrapper.payload);
});
}
public:
void runUnconditionally() override {
updateCacheAndStats();
limitOfflineQueueSize();
flushToDisk();
// If an operation is not in progress, flush messages from the live queue to the offline queue as necessary
if (!request_operation_.operationInProgress() && !live_queue_.empty()) {
while (live_queue_.size() > kMaxLiveMessages) {
auto it = live_queue_.begin();
if (it->policy.group_id.has_value())
active_group_ids_.insert(it->policy.group_id.value());
offline_queue_.pushBack(live_queue_.front());
live_queue_.erase(live_queue_.begin());
pending_messages_changed_ = true;
}
// Make sure live messages with the same group IDs also get moved to offline messages so that they're
// not sent out of order.
live_queue_.erase(
std::remove_if(
live_queue_.begin(),
live_queue_.end(),
[&](detail::PendingMessageWrapper const& wrapper) {
if (activeGroupsContains(wrapper.policy.group_id)) {
offline_queue_.pushBack(wrapper);
pending_messages_changed_ = true;
return true;
} else {
return false;
}
}
),
live_queue_.end()
);
}
}
void runStep(ocpp1_6::OcppRemote& remote) override {
// Send out a live message, if one is available
if (!request_operation_.operationInProgress() && !live_queue_.empty()) {
auto it = live_queue_.begin();
bool delete_message = false;
bool send_message = true;
if (blacklistContains(it->policy.group_id))
delete_message = true;
if (!it->action_id1_6.has_value())
delete_message = true;
if (it->attempts > 0 && it->attempts >= it->policy.message_attempts)
delete_message = true;
if (it->attempts > 0) {
auto const threshold = std::max(it->attempts, 1) * it->policy.retry_interval_seconds;
if (request_operation_.getIdleDurationSeconds() < threshold)
send_message = false;
}
if (delete_message) {
live_queue_.erase(it);
} else if (send_message) {
if (sendWithTransactionId(remote, *it)) {
it->attempts++;
request_operation_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
std::to_string(it->unique_id)
);
request_object_ = *it;
}
}
}
// Next send out an offline message, if one is available
if (!request_operation_.operationInProgress() && !offline_queue_.empty()) {
auto record = offline_queue_.pollFront();
if (record.has_value()) {
bool delete_message = false;
bool send_message = true;
if (blacklistContains(record->policy.group_id))
delete_message = true;
if (!record->action_id1_6.has_value())
delete_message = true;
if (record->attempts > 0 && record->attempts >= record->policy.message_attempts)
delete_message = true;
if (record->attempts > 0) {
auto const threshold = std::max(record->attempts, 1) * record->policy.retry_interval_seconds;
if (request_operation_.getIdleDurationSeconds() < threshold)
send_message = false;
}
if (delete_message) {
CHARGELAB_LOG_MESSAGE(info) << "popFront: delete_message";
offline_queue_.popFront();
pending_messages_changed_ = true;
} else if (send_message) {
if (sendWithTransactionId(remote, record.value())) {
record->attempts++;
offline_queue_.updateFront(record.value());
pending_messages_changed_ = true;
request_operation_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
std::to_string(record->unique_id)
);
request_object_ = record.value();
}
}
}
}
}
void runStep(ocpp2_0::OcppRemote &remote) override {
updateCacheAndStats();
limitOfflineQueueSize();
// If an operation is not in progress, flush messages from the live queue to the offline queue as necessary
if (!request_operation_.operationInProgress() && !live_queue_.empty()) {
while (live_queue_.size() > kMaxLiveMessages) {
auto it = live_queue_.begin();
if (it->policy.group_id.has_value())
active_group_ids_.insert(it->policy.group_id.value());
offline_queue_.pushBack(live_queue_.front());
live_queue_.erase(live_queue_.begin());
pending_messages_changed_ = true;
}
// Make sure live messages with the same group IDs also get moved to offline messages so that they're
// not sent out of order.
live_queue_.erase(
std::remove_if(
live_queue_.begin(),
live_queue_.end(),
[&](detail::PendingMessageWrapper const& wrapper) {
if (activeGroupsContains(wrapper.policy.group_id)) {
offline_queue_.pushBack(wrapper);
pending_messages_changed_ = true;
return true;
} else {
return false;
}
}
),
live_queue_.end()
);
}
// First send out a live message, if one is available
if (!request_operation_.operationInProgress() && !live_queue_.empty()) {
auto it = live_queue_.begin();
bool delete_message = false;
bool send_message = true;
if (blacklistContains(it->policy.group_id))
delete_message = true;
if (!it->action_id2_0.has_value())
delete_message = true;
if (it->attempts > 0 && it->attempts >= it->policy.message_attempts)
delete_message = true;
if (it->attempts > 0) {
auto const threshold = std::max(it->attempts, 1) * it->policy.retry_interval_seconds;
if (request_operation_.getIdleDurationSeconds() < threshold)
send_message = false;
}
if (delete_message) {
if (it->policy.group_id.has_value()) {
auto sequence = sequence_ids_.find(it->policy.group_id.value());
if (sequence != sequence_ids_.end()) {
if (shouldRemoveSequenceId(*it)) {
sequence_ids_.erase(sequence);
pending_messages_changed_ = true;
} else {
sequence->second++;
}
}
}
live_queue_.erase(it);
} else if (send_message) {
if (sendWithSequenceNumber(remote, *it)) {
it->attempts++;
request_operation_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
std::to_string(it->unique_id)
);
request_object_ = *it;
}
}
}
// Next send out an offline message, if one is available
if (!request_operation_.operationInProgress() && !offline_queue_.empty()) {
auto record = offline_queue_.pollFront();
if (record.has_value()) {
bool delete_message = false;
bool send_message = true;
if (blacklistContains(record->policy.group_id))
delete_message = true;
if (!record->action_id2_0.has_value())
delete_message = true;
if (record->attempts > 0 && record->attempts >= record->policy.message_attempts)
delete_message = true;
if (record->attempts > 0) {
auto const threshold = std::max(record->attempts, 1) * record->policy.retry_interval_seconds;
if (request_operation_.getIdleDurationSeconds() < threshold)
send_message = false;
}
if (delete_message) {
CHARGELAB_LOG_MESSAGE(info) << "popFront: delete_message";
if (record->policy.group_id.has_value()) {
auto sequence = sequence_ids_.find(record->policy.group_id.value());
if (sequence != sequence_ids_.end()) {
if (shouldRemoveSequenceId(record.value())) {
sequence_ids_.erase(sequence);
} else {
sequence->second++;
}
}
}
offline_queue_.popFront();
pending_messages_changed_ = true;
} else if (send_message) {
if (sendWithSequenceNumber(remote, record.value())) {
record->attempts++;
offline_queue_.updateFront(record.value());
pending_messages_changed_ = true;
request_operation_.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
std::to_string(record->unique_id)
);
request_object_ = record.value();
}
}
}
}
}
void onStartTransactionRsp(
const std::string &unique_id,
const ocpp1_6::ResponseMessage<ocpp1_6::StartTransactionRsp> &rsp
) override {
if (request_operation_ == unique_id && std::holds_alternative<ocpp1_6::StartTransactionRsp>(rsp)) {
auto const& message = std::get<ocpp1_6::StartTransactionRsp>(rsp);
if (!request_object_.has_value())
return;
if (!request_object_->policy.group_id.has_value())
return;
transaction_ids_.insert(std::make_pair(request_object_->policy.group_id.value(), message.transactionId));
// Flush the pending message to remove StartTransaction from the live_queue_ and
// update the StopTransaction with transaction Id is the StartTransaction is accepted
must_flush_to_disk_ = true;
}
}
private:
[[nodiscard]] bool activeGroupsContains(std::optional<int64_t> const& group_id) const {
if (!group_id.has_value())
return false;
return active_group_ids_.find(group_id.value()) != active_group_ids_.end();
}
[[nodiscard]] bool blacklistContains(std::optional<int64_t> const& group_id) const {
if (!group_id.has_value())
return false;
return group_blacklist_.find(group_id.value()) != group_blacklist_.end();
}
bool sendWithTransactionId(ocpp1_6::OcppRemote& remote, detail::PendingMessageWrapper const& wrapper) {
if (!wrapper.action_id1_6.has_value())
return false;
std::string payload = wrapper.payload;
if (wrapper.policy.group_id.has_value()) {
auto it = transaction_ids_.find(wrapper.policy.group_id.value());
if (it != transaction_ids_.end())
payload = insert_into_object(payload, "transactionId", it->second);
}
return remote.sendCall(
std::to_string(wrapper.unique_id),
wrapper.action_id1_6.value(),
payload
);
}
bool sendWithSequenceNumber(ocpp2_0::OcppRemote& remote, detail::PendingMessageWrapper const& wrapper) {
if (!wrapper.action_id2_0.has_value())
return false;
std::string payload = wrapper.payload;
if (wrapper.policy.add_message_sequence_number && wrapper.policy.group_id.has_value()) {
auto it = sequence_ids_.find(wrapper.policy.group_id.value());
if (it == sequence_ids_.end()) {
auto first = read_field_from_object<int>(payload, "seqNo");
sequence_ids_[wrapper.policy.group_id.value()] = first.value_or(0);
pending_messages_changed_ = true;
}
payload = insert_into_object(payload, "seqNo", sequence_ids_[wrapper.policy.group_id.value()]);
}
return remote.sendCall(
std::to_string(wrapper.unique_id),
wrapper.action_id2_0.value(),
payload
);
}
void limitOfflineQueueSize() {
auto const initial_total_bytes = offline_queue_.totalBytes();
if (initial_total_bytes < kOfflineSizeLimitBytes)
return;
CHARGELAB_LOG_MESSAGE(info) << "Dropping messages to limit offline queue size: "
<< initial_total_bytes << " > " << kOfflineSizeLimitBytes;
auto const start_ts = system_->steadyClockNow();
std::map<std::pair<int, std::optional<int>>, std::pair<int, int>> stats;
offline_queue_.visit([&] (std::string_view const& text, detail::PendingMessageWrapper const& wrapper) {
auto& value = stats[std::make_pair(wrapper.policy.priority, wrapper.policy.group_id)];
value.first += 1;
value.second += (int)text.size();
});
auto const middle_ts1 = system_->steadyClockNow();
int max_priority = std::numeric_limits<int>::min();
for (auto const& x : stats)
max_priority = std::max(x.first.first, max_priority);
bool delete_safely = false;
for (auto const& x : stats) {
if (!x.first.second.has_value()) {
delete_safely = true;
break;
}
if (x.first.first == max_priority && x.second.first > 2) {
delete_safely = true;
break;
}
}
int delete_count = 0;
for (auto const& x : stats) {
if (!x.first.second.has_value()) {
delete_count++;
} else if (delete_safely) {
delete_count += std::max(x.second.first - 2, 0);
} else {
delete_count += x.second.first;
}
}
std::size_t deleted_records = 0;
std::size_t deleted_decompressed_bytes = 0;
std::uniform_int_distribution<int64_t> distribution(0, delete_count-1);
std::map<int64_t, int> counter;
auto const middle_ts2 = system_->steadyClockNow();
offline_queue_.removeIf([&] (std::string_view const& text, detail::PendingMessageWrapper const& wrapper) {
if (wrapper.policy.priority != max_priority)
return false;
if (wrapper.policy.group_id.has_value() && delete_safely) {
auto index = counter[wrapper.policy.group_id.value()]++;
auto const& stat = stats[std::make_pair(wrapper.policy.priority, wrapper.policy.group_id)];
if (index == 0 || index == stat.first-1)
return false;
}
if (distribution(random_engine_) < kTargetDeleteRecordCount) {
deleted_records++;
deleted_decompressed_bytes += text.size();
CHARGELAB_LOG_MESSAGE(info) << "Dropping offline message: " << wrapper.payload;
pending_messages_changed_ = true;
return true;
} else {
return false;
}
});
auto const end_ts = system_->steadyClockNow();
CHARGELAB_LOG_MESSAGE(info) << "Delete stats:"
<< " processing_ms=" << (end_ts - start_ts)
<< " deleted_records=" << deleted_records
<< " deleted_decompressed_bytes=" << deleted_decompressed_bytes
<< " new_compressed_size=" << offline_queue_.totalBytes();
CHARGELAB_LOG_MESSAGE(info) << "Times: "
<< " block1=" << (middle_ts1 - start_ts)
<< " block2=" << (middle_ts2 - middle_ts1)
<< " block3=" << (end_ts - middle_ts2);
}
void onCallRsp(const std::string &unique_id, const ocpp1_6::ResponseMessage<common::RawJson>& payload) override {
if (request_operation_ == unique_id) {
request_operation_ = kNoOperation;
if (std::holds_alternative<common::RawJson>(payload)) {
if (!request_object_.has_value())
return;
auto onCallRsp = [this](detail::PendingMessageWrapper const &wrapper) {
if (wrapper.action_id1_6 && wrapper.action_id1_6.value() == ocpp1_6::ActionId::kStopTransaction) {
auto const request = read_json_from_string<ocpp1_6::StopTransactionReq>(wrapper.payload);
if (!request.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed read StopTransaction request from: " << wrapper.payload;
return;
}
// remove transaction id from transaction_ids_ if there is one
for (auto it = transaction_ids_.begin(); it != transaction_ids_.end(); ) {
if (it->second == request->transactionId) {
it = transaction_ids_.erase(it);
pending_messages_changed_ = true;
} else {
++it;
}
}
if (request->reason == ocpp1_6::Reason::kHardReset || request->reason == ocpp1_6::Reason::kSoftReset) {
must_flush_to_disk_ = true;
flushToDisk();
}
}
};
if (!live_queue_.empty() && live_queue_.front().unique_id == request_object_->unique_id) {
auto wrapper = live_queue_.front();
live_queue_.erase(live_queue_.begin());
onCallRsp(wrapper);
return;
}
if (offline_queue_.pollFront().has_value() && offline_queue_.pollFront()->unique_id == request_object_->unique_id) {
CHARGELAB_LOG_MESSAGE(info) << "popFront: onCallRsp";
auto record = offline_queue_.pollFront();
offline_queue_.popFront();
onCallRsp(record.value());
pending_messages_changed_ = true;
return;
}
}
}
}
void onCallRsp(const std::string &unique_id, const ocpp2_0::ResponseMessage<common::RawJson>& payload) override {
if (request_operation_ == unique_id) {
request_operation_ = kNoOperation;
if (std::holds_alternative<common::RawJson>(payload)) {
if (!request_object_.has_value())
return;
if (!live_queue_.empty() && live_queue_.front().unique_id == request_object_->unique_id) {
auto it = sequence_ids_.find(live_queue_.front().policy.group_id.value());
if (it != sequence_ids_.end()) {
if (shouldRemoveSequenceId(live_queue_.front())) {
sequence_ids_.erase(it);
pending_messages_changed_ = true;
} else {
it->second++;
}
}
live_queue_.erase(live_queue_.begin());
return;
}
auto const& record = offline_queue_.pollFront();
if (record.has_value() && record->unique_id == request_object_->unique_id) {
CHARGELAB_LOG_MESSAGE(info) << "popFront: onCallRsp";
auto it = sequence_ids_.find(record->policy.group_id.value());
if (it != sequence_ids_.end()) {
if (shouldRemoveSequenceId(record.value())) {
sequence_ids_.erase(it);
} else {
it->second++;
}
}
offline_queue_.popFront();
pending_messages_changed_ = true;
return;
}
}
}
}
private:
void updateCacheAndStats() {
auto const now = system_->steadyClockNow();
if (last_cache_update_.has_value()) {
auto const delta = now - last_cache_update_.value();
if (delta/1000 < kUpdateCacheAndStatsFrequencySeconds)
return;
}
last_cache_update_ = now;
std::size_t total_records = 0;
std::size_t total_decompressed_size = 0;
active_group_ids_.clear();
offline_queue_.visit([&](std::string_view const& text, detail::PendingMessageWrapper const& wrapper) {
if (wrapper.policy.group_id.has_value())
active_group_ids_.insert(wrapper.policy.group_id.value());
total_records++;
total_decompressed_size += text.size();
});
auto const total_compressed_size = offline_queue_.totalBytes();
double compression_ratio = 0;
if (total_decompressed_size > 0)
compression_ratio = (double)total_compressed_size/(double)total_decompressed_size;
CHARGELAB_LOG_MESSAGE(info) << "Offline message stats:"
<< " records=" << total_records
<< " compressed_bytes=" << total_compressed_size
<< " decompressed_bytes=" << total_decompressed_size
<< " compression_ratio=" << compression_ratio;
}
void flushToDisk() {
if (!must_flush_to_disk_ && !pending_messages_changed_) {
return; // No need to flush anything
}
auto const now = system_->steadyClockNow();
auto total_writing_bytes = calculateAllPendingSavedInfoSize();
total_writing_bytes = ((total_writing_bytes + 255)/256)*256;
int flush_interval = ((double)total_writing_bytes / kMaxStorageBlockSize) * kFlushToDiskFrequencyMillis;
CHARGELAB_LOG_MESSAGE(trace) << "flush interval: " << flush_interval << ", offline_queue_ bytes: " << offline_queue_.totalBytes()
<< ", must_flush_to_disk_: " << must_flush_to_disk_ << ", pending_messages_changed_: " << pending_messages_changed_;
if (!must_flush_to_disk_ && last_flush_to_disk_.has_value()) {
// Note: flushing to disk explicitly after the historic backlog has cleared after an offline period or
// after restarting.
auto const delta = now - last_flush_to_disk_.value();
if (delta < flush_interval)
return;
}
last_flush_to_disk_ = now;
must_flush_to_disk_ = false;
// Note: must flush to disk here even if online, otherwise the Ended messages aren't added to the stream and
// a dangling transaction is created if the station looses power. On the other hand, at one extra
// write per hour here we'll end up exhausting one block of flash memory on an ESP32 every ~11 years,
// which may not be worth optimizing out (particularly with a wear-leveling filesystem).
pending_messages_write_count_ ++;
saveToStorage();
settings_->MessageFlushCounter.setValue(pending_messages_write_count_);
pending_messages_changed_ = false;
}
void saveToStorage() {
// TODO: This could likely be optimized to avoid writing an empty file repeatedly while there's nothing in
// the buffer (and nothing added via the registered suppliers).
CHARGELAB_LOG_MESSAGE(info) << "Saving pending messages to storage, write count: " << pending_messages_write_count_;
storage_->write([&](auto file) {
offline_queue_.write([&](void* data, std::size_t size) {
auto length = (int)size;
std::fwrite(&length, sizeof(length), 1, file);
std::fwrite(data, size, 1, file);
});
int terminator = -1;
std::fwrite(&terminator, sizeof(terminator), 1, file);
file::json_write_object_to_file(file, active_group_ids_);
file::json_write_object_to_file(file, group_blacklist_);
file::json_write_object_to_file(file, transaction_ids_);
file::json_write_object_to_file(file, sequence_ids_);
file::json_write_object_to_file(file, pending_messages_write_count_);
for (auto const& x : live_queue_) {
file::json_write_object_to_file(file, x);
}
for (auto const& supplier : saved_message_suppliers_) {
if (supplier == nullptr)
continue;
(*supplier)([&](auto const& record) {
file::json_write_object_to_file(file, record);
});
}
return true;
});
}
template <typename T>
static void readFromFile(FILE* file, T& value) {
auto parsed = file::json_read_object_from_file<T>(file);
if (!parsed.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed reading value from file";
return;
}
value = parsed.value();
}
void loadFromStorage() {
CHARGELAB_LOG_MESSAGE(info) << "Reading pending messages from storage";
storage_->read([&](auto file) {
offline_queue_.read([&]() {
std::optional<std::vector<uint8_t>> result = std::nullopt;
int length;
if (std::fread(&length, sizeof(length), 1, file) != 1)
return result;
if (length == -1)
return result;
if (length < 0 || length > kMaxStorageBlockSize) {
CHARGELAB_LOG_MESSAGE(error) << "Bad block length encountered reading historic data: " << length;
return result;
}
std::vector<uint8_t> buffer;
buffer.resize(length);
if (std::fread(buffer.data(), sizeof(uint8_t), buffer.size(), file) != buffer.size()) {
CHARGELAB_LOG_MESSAGE(error) << "Failed reading block data";
return result;
}
result = std::move(buffer);
return result;
});
{
readFromFile(file, active_group_ids_);
readFromFile(file, group_blacklist_);
readFromFile(file, transaction_ids_);
readFromFile(file, sequence_ids_);
readFromFile(file, pending_messages_write_count_);
}
// Loading previous live queue into offline queue
while (true) {
auto record = file::json_read_object_from_file<detail::PendingMessageWrapper>(file);
if (!record.has_value())
break;
offline_queue_.pushBack(record.value());
}
return true;
});
}
int calculateAllPendingSavedInfoSize() {
int total_bytes = 0;
total_bytes += offline_queue_.totalBytes();
total_bytes += sizeof(int); // integer terminator size
total_bytes += calculate_size(active_group_ids_) + 1; // Count the newline '\n' character
total_bytes += calculate_size(group_blacklist_) + 1; // Count the newline '\n' character
total_bytes += calculate_size(transaction_ids_) + 1; // Count the newline '\n' character
total_bytes += calculate_size(sequence_ids_) + 1; // Count the newline '\n' character
total_bytes += calculate_size(pending_messages_write_count_) + 1; // CCount the newline '\n' character
for (auto const& x : live_queue_) {
if (x.action_id1_6.has_value() && x.action_id1_6.value() == ocpp1_6::ActionId::kStartTransaction)
continue;
total_bytes += calculate_size(x) + 1; // Count the newline '\n' character
}
for (auto const& supplier : saved_message_suppliers_) {
if (supplier == nullptr)
continue;
(*supplier)([&](auto const& record) {
total_bytes += calculate_size(record) + 1; // Count the newline '\n' character
});
}
return total_bytes;
}
bool shouldRemoveSequenceId(detail::PendingMessageWrapper const& wrapper) {
if (!wrapper.action_id2_0 || wrapper.action_id2_0.value() != chargelab::ocpp2_0::ActionId::kTransactionEvent)
return false;
auto const request = read_json_from_string<ocpp2_0::TransactionEventRequest>(wrapper.payload);
if (!request.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Failed read TransactionEvent request from: " << wrapper.payload;
return false;
}
return request->eventType == chargelab::ocpp2_0::TransactionEventEnumType::kEnded;
}
private:
std::shared_ptr<Settings> settings_;
std::shared_ptr<SystemInterface> system_;
std::shared_ptr<StorageInterface> storage_;
std::default_random_engine random_engine_;
std::vector<detail::PendingMessageWrapper> live_queue_;
CompressedQueueCustom<detail::PendingMessageWrapper, detail::PendingMessageSerializer> offline_queue_;
OperationHolder<std::string> request_operation_;
std::optional<detail::PendingMessageWrapper> request_object_;
std::vector<std::shared_ptr<saved_message_supplier>> saved_message_suppliers_ {};
bool must_flush_to_disk_ = false;
int64_t request_id_;
std::unordered_set<int64_t> active_group_ids_;
std::unordered_set<int64_t> group_blacklist_;
std::unordered_map<int64_t, int> transaction_ids_;
std::unordered_map<int64_t, int> sequence_ids_;
std::optional<SteadyPointMillis> last_cache_update_ = std::nullopt;
std::optional<SteadyPointMillis> last_flush_to_disk_ = std::nullopt;
bool pending_messages_changed_ = false;
int32_t pending_messages_write_count_ = 0;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_PENDING_MESSAGES_MODULE_H

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_RESET_MODULE_H
#define CHARGELAB_OPEN_FIRMWARE_RESET_MODULE_H
#include "openocpp/module/common_templates.h"
#include "openocpp/module/connector_status_module.h"
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/common/settings.h"
namespace chargelab {
class ResetModule : public ServiceStatefulGeneral {
private:
static constexpr int kMinimumResetDelayMillis = 1*1000; // 3 seconds
public:
ResetModule(
std::shared_ptr<Settings> settings,
std::shared_ptr<SystemInterface> system,
std::shared_ptr<ConnectorStatusModule> connector_status_module
)
: settings_(std::move(settings)),
system_(std::move(system)),
connector_status_module_(std::move(connector_status_module))
{
}
~ResetModule() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting ResetModule";
}
public:
[[nodiscard]] bool pendingReset() const {
return hard_reset_threshold_.has_value();
}
/**
* Resets the charger when it's no longer in use with the provided reason code. This method is thread safe.
*
* @param reason
*/
void resetOnIdle(ocpp2_0::BootReasonEnumType reason) {
if (hard_reset_requested_) {
CHARGELAB_LOG_MESSAGE(info) << "Ignoring soft reset request - hard reset already scheduled";
return;
}
reset_reason_ = reason;
soft_reset_requested_ = true;
}
/**
* Resets the charger immediately with the provided reason code. This method is thread safe.
*
* @param reason
*/
void resetImmediately(ocpp2_0::BootReasonEnumType reason) {
reset_reason_ = reason;
hard_reset_requested_ = true;
}
private:
void runStep(ocpp1_6::OcppRemote&) override {
runStepCommon();
}
void runStep(ocpp2_0::OcppRemote&) override {
runStepCommon();
}
std::optional<ocpp2_0::ResponseToRequest <ocpp2_0::ResetResponse>>
onResetReq(const ocpp2_0::ResetRequest &req) override {
// B11.FR.09
if (req.evseId.has_value()) {
return ocpp2_0::ResetResponse {ocpp2_0::ResetStatusEnumType::kRejected};
}
switch (req.type) {
default:
CHARGELAB_LOG_MESSAGE(warning) << "Invalid reset request type - treating as OnIdle";
[[fallthrough]];
case ocpp2_0::ResetEnumType::kOnIdle:
if (hard_reset_requested_) {
CHARGELAB_LOG_MESSAGE(warning) << "Rejecting soft reset request - hard reset already scheduled";
return ocpp2_0::ResetResponse {ocpp2_0::ResetStatusEnumType::kRejected};
}
if (soft_reset_requested_) {
CHARGELAB_LOG_MESSAGE(warning) << "Rejecting soft reset request - soft reset already scheduled";
return ocpp2_0::ResetResponse {ocpp2_0::ResetStatusEnumType::kRejected};
}
soft_reset_requested_ = true;
// B12.FR.01
if (connector_status_module_->isChargingEnabled()) {
reset_reason_ = ocpp2_0::BootReasonEnumType::kScheduledReset;
return ocpp2_0::ResetResponse {ocpp2_0::ResetStatusEnumType::kScheduled};
}
break;
case ocpp2_0::ResetEnumType::kImmediate:
if (hard_reset_requested_) {
CHARGELAB_LOG_MESSAGE(warning) << "Rejecting soft reset request - hard reset already scheduled";
return ocpp2_0::ResetResponse {ocpp2_0::ResetStatusEnumType::kRejected};
}
hard_reset_requested_ = true;
break;
}
reset_reason_ = ocpp2_0::BootReasonEnumType::kRemoteReset;
return ocpp2_0::ResetResponse {ocpp2_0::ResetStatusEnumType::kAccepted};
}
std::optional<ocpp1_6::ResponseToRequest <ocpp1_6::ResetRsp>>
onResetReq(const ocpp1_6::ResetReq &req) override {
switch (req.type) {
case ocpp1_6::ResetType::kValueNotFoundInEnum:
CHARGELAB_LOG_MESSAGE(warning) << "Invalid reset request type - treating as Soft";
[[fallthrough]];
case ocpp1_6::ResetType::kSoft:
if (hard_reset_requested_) {
CHARGELAB_LOG_MESSAGE(warning) << "Rejecting soft reset request - hard reset already scheduled";
return ocpp1_6::ResetRsp{ocpp1_6::ResetStatus::kRejected};
}
if (soft_reset_requested_) {
CHARGELAB_LOG_MESSAGE(warning) << "Rejecting soft reset request - soft reset already scheduled";
return ocpp1_6::ResetRsp{ocpp1_6::ResetStatus::kRejected};
}
soft_reset_requested_ = true;
break;
case ocpp1_6::ResetType::kHard:
if (hard_reset_requested_) {
CHARGELAB_LOG_MESSAGE(warning) << "Rejecting soft reset request - hard reset already scheduled";
return ocpp1_6::ResetRsp{ocpp1_6::ResetStatus::kRejected};
}
hard_reset_requested_ = true;
break;
}
return ocpp1_6::ResetRsp {ocpp1_6::ResetStatus::kAccepted};
}
private:
void runStepCommon() {
ocpp2_0::BootReasonEnumType const reason = reset_reason_;
auto const now = system_->steadyClockNow();
bool running_transactions = connector_status_module_->isChargingEnabled();
if (hard_reset_requested_) {
if (!hard_reset_threshold_.has_value()) {
hard_reset_threshold_ = static_cast<SteadyPointMillis> (now + kMinimumResetDelayMillis);
connector_status_module_->setPendingReset(true, true);
CHARGELAB_LOG_MESSAGE(info) << "Hard reset requested - restarting in: " << kMinimumResetDelayMillis << " millis";
}
auto const delta = now - hard_reset_threshold_.value();
if (delta >= 0) {
CHARGELAB_LOG_MESSAGE(info) << "Resetting system";
hard_reset_threshold_ = std::nullopt;
settings_->CustomBootReason.setValue(reason.to_string());
settings_->saveIfModified();
system_->resetHard();
}
} else if (soft_reset_requested_) {
if (!running_transactions) {
if (!soft_reset_threshold_.has_value()) {
soft_reset_threshold_ = static_cast<SteadyPointMillis> (now + kMinimumResetDelayMillis);
connector_status_module_->setPendingReset(true, false);
CHARGELAB_LOG_MESSAGE(info) << "Soft reset requested - restarting in: " << kMinimumResetDelayMillis << " millis";
}
auto const delta = now - soft_reset_threshold_.value();
if (delta >= 0) {
CHARGELAB_LOG_MESSAGE(info) << "Resetting system";
soft_reset_threshold_ = std::nullopt;
settings_->CustomBootReason.setValue(reason.to_string());
settings_->saveIfModified();
system_->resetSoft();
}
} else {
soft_reset_threshold_ = std::nullopt;
// Tell the transaction module to stop transactions indirectly
connector_status_module_->setPendingReset(true, false);
CHARGELAB_LOG_MESSAGE(trace) << "Reset pending - waiting for running transactions to complete";
}
}
}
private:
std::shared_ptr<Settings> settings_;
std::shared_ptr<SystemInterface> system_;
std::shared_ptr<ConnectorStatusModule> connector_status_module_;
std::optional<SteadyPointMillis> hard_reset_threshold_ = std::nullopt;
std::optional<SteadyPointMillis> soft_reset_threshold_ = std::nullopt;
std::atomic<ocpp2_0::BootReasonEnumType> reset_reason_ = {ocpp2_0::BootReasonEnumType::kUnknown};
std::atomic<bool> soft_reset_requested_ = false;
std::atomic<bool> hard_reset_requested_ = false;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_RESET_MODULE_H

View File

@@ -0,0 +1,766 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_TRANSACTION_MODULE1_6_H
#define CHARGELAB_OPEN_FIRMWARE_TRANSACTION_MODULE1_6_H
#include "openocpp/interface/platform_interface.h"
#include "openocpp/interface/station_interface.h"
#include "openocpp/module/common_templates.h"
#include "openocpp/module/pending_messages_module.h"
#include "openocpp/module/boot_notification_module.h"
#include "openocpp/module/power_management_module1_6.h"
#include "openocpp/module/connector_status_module.h"
#include "openocpp/common/operation_holder.h"
#include "openocpp/common/logging.h"
#include <utility>
#include <random>
#include <unordered_set>
#include <deque>
#include <vector>
#include <memory>
#include <string>
#include <optional>
namespace chargelab {
namespace transaction_module1_6 {
struct TransactionContainer {
int connector_id;
uint64_t group_id;
std::string id_tag;
std::string start_transaction_request_id;
std::optional<int> transaction_id = std::nullopt;
std::optional<SteadyPointMillis> last_reading_timestamp = std::nullopt;
std::optional<SteadyPointMillis> last_clock_aligned_reading_timestamp = std::nullopt;
bool connector_unplugged_after_transaction_ended = false;
};
struct PendingStartRequest {
PendingStartRequest(std::shared_ptr<PlatformInterface> const& platform, int connector_id, std::string tag_id)
: created_timestamp {platform->steadyClockNow()},
pending_authorize_req {platform},
connector_id {connector_id},
tag_id {tag_id},
charging_profile {},
authorize_finished {false}
{
}
PendingStartRequest(std::shared_ptr<PlatformInterface> const& platform, ocpp1_6::RemoteStartTransactionReq const& req)
: created_timestamp {platform->steadyClockNow()},
pending_authorize_req {platform},
connector_id {req.connectorId.value_or(0)},
tag_id {req.idTag.value()},
charging_profile {req.chargingProfile},
authorize_finished {!platform->getSettings()->AuthorizeRemoteTxRequests.getValue()}
{
}
SteadyPointMillis created_timestamp;
OperationHolder<std::string> pending_authorize_req;
int connector_id;
std::string tag_id;
std::optional<ocpp1_6::ChargingProfile> charging_profile;
bool authorize_finished;
};
}
class TransactionModule1_6 : public ServiceStateful1_6 {
private:
static constexpr int kPriorityStartTransaction = 0;
static constexpr int kPriorityStopTransaction = 0;
static constexpr int kPriorityMeterValue = 5;
static const int kMaxReadingsBytes = 10*1024;
static const int kGeneralOperationMaxFailures = 4;
static constexpr int kOfflineTransactions = 3;
static constexpr char const* kDefaultTagId = "missing-tag";
static constexpr int kTransactionMessageRetryIntervalSeconds = 10;
static const int kSecondsPerDay = 86400; // 24*60*60
public:
TransactionModule1_6(
std::shared_ptr<PlatformInterface> const& platform,
std::shared_ptr<BootNotificationModule> boot_notification_module,
std::shared_ptr<PowerManagementModule1_6> power_management_module,
std::shared_ptr<PendingMessagesModule> pending_messages_module,
std::shared_ptr<ConnectorStatusModule> connector_status_module,
std::shared_ptr<StationInterface> station
) : platform_(platform),
boot_notification_module_(std::move(boot_notification_module)),
power_management_module_(std::move(power_management_module)),
pending_messages_module_(std::move(pending_messages_module)),
connector_status_module_(std::move(connector_status_module)),
station_(std::move(station))
{
settings_ = platform_->getSettings();
std::default_random_engine random_engine {std::random_device{}()};
std::uniform_int_distribution<int64_t> distribution(
std::numeric_limits<int64_t>::min(),
std::numeric_limits<int64_t>::max()
);
unique_index_ = distribution(random_engine);
stop_transaction_supplier_ = std::make_shared<PendingMessagesModule::saved_message_supplier> (
[&](std::function<void(detail::PendingMessageWrapper const&)> const& processor) {
for (auto& entry : active_transactions_) {
if (!entry.second.has_value())
continue;
auto const request = generateStopRequest(entry.first, ocpp1_6::Reason::kPowerLoss);
if (request.has_value())
processor(request.value());
}
}
);
pending_messages_module_->registerOnSaveMessageSupplier(stop_transaction_supplier_);
}
~TransactionModule1_6() override {
CHARGELAB_LOG_MESSAGE(debug) << "Deleting TransactionModule1_6";
}
private:
void runStep(ocpp1_6::OcppRemote& remote) override {
// If a reset was requested stop all active transactions
if (connector_status_module_->getPendingReset()) {
for (auto& x : active_transactions_) {
if (!x.second.has_value())
continue;
CHARGELAB_LOG_MESSAGE(info) << "Stopping transaction (pending reset) - transaction ID: " << x.second->transaction_id;
stopTransaction(x.first, connector_status_module_->getResetReasonHard() ? ocpp1_6::Reason::kHardReset : ocpp1_6::Reason::kSoftReset);
}
return; // We should not handle any other events when reset is pending.
}
// Process an RFID tap if there was a state change
auto id_token = station_->readToken1_6();
if (id_token != last_rfid_tag_id_) {
last_rfid_tag_id_ = id_token;
if (id_token.has_value())
processRfidTap(id_token->value());
}
// Process plugged in state changes (PlugAndCharge and stopping transaction on disconnect)
for (auto const& entry: station_->getConnectorMetadata()) {
// Only take action if the connector state has changed. If, for example, a pending RemoteStarTransaction
// request timed out do not attempt an autostart transaction until the cable is unplugged and plugged
// back in.
auto const connector_status = station_->pollConnectorStatus(entry.first);
if (!connector_status.has_value())
continue;
auto const id = entry.second.connector_id1_6;
if (connector_status->vehicle_connected == last_plugged_in_state_[id])
continue;
last_plugged_in_state_[id] = connector_status->vehicle_connected;
processVehicleConnectedStateChanged(id, connector_status->vehicle_connected);
}
// First process any pending start requests for a specific connector, then process any for the station
for (auto& entry : pending_start_req_) {
if (entry.first > 0)
processPendingStartRequest(remote, entry.second);
}
for (auto& entry : pending_start_req_) {
if (entry.first <= 0)
processPendingStartRequest(remote, entry.second);
}
std::vector<std::optional<ocpp2_0::EVSEType>> pending;
for (auto& entry : pending_start_req_) {
if (entry.second.has_value()) {
if (entry.first > 0) {
pending.emplace_back(ocpp2_0::EVSEType {entry.first, 1});
} else if (entry.first == 0 && entry.second != std::nullopt) {
pending.emplace_back(std::nullopt);
}
}
}
connector_status_module_->setPendingStartRequests(pending);
// Process transactions
for (auto& entry : active_transactions_) {
if (!entry.second.has_value())
continue;
auto const evse = station_->lookupConnectorId1_6(entry.first);
if (!evse.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state: connector did not exist for running transaction - dropping: " << entry.second->transaction_id;
entry.second = std::nullopt;
continue;
}
bool first_reading = !entry.second->last_reading_timestamp.has_value();
if (shouldTakeReading(entry.second->last_reading_timestamp, settings_->MeterValueSampleInterval.getValue()))
addReading(entry.second.value(), first_reading ? ocpp1_6::ReadingContext::kTransactionBegin : ocpp1_6::ReadingContext::kSamplePeriodic);
first_reading = !entry.second->last_clock_aligned_reading_timestamp.has_value();
if (shouldTakeReading(entry.second->last_clock_aligned_reading_timestamp, settings_->ClockAlignedDataInterval.getValue()))
addReading(entry.second.value(), first_reading ? ocpp1_6::ReadingContext::kTransactionBegin : ocpp1_6::ReadingContext::kSampleClock);
}
#if 0 // For OCTT _012
for (auto it = pending_stop_transaction_times_.begin(); it != pending_stop_transaction_times_.end(); ) {
if (platform_->steadyClockNow() - it->second > 5000 ) {
stopTransaction(it->first, ocpp1_6::Reason::kRemote);
it = pending_stop_transaction_times_.erase(it); // erase returns the next iterator
} else {
++it;
}
}
#endif
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::RemoteStartTransactionRsp>> onRemoteStartTransactionReq(
const ocpp1_6::RemoteStartTransactionReq &req
) override {
int connector_id = 0;
if (req.connectorId.has_value()) {
connector_id = req.connectorId.value();
bool found = false;
for (auto const& entry : station_->getConnectorMetadata()) {
if (entry.second.connector_id1_6 == connector_id) {
found = true;
break;
}
}
if (!found) {
return ocpp1_6::RemoteStartTransactionRsp{ocpp1_6::RemoteStartStopStatus::kRejected};
}
}
if (req.chargingProfile.has_value()) {
// Note: as per 5.16.2. the profile purpose must be TxProfile
if (req.chargingProfile->chargingProfilePurpose != ocpp1_6::ChargingProfilePurposeType::kTxProfile)
return ocpp1_6::RemoteStartTransactionRsp {ocpp1_6::RemoteStartStopStatus::kRejected};
// Note: as per 5.16.2. the transaction ID must not be set
if (req.chargingProfile->transactionId.has_value())
return ocpp1_6::RemoteStartTransactionRsp {ocpp1_6::RemoteStartStopStatus::kRejected};
}
bool available_connector = true;
// check if there is an active transaction already
for (auto& x : active_transactions_) {
if (req.connectorId && req.connectorId == x.first && x.second) {
available_connector = false;
break;
}
}
if (!available_connector) {
return ocpp1_6::RemoteStartTransactionRsp{ocpp1_6::RemoteStartStopStatus::kRejected};
}
// Note: adopting the convention that a new RemoteStartTransaction request will replace an existing pending
// one, rather than be blocked by it.
pending_start_req_[connector_id] = transaction_module1_6::PendingStartRequest {platform_, req};
return ocpp1_6::RemoteStartTransactionRsp {ocpp1_6::RemoteStartStopStatus::kAccepted};
}
void onAuthorizeRsp(
const std::string &unique_id,
const std::variant<ocpp1_6::AuthorizeRsp, ocpp1_6::CallError> &rsp
) override {
for (auto& entry : pending_start_req_) {
if (!entry.second.has_value())
continue;
if (entry.second->pending_authorize_req != unique_id)
continue;
if (std::holds_alternative<ocpp1_6::AuthorizeRsp>(rsp)) {
// Note: only clearing the operation if a valid response was received, otherwise treating as absent
// and retrying after configured timeout.
entry.second->pending_authorize_req = kNoOperation;
auto const& value = std::get<ocpp1_6::AuthorizeRsp>(rsp);
CHARGELAB_LOG_MESSAGE(info) << "Received authorize response: " << value;
switch (value.idTagInfo.status) {
case ocpp1_6::AuthorizationStatus::kAccepted:
entry.second->authorize_finished = true;
CHARGELAB_LOG_MESSAGE(info) << "Set authorize_finished to true: " << entry.second->authorize_finished;
break;
case ocpp1_6::AuthorizationStatus::kValueNotFoundInEnum:
case ocpp1_6::AuthorizationStatus::kBlocked:
case ocpp1_6::AuthorizationStatus::kExpired:
case ocpp1_6::AuthorizationStatus::kInvalid:
case ocpp1_6::AuthorizationStatus::kConcurrentTx:
entry.second = std::nullopt;
break;
}
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to Authorize request: " << std::get<ocpp1_6::CallError> (rsp);
}
}
}
void onStartTransactionRsp(
const std::string &unique_id,
const std::variant<ocpp1_6::StartTransactionRsp, ocpp1_6::CallError> &rsp
) override {
// Note: this is processed here in addition to pending_messages to allow this module to take action if the
// back-end does not accept the provided idTag.
for (auto& entry : active_transactions_) {
if (!entry.second.has_value())
continue;
if (unique_id != entry.second->start_transaction_request_id)
continue;
if (std::holds_alternative<ocpp1_6::StartTransactionRsp>(rsp)) {
auto const &value = std::get<ocpp1_6::StartTransactionRsp>(rsp);
entry.second->transaction_id = value.transactionId;
switch (value.idTagInfo.status) {
case ocpp1_6::AuthorizationStatus::kAccepted:
power_management_module_->onActiveTransactionIdAssigned(entry.first, value.transactionId);
break;
case ocpp1_6::AuthorizationStatus::kValueNotFoundInEnum:
case ocpp1_6::AuthorizationStatus::kBlocked:
case ocpp1_6::AuthorizationStatus::kExpired:
case ocpp1_6::AuthorizationStatus::kInvalid:
case ocpp1_6::AuthorizationStatus::kConcurrentTx:
// TODO: Support MaxEnergyOnInvalidId?
stopTransaction(entry.first, ocpp1_6::Reason::kDeAuthorized);
break;
}
} else {
CHARGELAB_LOG_MESSAGE(warning) << "Error response to StartTransaction request: " << std::get<ocpp1_6::CallError>(rsp);
}
}
}
std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::RemoteStopTransactionRsp>> onRemoteStopTransactionReq(
const ocpp1_6::RemoteStopTransactionReq &req
) override {
for (auto& entry : active_transactions_) {
if (!entry.second.has_value())
continue;
if (!entry.second->transaction_id.has_value())
continue;
if (entry.second->transaction_id.value() != req.transactionId)
continue;
#if 0 // For OCTT _012
pending_stop_transaction_times_[entry.first] = platform_->steadyClockNow();
#else
stopTransaction(entry.first, ocpp1_6::Reason::kRemote);
#endif
return ocpp1_6::RemoteStopTransactionRsp {ocpp1_6::RemoteStartStopStatus::kAccepted};
}
// TODO: unrecognized transaction module behaves badly with this one. Maybe fold that implementation into
// this one?
//return ocpp1_6::RemoteStopTransactionRsp {ocpp1_6::RemoteStartStopStatus::kRejected};
if (!settings_->StopTransactionWithDifferentId.getValue())
return ocpp1_6::RemoteStopTransactionRsp {ocpp1_6::RemoteStartStopStatus::kRejected};
if (!force_stop_transaction_.has_value()) {
force_stop_transaction_ = req.transactionId;
return ocpp1_6::RemoteStopTransactionRsp {ocpp1_6::RemoteStartStopStatus::kAccepted};
} else {
return ocpp1_6::RemoteStopTransactionRsp {ocpp1_6::RemoteStartStopStatus::kRejected};
}
return std::nullopt;
}
std::optional<ocpp1_6::ResponseToRequest <ocpp1_6::TriggerMessageRsp>>
onTriggerMessageReq(const ocpp1_6::TriggerMessageReq &req) override {
if (req.requestedMessage != ocpp1_6::MessageTrigger::kMeterValues)
return std::nullopt;
for (auto& entry : active_transactions_) {
if (!entry.second.has_value())
continue;
if (req.connectorId.has_value() && req.connectorId.value() != entry.first)
continue;
addReading(entry.second.value(), ocpp1_6::ReadingContext::kTrigger);
return ocpp1_6::TriggerMessageRsp {ocpp1_6::TriggerMessageStatus::kAccepted};
}
return ocpp1_6::TriggerMessageRsp {ocpp1_6::TriggerMessageStatus::kRejected};
}
private:
void startTransaction(
int connector_id,
std::string const& id_tag,
std::optional<ocpp1_6::ChargingProfile> const& charging_profile = std::nullopt
) {
auto const group_id = unique_index_++;
auto const evse = station_->lookupConnectorId1_6(connector_id);
if (!evse.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - connector ID does not exist: " << connector_id;
return;
}
auto const status = station_->pollConnectorStatus(evse.value());
if (!status.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - failed fetching connector status for EVSE: " << evse;
return;
}
auto start_transaction_request_id = pending_messages_module_->sendRequest1_6(
ocpp1_6::StartTransactionReq {
connector_id,
id_tag,
(int)status->meter_watt_hours,
std::nullopt,
platform_->systemClockNow()
},
PendingMessagePolicy {
PendingMessageType::kTransactionEvent,
group_id,
settings_->TransactionMessageAttempts.getValue(),
settings_->TransactionMessageRetryInterval.getValue(),
kPriorityStartTransaction,
false,
false,
true
}
);
// Setting enabling charging earlier for OCTT test case _010
station_->setChargingEnabled(evse.value(), true);
active_transactions_[connector_id] = transaction_module1_6::TransactionContainer {
connector_id,
group_id,
id_tag,
start_transaction_request_id
};
power_management_module_->onActiveTransactionStarted(
connector_id,
charging_profile
);
}
std::optional<detail::PendingMessageWrapper> generateStopRequest(int connector_id, ocpp1_6::Reason const& reason) {
auto const& transaction = active_transactions_[connector_id];
if (!transaction.has_value())
return std::nullopt;
auto const evse = station_->lookupConnectorId1_6(connector_id);
if (!evse.has_value()) {
// Note: the transaction is not stopped here intentionally; we'd have to provide a bad watt hour reading
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - connector ID does not exist: " << connector_id;
return std::nullopt;
}
auto const status = station_->pollConnectorStatus(evse.value());
if (!status.has_value()) {
// Note: the transaction is not stopped here intentionally; we'd have to provide a bad watt hour reading
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - failed fetching connector status for EVSE: " << evse;
return std::nullopt;
}
return pending_messages_module_->generateRequest1_6(
ocpp1_6::StopTransactionReq {
transaction->id_tag,
(int)status->meter_watt_hours,
platform_->systemClockNow(),
transaction->transaction_id.value_or(0),
reason,
{} // TODO: Stop transaction data
},
PendingMessagePolicy {
PendingMessageType::kTransactionEvent,
transaction->group_id,
settings_->TransactionMessageAttempts.getValue(),
settings_->TransactionMessageRetryInterval.getValue(),
kPriorityStopTransaction,
false,
false
}
);
power_management_module_->onActiveTransactionFinished(connector_id);
}
void stopTransaction(int connector_id, ocpp1_6::Reason const& reason) {
std::optional<transaction_module1_6::TransactionContainer> transaction = std::nullopt;
std::swap(transaction, active_transactions_[connector_id]);
if (!transaction.has_value())
return;
auto const evse = station_->lookupConnectorId1_6(connector_id);
if (!evse.has_value()) {
// Note: the transaction is not stopped here intentionally; we'd have to provide a bad watt hour reading
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - connector ID does not exist: " << connector_id;
return;
}
auto const status = station_->pollConnectorStatus(evse.value());
station_->setChargingEnabled(evse.value(), false);
if (!status.has_value()) {
// Note: the transaction is not stopped here intentionally; we'd have to provide a bad watt hour reading
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - failed fetching connector status for EVSE: " << evse;
return;
}
pending_messages_module_->sendRequest1_6(
ocpp1_6::StopTransactionReq {
transaction->id_tag,
(int)status->meter_watt_hours,
platform_->systemClockNow(),
transaction->transaction_id.value_or(0),
reason,
{} // TODO: Stop transaction data
},
PendingMessagePolicy {
PendingMessageType::kTransactionEvent,
transaction->group_id,
settings_->TransactionMessageAttempts.getValue(),
settings_->TransactionMessageRetryInterval.getValue(),
kPriorityStopTransaction,
false,
false,
true
}
);
power_management_module_->onActiveTransactionFinished(connector_id);
}
void processRfidTap(std::string const& id_tag) {
// Check if a running transaction already uses this tag; if so it should be interpreted as a stop
// request.
for (auto& x : active_transactions_) {
if (!x.second.has_value())
continue;
if (string::EqualsIgnoreCaseAscii(x.second->id_tag, id_tag)) {
stopTransaction(x.first, ocpp1_6::Reason::kLocal);
return;
}
}
// Need to check if there's availabe connector for another transaction, otherwise ignore the rfid tap event.
// This is for the OCTT 1.6 test case _068 where a different RFID should not trigger the sending of the
// Authorize request if there is an active transaction.
bool found_available_connector = false;
for (auto const& entry: station_->getConnectorMetadata()) {
if (entry.first.id == 0) continue;
auto const connector_status = station_->pollConnectorStatus(entry.first);
if (!connector_status.has_value())
continue;
if (connector_status->connector_available) {
found_available_connector = true;
break;
}
}
if (!found_available_connector)
return;
// Add the PlugAndCharge entry
pending_start_req_[0] = transaction_module1_6::PendingStartRequest{platform_, 0, id_tag};
}
void processVehicleConnectedStateChanged(int connector_id, bool connected) {
if (active_transactions_[connector_id].has_value()) {
if (!connected) {
stopTransaction(connector_id, ocpp1_6::Reason::kEVDisconnected);
}
// Note: PlugAndCharge should only trigger if there's no activate transaction.
return;
}
auto plug_and_charge_id = settings_->PlugAndChargeId.getValue();
if (connected && !plug_and_charge_id.empty()) {
// If there's another potentially applicable start operation pending (remote start request, RFID tap,
// etc) it should take precedence and autostart should be ignored.
if (pending_start_req_[0].has_value() || pending_start_req_[connector_id].has_value())
return;
// Add the PlugAndCharge entry
pending_start_req_[connector_id] = transaction_module1_6::PendingStartRequest {
platform_,
connector_id,
plug_and_charge_id
};
}
}
void processPendingStartRequest(ocpp1_6::OcppRemote& remote, std::optional<transaction_module1_6::PendingStartRequest>& pending) {
if (!pending.has_value())
return;
// If the request has expired remove it
auto const elapsed = platform_->steadyClockNow() - pending->created_timestamp;
if (elapsed >= settings_->ConnectionTimeOut.getValue()*1000) {
pending = std::nullopt;
return;
}
// Attempt to authorize the request if necessary
if (!pending->authorize_finished) {
if (treatAsConnected()) {
if (!pending->pending_authorize_req.operationInProgress()) {
pending->pending_authorize_req.setWithTimeout(
settings_->DefaultMessageTimeout.getValue(),
remote.sendAuthorizeReq(ocpp1_6::AuthorizeReq {pending->tag_id})
);
}
return;
} else if (!settings_->AllowOfflineTxForUnknownId.getValue()) {
return;
}
}
// Attempt to start a transaction
for (auto const& entry : station_->getConnectorMetadata()) {
auto const id = entry.second.connector_id1_6;
if (pending->connector_id != 0 && pending->connector_id != id)
continue;
auto const status = station_->pollConnectorStatus(entry.first);
if (!status.has_value() || !status->vehicle_connected)
continue;
if (active_transactions_[id].has_value())
continue;
// Start new transaction
startTransaction(id, pending->tag_id, pending->charging_profile);
pending = std::nullopt;
}
}
void addReading(
transaction_module1_6::TransactionContainer& entry,
ocpp1_6::ReadingContext const& context
) {
auto const evse = station_->lookupConnectorId1_6(entry.connector_id);
if (!evse.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - connector ID does not exist: " << entry.connector_id;
return;
}
auto enabled = parseMeasurandsString(settings_->MeterValuesSampledData.getValue());
auto sampled_values = station_->pollMeterValues1_6(evse.value());
sampled_values.erase(
std::remove_if(
sampled_values.begin(),
sampled_values.end(),
[&](ocpp1_6::SampledValue const& value) {
auto measurand = value.measurand.value_or(ocpp1_6::Measurand::kEnergyActiveImportRegister);
return enabled.find(measurand) == enabled.end();
}
),
sampled_values.end()
);
// Update the context
for (auto& value : sampled_values)
value.context = context;
// TODO: It may be helpful to batch these while offline so that fewer requests are sent when the station
// reconnects instead of persisting individual samples.
pending_messages_module_->sendRequest1_6(
ocpp1_6::MeterValuesReq {
entry.connector_id,
entry.transaction_id.value_or(0),
{
ocpp1_6::MeterValue {
platform_->systemClockNow(),
std::move(sampled_values)
}
}
},
PendingMessagePolicy {
PendingMessageType::kTransactionEvent,
entry.group_id,
settings_->TransactionMessageAttempts.getValue(),
settings_->TransactionMessageRetryInterval.getValue(),
kPriorityMeterValue,
false,
false
}
);
}
bool shouldTakeReading(std::optional<SteadyPointMillis>& last, int interval_seconds) {
// Treat a configured interval of 0 (or negative) as disabled
if (interval_seconds <= 0)
return false;
auto const now = platform_->steadyClockNow();
if (last.has_value()) {
auto const delta = now - last.value();
if (delta < interval_seconds*1000)
return false;
}
last = now;
return true;
}
bool treatAsConnected() {
if (!boot_notification_module_->registrationComplete())
return false;
auto const websocket = platform_->ocppConnection();
if (websocket == nullptr)
return false;
return websocket->isConnected();
}
static std::unordered_set<ocpp1_6::Measurand::Value> parseMeasurandsString(std::string const& measurands) {
// comma separated list, e.g. "Current.Import,Energy.Active.Import.Register"
std::unordered_set<ocpp1_6::Measurand::Value> ret;
string::SplitVisitor(measurands, ",", [&](std::string const& value) {
auto value_as_enum = ocpp1_6::Measurand::from_string(value);
if (value_as_enum == ocpp1_6::Measurand::kValueNotFoundInEnum) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected measurand: " << value;
} else {
ret.insert(value_as_enum);
}
});
return ret;
}
private:
std::shared_ptr<PlatformInterface> platform_;
std::shared_ptr<BootNotificationModule> boot_notification_module_;
std::shared_ptr<PowerManagementModule1_6> power_management_module_;
std::shared_ptr<PendingMessagesModule> pending_messages_module_;
std::shared_ptr<ConnectorStatusModule> connector_status_module_;
std::shared_ptr<StationInterface> station_;
std::shared_ptr<PendingMessagesModule::saved_message_supplier> stop_transaction_supplier_;
std::shared_ptr<Settings> settings_;
std::atomic<uint64_t> unique_index_;
// key: connector ID
std::unordered_map<int, std::optional<transaction_module1_6::PendingStartRequest>> pending_start_req_;
// key: connector ID
std::unordered_map<int, bool> last_plugged_in_state_;
std::optional<ocpp1_6::IdToken> last_rfid_tag_id_ = std::nullopt;
// Note: a transaction will remain here until the connector is unplugged or a new transaction starts
// key: connector ID
std::unordered_map<int, std::optional<transaction_module1_6::TransactionContainer>> active_transactions_;
std::optional<int> connector_hold_id_ {std::nullopt};
std::optional<int> force_stop_transaction_ = std::nullopt;
#if 0 // For OCTT _012
std::unordered_map<int, SteadyPointMillis> pending_stop_transaction_times_;
#endif
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_TRANSACTION_MODULE1_6_H

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,324 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_DATE_TIME_H
#define CHARGELAB_OPEN_FIRMWARE_DATE_TIME_H
#include "openocpp/model/system_types.h"
#include "openocpp/helpers/json.h"
#include "openocpp/common/logging.h"
#include <string>
#include <utility>
#include <ctime>
#include <stdio.h>
#include <chrono>
#if defined (USE_HH_DATE)
#include <date/date.h>
#endif
namespace chargelab::common {
class DateTime {
static constexpr int kMilliSecondSize = 3;
public:
DateTime() = default;
DateTime(const DateTime& other) = default;
DateTime& operator=(const DateTime& other) = default;
DateTime(SystemTimeMillis timestamp) : text_(timestampToText(timestamp)), timestamp_(timestamp) {
}
DateTime(std::string text) : text_(text), timestamp_(textToTimestamp(text)) {
}
[[nodiscard]] std::optional<std::string> getText() const {
return text_;
}
[[nodiscard]] std::optional<SystemTimeMillis> getTimestamp() const {
return timestamp_;
}
static void write_json(json::JsonWriter& writer, DateTime const& value) {
if (value.text_.has_value()) {
writer.String(value.text_.value());
} else {
writer.Null();
}
}
static bool read_json(json::JsonReader& reader, DateTime& value) {
auto const token = reader.nextToken();
if (!token.has_value())
return false;
if (!std::holds_alternative<json::StringType>(token.value()))
return false;
auto const& text = std::get<json::StringType>(token.value());
value.text_ = std::string{text.str, text.length};
value.timestamp_ = textToTimestamp(value.text_.value());
return true;
}
bool operator==(DateTime const& rhs) const {
return getTimestamp() == rhs.getTimestamp();
}
bool operator!=(DateTime const& rhs) const {
return !operator==(rhs);
}
[[nodiscard]] bool isAfter(SystemTimeMillis ts, bool default_value) const {
if (!timestamp_.has_value())
return default_value;
return timestamp_.value() - ts > 0;
}
[[nodiscard]] bool isBefore(SystemTimeMillis ts, bool default_value) const {
if (!timestamp_.has_value())
return default_value;
return timestamp_.value() - ts < 0;
}
#if !defined(USE_HH_DATE)
static std::optional<std::string> timestampToText(SystemTimeMillis const& timestamp) {
time_t time = timestamp/1000;
int millisecond = timestamp%1000;
if (millisecond < 0) { // adjust the minus value to be positive, borrow 1 second
time -= 1;
millisecond += 1000;
}
auto tm = gmtime(&time);
char buf[100]{};
sprintf(buf, "%04d-%02d-%02dT%02d:%02d:%02dZ", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec);
return std::string(buf);
}
static std::optional<SystemTimeMillis> textToTimestamp(std::string const& text) {
auto text2 = removeExcessiveFractionDigits(text);
auto timezone_minutes = retrieveTimezoneMinutes(text2);
if (!timezone_minutes.has_value()) {
CHARGELAB_LOG_MESSAGE(error) << "wrong time zone format:" << text;
return std::nullopt;
}
// "2022-12-17T14:37:09.894Z"
auto tm = parseTm(text2);
if (tm) {
std::int64_t ret = timegm2(&tm.value()); // timegm() is not supported by ESP32
ret -= timezone_minutes.value() * 60;
return static_cast<SystemTimeMillis>(ret * 1000 + retrieveMillisecond(text2));
}
CHARGELAB_LOG_MESSAGE(error) << "wrong date time format:" << text;
return std::nullopt;
}
#else
static std::optional<std::string> timestampToText(SystemTimeMillis const& timestamp) {
date::sys_time<std::chrono::milliseconds> ts {std::chrono::milliseconds(timestamp)};
CHARGELAB_TRY {
std::ostringstream out;
// TODO: Need to confirm this *always* uses UTC timezone
out << date::format("%FT%TZ", ts);
if (!out.fail()) {
return out.str();
}
} CHARGELAB_CATCH {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected exception converting timestamp to date/time string: " << e.what();
}
return std::nullopt;
}
static std::optional<SystemTimeMillis> textToTimestamp(std::string const& text) {
auto text2 = removeExcessiveFractionDigits(text);
date::sys_time<std::chrono::milliseconds> ts;
CHARGELAB_TRY {
std::istringstream in{text2};
in >> date::parse("%FT%TZ", ts);
if (!in.fail()) {
auto const millis = std::chrono::duration_cast<std::chrono::milliseconds>(ts.time_since_epoch()).count();
return static_cast<SystemTimeMillis> (millis);
}
} CHARGELAB_CATCH {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected exception converting date/time string to timestamp: " << e.what();
}
CHARGELAB_TRY {
std::istringstream in{text2};
in >> date::parse("%FT%T%Ez", ts);
if (!in.fail()) {
auto const millis = std::chrono::duration_cast<std::chrono::milliseconds>(ts.time_since_epoch()).count();
return static_cast<SystemTimeMillis> (millis);
}
} CHARGELAB_CATCH {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected exception converting date/time string to timestamp: " << e.what();
}
return std::nullopt;
}
#endif
private:
static std::string removeExcessiveFractionDigits(std::string const& time_text) {
int pos = time_text.find('.');
if (pos == -1) return time_text;
int decimal_digit_count = 0;
unsigned int i = pos + 1;
while (i < time_text.length() && decimal_digit_count < kMilliSecondSize) {
if (!std::isdigit(time_text[i])) break;
++decimal_digit_count;
++i;
}
if (decimal_digit_count == kMilliSecondSize) {
auto start = i;
while (i < time_text.length()) {
if (!std::isdigit(time_text[i])) break;
++i;
}
if (start != i) {
return time_text.substr(0, start) + time_text.substr(i);
}
}
return time_text;
}
static int retrieveMillisecond(std::string const& timestamp) {
auto find = timestamp.find('.');
if (find == std::string::npos) {
return 0;
}
int milliseconds = 0;
unsigned int multiplier = 100;
for (unsigned int i = find+1; i < timestamp.length(); ++i) {
if (!std::isdigit(timestamp[i])) break;
milliseconds += (timestamp[i] - '0') * multiplier;
multiplier /= 10;
}
return milliseconds;
}
// timestamp: e.g. "2022-10-17T14:37:09.894+03:00" https://www.w3.org/TR/NOTE-datetime
static std::optional<int> retrieveTimezoneMinutes(std::string & timestamp) {
auto find = timestamp.find('Z');
if (find != std::string::npos) {
timestamp = timestamp.substr(0, find); // ignore anything after 'Z'
return 0;
}
bool negative = false;
find = timestamp.find('+');
if (find == std::string::npos) {
find = timestamp.rfind('-');
negative = true;
}
if (find == std::string::npos) return std::nullopt;
int hour = 0;
int minute = 0;
auto find2 = timestamp.find(':', find);
if (find2 == std::string::npos) { // only have hours
if (sscanf(timestamp.c_str() + find + 1, "%2d", &hour) != 1) return std::nullopt;
} else {
if (sscanf(timestamp.c_str() + find + 1, "%2d:%2d", &hour, &minute) != 2) return std::nullopt;
}
timestamp = timestamp.substr(0, find); // remove timezone from the timestamp for future process
return (hour*60 + minute)*(negative ? -1 : 1);
}
static std::optional<std::tm> parseTm(std::string const& timestamp) {
int year, month, day, hour, minute, second;
if (sscanf(timestamp.c_str(), "%4d-%2d-%2dT%2d:%2d:%2d", &year, &month, &day, &hour, &minute, &second) != 6) {
return std::nullopt;
}
//
if (month < 1 || month > 12 || day < 1 || day > getDaysOfMonth(year, month) || hour < 0 || hour >= 24 ||
minute < 0 || minute >= 60 || second < 0 || second >= 60) { // no leap seconds supported
return std::nullopt;
}
std::tm tm{};
tm.tm_sec = second;
tm.tm_min = minute;
tm.tm_hour = hour;
tm.tm_mday = day;
tm.tm_mon = month-1;
tm.tm_year = year-1900;
return tm;
};
static std::time_t timegm2(std::tm const *tm) {
if (tm == nullptr) return -1;
if (tm->tm_mon >= 12 || tm->tm_hour >= 24 || tm->tm_min >= 60 || tm->tm_sec >= 61) {
return -1;
}
if (getDaysOfMonth(tm->tm_year + 1900, tm->tm_mon + 1) == -1) {
return -1;
}
// get the day to the tm date since 1900-01-01 00:00:00 +0000, UTC instead of the Epoch (1970-01-01 00:00:00 +0000, UTC)
std::int64_t total_days = 0; //
int year = tm->tm_year + 1900;
auto const year_since_epch = year - 1970;
int leap_years = totalLeapYears(year - 1) - totalLeapYears(1970);
total_days = year_since_epch*365 + leap_years;
for (int i = 0; i < tm->tm_mon; ++i) {
total_days += getDaysOfMonth(year, i+1);
}
total_days += tm->tm_mday - 1; // tm_mday is 1-31
std::int64_t total_hours = total_days*24 + tm->tm_hour;
std::time_t total_seconds = (total_hours*60 + tm->tm_min)*60 + tm->tm_sec;
return total_seconds /*+ kSecondsFrom1990*/;
}
static bool isLeapYear(unsigned int year) {
return (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0));
}
// month: 1-based
static int getDaysOfMonth(unsigned int year, int month) {
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 4:
case 6:
case 9:
case 11:
return 30;
case 2:
return isLeapYear(year) ? 29 : 28;
default:
return -1;
}
}
static int totalLeapYears(int year) {
return year/4 - year/100 + year/400;
}
private:
std::optional<std::string> text_ = std::nullopt;
std::optional<SystemTimeMillis> timestamp_ = std::nullopt;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_DATE_TIME_H

View File

@@ -0,0 +1,12 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_PROTOCOL_CONSTANTS_H
#define CHARGELAB_OPEN_FIRMWARE_PROTOCOL_CONSTANTS_H
namespace chargelab {
class ProtocolConstants {
public:
static constexpr char const* kProtocolOcpp1_6 = "ocpp1.6";
static constexpr char const* kProtocolOcpp2_0_1 = "ocpp2.0.1";
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_PROTOCOL_CONSTANTS_H

View File

@@ -0,0 +1,211 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_RAW_JSON_H
#define CHARGELAB_OPEN_FIRMWARE_RAW_JSON_H
#include "openocpp/helpers/json.h"
#include <string>
#include <memory>
namespace chargelab::common {
namespace detail {
class RawJsonInterface {
public:
virtual ~RawJsonInterface() = default;
virtual std::string data() const = 0;
virtual void write_json(json::JsonWriter& writer) const = 0;
};
template <typename T>
class RawJsonLazy : public RawJsonInterface {
public:
RawJsonLazy(T value) : value_(std::move(value)) {}
std::string data() const override {
return write_json_to_string(value_);
}
void write_json(json::JsonWriter& writer) const override {
json::WriteValue<T>::write_json(writer, value_);
}
private:
T value_;
};
}
class RawJson {
private:
using text_type = std::string;
using lazy_type = std::shared_ptr<detail::RawJsonInterface>;
using data_type = std::variant<text_type, lazy_type>;
public:
RawJson() = default;
RawJson(std::string data) : data_(std::move(data)) {}
RawJson(std::shared_ptr<detail::RawJsonInterface> data) : data_(std::move(data)) {}
bool operator==(RawJson const& rhs) const {
return data() == rhs.data();
}
bool operator!=(RawJson const& rhs) const {
return !operator==(rhs);
}
std::string data() const {
if (std::holds_alternative<text_type>(data_)) {
return std::get<text_type>(data_);
} else {
return std::get<lazy_type>(data_)->data();
}
}
static bool read_json(json::JsonReader& reader, RawJson& value) {
stream::StringWriter stream;
json::JsonWriter writer {stream};
int object_stack = 0;
int array_stack = 0;
do {
auto const& next = reader.nextToken();
if (!next.has_value())
return false;
auto const& token = next.value();
if (std::holds_alternative<json::StartObjectType>(token)) {
writer.StartObject();
object_stack++;
} else if (std::holds_alternative<json::EndObjectType>(token)) {
writer.EndObject();
object_stack--;
} else if (std::holds_alternative<json::StartArrayType>(token)) {
writer.StartArray();
array_stack++;
} else if (std::holds_alternative<json::EndArrayType>(token)) {
writer.EndArray();
array_stack--;
} else if (std::holds_alternative<json::NullType>(token)) {
writer.Null();
} else if (std::holds_alternative<json::BoolType>(token)) {
auto const& x = std::get<json::BoolType>(token);
writer.Bool(x.value);
} else if (std::holds_alternative<json::IntType>(token)) {
auto const& x = std::get<json::IntType>(token);
writer.Int(x.value);
} else if (std::holds_alternative<json::UintType>(token)) {
auto const& x = std::get<json::UintType>(token);
writer.Uint(x.value);
} else if (std::holds_alternative<json::Int64Type>(token)) {
auto const& x = std::get<json::Int64Type>(token);
writer.Int64(x.value);
} else if (std::holds_alternative<json::Uint64Type>(token)) {
auto const& x = std::get<json::Uint64Type>(token);
writer.Uint64(x.value);
} else if (std::holds_alternative<json::DoubleType>(token)) {
auto const& x = std::get<json::DoubleType>(token);
writer.Double(x.value);
} else if (std::holds_alternative<json::RawNumberType>(token)) {
auto const& x = std::get<json::RawNumberType>(token);
writer.RawNumber(x.str, x.length);
} else if (std::holds_alternative<json::StringType>(token)) {
auto const& x = std::get<json::StringType>(token);
writer.String(x.str, x.length);
} else if (std::holds_alternative<json::KeyType>(token)) {
auto const& x = std::get<json::KeyType>(token);
writer.Key(x.str, x.length);
} else {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected JSON token";
}
} while (object_stack > 0 || array_stack > 0);
value.data_ = stream.str();
return true;
}
static void write_json(json::JsonWriter& writer, RawJson const& value) {
if (std::holds_alternative<lazy_type>(value.data_)) {
std::get<lazy_type>(value.data_)->write_json(writer);
return;
}
stream::StringReader stream {std::get<text_type>(value.data_)};
json::JsonReader reader {stream};
while (true) {
auto const& next = reader.nextToken();
if (!next.has_value())
break;
auto const& token = next.value();
if (std::holds_alternative<json::StartObjectType>(token)) {
writer.StartObject();
} else if (std::holds_alternative<json::EndObjectType>(token)) {
writer.EndObject();
} else if (std::holds_alternative<json::StartArrayType>(token)) {
writer.StartArray();
} else if (std::holds_alternative<json::EndArrayType>(token)) {
writer.EndArray();
} else if (std::holds_alternative<json::NullType>(token)) {
writer.Null();
} else if (std::holds_alternative<json::BoolType>(token)) {
auto const& x = std::get<json::BoolType>(token);
writer.Bool(x.value);
} else if (std::holds_alternative<json::IntType>(token)) {
auto const& x = std::get<json::IntType>(token);
writer.Int(x.value);
} else if (std::holds_alternative<json::UintType>(token)) {
auto const& x = std::get<json::UintType>(token);
writer.Uint(x.value);
} else if (std::holds_alternative<json::Int64Type>(token)) {
auto const& x = std::get<json::Int64Type>(token);
writer.Int64(x.value);
} else if (std::holds_alternative<json::Uint64Type>(token)) {
auto const& x = std::get<json::Uint64Type>(token);
writer.Uint64(x.value);
} else if (std::holds_alternative<json::DoubleType>(token)) {
auto const& x = std::get<json::DoubleType>(token);
writer.Double(x.value);
} else if (std::holds_alternative<json::RawNumberType>(token)) {
auto const& x = std::get<json::RawNumberType>(token);
writer.RawNumber(x.str, x.length);
} else if (std::holds_alternative<json::StringType>(token)) {
auto const& x = std::get<json::StringType>(token);
writer.String(x.str, x.length);
} else if (std::holds_alternative<json::KeyType>(token)) {
auto const& x = std::get<json::KeyType>(token);
writer.Key(x.str, x.length);
} else {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected JSON token";
}
}
}
template <typename T>
static RawJson from_value(T const& value) {
RawJson result {};
stream::StringReader stream {write_json_to_string(value)};
json::JsonReader reader {stream};
if (!read_json(reader, result)) {
CHARGELAB_LOG_MESSAGE(error) << "Failed creating RawJson from record";
return RawJson {};
}
return result;
}
template <typename T>
static RawJson from_value_lazy(T const& value) {
return RawJson{std::shared_ptr<detail::RawJsonInterface> {new detail::RawJsonLazy<T> {value}}};
}
static RawJson empty_object() {
return RawJson{};
}
private:
data_type data_ = "{}";
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_RAW_JSON_H

View File

@@ -0,0 +1,124 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_SMALL_STRING_H
#define CHARGELAB_OPEN_FIRMWARE_SMALL_STRING_H
#include "openocpp/common/logging.h"
#include <string>
#include <exception>
namespace chargelab {
class SmallString {
public:
~SmallString() {
if (allocated_) {
delete [] str_;
str_ = nullptr;
allocated_ = false;
}
}
SmallString() {
str_ = nullptr;
size_ = 0;
allocated_ = false;
}
// TODO: This can be improved with various overrides, wrappers, and constexpr to restrict this constructor to
// string literals more reliably, but this is likely sufficient for now.
template <std::size_t N>
SmallString(char const (&literal)[N]) {
str_ = literal;
size_ = N-1;
allocated_ = false;
}
SmallString(std::string const& text) {
size_ = text.size();
allocated_ = true;
auto block = new char[size_];
std::memcpy(block, text.data(), size_);
str_ = block;
}
SmallString(SmallString const& other) {
if (other.allocated_) {
size_ = other.size_;
allocated_ = true;
auto block = new char[size_];
std::memcpy(block, other.str_, other.size_);
str_ = block;
} else {
str_ = other.str_;
size_ = other.size_;
allocated_ = other.allocated_;
}
}
SmallString(SmallString&& other) noexcept {
std::swap(str_, other.str_);
std::swap(size_, other.size_);
std::swap(allocated_, other.allocated_);
}
SmallString& operator=(SmallString const& other) {
if (this == &other)
return *this;
if (allocated_) {
delete str_;
str_ = nullptr;
allocated_ = false;
}
if (other.allocated_) {
size_ = other.size_;
allocated_ = true;
auto block = new char[size_];
std::memcpy(block, other.str_, other.size_);
str_ = block;
} else {
str_ = other.str_;
size_ = other.size_;
allocated_ = other.allocated_;
}
return *this;
}
SmallString& operator=(SmallString&& other) noexcept {
if (this == &other)
return *this;
std::swap(str_, other.str_);
std::swap(size_, other.size_);
std::swap(allocated_, other.allocated_);
return *this;
}
[[nodiscard]] std::string value() const {
if (str_ != nullptr) {
return {str_, size_};
} else {
return {};
}
}
bool operator==(SmallString const& rhs) const {
return value() == rhs.value();
}
bool operator!=(SmallString const& rhs) const {
return !operator==(rhs);
}
private:
char const* str_ = nullptr;
std::size_t size_ = 0;
bool allocated_ = false;
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_SMALL_STRING_H

View File

@@ -0,0 +1,137 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_VECTOR_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_VECTOR_H
#include <variant>
#include "openocpp/helpers/json.h"
namespace chargelab {
template <typename T, bool Optional = false, int MinElements = 0, int MaxElements = -1>
struct Vector {
public:
using this_type = Vector<T, Optional, MinElements, MaxElements>;
using data_type = std::vector<T>;
using visitor_type = std::function<void(std::function<void(T const&)>)>;
public:
Vector() {}
Vector(data_type data) : supplier_(std::move(data)) {}
Vector(visitor_type visitor) : supplier_(std::move(visitor)) {}
Vector(const this_type& other) = default;
this_type& operator=(const this_type& other) = default;
data_type value() const {
if (std::holds_alternative<data_type>(supplier_)) {
return std::get<data_type>(supplier_);
} else {
std::vector<T> data;
std::get<visitor_type>(supplier_)([&](auto const& x) {
data.push_back(x);
});
return data;
}
}
template <typename Visitor>
void visit(Visitor&& visitor) const {
if (std::holds_alternative<data_type>(supplier_)) {
auto const& data = std::get<data_type>(supplier_);
for (auto const& x : data)
visitor(x);
} else {
std::get<visitor_type>(supplier_)(std::forward<Visitor>(visitor));
}
}
std::size_t size() const {
std::size_t result = 0;
visit([&](auto const&) {
result++;
});
return result;
}
static bool read_json(json::JsonReader& reader, this_type& value) {
auto next = reader.peekToken();
if (!next.has_value())
return false;
if (std::holds_alternative<json::NullType>(next.value())) {
value.supplier_ = data_type {};
} else {
data_type data;
json::ReadValue<data_type>::read_json(reader, data);
value.supplier_ = std::move(data);
}
return true;
}
static void write_json(json::JsonWriter& writer, this_type const& value) {
if (std::holds_alternative<data_type>(value.supplier_)) {
json::WriteValue<data_type>::write_json(writer, std::get<data_type>(value.supplier_));
} else {
writer.StartArray();
std::get<visitor_type>(value.supplier_)([&](auto const& element) {
json::WriteValue<T>::write_json(writer, element);
});
writer.EndArray();
}
}
static bool include_field(this_type const& value) {
return !Optional || value.size() > 0;
}
static bool is_required() {
return !Optional;
}
static bool validate(this_type const& value) {
auto const size = value.size();
if (MinElements >= 0) {
if (size < MinElements)
return false;
}
if (MaxElements >= 0) {
if (size > MaxElements)
return false;
}
bool valid = true;
value.visit([&](auto const& element) {
if (!valid)
return;
valid = json::Validate<T>::validate(element);
});
return valid;
}
friend bool operator==(this_type const& lhs, this_type const& rhs) {
auto const& lhs_data = lhs.value();
auto const& rhs_data = rhs.value();
if (lhs_data.size() != rhs_data.size())
return false;
for (auto it_lhs=lhs_data.begin(), it_rhs=rhs_data.begin(); it_lhs != lhs_data.end(); ++it_lhs, ++it_rhs) {
if (*it_lhs != *it_rhs)
return false;
}
return true;
}
friend bool operator!=(this_type const& lhs, this_type const& rhs) {
return !(lhs == rhs);
}
private:
std::variant<data_type, visitor_type> supplier_ = data_type {};
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_VECTOR_H

View File

@@ -0,0 +1,58 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_REQUEST_HANDLER_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_REQUEST_HANDLER_H
#include "openocpp/protocol/ocpp1_6/messages/authorize.h"
#include "openocpp/protocol/ocpp1_6/messages/boot_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/cancel_reservation.h"
#include "openocpp/protocol/ocpp1_6/messages/change_availability.h"
#include "openocpp/protocol/ocpp1_6/messages/change_configuration.h"
#include "openocpp/protocol/ocpp1_6/messages/clear_cache.h"
#include "openocpp/protocol/ocpp1_6/messages/clear_charging_profile.h"
#include "openocpp/protocol/ocpp1_6/messages/data_transfer.h"
#include "openocpp/protocol/ocpp1_6/messages/diagnostics_status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/firmware_status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/get_composite_schedule.h"
#include "openocpp/protocol/ocpp1_6/messages/get_configuration.h"
#include "openocpp/protocol/ocpp1_6/messages/get_diagnostics.h"
#include "openocpp/protocol/ocpp1_6/messages/get_local_list_version.h"
#include "openocpp/protocol/ocpp1_6/messages/heartbeat.h"
#include "openocpp/protocol/ocpp1_6/messages/meter_values.h"
#include "openocpp/protocol/ocpp1_6/messages/remote_start_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/remote_stop_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/reserve_now.h"
#include "openocpp/protocol/ocpp1_6/messages/reset.h"
#include "openocpp/protocol/ocpp1_6/messages/send_local_list.h"
#include "openocpp/protocol/ocpp1_6/messages/set_charging_profile.h"
#include "openocpp/protocol/ocpp1_6/messages/start_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/stop_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/trigger_message.h"
#include "openocpp/protocol/ocpp1_6/messages/unlock_connector.h"
#include "openocpp/protocol/ocpp1_6/messages/update_firmware.h"
#include "openocpp/protocol/ocpp1_6/types/call_result.h"
#include "openocpp/protocol/ocpp1_6/handlers/ocpp_remote.h"
#include <variant>
namespace chargelab::ocpp1_6 {
class AbstractRequestHandler {
public:
#define CHARGELAB_REQUEST_HANDLER_TEMPLATE(type) \
virtual std::optional<ocpp1_6::ResponseToRequest<ocpp1_6::type ## Rsp>> on ## type ## Req(ocpp1_6::type ## Req const&) { \
return std::nullopt; \
}
CHARGELAB_PASTE(CHARGELAB_REQUEST_HANDLER_TEMPLATE, CHARGELAB_OCPP_1_6_ACTION_IDS)
#undef CHARGELAB_REQUEST_HANDLER_TEMPLATE
virtual std::optional<ocpp1_6::ResponseToRequest<common::RawJson>> onCall(
ocpp1_6::ActionId const& actionId,
common::RawJson const& payload
) {
(void)actionId;
(void)payload;
return std::nullopt;
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_REQUEST_HANDLER_H

View File

@@ -0,0 +1,48 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_RESPONSE_HANDLER_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_RESPONSE_HANDLER_H
#include "openocpp/protocol/ocpp1_6/messages/authorize.h"
#include "openocpp/protocol/ocpp1_6/messages/boot_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/cancel_reservation.h"
#include "openocpp/protocol/ocpp1_6/messages/change_availability.h"
#include "openocpp/protocol/ocpp1_6/messages/change_configuration.h"
#include "openocpp/protocol/ocpp1_6/messages/clear_cache.h"
#include "openocpp/protocol/ocpp1_6/messages/clear_charging_profile.h"
#include "openocpp/protocol/ocpp1_6/messages/data_transfer.h"
#include "openocpp/protocol/ocpp1_6/messages/diagnostics_status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/firmware_status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/get_composite_schedule.h"
#include "openocpp/protocol/ocpp1_6/messages/get_configuration.h"
#include "openocpp/protocol/ocpp1_6/messages/get_diagnostics.h"
#include "openocpp/protocol/ocpp1_6/messages/get_local_list_version.h"
#include "openocpp/protocol/ocpp1_6/messages/heartbeat.h"
#include "openocpp/protocol/ocpp1_6/messages/meter_values.h"
#include "openocpp/protocol/ocpp1_6/messages/remote_start_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/remote_stop_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/reserve_now.h"
#include "openocpp/protocol/ocpp1_6/messages/reset.h"
#include "openocpp/protocol/ocpp1_6/messages/send_local_list.h"
#include "openocpp/protocol/ocpp1_6/messages/set_charging_profile.h"
#include "openocpp/protocol/ocpp1_6/messages/start_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/stop_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/trigger_message.h"
#include "openocpp/protocol/ocpp1_6/messages/unlock_connector.h"
#include "openocpp/protocol/ocpp1_6/messages/update_firmware.h"
#include "openocpp/protocol/ocpp1_6/types/call_result.h"
#include <variant>
namespace chargelab::ocpp1_6 {
class AbstractResponseHandler {
public:
#define CHARGELAB_RESPONSE_HANDLER_TEMPLATE(type) \
virtual void on ## type ## Rsp(std::string const&, ocpp1_6::ResponseMessage<type ## Rsp> const&) {}
CHARGELAB_PASTE(CHARGELAB_RESPONSE_HANDLER_TEMPLATE, CHARGELAB_OCPP_1_6_ACTION_IDS)
#undef CHARGELAB_RESPONSE_HANDLER_TEMPLATE
virtual void onCallRsp(std::string const&, ocpp1_6::ResponseMessage<common::RawJson> const&) {}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_RESPONSE_HANDLER_H

View File

@@ -0,0 +1,20 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_SERVICE_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_SERVICE_H
#include "openocpp/protocol/ocpp1_6/handlers/ocpp_remote.h"
#include "openocpp/protocol/common/raw_json.h"
namespace chargelab::ocpp1_6 {
class AbstractService {
public:
virtual void runStep(OcppRemote& remote) {
(void)remote;
};
virtual void onUnmanagedMessage(std::string const& message) {
(void)message;
}
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_ABSTRACT_SERVICE_H

View File

@@ -0,0 +1,537 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_OCPP_MESSAGE_HANDLER_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_OCPP_MESSAGE_HANDLER_H
#include "openocpp/protocol/ocpp1_6/handlers/abstract_service.h"
#include "openocpp/protocol/ocpp1_6/handlers/abstract_request_handler.h"
#include "openocpp/protocol/ocpp1_6/handlers/abstract_response_handler.h"
#include "openocpp/protocol/ocpp1_6/handlers/ocpp_remote.h"
#include "openocpp/protocol/ocpp1_6/types/message_type.h"
#include "openocpp/protocol/ocpp1_6/types/error_code.h"
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/call_result.h"
#include "openocpp/interface/element/websocket_interface.h"
#include "openocpp/interface/component/system_interface.h"
#include "openocpp/module/abstract_module.h"
#include "openocpp/common/logging.h"
#include "openocpp/common/ring_buffer.h"
#include "openocpp/helpers/string.h"
#include "openocpp/common/settings.h"
#include <memory>
#include <utility>
#include <random>
namespace chargelab::ocpp1_6 {
namespace detail {
struct PendingCall {
std::string unique_id;
ActionId action_id;
SteadyPointMillis timestamp;
CHARGELAB_JSON_INTRUSIVE(PendingCall, unique_id, action_id, timestamp)
};
}
class OcppMessageHandler {
private:
static constexpr const int kMaxMessageProcessedPerStep = 4;
public:
OcppMessageHandler(
std::shared_ptr<Settings> settings,
std::shared_ptr<SystemInterface> system,
std::vector<std::shared_ptr<AbstractModuleInterface>> modules,
std::function<bool()> registrationComplete
)
: settings_(std::move(settings)),
system_(std::move(system)),
modules_(std::move(modules)),
registration_complete_(std::move(registrationComplete))
{
updatePointers();
}
void runStep(std::shared_ptr<WebsocketInterface> const& websocket) {
for (auto& x : pure_services_)
x->runUnconditionally();
if (websocket == nullptr) {
CHARGELAB_LOG_MESSAGE(error) << "Unexpected null websocket pointer provided";
return;
}
auto const subprotocol = websocket->getSubprotocol();
if (!subprotocol.has_value() || !string::EqualsIgnoreCaseAscii(subprotocol.value(), "ocpp1.6")) {
CHARGELAB_LOG_MESSAGE(trace) << "Skipping OCPP 1.6 message handler based on websocket subprotocol: " << subprotocol;
return;
}
for (int i=0; i < kMaxMessageProcessedPerStep; i++) {
auto message = websocket->pollMessages();
if (!message.has_value())
break;
CHARGELAB_TRY {
onMessage(*websocket, message.value());
} CHARGELAB_CATCH {
CHARGELAB_LOG_MESSAGE(debug) << "Unexpected failed processing message: " << e.what();
dispatchUnexpectedMessage(message.value());
}
}
OcppRemote remote {
*websocket,
registration_complete_,
[this](std::string const& unique_id, ActionId const& action) {
auto const now = system_->steadyClockNow();
if (last_call_.has_value()) {
auto const elapsed_seconds = (now - last_call_->timestamp)/1000;
if (elapsed_seconds < settings_->DefaultMessageTimeout.getValue()) {
CHARGELAB_LOG_MESSAGE(info) << "Call already in process - blocking call '" << action << "' waiting for: " << last_call_;
return false;
}
CHARGELAB_LOG_MESSAGE(info) << "Call already in process but timeout elapsed - allowing call '" << action << "' waiting for: " << last_call_;
}
last_call_ = detail::PendingCall {unique_id, action, now};
return true;
}
};
for (auto& x : services_)
x->runStep(remote);
}
template <typename T, typename... Args>
void addAfter(std::shared_ptr<T> module, Args&&... args) {
auto index = getMaxIndex(std::forward<Args>(args)...);
if (index.has_value()) {
modules_.insert(modules_.begin()+index.value()+1, std::move(module));
} else {
modules_.push_back(std::move(module));
}
updatePointers();
}
template <typename T, typename... Args>
void addBefore(std::shared_ptr<T> module, Args&&... args) {
auto index = getMinIndex(std::forward<Args>(args)...);
if (index.has_value()) {
modules_.insert(modules_.begin()+index.value(), std::move(module));
} else {
modules_.push_back(std::move(module));
}
updatePointers();
}
private:
void updatePointers() {
services_.clear();
request_handlers_.clear();
response_handlers_.clear();
pure_services_.clear();
for (auto const& x : modules_) {
if (x != nullptr) {
auto implementations = x->getImplementations();
addIfNotNull(services_, implementations.ocpp1_6.service);
addIfNotNull(request_handlers_, implementations.ocpp1_6.request_handler);
addIfNotNull(response_handlers_, implementations.ocpp1_6.response_handler);
addIfNotNull(pure_services_, implementations.pure_service);
}
}
}
std::optional<std::size_t> getMinIndex() {
return std::nullopt;
}
template <typename Head, typename... Args>
std::optional<std::size_t> getMinIndex(Head&& head, Args&&... args) {
auto const next = getMinIndex(std::forward<Args>(args)...);
for (std::size_t i=0; i < modules_.size(); i++) {
if (head == modules_[i]) {
if (next.has_value()) {
return std::min(i, next.value());
} else {
return i;
}
}
}
return next;
}
std::optional<std::size_t> getMaxIndex() {
return std::nullopt;
}
template <typename Head, typename... Args>
std::optional<std::size_t> getMaxIndex(Head&& head, Args&&... args) {
auto const next = getMaxIndex(std::forward<Args>(args)...);
for (std::size_t i=0; i < modules_.size(); i++) {
if (head == modules_[i]) {
if (next.has_value()) {
return std::max(i, next.value());
} else {
return i;
}
}
}
return next;
}
private:
void onMessage(WebsocketInterface& websocket, std::string const& message) {
CHARGELAB_LOG_MESSAGE(debug) << "Received OCPP message: " << message;
stream::StringReader stream {message};
json::JsonReader reader {stream};
if (!json::expect_type<json::StartArrayType>(reader)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - expected array: " << message;
dispatchUnexpectedMessage(message);
return;
}
MessageType message_type;
if (!json::ReadValue<MessageType>::read_json(reader, message_type)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad message type: " << message;
dispatchUnexpectedMessage(message);
return;
}
switch (message_type) {
case MessageType::kValueNotFoundInEnum:
dispatchUnexpectedMessage(message);
return;
case MessageType::kCall:
{
std::string unique_id;
if (!json::ReadValue<std::string>::read_json(reader, unique_id)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad unique ID: " << message;
dispatchUnexpectedMessage(message);
return;
}
ActionId action_id;
if (!json::ReadValue<ActionId>::read_json(reader, action_id)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad action ID: " << message;
dispatchUnexpectedMessage(message);
return;
}
switch (action_id) {
default:
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - bad action ID: " << message;
dispatchUnexpectedMessage(message);
return;
#define CHARGELAB_ON_CALL_IMPL(TYPE) \
case ActionId::k ## TYPE: \
{ \
TYPE ## Req parsed {}; \
if (!json::ReadValue<TYPE ## Req>::read_json(reader, parsed)) { \
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - failed deserializing message payload: " << message; \
dispatchUnexpectedMessage(message); \
break; \
} \
\
dispatchCall( \
websocket, \
action_id, \
unique_id, \
common::RawJson::from_value_lazy(parsed), \
[]( \
AbstractRequestHandler* handler, \
bool first, \
TYPE ## Req const& req, \
WebsocketInterface& websocket, \
std::string const& unique_id \
) { \
auto const response = handler->on ## TYPE ## Req(req); \
if (response.has_value()) { \
if (first) { \
sendResponse(websocket, unique_id, response.value()); \
} \
\
return true; \
} else { \
return false; \
} \
}, \
parsed, \
websocket, \
unique_id \
); \
break; \
}
CHARGELAB_PASTE(CHARGELAB_ON_CALL_IMPL, CHARGELAB_OCPP_1_6_ACTION_IDS)
#undef CHARGELAB_ON_CALL_IMPL
}
if (!json::expect_type<json::EndArrayType>(reader))
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected elements in OCPP call: " << message;
return;
}
case MessageType::kCallResult:
{
std::string unique_id;
if (!json::ReadValue<std::string>::read_json(reader, unique_id)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad unique ID: " << message;
dispatchUnexpectedMessage(message);
return;
}
std::optional<ActionId> action_id;
if (last_call_.has_value() && string::EqualsIgnoreCaseAscii(last_call_->unique_id, unique_id)) {
action_id = last_call_->action_id;
last_call_ = std::nullopt;
}
if (!action_id.has_value()) {
CHARGELAB_LOG_MESSAGE(warning) << "Response ID not found in pending call list - treating as unexpected message: " << unique_id;
dispatchUnexpectedMessage(message);
return;
}
switch (action_id.value()) {
default:
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - bad action ID: " << message;
dispatchUnexpectedMessage(message);
return;
#define CHARGELAB_ON_CALL_RESPONSE_IMPL(TYPE) \
case ActionId::k ## TYPE: \
{ \
TYPE ## Rsp parsed {}; \
if (!json::ReadValue<TYPE ## Rsp>::read_json(reader, parsed)) { \
CHARGELAB_LOG_MESSAGE(error) << "Unexpected state - failed deserializing message payload: " << message; \
dispatchUnexpectedMessage(message); \
return; \
} \
\
for (auto& ptr : response_handlers_) { \
if (ptr != nullptr) { \
ptr->on ## TYPE ## Rsp(unique_id, parsed); \
ptr->onCallRsp(unique_id, common::RawJson::from_value_lazy(parsed)); \
} \
} \
break; \
}
CHARGELAB_PASTE(CHARGELAB_ON_CALL_RESPONSE_IMPL, CHARGELAB_OCPP_1_6_ACTION_IDS)
#undef CHARGELAB_ON_CALL_RESPONSE_IMPL
}
// Note: ignoring and allowing
if (!json::expect_type<json::EndArrayType>(reader))
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected elements in OCPP call: " << message;
return;
}
case MessageType::kCallError:
{
std::string unique_id;
if (!json::ReadValue<std::string>::read_json(reader, unique_id)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad unique ID: " << message;
dispatchUnexpectedMessage(message);
return;
}
ErrorCode error_code;
if (!json::ReadValue<ErrorCode>::read_json(reader, error_code)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad error code: " << message;
dispatchUnexpectedMessage(message);
return;
}
std::string description;
if (!json::ReadValue<std::string>::read_json(reader, description)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad description: " << message;
dispatchUnexpectedMessage(message);
return;
}
common::RawJson details;
if (!json::ReadValue<common::RawJson>::read_json(reader, details)) {
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - missing or bad details: " << message;
dispatchUnexpectedMessage(message);
return;
}
// Note: ignoring and allowing
if (!json::expect_type<json::EndArrayType>(reader))
CHARGELAB_LOG_MESSAGE(warning) << "Unexpected elements in OCPP call: " << message;
auto const error = CallError {error_code, description, details};
CHARGELAB_LOG_MESSAGE(debug) << "Received OCPP error: id=" << unique_id << ", error=" << error;
std::optional<ActionId> action_id;
if (last_call_.has_value() && string::EqualsIgnoreCaseAscii(last_call_->unique_id, unique_id)) {
action_id = last_call_->action_id;
last_call_ = std::nullopt;
}
if (!action_id.has_value()) {
dispatchUnexpectedMessage(message);
return;
}
switch (action_id.value()) {
default:
CHARGELAB_LOG_MESSAGE(warning) << "Bad message payload - bad action ID: " << message;
dispatchUnexpectedMessage(message);
return;
#define CHARGELAB_ON_CALL_ERROR_IMPL(TYPE) \
case ActionId::k ## TYPE: \
{ \
for (auto& ptr : response_handlers_) { \
if (ptr != nullptr) { \
ptr->on ## TYPE ## Rsp(unique_id, error); \
} \
} \
break; \
}
CHARGELAB_PASTE(CHARGELAB_ON_CALL_ERROR_IMPL, CHARGELAB_OCPP_1_6_ACTION_IDS)
#undef CHARGELAB_ON_CALL_ERROR_IMPL
}
for (auto& ptr : response_handlers_) {
if (ptr != nullptr) {
ptr->onCallRsp(unique_id, error);
}
}
return;
}
}
}
template<typename T>
static void sendResponse(WebsocketInterface& websocket, std::string const& unique_id, ResponseToRequest<T> const& result) {
if (std::holds_alternative<T>(result)) {
websocket.sendCustom([&](ByteWriterInterface& stream) {
json::JsonWriter writer {stream};
writer.StartArray();
writer.Int((int)MessageType::kCallResult);
writer.String(unique_id);
json::WriteValue<T>::write_json(writer, std::get<T>(result));
writer.EndArray();
});
} else if (std::holds_alternative<CallError>(result)) {
sendError(websocket, unique_id, std::get<CallError>(result));
} else {
std::get<CustomResponse>(result)(websocket, unique_id);
}
}
static void sendError(WebsocketInterface& websocket, std::string const& unique_id, CallError const& error) {
websocket.sendCustom([&](ByteWriterInterface& stream) {
json::JsonWriter writer {stream};
writer.StartArray();
writer.Int((int)MessageType::kCallError);
writer.String(unique_id);
json::WriteValue<ErrorCode>::write_json(writer, error.code);
writer.String(error.description);
json::WriteValue<common::RawJson>::write_json(writer, error.details);
writer.EndArray();
});
}
template <typename F, typename... Args>
void dispatchCall(
WebsocketInterface& websocket,
ActionId const& action_id,
std::string const& unique_id,
common::RawJson const& payload,
F&& function,
Args&&... args
) {
bool responded = false;
for (auto& ptr : request_handlers_) {
if (ptr != nullptr) {
if (!responded) {
responded = function(ptr, true, std::forward<Args>(args)...);
} else if (allowFallthrough(action_id)) {
function(ptr, false, std::forward<Args>(args)...);
}
if (!responded) {
auto response = ptr->onCall(action_id, payload);
if (response.has_value()) {
sendResponse(websocket, unique_id, response.value());
responded = true;
}
} else if (allowFallthrough(action_id)) {
ptr->onCall(action_id, payload);
}
}
}
if (!responded) {
sendError(websocket, unique_id, CallError {
ErrorCode::kNotImplemented,
"Not implemented",
common::RawJson::empty_object()
});
}
}
bool allowFallthrough(ActionId const& action_id) {
switch (action_id) {
default:
return false;
case ActionId::kTriggerMessage:
case ActionId::kChangeConfiguration:
return true;
}
}
void dispatchUnexpectedMessage(std::string const& message) {
CHARGELAB_LOG_MESSAGE(debug) << "Dispatching unexpected message: " << message;
for (auto& ptr : services_) {
if (ptr != nullptr)
ptr->onUnmanagedMessage(message);
}
}
template <typename T>
static void addIfNotNull(std::vector<T*>& container, T* x) {
if (x != nullptr) {
container.push_back(x);
}
}
private:
std::shared_ptr<Settings> settings_;
std::shared_ptr<SystemInterface> system_;
std::vector<std::shared_ptr<AbstractModuleInterface>> modules_;
std::function<bool()> registration_complete_;
std::vector<std::shared_ptr<void>> wrappers_;
std::vector<AbstractService*> services_;
std::vector<AbstractRequestHandler*> request_handlers_;
std::vector<AbstractResponseHandler*> response_handlers_;
std::vector<chargelab::detail::PureServiceInterface*> pure_services_;
std::optional<detail::PendingCall> last_call_ = std::nullopt;
};
}
#undef CHARGELAB_MERGE_NAMES
#undef CHARGELAB_CALL_FUNCTION_TEMPLATE
#undef CHARGELAB_CALL_RESPONSE_TEMPLATE
#undef CHARGELAB_CALL_ERROR_TEMPLATE
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_OCPP_MESSAGE_HANDLER_H

View File

@@ -0,0 +1,172 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_OCPP_REMOTE_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_OCPP_REMOTE_H
#include "openocpp/protocol/ocpp1_6/messages/authorize.h"
#include "openocpp/protocol/ocpp1_6/messages/boot_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/cancel_reservation.h"
#include "openocpp/protocol/ocpp1_6/messages/change_availability.h"
#include "openocpp/protocol/ocpp1_6/messages/change_configuration.h"
#include "openocpp/protocol/ocpp1_6/messages/clear_cache.h"
#include "openocpp/protocol/ocpp1_6/messages/clear_charging_profile.h"
#include "openocpp/protocol/ocpp1_6/messages/data_transfer.h"
#include "openocpp/protocol/ocpp1_6/messages/diagnostics_status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/firmware_status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/get_composite_schedule.h"
#include "openocpp/protocol/ocpp1_6/messages/get_configuration.h"
#include "openocpp/protocol/ocpp1_6/messages/get_diagnostics.h"
#include "openocpp/protocol/ocpp1_6/messages/get_local_list_version.h"
#include "openocpp/protocol/ocpp1_6/messages/heartbeat.h"
#include "openocpp/protocol/ocpp1_6/messages/meter_values.h"
#include "openocpp/protocol/ocpp1_6/messages/remote_start_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/remote_stop_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/reserve_now.h"
#include "openocpp/protocol/ocpp1_6/messages/reset.h"
#include "openocpp/protocol/ocpp1_6/messages/send_local_list.h"
#include "openocpp/protocol/ocpp1_6/messages/set_charging_profile.h"
#include "openocpp/protocol/ocpp1_6/messages/start_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/status_notification.h"
#include "openocpp/protocol/ocpp1_6/messages/stop_transaction.h"
#include "openocpp/protocol/ocpp1_6/messages/trigger_message.h"
#include "openocpp/protocol/ocpp1_6/messages/unlock_connector.h"
#include "openocpp/protocol/ocpp1_6/messages/update_firmware.h"
#include "openocpp/protocol/ocpp1_6/types/call_result.h"
#include "openocpp/protocol/ocpp1_6/types/message_type.h"
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/interface/element/websocket_interface.h"
#include <utility>
#include <random>
namespace chargelab::ocpp1_6 {
class OcppRemote {
private:
using OnManagedCall = std::function<bool(std::string const&, ActionId const&)>;
public:
explicit OcppRemote(
WebsocketInterface& websocket_interface,
std::function<bool()> registration_complete,
OnManagedCall on_managed_message
)
: websocket_interface_(websocket_interface),
registration_complete_(std::move(registration_complete)),
on_managed_message_(std::move(on_managed_message))
{
std::random_device random_device;
std::default_random_engine random_engine {random_device()};
std::uniform_int_distribution<unsigned long> distribution(
std::numeric_limits<unsigned long>::min(),
std::numeric_limits<unsigned long>::max()
);
request_id_ = distribution(random_engine);
}
public:
#define CHARGELAB_REQUEST_HANDLER_TEMPLATE(type) \
std::optional<std::string> send ## type ## Req(const type ## Req &req) { \
return executeCall(ActionId::k ## type, req); \
}
CHARGELAB_PASTE(CHARGELAB_REQUEST_HANDLER_TEMPLATE, CHARGELAB_OCPP_1_6_ACTION_IDS)
#undef CHARGELAB_REQUEST_HANDLER_TEMPLATE
template <typename T>
std::optional<std::string> sendCall(T const& req) {
return executeCall(T::kActionId, req);
}
bool sendCall(std::string const& unique_id, ActionId const& action, std::string const& payload) {
if (!websocket_interface_.isConnected())
return false;
if (!registration_complete_()) {
switch (action) {
default:
CHARGELAB_LOG_MESSAGE(info) << "Registration not complete - blocking call: " << action;
return false;
// Allow these requests to be sent while registration is pending
case ActionId::kBootNotification:
break;
}
}
if (!on_managed_message_(unique_id, action))
return false;
websocket_interface_.sendCustom([&](ByteWriterInterface& stream) {
json::JsonWriter writer {stream};
writer.StartArray();
writer.Int((int)MessageType::kCall);
writer.String(unique_id);
writer.String(action.to_string()),
json::WriteValue<common::RawJson>::write_json(writer, {payload});
writer.EndArray();
});
return true;
}
bool sendUnmanagedMessage(std::function<void(ByteWriterInterface&)> payload) {
if (!websocket_interface_.isConnected())
return false;
websocket_interface_.sendCustom(std::move(payload));
return true;
}
unsigned long getRequestId() {
return request_id_++;
}
private:
template <typename T>
std::optional<std::string> executeCall(ActionId const& action, T const& payload) {
if (!websocket_interface_.isConnected())
return std::nullopt;
if (!registration_complete_()) {
switch (action) {
default:
CHARGELAB_LOG_MESSAGE(info) << "Registration not complete - blocking call: " << action;
return std::nullopt;
// Allow these requests to be sent while registration is pending
case ActionId::kBootNotification:
break;
}
}
CHARGELAB_TRY {
auto unique_id = std::to_string(request_id_++);
if (!on_managed_message_(unique_id, action))
return std::nullopt;
websocket_interface_.sendCustom([&](ByteWriterInterface& stream) {
json::JsonWriter writer {stream};
writer.StartArray();
writer.Int((int)MessageType::kCall);
writer.String(unique_id);
writer.String(action.to_string()),
json::WriteValue<T>::write_json(writer, payload);
writer.EndArray();
});
return unique_id;
} CHARGELAB_CATCH {
CHARGELAB_LOG_MESSAGE(error) << "Failed sending call with: " << e.what();
return std::nullopt;
}
}
private:
WebsocketInterface& websocket_interface_;
std::function<bool()> registration_complete_;
OnManagedCall on_managed_message_;
std::atomic<unsigned long> request_id_;
};
}
#undef CHARGELAB_MERGE_NAMES
#undef CHARGELAB_REQUEST_HANDLER_TEMPLATE
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_OCPP_REMOTE_H

View File

@@ -0,0 +1,22 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_AUTHORIZE_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_AUTHORIZE_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/id_token.h"
#include "openocpp/protocol/ocpp1_6/types/id_tag_info.h"
#include "openocpp/helpers/json.h"
namespace chargelab::ocpp1_6 {
struct AuthorizeReq {
IdToken idTag;
CHARGELAB_JSON_INTRUSIVE_CALL(AuthorizeReq, kAuthorize, idTag)
};
struct AuthorizeRsp {
IdTagInfo idTagInfo;
CHARGELAB_JSON_INTRUSIVE(AuthorizeRsp, idTagInfo)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_AUTHORIZE_H

View File

@@ -0,0 +1,35 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_BOOT_NOTIFICATION_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_BOOT_NOTIFICATION_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/ci_string_type.h"
#include "openocpp/protocol/ocpp1_6/types/date_time.h"
#include "openocpp/protocol/ocpp1_6/types/registration_status.h"
#include "openocpp/helpers/json.h"
#include <optional>
namespace chargelab::ocpp1_6 {
struct BootNotificationReq {
std::optional<CiString25Type> chargeBoxSerialNumber = std::nullopt;
CiString20Type chargePointModel {};
std::optional<CiString25Type> chargePointSerialNumber = std::nullopt;
CiString20Type chargePointVendor {};
std::optional<CiString50Type> firmwareVersion = std::nullopt;
std::optional<CiString20Type> iccid = std::nullopt;
std::optional<CiString20Type> imsi = std::nullopt;
std::optional<CiString25Type> meterSerialNumber = std::nullopt;
std::optional<CiString25Type> meterType = std::nullopt;
CHARGELAB_JSON_INTRUSIVE_CALL(BootNotificationReq, kBootNotification, chargeBoxSerialNumber, chargePointModel, chargePointSerialNumber, chargePointVendor, firmwareVersion, iccid, imsi, meterSerialNumber, meterType)
};
struct BootNotificationRsp {
DateTime currentTime;
int interval;
RegistrationStatus status;
CHARGELAB_JSON_INTRUSIVE(BootNotificationRsp, currentTime, interval, status)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_BOOT_NOTIFICATION_H

View File

@@ -0,0 +1,23 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_CANCEL_RESERVATION_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_CANCEL_RESERVATION_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/cancel_reservation_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct CancelReservationReq {
int reservationId;
CHARGELAB_JSON_INTRUSIVE_CALL(CancelReservationReq, kCancelReservation, reservationId)
};
struct CancelReservationRsp {
CancelReservationStatus status;
CHARGELAB_JSON_INTRUSIVE(CancelReservationRsp, status)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_CANCEL_RESERVATION_H

View File

@@ -0,0 +1,25 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_CHANGE_AVAILABILITY_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_CHANGE_AVAILABILITY_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/availability_type.h"
#include "openocpp/protocol/ocpp1_6/types/availability_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct ChangeAvailabilityReq {
int connectorId;
AvailabilityType type;
CHARGELAB_JSON_INTRUSIVE_CALL(ChangeAvailabilityReq, kChangeAvailability, connectorId, type)
};
struct ChangeAvailabilityRsp {
AvailabilityStatus status;
CHARGELAB_JSON_INTRUSIVE(ChangeAvailabilityRsp, status)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_CHANGE_AVAILABILITY_H

View File

@@ -0,0 +1,25 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_CHANGE_CONFIGURATION_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_CHANGE_CONFIGURATION_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/ci_string_type.h"
#include "openocpp/protocol/ocpp1_6/types/configuration_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct ChangeConfigurationReq {
CiString50Type key;
CiString500Type value;
CHARGELAB_JSON_INTRUSIVE_CALL(ChangeConfigurationReq, kChangeConfiguration, key, value)
};
struct ChangeConfigurationRsp {
ConfigurationStatus status;
CHARGELAB_JSON_INTRUSIVE(ChangeConfigurationRsp, status)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_CHANGE_CONFIGURATION_H

View File

@@ -0,0 +1,22 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_CLEAR_CACHE_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_CLEAR_CACHE_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/clear_cache_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct ClearCacheReq {
CHARGELAB_JSON_INTRUSIVE_EMPTY_CALL(ClearCacheReq, kClearCache)
};
struct ClearCacheRsp {
ClearCacheStatus status;
CHARGELAB_JSON_INTRUSIVE(ClearCacheRsp, status)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_CLEAR_CACHE_H

View File

@@ -0,0 +1,27 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_CLEAR_CHARGING_PROFILE_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_CLEAR_CHARGING_PROFILE_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/charging_profile_purpose_type.h"
#include "openocpp/protocol/ocpp1_6/types/clear_charging_profile_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct ClearChargingProfileReq {
std::optional<int> id;
std::optional<int> connectorId;
std::optional<ChargingProfilePurposeType> chargingProfilePurpose;
std::optional<int> stackLevel;
CHARGELAB_JSON_INTRUSIVE_CALL(ClearChargingProfileReq, kClearChargingProfile, id, connectorId, chargingProfilePurpose, stackLevel)
};
struct ClearChargingProfileRsp {
ClearChargingProfileStatus status;
CHARGELAB_JSON_INTRUSIVE(ClearChargingProfileRsp, status)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_CLEAR_CHARGING_PROFILE_H

View File

@@ -0,0 +1,27 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_DATA_TRANSFER_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_DATA_TRANSFER_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/ci_string_type.h"
#include "openocpp/protocol/ocpp1_6/types/data_transfer_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct DataTransferReq {
CiString255Type vendorId;
std::optional<CiString50Type> messageId;
std::optional<std::string> data;
CHARGELAB_JSON_INTRUSIVE_CALL(DataTransferReq, kDataTransfer, vendorId, messageId, data)
};
struct DataTransferRsp {
DataTransferStatus status {};
std::optional<std::string> data {};
CHARGELAB_JSON_INTRUSIVE(DataTransferRsp, status, data)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_DATA_TRANSFER_H

View File

@@ -0,0 +1,22 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_DIAGNOSTICS_STATUS_NOTIFICATION_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_DIAGNOSTICS_STATUS_NOTIFICATION_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/diagnostics_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct DiagnosticsStatusNotificationReq {
DiagnosticsStatus status;
CHARGELAB_JSON_INTRUSIVE_CALL(DiagnosticsStatusNotificationReq, kDiagnosticsStatusNotification, status)
};
struct DiagnosticsStatusNotificationRsp {
CHARGELAB_JSON_INTRUSIVE_EMPTY(DiagnosticsStatusNotificationRsp)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_DIAGNOSTICS_STATUS_NOTIFICATION_H

View File

@@ -0,0 +1,22 @@
#ifndef CHARGELAB_OPEN_FIRMWARE_1_6_FIRMWARE_STATUS_NOTIFICATION_H
#define CHARGELAB_OPEN_FIRMWARE_1_6_FIRMWARE_STATUS_NOTIFICATION_H
#include "openocpp/protocol/ocpp1_6/types/action_id.h"
#include "openocpp/protocol/ocpp1_6/types/firmware_status.h"
#include "openocpp/helpers/json.h"
#include <string>
namespace chargelab::ocpp1_6 {
struct FirmwareStatusNotificationReq {
FirmwareStatus status;
CHARGELAB_JSON_INTRUSIVE_CALL(FirmwareStatusNotificationReq, kFirmwareStatusNotification, status)
};
struct FirmwareStatusNotificationRsp {
CHARGELAB_JSON_INTRUSIVE_EMPTY(FirmwareStatusNotificationRsp)
};
}
#endif //CHARGELAB_OPEN_FIRMWARE_1_6_FIRMWARE_STATUS_NOTIFICATION_H

Some files were not shown because too many files have changed in this diff Show More