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

View File

@@ -0,0 +1 @@
add_subdirectory(pionix_chargebridge)

View File

@@ -0,0 +1,31 @@
# Applications related to EVerest
This directory will contain applications that are used during the EVerest build process,
deployed to the target hardware and useful scripts for development.
## containers/
containers/ contains multiple useful Dockerfile based containers that can be used for development.
* `mosquittto` : An mqtt broker
* `mqtt-explorer` : Tool to inspect messages on mqtt topics
* `nodered` : NodeRED application to run i.e. sil flows
* `steve` : OCPP backend
## dependency_manager
Contains the EDM (EVerest Dependency Manager) tool which is a cli to
manage an EVerest workspace and external dependencies during building.
## everest_dev_tool
A CLI designed to provide helpful commands/aliases for developing.
## utils/
utils/ contains ev-cli and everest-testing as well as additional code integrated from everest-utils.
This might get broken up further in the future.
## devrd
Script to manage devcontainer + containerized services in the development environment

View File

@@ -0,0 +1,15 @@
FROM eclipse-mosquitto:2.0.22
COPY mosquitto.conf /mosquitto/config/mosquitto.conf
ARG TARGETARCH
COPY entrypoint_wrapper.sh /entrypoint_wrapper.sh
RUN if [ ${TARGETARCH} != "amd64" ]; then \
mv /docker-entrypoint.sh /wrapped_entrypoint.sh; \
cp /entrypoint_wrapper.sh /entrypoint.sh; \
else \
mv /docker-entrypoint.sh /entrypoint.sh; \
fi; \
rm /entrypoint_wrapper.sh
ENTRYPOINT [ "/entrypoint.sh" ]
CMD ["/usr/sbin/mosquitto", "-c", "/mosquitto/config/mosquitto.conf"]

View File

@@ -0,0 +1,37 @@
#!/bin/ash
# ---------------------------------------------
# Architecture Warning Wrapper Script
#
# This script is used as an entrypoint wrapper to emit a warning
# when the container is not running on the officially supported
# amd64 (x86_64) architecture.
#
# It checks for the presence of a wrapped entrypoint script
# (/wrapped_entrypoint.sh) and executes it if found; otherwise,
# it falls back to executing the provided command directly.
#
# The warning is shown both before and after the wrapped command
# to ensure visibility.
# ---------------------------------------------
function print_warning {
echo -e "\033[0;31m"
echo "-------------------------------------------------------------"
echo "⚠️ WARNING: Unsupported Architecture Detected"
echo
echo "This Docker image is not running on the amd64 (x86_64) architecture."
echo "It is recommended to use the amd64-based image for full compatibility."
echo "Other architectures are not officially supported and may cause issues."
echo
echo "-------------------------------------------------------------"
echo -e "\033[0m"
}
print_warning
if [ -f /wrapped_entrypoint.sh ]; then
exec /wrapped_entrypoint.sh "$@"
else
exec "$@"
fi

View File

@@ -0,0 +1,876 @@
# Config file for mosquitto
#
# See mosquitto.conf(5) for more information.
#
# Default values are shown, uncomment to change.
#
# Use the # character to indicate a comment, but only if it is the
# very first character on the line.
# =================================================================
# General configuration
# =================================================================
# Use per listener security settings.
#
# It is recommended this option be set before any other options.
#
# If this option is set to true, then all authentication and access control
# options are controlled on a per listener basis. The following options are
# affected:
#
# password_file acl_file psk_file auth_plugin auth_opt_* allow_anonymous
# auto_id_prefix allow_zero_length_clientid
#
# Note that if set to true, then a durable client (i.e. with clean session set
# to false) that has disconnected will use the ACL settings defined for the
# listener that it was most recently connected to.
#
# The default behaviour is for this to be set to false, which maintains the
# setting behaviour from previous versions of mosquitto.
#per_listener_settings false
# This option controls whether a client is allowed to connect with a zero
# length client id or not. This option only affects clients using MQTT v3.1.1
# and later. If set to false, clients connecting with a zero length client id
# are disconnected. If set to true, clients will be allocated a client id by
# the broker. This means it is only useful for clients with clean session set
# to true.
#allow_zero_length_clientid true
# If allow_zero_length_clientid is true, this option allows you to set a prefix
# to automatically generated client ids to aid visibility in logs.
# Defaults to 'auto-'
#auto_id_prefix auto-
# This option affects the scenario when a client subscribes to a topic that has
# retained messages. It is possible that the client that published the retained
# message to the topic had access at the time they published, but that access
# has been subsequently removed. If check_retain_source is set to true, the
# default, the source of a retained message will be checked for access rights
# before it is republished. When set to false, no check will be made and the
# retained message will always be published. This affects all listeners.
#check_retain_source true
# QoS 1 and 2 messages will be allowed inflight per client until this limit
# is exceeded. Defaults to 0. (No maximum)
# See also max_inflight_messages
#max_inflight_bytes 0
# The maximum number of QoS 1 and 2 messages currently inflight per
# client.
# This includes messages that are partway through handshakes and
# those that are being retried. Defaults to 20. Set to 0 for no
# maximum. Setting to 1 will guarantee in-order delivery of QoS 1
# and 2 messages.
#max_inflight_messages 20
# For MQTT v5 clients, it is possible to have the server send a "server
# keepalive" value that will override the keepalive value set by the client.
# This is intended to be used as a mechanism to say that the server will
# disconnect the client earlier than it anticipated, and that the client should
# use the new keepalive value. The max_keepalive option allows you to specify
# that clients may only connect with keepalive less than or equal to this
# value, otherwise they will be sent a server keepalive telling them to use
# max_keepalive. This only applies to MQTT v5 clients. The maximum value
# allowable is 65535. Do not set below 10.
#max_keepalive 65535
# For MQTT v5 clients, it is possible to have the server send a "maximum packet
# size" value that will instruct the client it will not accept MQTT packets
# with size greater than max_packet_size bytes. This applies to the full MQTT
# packet, not just the payload. Setting this option to a positive value will
# set the maximum packet size to that number of bytes. If a client sends a
# packet which is larger than this value, it will be disconnected. This applies
# to all clients regardless of the protocol version they are using, but v3.1.1
# and earlier clients will of course not have received the maximum packet size
# information. Defaults to no limit. Setting below 20 bytes is forbidden
# because it is likely to interfere with ordinary client operation, even with
# very small payloads.
#max_packet_size 0
# QoS 1 and 2 messages above those currently in-flight will be queued per
# client until this limit is exceeded. Defaults to 0. (No maximum)
# See also max_queued_messages.
# If both max_queued_messages and max_queued_bytes are specified, packets will
# be queued until the first limit is reached.
#max_queued_bytes 0
# Set the maximum QoS supported. Clients publishing at a QoS higher than
# specified here will be disconnected.
#max_qos 2
# The maximum number of QoS 1 and 2 messages to hold in a queue per client
# above those that are currently in-flight. Defaults to 1000. Set
# to 0 for no maximum (not recommended).
# See also queue_qos0_messages.
# See also max_queued_bytes.
#max_queued_messages 1000
#
# This option sets the maximum number of heap memory bytes that the broker will
# allocate, and hence sets a hard limit on memory use by the broker. Memory
# requests that exceed this value will be denied. The effect will vary
# depending on what has been denied. If an incoming message is being processed,
# then the message will be dropped and the publishing client will be
# disconnected. If an outgoing message is being sent, then the individual
# message will be dropped and the receiving client will be disconnected.
# Defaults to no limit.
#memory_limit 0
# This option sets the maximum publish payload size that the broker will allow.
# Received messages that exceed this size will not be accepted by the broker.
# The default value is 0, which means that all valid MQTT messages are
# accepted. MQTT imposes a maximum payload size of 268435455 bytes.
#message_size_limit 0
# This option allows persistent clients (those with clean session set to false)
# to be removed if they do not reconnect within a certain time frame.
#
# This is a non-standard option in MQTT V3.1 but allowed in MQTT v3.1.1.
#
# Badly designed clients may set clean session to false whilst using a randomly
# generated client id. This leads to persistent clients that will never
# reconnect. This option allows these clients to be removed.
#
# The expiration period should be an integer followed by one of h d w m y for
# hour, day, week, month and year respectively. For example
#
# persistent_client_expiration 2m
# persistent_client_expiration 14d
# persistent_client_expiration 1y
#
# The default if not set is to never expire persistent clients.
#persistent_client_expiration
# Write process id to a file. Default is a blank string which means
# a pid file shouldn't be written.
# This should be set to /var/run/mosquitto/mosquitto.pid if mosquitto is
# being run automatically on boot with an init script and
# start-stop-daemon or similar.
#pid_file
# Set to true to queue messages with QoS 0 when a persistent client is
# disconnected. These messages are included in the limit imposed by
# max_queued_messages and max_queued_bytes
# Defaults to false.
# This is a non-standard option for the MQTT v3.1 spec but is allowed in
# v3.1.1.
#queue_qos0_messages false
# Set to false to disable retained message support. If a client publishes a
# message with the retain bit set, it will be disconnected if this is set to
# false.
#retain_available true
# Disable Nagle's algorithm on client sockets. This has the effect of reducing
# latency of individual messages at the potential cost of increasing the number
# of packets being sent.
#set_tcp_nodelay false
# Time in seconds between updates of the $SYS tree.
# Set to 0 to disable the publishing of the $SYS tree.
#sys_interval 10
# The MQTT specification requires that the QoS of a message delivered to a
# subscriber is never upgraded to match the QoS of the subscription. Enabling
# this option changes this behaviour. If upgrade_outgoing_qos is set true,
# messages sent to a subscriber will always match the QoS of its subscription.
# This is a non-standard option explicitly disallowed by the spec.
#upgrade_outgoing_qos false
# When run as root, drop privileges to this user and its primary
# group.
# Set to root to stay as root, but this is not recommended.
# If set to "mosquitto", or left unset, and the "mosquitto" user does not exist
# then it will drop privileges to the "nobody" user instead.
# If run as a non-root user, this setting has no effect.
# Note that on Windows this has no effect and so mosquitto should be started by
# the user you wish it to run as.
#user mosquitto
# =================================================================
# Listeners
# =================================================================
# Listen on a port/ip address combination. By using this variable
# multiple times, mosquitto can listen on more than one port. If
# this variable is used and neither bind_address nor port given,
# then the default listener will not be started.
# The port number to listen on must be given. Optionally, an ip
# address or host name may be supplied as a second argument. In
# this case, mosquitto will attempt to bind the listener to that
# address and so restrict access to the associated network and
# interface. By default, mosquitto will listen on all interfaces.
# Note that for a websockets listener it is not possible to bind to a host
# name.
#
# On systems that support Unix Domain Sockets, it is also possible
# to create a # Unix socket rather than opening a TCP socket. In
# this case, the port number should be set to 0 and a unix socket
# path must be provided, e.g.
# listener 0 /tmp/mosquitto.sock
#
# listener port-number [ip address/host name/unix socket path]
listener 1883
# By default, a listener will attempt to listen on all supported IP protocol
# versions. If you do not have an IPv4 or IPv6 interface you may wish to
# disable support for either of those protocol versions. In particular, note
# that due to the limitations of the websockets library, it will only ever
# attempt to open IPv6 sockets if IPv6 support is compiled in, and so will fail
# if IPv6 is not available.
#
# Set to `ipv4` to force the listener to only use IPv4, or set to `ipv6` to
# force the listener to only use IPv6. If you want support for both IPv4 and
# IPv6, then do not use the socket_domain option.
#
#socket_domain
# Bind the listener to a specific interface. This is similar to
# the [ip address/host name] part of the listener definition, but is useful
# when an interface has multiple addresses or the address may change. If used
# with the [ip address/host name] part of the listener definition, then the
# bind_interface option will take priority.
# Not available on Windows.
#
# Example: bind_interface eth0
#bind_interface
# When a listener is using the websockets protocol, it is possible to serve
# http data as well. Set http_dir to a directory which contains the files you
# wish to serve. If this option is not specified, then no normal http
# connections will be possible.
#http_dir
# The maximum number of client connections to allow. This is
# a per listener setting.
# Default is -1, which means unlimited connections.
# Note that other process limits mean that unlimited connections
# are not really possible. Typically the default maximum number of
# connections possible is around 1024.
#max_connections -1
# The listener can be restricted to operating within a topic hierarchy using
# the mount_point option. This is achieved be prefixing the mount_point string
# to all topics for any clients connected to this listener. This prefixing only
# happens internally to the broker; the client will not see the prefix.
#mount_point
# Choose the protocol to use when listening.
# This can be either mqtt or websockets.
# Certificate based TLS may be used with websockets, except that only the
# cafile, certfile, keyfile, ciphers, and ciphers_tls13 options are supported.
#protocol mqtt
# Set use_username_as_clientid to true to replace the clientid that a client
# connected with with its username. This allows authentication to be tied to
# the clientid, which means that it is possible to prevent one client
# disconnecting another by using the same clientid.
# If a client connects with no username it will be disconnected as not
# authorised when this option is set to true.
# Do not use in conjunction with clientid_prefixes.
# See also use_identity_as_username.
#use_username_as_clientid
# Change the websockets headers size. This is a global option, it is not
# possible to set per listener. This option sets the size of the buffer used in
# the libwebsockets library when reading HTTP headers. If you are passing large
# header data such as cookies then you may need to increase this value. If left
# unset, or set to 0, then the default of 1024 bytes will be used.
#websockets_headers_size
# -----------------------------------------------------------------
# Certificate based SSL/TLS support
# -----------------------------------------------------------------
# The following options can be used to enable certificate based SSL/TLS support
# for this listener. Note that the recommended port for MQTT over TLS is 8883,
# but this must be set manually.
#
# See also the mosquitto-tls man page and the "Pre-shared-key based SSL/TLS
# support" section. Only one of certificate or PSK encryption support can be
# enabled for any listener.
# Both of certfile and keyfile must be defined to enable certificate based
# TLS encryption.
# Path to the PEM encoded server certificate.
#certfile
# Path to the PEM encoded keyfile.
#keyfile
# If you wish to control which encryption ciphers are used, use the ciphers
# option. The list of available ciphers can be optained using the "openssl
# ciphers" command and should be provided in the same format as the output of
# that command. This applies to TLS 1.2 and earlier versions only. Use
# ciphers_tls1.3 for TLS v1.3.
#ciphers
# Choose which TLS v1.3 ciphersuites are used for this listener.
# Defaults to "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
#ciphers_tls1.3
# If you have require_certificate set to true, you can create a certificate
# revocation list file to revoke access to particular client certificates. If
# you have done this, use crlfile to point to the PEM encoded revocation file.
#crlfile
# To allow the use of ephemeral DH key exchange, which provides forward
# security, the listener must load DH parameters. This can be specified with
# the dhparamfile option. The dhparamfile can be generated with the command
# e.g. "openssl dhparam -out dhparam.pem 2048"
#dhparamfile
# By default an TLS enabled listener will operate in a similar fashion to a
# https enabled web server, in that the server has a certificate signed by a CA
# and the client will verify that it is a trusted certificate. The overall aim
# is encryption of the network traffic. By setting require_certificate to true,
# the client must provide a valid certificate in order for the network
# connection to proceed. This allows access to the broker to be controlled
# outside of the mechanisms provided by MQTT.
#require_certificate false
# cafile and capath define methods of accessing the PEM encoded
# Certificate Authority certificates that will be considered trusted when
# checking incoming client certificates.
# cafile defines the path to a file containing the CA certificates.
# capath defines a directory that will be searched for files
# containing the CA certificates. For capath to work correctly, the
# certificate files must have ".crt" as the file ending and you must run
# "openssl rehash <path to capath>" each time you add/remove a certificate.
#cafile
#capath
# If require_certificate is true, you may set use_identity_as_username to true
# to use the CN value from the client certificate as a username. If this is
# true, the password_file option will not be used for this listener.
#use_identity_as_username false
# -----------------------------------------------------------------
# Pre-shared-key based SSL/TLS support
# -----------------------------------------------------------------
# The following options can be used to enable PSK based SSL/TLS support for
# this listener. Note that the recommended port for MQTT over TLS is 8883, but
# this must be set manually.
#
# See also the mosquitto-tls man page and the "Certificate based SSL/TLS
# support" section. Only one of certificate or PSK encryption support can be
# enabled for any listener.
# The psk_hint option enables pre-shared-key support for this listener and also
# acts as an identifier for this listener. The hint is sent to clients and may
# be used locally to aid authentication. The hint is a free form string that
# doesn't have much meaning in itself, so feel free to be creative.
# If this option is provided, see psk_file to define the pre-shared keys to be
# used or create a security plugin to handle them.
#psk_hint
# When using PSK, the encryption ciphers used will be chosen from the list of
# available PSK ciphers. If you want to control which ciphers are available,
# use the "ciphers" option. The list of available ciphers can be optained
# using the "openssl ciphers" command and should be provided in the same format
# as the output of that command.
#ciphers
# Set use_identity_as_username to have the psk identity sent by the client used
# as its username. Authentication will be carried out using the PSK rather than
# the MQTT username/password and so password_file will not be used for this
# listener.
#use_identity_as_username false
listener 9001
protocol websockets
# =================================================================
# Persistence
# =================================================================
# If persistence is enabled, save the in-memory database to disk
# every autosave_interval seconds. If set to 0, the persistence
# database will only be written when mosquitto exits. See also
# autosave_on_changes.
# Note that writing of the persistence database can be forced by
# sending mosquitto a SIGUSR1 signal.
#autosave_interval 1800
# If true, mosquitto will count the number of subscription changes, retained
# messages received and queued messages and if the total exceeds
# autosave_interval then the in-memory database will be saved to disk.
# If false, mosquitto will save the in-memory database to disk by treating
# autosave_interval as a time in seconds.
#autosave_on_changes false
# Save persistent message data to disk (true/false).
# This saves information about all messages, including
# subscriptions, currently in-flight messages and retained
# messages.
# retained_persistence is a synonym for this option.
#persistence false
# The filename to use for the persistent database, not including
# the path.
#persistence_file mosquitto.db
# Location for persistent database.
# Default is an empty string (current directory).
# Set to e.g. /var/lib/mosquitto if running as a proper service on Linux or
# similar.
#persistence_location
# =================================================================
# Logging
# =================================================================
# Places to log to. Use multiple log_dest lines for multiple
# logging destinations.
# Possible destinations are: stdout stderr syslog topic file dlt
#
# stdout and stderr log to the console on the named output.
#
# syslog uses the userspace syslog facility which usually ends up
# in /var/log/messages or similar.
#
# topic logs to the broker topic '$SYS/broker/log/<severity>',
# where severity is one of D, E, W, N, I, M which are debug, error,
# warning, notice, information and message. Message type severity is used by
# the subscribe/unsubscribe log_types and publishes log messages to
# $SYS/broker/log/M/susbcribe or $SYS/broker/log/M/unsubscribe.
#
# The file destination requires an additional parameter which is the file to be
# logged to, e.g. "log_dest file /var/log/mosquitto.log". The file will be
# closed and reopened when the broker receives a HUP signal. Only a single file
# destination may be configured.
#
# The dlt destination is for the automotive `Diagnostic Log and Trace` tool.
# This requires that Mosquitto has been compiled with DLT support.
#
# Note that if the broker is running as a Windows service it will default to
# "log_dest none" and neither stdout nor stderr logging is available.
# Use "log_dest none" if you wish to disable logging.
#log_dest stderr
# Types of messages to log. Use multiple log_type lines for logging
# multiple types of messages.
# Possible types are: debug, error, warning, notice, information,
# none, subscribe, unsubscribe, websockets, all.
# Note that debug type messages are for decoding the incoming/outgoing
# network packets. They are not logged in "topics".
#log_type error
#log_type warning
#log_type notice
#log_type information
# If set to true, client connection and disconnection messages will be included
# in the log.
#connection_messages true
# If using syslog logging (not on Windows), messages will be logged to the
# "daemon" facility by default. Use the log_facility option to choose which of
# local0 to local7 to log to instead. The option value should be an integer
# value, e.g. "log_facility 5" to use local5.
#log_facility
# If set to true, add a timestamp value to each log message.
#log_timestamp true
# Set the format of the log timestamp. If left unset, this is the number of
# seconds since the Unix epoch.
# This is a free text string which will be passed to the strftime function. To
# get an ISO 8601 datetime, for example:
# log_timestamp_format %Y-%m-%dT%H:%M:%S
#log_timestamp_format
# Change the websockets logging level. This is a global option, it is not
# possible to set per listener. This is an integer that is interpreted by
# libwebsockets as a bit mask for its lws_log_levels enum. See the
# libwebsockets documentation for more details. "log_type websockets" must also
# be enabled.
#websockets_log_level 0
# =================================================================
# Security
# =================================================================
# If set, only clients that have a matching prefix on their
# clientid will be allowed to connect to the broker. By default,
# all clients may connect.
# For example, setting "secure-" here would mean a client "secure-
# client" could connect but another with clientid "mqtt" couldn't.
#clientid_prefixes
# Boolean value that determines whether clients that connect
# without providing a username are allowed to connect. If set to
# false then a password file should be created (see the
# password_file option) to control authenticated client access.
#
# Defaults to false, unless there are no listeners defined in the configuration
# file, in which case it is set to true, but connections are only allowed from
# the local machine.
allow_anonymous true
# -----------------------------------------------------------------
# Default authentication and topic access control
# -----------------------------------------------------------------
# Control access to the broker using a password file. This file can be
# generated using the mosquitto_passwd utility. If TLS support is not compiled
# into mosquitto (it is recommended that TLS support should be included) then
# plain text passwords are used, in which case the file should be a text file
# with lines in the format:
# username:password
# The password (and colon) may be omitted if desired, although this
# offers very little in the way of security.
#
# See the TLS client require_certificate and use_identity_as_username options
# for alternative authentication options. If an auth_plugin is used as well as
# password_file, the auth_plugin check will be made first.
#password_file
# Access may also be controlled using a pre-shared-key file. This requires
# TLS-PSK support and a listener configured to use it. The file should be text
# lines in the format:
# identity:key
# The key should be in hexadecimal format without a leading "0x".
# If an auth_plugin is used as well, the auth_plugin check will be made first.
#psk_file
# Control access to topics on the broker using an access control list
# file. If this parameter is defined then only the topics listed will
# have access.
# If the first character of a line of the ACL file is a # it is treated as a
# comment.
# Topic access is added with lines of the format:
#
# topic [read|write|readwrite|deny] <topic>
#
# The access type is controlled using "read", "write", "readwrite" or "deny".
# This parameter is optional (unless <topic> contains a space character) - if
# not given then the access is read/write. <topic> can contain the + or #
# wildcards as in subscriptions.
#
# The "deny" option can used to explicity deny access to a topic that would
# otherwise be granted by a broader read/write/readwrite statement. Any "deny"
# topics are handled before topics that grant read/write access.
#
# The first set of topics are applied to anonymous clients, assuming
# allow_anonymous is true. User specific topic ACLs are added after a
# user line as follows:
#
# user <username>
#
# The username referred to here is the same as in password_file. It is
# not the clientid.
#
#
# If is also possible to define ACLs based on pattern substitution within the
# topic. The patterns available for substition are:
#
# %c to match the client id of the client
# %u to match the username of the client
#
# The substitution pattern must be the only text for that level of hierarchy.
#
# The form is the same as for the topic keyword, but using pattern as the
# keyword.
# Pattern ACLs apply to all users even if the "user" keyword has previously
# been given.
#
# If using bridges with usernames and ACLs, connection messages can be allowed
# with the following pattern:
# pattern write $SYS/broker/connection/%c/state
#
# pattern [read|write|readwrite] <topic>
#
# Example:
#
# pattern write sensor/%u/data
#
# If an auth_plugin is used as well as acl_file, the auth_plugin check will be
# made first.
#acl_file
# -----------------------------------------------------------------
# External authentication and topic access plugin options
# -----------------------------------------------------------------
# External authentication and access control can be supported with the
# auth_plugin option. This is a path to a loadable plugin. See also the
# auth_opt_* options described below.
#
# The auth_plugin option can be specified multiple times to load multiple
# plugins. The plugins will be processed in the order that they are specified
# here. If the auth_plugin option is specified alongside either of
# password_file or acl_file then the plugin checks will be made first.
#
#auth_plugin
# If the auth_plugin option above is used, define options to pass to the
# plugin here as described by the plugin instructions. All options named
# using the format auth_opt_* will be passed to the plugin, for example:
#
# auth_opt_db_host
# auth_opt_db_port
# auth_opt_db_username
# auth_opt_db_password
# =================================================================
# Bridges
# =================================================================
# A bridge is a way of connecting multiple MQTT brokers together.
# Create a new bridge using the "connection" option as described below. Set
# options for the bridges using the remaining parameters. You must specify the
# address and at least one topic to subscribe to.
#
# Each connection must have a unique name.
#
# The address line may have multiple host address and ports specified. See
# below in the round_robin description for more details on bridge behaviour if
# multiple addresses are used. Note that if you use an IPv6 address, then you
# are required to specify a port.
#
# The direction that the topic will be shared can be chosen by
# specifying out, in or both, where the default value is out.
# The QoS level of the bridged communication can be specified with the next
# topic option. The default QoS level is 0, to change the QoS the topic
# direction must also be given.
#
# The local and remote prefix options allow a topic to be remapped when it is
# bridged to/from the remote broker. This provides the ability to place a topic
# tree in an appropriate location.
#
# For more details see the mosquitto.conf man page.
#
# Multiple topics can be specified per connection, but be careful
# not to create any loops.
#
# If you are using bridges with cleansession set to false (the default), then
# you may get unexpected behaviour from incoming topics if you change what
# topics you are subscribing to. This is because the remote broker keeps the
# subscription for the old topic. If you have this problem, connect your bridge
# with cleansession set to true, then reconnect with cleansession set to false
# as normal.
#connection <name>
#address <host>[:<port>] [<host>[:<port>]]
#topic <topic> [[[out | in | both] qos-level] local-prefix remote-prefix]
# If you need to have the bridge connect over a particular network interface,
# use bridge_bind_address to tell the bridge which local IP address the socket
# should bind to, e.g. `bridge_bind_address 192.168.1.10`
#bridge_bind_address
# If a bridge has topics that have "out" direction, the default behaviour is to
# send an unsubscribe request to the remote broker on that topic. This means
# that changing a topic direction from "in" to "out" will not keep receiving
# incoming messages. Sending these unsubscribe requests is not always
# desirable, setting bridge_attempt_unsubscribe to false will disable sending
# the unsubscribe request.
#bridge_attempt_unsubscribe true
# Set the version of the MQTT protocol to use with for this bridge. Can be one
# of mqttv50, mqttv311 or mqttv31. Defaults to mqttv311.
#bridge_protocol_version mqttv311
# Set the clean session variable for this bridge.
# When set to true, when the bridge disconnects for any reason, all
# messages and subscriptions will be cleaned up on the remote
# broker. Note that with cleansession set to true, there may be a
# significant amount of retained messages sent when the bridge
# reconnects after losing its connection.
# When set to false, the subscriptions and messages are kept on the
# remote broker, and delivered when the bridge reconnects.
#cleansession false
# Set the amount of time a bridge using the lazy start type must be idle before
# it will be stopped. Defaults to 60 seconds.
#idle_timeout 60
# Set the keepalive interval for this bridge connection, in
# seconds.
#keepalive_interval 60
# Set the clientid to use on the local broker. If not defined, this defaults to
# 'local.<clientid>'. If you are bridging a broker to itself, it is important
# that local_clientid and clientid do not match.
#local_clientid
# If set to true, publish notification messages to the local and remote brokers
# giving information about the state of the bridge connection. Retained
# messages are published to the topic $SYS/broker/connection/<clientid>/state
# unless the notification_topic option is used.
# If the message is 1 then the connection is active, or 0 if the connection has
# failed.
# This uses the last will and testament feature.
#notifications true
# Choose the topic on which notification messages for this bridge are
# published. If not set, messages are published on the topic
# $SYS/broker/connection/<clientid>/state
#notification_topic
# Set the client id to use on the remote end of this bridge connection. If not
# defined, this defaults to 'name.hostname' where name is the connection name
# and hostname is the hostname of this computer.
# This replaces the old "clientid" option to avoid confusion. "clientid"
# remains valid for the time being.
#remote_clientid
# Set the password to use when connecting to a broker that requires
# authentication. This option is only used if remote_username is also set.
# This replaces the old "password" option to avoid confusion. "password"
# remains valid for the time being.
#remote_password
# Set the username to use when connecting to a broker that requires
# authentication.
# This replaces the old "username" option to avoid confusion. "username"
# remains valid for the time being.
#remote_username
# Set the amount of time a bridge using the automatic start type will wait
# until attempting to reconnect.
# This option can be configured to use a constant delay time in seconds, or to
# use a backoff mechanism based on "Decorrelated Jitter", which adds a degree
# of randomness to when the restart occurs.
#
# Set a constant timeout of 20 seconds:
# restart_timeout 20
#
# Set backoff with a base (start value) of 10 seconds and a cap (upper limit) of
# 60 seconds:
# restart_timeout 10 30
#
# Defaults to jitter with a base of 5 and cap of 30
#restart_timeout 5 30
# If the bridge has more than one address given in the address/addresses
# configuration, the round_robin option defines the behaviour of the bridge on
# a failure of the bridge connection. If round_robin is false, the default
# value, then the first address is treated as the main bridge connection. If
# the connection fails, the other secondary addresses will be attempted in
# turn. Whilst connected to a secondary bridge, the bridge will periodically
# attempt to reconnect to the main bridge until successful.
# If round_robin is true, then all addresses are treated as equals. If a
# connection fails, the next address will be tried and if successful will
# remain connected until it fails
#round_robin false
# Set the start type of the bridge. This controls how the bridge starts and
# can be one of three types: automatic, lazy and once. Note that RSMB provides
# a fourth start type "manual" which isn't currently supported by mosquitto.
#
# "automatic" is the default start type and means that the bridge connection
# will be started automatically when the broker starts and also restarted
# after a short delay (30 seconds) if the connection fails.
#
# Bridges using the "lazy" start type will be started automatically when the
# number of queued messages exceeds the number set with the "threshold"
# parameter. It will be stopped automatically after the time set by the
# "idle_timeout" parameter. Use this start type if you wish the connection to
# only be active when it is needed.
#
# A bridge using the "once" start type will be started automatically when the
# broker starts but will not be restarted if the connection fails.
#start_type automatic
# Set the number of messages that need to be queued for a bridge with lazy
# start type to be restarted. Defaults to 10 messages.
# Must be less than max_queued_messages.
#threshold 10
# If try_private is set to true, the bridge will attempt to indicate to the
# remote broker that it is a bridge not an ordinary client. If successful, this
# means that loop detection will be more effective and that retained messages
# will be propagated correctly. Not all brokers support this feature so it may
# be necessary to set try_private to false if your bridge does not connect
# properly.
#try_private true
# Some MQTT brokers do not allow retained messages. MQTT v5 gives a mechanism
# for brokers to tell clients that they do not support retained messages, but
# this is not possible for MQTT v3.1.1 or v3.1. If you need to bridge to a
# v3.1.1 or v3.1 broker that does not support retained messages, set the
# bridge_outgoing_retain option to false. This will remove the retain bit on
# all outgoing messages to that bridge, regardless of any other setting.
#bridge_outgoing_retain true
# If you wish to restrict the size of messages sent to a remote bridge, use the
# bridge_max_packet_size option. This sets the maximum number of bytes for
# the total message, including headers and payload.
# Note that MQTT v5 brokers may provide their own maximum-packet-size property.
# In this case, the smaller of the two limits will be used.
# Set to 0 for "unlimited".
#bridge_max_packet_size 0
# -----------------------------------------------------------------
# Certificate based SSL/TLS support
# -----------------------------------------------------------------
# Either bridge_cafile or bridge_capath must be defined to enable TLS support
# for this bridge.
# bridge_cafile defines the path to a file containing the
# Certificate Authority certificates that have signed the remote broker
# certificate.
# bridge_capath defines a directory that will be searched for files containing
# the CA certificates. For bridge_capath to work correctly, the certificate
# files must have ".crt" as the file ending and you must run "openssl rehash
# <path to capath>" each time you add/remove a certificate.
#bridge_cafile
#bridge_capath
# If the remote broker has more than one protocol available on its port, e.g.
# MQTT and WebSockets, then use bridge_alpn to configure which protocol is
# requested. Note that WebSockets support for bridges is not yet available.
#bridge_alpn
# When using certificate based encryption, bridge_insecure disables
# verification of the server hostname in the server certificate. This can be
# useful when testing initial server configurations, but makes it possible for
# a malicious third party to impersonate your server through DNS spoofing, for
# example. Use this option in testing only. If you need to resort to using this
# option in a production environment, your setup is at fault and there is no
# point using encryption.
#bridge_insecure false
# Path to the PEM encoded client certificate, if required by the remote broker.
#bridge_certfile
# Path to the PEM encoded client private key, if required by the remote broker.
#bridge_keyfile
# -----------------------------------------------------------------
# PSK based SSL/TLS support
# -----------------------------------------------------------------
# Pre-shared-key encryption provides an alternative to certificate based
# encryption. A bridge can be configured to use PSK with the bridge_identity
# and bridge_psk options. These are the client PSK identity, and pre-shared-key
# in hexadecimal format with no "0x". Only one of certificate and PSK based
# encryption can be used on one
# bridge at once.
#bridge_identity
#bridge_psk
# =================================================================
# External config files
# =================================================================
# External configuration files may be included by using the
# include_dir option. This defines a directory that will be searched
# for config files. All files that end in '.conf' will be loaded as
# a configuration file. It is best to have this as the last option
# in the main file. This option will only be processed from the main
# configuration file. The directory specified must not contain the
# main configuration file.
# Files within include_dir will be loaded sorted in case-sensitive
# alphabetical order, with capital letters ordered first. If this option is
# given multiple times, all of the files from the first instance will be
# processed before the next instance. See the man page for examples.
#include_dir

View File

@@ -0,0 +1,19 @@
FROM smeagolworms4/mqtt-explorer:browser-1.0.3
COPY ./settings.json /mqtt-explorer/config/settings.json
ARG TARGETARCH
COPY entrypoint_wrapper.sh /entrypoint_wrapper.sh
RUN if [ ${TARGETARCH} != "amd64" ]; then \
mv /entrypoint.sh /wrapped_entrypoint.sh; \
cp /entrypoint_wrapper.sh /entrypoint.sh; \
fi; \
rm /entrypoint_wrapper.sh
ENTRYPOINT [ "/entrypoint.sh" ]
CMD node node-server/server/dist/node-server/server/src/index.js \
--http-port=$HTTP_PORT \
--config-path=$CONFIG_PATH \
--http-user=$HTTP_USER \
--http-password=$HTTP_PASSWORD\
--ssl-key-path=$SSL_KEY_PATH\
--ssl-cert-path=$SSL_CERT_PATH

View File

@@ -0,0 +1,37 @@
#!/bin/sh
# ---------------------------------------------
# Architecture Warning Wrapper Script
#
# This script is used as an entrypoint wrapper to emit a warning
# when the container is not running on the officially supported
# amd64 (x86_64) architecture.
#
# It checks for the presence of a wrapped entrypoint script
# (/wrapped_entrypoint.sh) and executes it if found; otherwise,
# it falls back to executing the provided command directly.
#
# The warning is shown both before and after the wrapped command
# to ensure visibility.
# ---------------------------------------------
function print_warning {
echo -e "\033[0;31m"
echo "-------------------------------------------------------------"
echo "⚠️ WARNING: Unsupported Architecture Detected"
echo
echo "This Docker image is not running on the amd64 (x86_64) architecture."
echo "It is recommended to use the amd64-based image for full compatibility."
echo "Other architectures are not officially supported and may cause issues."
echo
echo "-------------------------------------------------------------"
echo -e "\033[0m"
}
print_warning
if [ -f /wrapped_entrypoint.sh ]; then
exec /wrapped_entrypoint.sh "$@"
else
exec "$@"
fi

View File

@@ -0,0 +1,26 @@
{
"ConnectionManager_connections": {
"mqtt-server": {
"configVersion": 1,
"certValidation": true,
"clientId": "mqtt-explorer-e1085971",
"id": "mqtt-server",
"name": "MQTT Server",
"encryption": false,
"subscriptions": [
{
"topic": "#",
"qos": 0
},
{
"topic": "$SYS/#",
"qos": 0
}
],
"type": "mqtt",
"host": "mqtt-server",
"port": 1883,
"protocol": "mqtt"
}
}
}

View File

@@ -0,0 +1,19 @@
FROM nodered/node-red:4.1.2
RUN npm install node-red-dashboard@3.6.6
RUN npm install node-red-contrib-ui-actions@0.1.8
RUN npm install node-red-node-ui-table@0.4.5
RUN npm install node-red-contrib-ui-level@0.1.46
COPY nodered-settings.js /data/settings.js
USER root
COPY entrypoint.sh /entrypoint.sh
ARG TARGETARCH
COPY entrypoint_wrapper.sh /entrypoint_wrapper.sh
RUN if [ ${TARGETARCH} != "amd64" ]; then \
mv /entrypoint.sh /wrapped_entrypoint.sh; \
cp /entrypoint_wrapper.sh /entrypoint.sh; \
fi; \
rm /entrypoint_wrapper.sh
USER node-red
ENTRYPOINT [ "/entrypoint.sh" ]

View File

@@ -0,0 +1,10 @@
#!/bin/sh
exec npm \
--no-update-notifier \
--no-fund \
start \
--cache /data/.npm \
-- \
--userDir /data \
"$@"

View File

@@ -0,0 +1,37 @@
#!/bin/sh
# ---------------------------------------------
# Architecture Warning Wrapper Script
#
# This script is used as an entrypoint wrapper to emit a warning
# when the container is not running on the officially supported
# amd64 (x86_64) architecture.
#
# It checks for the presence of a wrapped entrypoint script
# (/wrapped_entrypoint.sh) and executes it if found; otherwise,
# it falls back to executing the provided command directly.
#
# The warning is shown both before and after the wrapped command
# to ensure visibility.
# ---------------------------------------------
function print_warning {
echo -e "\033[0;31m"
echo "-------------------------------------------------------------"
echo "⚠️ WARNING: Unsupported Architecture Detected"
echo
echo "This Docker image is not running on the amd64 (x86_64) architecture."
echo "It is recommended to use the amd64-based image for full compatibility."
echo "Other architectures are not officially supported and may cause issues."
echo
echo "-------------------------------------------------------------"
echo -e "\033[0m"
}
print_warning
if [ -f /wrapped_entrypoint.sh ]; then
exec /wrapped_entrypoint.sh "$@"
else
exec "$@"
fi

View File

@@ -0,0 +1,20 @@
module.exports = {
// Flow file location
flowFile: 'flows.json',
// Enable projects
enableProjects: process.env.NODE_RED_ENABLE_PROJECTS === 'true',
// HTTP settings
httpNodeRoot: '/',
httpAdminRoot: '/',
// Logging
logging: {
console: {
level: "info",
metrics: false,
audit: false
}
}
};

View File

@@ -0,0 +1,30 @@
FROM maven:3.6.1-jdk-11
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
WORKDIR /steve
ENV DOCKERIZE_VERSION v0.6.1
RUN wget --no-verbose https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
RUN wget -qO- https://github.com/steve-community/steve/archive/steve-3.6.0.tar.gz | tar xz --strip-components=1
COPY main.properties src/main/resources/config/docker
COPY init.sh .
COPY keystore.jks .
ARG TARGETARCH
COPY entrypoint_wrapper.sh /entrypoint_wrapper.sh
RUN if [ ${TARGETARCH} != "amd64" ]; then \
ln -s /usr/local/bin/mvn-entrypoint.sh /wrapped_entrypoint.sh; \
cp /entrypoint_wrapper.sh /entrypoint.sh; \
else \
ln -s /usr/local/bin/mvn-entrypoint.sh /entrypoint.sh; \
fi; \
rm /entrypoint_wrapper.sh
ENTRYPOINT [ "/entrypoint.sh" ]
CMD /steve/init.sh

View File

@@ -0,0 +1,37 @@
#!/bin/bash
# ---------------------------------------------
# Architecture Warning Wrapper Script
#
# This script is used as an entrypoint wrapper to emit a warning
# when the container is not running on the officially supported
# amd64 (x86_64) architecture.
#
# It checks for the presence of a wrapped entrypoint script
# (/wrapped_entrypoint.sh) and executes it if found; otherwise,
# it falls back to executing the provided command directly.
#
# The warning is shown both before and after the wrapped command
# to ensure visibility.
# ---------------------------------------------
function print_warning {
echo -e "\033[0;31m"
echo "-------------------------------------------------------------"
echo "⚠️ WARNING: Unsupported Architecture Detected"
echo
echo "This Docker image is not running on the amd64 (x86_64) architecture."
echo "It is recommended to use the amd64-based image for full compatibility."
echo "Other architectures are not officially supported and may cause issues."
echo
echo "-------------------------------------------------------------"
echo -e "\033[0m"
}
print_warning
if [ -f /wrapped_entrypoint.sh ]; then
exec /wrapped_entrypoint.sh "$@"
else
exec "$@"
fi

View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -e # exit on any error
dockerize -wait tcp://ocpp-db:3306 -timeout 60s
if [ ! -f ".buildsuccess" ]; then
mvn clean package -Pdocker -Djdk.tls.client.protocols="TLSv1,TLSv1.1,TLSv1.2"
touch .buildsuccess
fi
java -jar target/steve.jar

View File

@@ -0,0 +1,57 @@
# Just to be backwards compatible with previous versions, this is set to "steve",
# since there might be already configured chargepoints expecting the older path.
# Otherwise, might as well be changed to something else or be left empty.
#
context.path = steve
# Database configuration
#
db.ip = ocpp-db
db.port = 3306
db.schema = ocpp-db
db.user = ocpp
db.password = ocpp
# Credentials for Web interface access
#
auth.user = admin
auth.password = 1234
# Jetty configuration
#
server.host = 0.0.0.0
server.gzip.enabled = false
# Jetty HTTP configuration
#
http.enabled = true
http.port = 8180
# Jetty HTTPS configuration
#
https.enabled = true
https.port = 8443
keystore.path = /steve/keystore.jks
keystore.password = 123456
# When the WebSocket/Json charge point opens more than one WebSocket connection,
# we need a mechanism/strategy to select one of them for outgoing requests.
# For allowed values see de.rwth.idsg.steve.ocpp.ws.custom.WsSessionSelectStrategyEnum.
#
ws.session.select.strategy = ALWAYS_LAST
# if BootNotification messages arrive (SOAP) or WebSocket connection attempts are made (JSON) from unknown charging
# stations, we reject these charging stations, because stations with these chargeBoxIds were NOT inserted into database
# beforehand. by setting this property to true, this behaviour can be modified to automatically insert unknown
# stations into database and accept their requests.
#
# CAUTION: setting this property to true is very dangerous, because we will accept EVERY BootNotification or WebSocket
# connection attempt from ANY sender as long as the sender knows the URL and sends a valid message.
#
auto.register.unknown.stations = false
### DO NOT MODIFY ###
steve.version = ${project.version}
git.describe = ${git.commit.id.describe}
db.sql.logging = false
profile = prod

View File

@@ -0,0 +1,3 @@
build
__pycache__
*.egg-info

View File

@@ -0,0 +1,24 @@
ev_setup_cmake_variables_python_wheel()
ev_add_pip_package(
NAME edm
SOURCE_DIRECTORY .
)
ev_is_python_venv_active(
RESULT_VAR IS_PYTHON_VENV_ACTIVE
)
if(NOT ${IS_PYTHON_VENV_ACTIVE})
message(WARNING "Python venv is not active. Please ensure that edm is available in your environment.")
else()
get_target_property(SOURCE_DIRECTORY ev_pip_package_edm SOURCE_DIRECTORY)
message(STATUS "Installing edm from: ${SOURCE_DIRECTORY}")
ev_pip_install_local(
PACKAGE_NAME "edm"
PACKAGE_SOURCE_DIRECTORY "${SOURCE_DIRECTORY}"
)
unset(EVEREST_DEPENDENCY_MANAGER CACHE)
find_program(EVEREST_DEPENDENCY_MANAGER edm HINTS ${EV_ACTIVATE_PYTHON_VENV_PATH_TO_VENV}/bin REQUIRED)
message(STATUS "Using edm from: ${EDM}")
endif()

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.

View File

@@ -0,0 +1,232 @@
# Dependency Manager for EVerest
- [Dependency Manager for EVerest](#dependency-manager-for-everest)
- [Install and Quick Start](#install-and-quick-start)
- [Installing edm](#installing-edm)
- [Enabling CPM_SOURCE_CACHE](#enabling-cpm_source_cache)
- [Python packages needed to run edm](#python-packages-needed-to-run-edm)
- [Setting up CMake integration](#setting-up-cmake-integration)
- [Setting up a workspace](#setting-up-a-workspace)
- [Updating a workspace](#updating-a-workspace)
- [Using the EDM CMake module and dependencies.yaml](#using-the-edm-cmake-module-and-dependenciesyaml)
- [Modifying dependencies](#modifying-dependencies)
- [Create a workspace config from an existing directory tree](#create-a-workspace-config-from-an-existing-directory-tree)
- [Git information at a glance](#git-information-at-a-glance)
## Install and Quick Start
To install the **edm** dependency manager for EVerest you have to perform the following steps.
Please make sure you are running a sufficiently recent version of **Python3 (>=3.6)** and that you are able to install Python packages from source. Usually you just have to ensure that you have **pip**, **setuptools** and **wheel** available. Refer to [the Python *Installing Packages* documentation](https://packaging.python.org/tutorials/installing-packages/#requirements-for-installing-packages) for indepth guidance if any problems arise.
```bash
python3 -m pip install --upgrade pip setuptools wheel
```
### Installing edm
Now you can clone *this repository* and install **edm**:
(make sure you have set up your [ssh key](https://www.atlassian.com/git/tutorials/git-ssh) in github first!)
```bash
git clone git@github.com:EVerest/everest-dev-environment.git
cd everest-dev-environment/dependency_manager
python3 -m pip install .
edm --config ../everest-complete.yaml --workspace ~/checkout/everest-workspace
```
The last command registers the [**EDM** CMake module](#setting-up-cmake-integration) and creates a workspace in the *~/checkout/everest-workspace* directory from [a config that is shipped with this repository](../everest-complete.yaml).
The workspace will have the following structure containing all current dependencies for EVerest:
```bash
everest-workspace/
├── EVerest
├── everest-deploy-devkit
├── everest-dev-environment
├── everest-framework
├── everest-utils
├── liblog
├── libmodbus
├── libocpp
├── libsunspec
├── libtimer
├── open-plc-utils
├── RISE-V2G
└── workspace-config.yaml
```
The *workspace-config.yaml* contains a copy of the config that was used to create this workspace.
### Enabling CPM_SOURCE_CACHE
The **edm** dependency manager uses [CPM](https://github.com/cpm-cmake/CPM.cmake) for its CMake integration.
This means you *can* and **should** set the *CPM_SOURCE_CACHE* environment variable. This makes sure that dependencies that you do not manage in the workspace are not re-downloaded multiple times. For detailed information and other useful environment variables please refer to the [CPM Documentation](https://github.com/cpm-cmake/CPM.cmake/blob/master/README.md#CPM_SOURCE_CACHE).
```bash
export CPM_SOURCE_CACHE=$HOME/.cache/CPM
```
### Python packages needed to run edm
The following Python3 packages are needed to run the **edm** dependency manager.
If you installed **edm** using the guide above they were already installed automatically.
- Python >= 3.6
- Jinja2 >= 3.0
- PyYAML >= 5.4
## Setting up and updating a workspace
For letting **edm** do the work of setting up an initial EVerest workspace,
do this:
```bash
edm init --workspace ~/checkout/everest-workspace
```
If you are currently in the *everest-workspace* directory the following command has the same effect:
```bash
edm init
```
For using a dedicated release version, you can do this:
```bash
edm init 2023.7.0
```
In this example, version 2023.7.0 is pulled from the server. This will only work if
you previous code is not in a "dirty" state.
## Using the EDM CMake module and dependencies.yaml
To use **edm** from CMake you have to add the following line to the top-level *CMakeLists.txt* file in the respective source repository:
```cmake
find_package(EDM REQUIRED)
```
The **EDM** CMake module will be discovered automatically if you [registered the CMake module in the way it described in the *Setting up CMake integration* section of this readme](#setting-up-cmake-integration).
To define dependencies you can now add a **dependencies.yaml** file to your source repository. It should look like this:
```yaml
---
liblog:
git: git@github.com:EVerest/liblog.git
git_tag: main
options: ["BUILD_EXAMPLES OFF"]
libtimer:
git: git@github.com:EVerest/libtimer.git
git_tag: main
options: ["BUILD_EXAMPLES OFF"]
```
If you want to conditionally include some dependencies, eg. for testing, you can do this in the following way:
```yaml
catch2:
git: https://github.com/catchorg/Catch2.git
git_tag: v3.4.0
cmake_condition: "BUILD_TESTING"
```
Here *cmake_condition* can be any string that CMake can use in an if() block. Please be aware that any variables you use here must be defined before a call to *evc_setup_edm()* is made in your CMakeLists.txt
## Selective library consumption
If your project only needs specific everest-core libraries (e.g. `liblog`, `everest-util`, `everest-io`, `libocpp`) without building the full module framework, use the `EVEREST_LIBS_ONLY` and `EVEREST_INCLUDE_LIBS` CMake options.
In your project's `dependencies.yaml`:
```yaml
everest-core:
git: https://github.com/EVerest/everest-core.git
git_tag: 2026.02.0
options:
- "EVEREST_LIBS_ONLY ON"
- "EVEREST_INCLUDE_LIBS log;util;io"
```
Or directly via CMake:
```bash
cmake -S . -B build \
-DEVEREST_LIBS_ONLY=ON \
-DEVEREST_INCLUDE_LIBS="log;util;io"
```
| Option | Default | Description |
|---|---|---|
| `EVEREST_LIBS_ONLY` | OFF | Skip modules, applications, config — only build libraries |
| `EVEREST_INCLUDE_LIBS` | (empty) | Semicolon-separated allowlist; transitive deps auto-resolved |
| `EVEREST_EXCLUDE_LIBS` | (empty) | Semicolon-separated blocklist of libraries to skip |
Transitive dependencies are resolved automatically. For example, `EVEREST_INCLUDE_LIBS="ocpp"` will also build `log`, `timer`, `evse_security`, `sqlite`, and `cbv2g`.
See the [EDM documentation](../../docs/source/explanation/dev-tools/edm.rst) for the full list of available libraries and their dependency chains.
## Modifying dependencies
To change dependency git URLs you can set the *EVEREST_MODIFY_DEPENDENCIES_URLS* environment variable to a string containing prefixes and replacements delimited by whitespace characters.
For example:
```bash
EVEREST_MODIFY_DEPENDENCIES_URLS="prefix=https://github.com/EVerest/ replace=git@github.com:EVerest/"
```
This would change all dependency git URLs that start with *https://github.com/EVerest/* to *git@github.com:EVerest/*.
Multiple prefix and replace pairs can be chained together delimited by whitespace characters.
For example:
```bash
EVEREST_MODIFY_DEPENDENCIES_URLS="prefix=https://github.com/EVerest/ replace=git@github.com:EVerest/ prefix=https://github.com/EVerest/everest-framework.git replace=https://github.com/EVerest/everest-framework.git"
```
This would change all dependency git URLs that start with *https://github.com/EVerest/* to *git@github.com:EVerest/* as well as keeping the dependency https URL of *https://github.com/EVerest/everest-framework.git* as *https://github.com/EVerest/everest-framework.git*.
Additionally you can set the *EVEREST_MODIFY_DEPENDENCIES* environment variable to a file containing modifications to the projects dependencies.yaml files when running cmake:
```bash
EVEREST_MODIFY_DEPENDENCIES=../dependencies_modified.yaml cmake -S . -B build
```
The *dependencies_modified.yaml* file can contain something along these lines:
```yaml
nlohmann_json:
git: null # this makes edm look for nlohmann_json via find_package
libfmt:
rename: fmt # if find_package needs a different dependency name you can rename it
git: null
catch2:
git_tag: v1.2.3 # if you want to select a different git tag for a build this is also possible
```
## Create a workspace config from an existing directory tree
Suppose you already have a directory tree that you want to save into a config file.
You can do this with the following command:
```bash
edm --create-config custom-config.yaml
```
This is a short form of
```bash
edm --create-config custom-config.yaml --include-remotes git@github.com:EVerest/*
```
and only includes repositories from the *EVerest* namespace. You can add as many remotes to this list as you want.
For example if you only want to include certain repositories you can use the following command.
```bash
edm --create-config custom-config.yaml --include-remotes git@github.com:EVerest/everest* git@github.com:EVerest/liblog.git
```
If you want to include all repositories, including external dependencies, in the config you can use the following command.
```bash
edm --create-config custom-config.yaml --external-in-config
```
## Git information at a glance
You can get a list of all git repositories in the current directory and their state using the following command.
```bash
edm --git-info --git-fetch
```
If you want to know the state of all repositories in a workspace you can use the following command.
```bash
edm --workspace ~/checkout/everest-workspace --git-info --git-fetch
```
This creates output that is similar to the following example.
```bash
[edm]: Git info for "~/checkout/everest-workspace":
[edm]: Using git-fetch to update remote information. This might take a few seconds.
[edm]: "everest-dev-environment" @ branch: main [remote: origin/main] [behind 6] [clean]
[edm]: "everest-framework" @ branch: main [remote: origin/main] [dirty]
[edm]: "everest-deploy-devkit" @ branch: main [remote: origin/main] [clean]
[edm]: "libtimer" @ branch: main [remote: origin/main] [dirty]
[edm]: 2/4 repositories are dirty.
```

View File

@@ -0,0 +1,12 @@
#!/bin/bash
##
## SPDX-License-Identifier: Apache-2.0
## Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
##
echo "generating bash-completion file"
SRC_DIR="$(dirname "${BASH_SOURCE[0]}")/src"
echo "Using module found in ${SRC_DIR}"
cd "${SRC_DIR}"
BASH_COMPLETION_FILE_DIR="$(pwd)"
BASH_COMPLETION_FILE="${BASH_COMPLETION_FILE_DIR}/edm_tool/edm-completion.bash"
shtab --shell=bash -u edm_tool.get_parser --prog edm > "${BASH_COMPLETION_FILE}"

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

View File

@@ -0,0 +1,3 @@
Jinja2>=2.11
PyYAML>=5.3
requests>=2

View File

@@ -0,0 +1,38 @@
[metadata]
name = edm_tool
version = attr: edm_tool.__version__
description= A simple dependency manager
long_description = file: README.md
long_description_content_type= text/markdown
url= https://github.com/EVerest/everest-dev-environment
author = Kai-Uwe Hermann
author_email = kai-uwe.hermann@pionix.de
classifiers =
Development Status :: 3 - Alpha
Intended Audience :: Developers
Topic :: Software Development :: Build Tools
License :: OSI Approved :: Apache Software License
[options]
packages = edm_tool
package_dir =
= src
python_requires = >=3.6
install_requires =
Jinja2>=2.11
PyYAML>=5.3
requests>=2
[options.entry_points]
console_scripts =
edm = edm_tool:main
[options.package_data]
edm_tool =
templates/cpm.jinja
cmake/CPM.cmake
cmake/EDMConfig.cmake
edm-completion.bash
[pycodestyle]
max-line-length = 120

View File

@@ -0,0 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest
"""Everest Dependency Manager."""
from setuptools import setup, find_packages
setup(
# see setup.cfg
)

View File

@@ -0,0 +1,18 @@
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
"""Everest Dependency Manager."""
from edm_tool import edm
__version__ = "0.8.0"
def get_parser():
"""Return the command line parser."""
return edm.get_parser(__version__)
def main():
"""Main entrypoint of edm."""
edm.main(get_parser())

View File

@@ -0,0 +1,88 @@
"Bazel related functions for edm_tool."
import yaml
from typing import List, Optional, Dict
def _format_optional_string(value: Optional[str]):
"""Formats a string value as a string literal (with quotes) or `None` if the value is None."""
if value is None:
return "None"
return f'"{value}"'
def _is_commit(revision: str):
# Revision is a commit if it is a hexadecimal 40-character string
return len(revision) == 40 and all(c in "0123456789abcdef" for c in revision.lower())
def _get_depname_for_label(label: str) -> str:
build, depname, bazel = label.split(":")[1].split(".")
if build != "BUILD" or bazel != "bazel":
raise ValueError(f"Invalid build file name: {label}")
return depname
def _parse_build_file_labels(labels: Optional[List[str]]) -> Dict[str, str]:
# For easier matching of build files with dependencies
# we convert the list of build files:
# ```
# [
# "@workspace//path/to/build:BUILD.<depname>.bazel",
# ...
# ]
# ```
# into a dictionary:
# ```
# {
# "<depname>": "@workspace//path/to/build:BUILD.<depname>.bazel",
# ...
# }
# ```
# and check that all build files have proper names.
if labels is None:
return {}
return dict((_get_depname_for_label(label), label) for label in labels)
def generate_deps(args):
"Parse the dependencies.yaml and print content of *.bzl file to stdout."
with open(args.dependencies_yaml, 'r', encoding='utf-8') as f:
deps = yaml.safe_load(f)
build_files = _parse_build_file_labels(args.build_file)
for name in build_files:
if name not in deps:
raise ValueError(f"Build file {name} does not have a corresponding dependency in {args.dependencies_yaml}")
print("""
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
def edm_deps():""")
for name, desc in deps.items():
repo = desc["git"]
# The parameter is called `git_tag` but it can be a tag or a commit
revision = desc["git_tag"]
tag = None
commit = None
if _is_commit(revision):
commit = revision
else:
tag = revision
build_file = build_files.get(name)
print(
f"""
maybe(
git_repository,
name = "{name}",
remote = "{repo}",
tag = {_format_optional_string(tag)},
commit = {_format_optional_string(commit)},
build_file = {_format_optional_string(build_file)},
)
"""
)

View File

@@ -0,0 +1,144 @@
# AUTOMATCALLY GENERATED by `shtab`
_shtab_edm_tool_option_strings=('-h' '--help' '--version' '--workspace' '--working_dir' '--out' '--include_deps' '--config' '--create-vscode-workspace' '--update' '--cmake' '--verbose' '--nocolor' '--install-bash-completion' '--create-config' '--external-in-config' '--include-remotes' '--create-snapshot' '--git-info' '--git-fetch' '--git-pull')
_shtab_edm_tool_pos_0_nargs=A...
_shtab_edm_tool__h_nargs=0
_shtab_edm_tool___help_nargs=0
_shtab_edm_tool___version_nargs=0
_shtab_edm_tool___include_deps_nargs=0
_shtab_edm_tool___create_vscode_workspace_nargs=0
_shtab_edm_tool___update_nargs=0
_shtab_edm_tool___cmake_nargs=0
_shtab_edm_tool___verbose_nargs=0
_shtab_edm_tool___nocolor_nargs=0
_shtab_edm_tool___install_bash_completion_nargs=0
_shtab_edm_tool___external_in_config_nargs=0
_shtab_edm_tool___include_remotes_nargs=*
_shtab_edm_tool___create_snapshot_nargs=?
_shtab_edm_tool___git_info_nargs=0
_shtab_edm_tool___git_fetch_nargs=0
_shtab_edm_tool___git_pull_nargs=*
# $1=COMP_WORDS[1]
_shtab_compgen_files() {
compgen -f -- $1 # files
}
# $1=COMP_WORDS[1]
_shtab_compgen_dirs() {
compgen -d -- $1 # recurse into subdirs
}
# $1=COMP_WORDS[1]
_shtab_replace_nonword() {
echo "${1//[^[:word:]]/_}"
}
# set default values (called for the initial parser & any subparsers)
_set_parser_defaults() {
local subparsers_var="${prefix}_subparsers[@]"
sub_parsers=${!subparsers_var}
local current_option_strings_var="${prefix}_option_strings[@]"
current_option_strings=${!current_option_strings_var}
completed_positional_actions=0
_set_new_action "pos_${completed_positional_actions}" true
}
# $1=action identifier
# $2=positional action (bool)
# set all identifiers for an action's parameters
_set_new_action() {
current_action="${prefix}_$(_shtab_replace_nonword $1)"
local current_action_compgen_var=${current_action}_COMPGEN
current_action_compgen="${!current_action_compgen_var}"
local current_action_choices_var="${current_action}_choices"
current_action_choices="${!current_action_choices_var}"
local current_action_nargs_var="${current_action}_nargs"
if [ -n "${!current_action_nargs_var}" ]; then
current_action_nargs="${!current_action_nargs_var}"
else
current_action_nargs=1
fi
current_action_args_start_index=$(( $word_index + 1 ))
current_action_is_positional=$2
}
# Notes:
# `COMPREPLY`: what will be rendered after completion is triggered
# `completing_word`: currently typed word to generate completions for
# `${!var}`: evaluates the content of `var` and expand its content as a variable
# hello="world"
# x="hello"
# ${!x} -> ${hello} -> "world"
_shtab_edm_tool() {
local completing_word="${COMP_WORDS[COMP_CWORD]}"
COMPREPLY=()
prefix=_shtab_edm_tool
word_index=0
_set_parser_defaults
word_index=1
# determine what arguments are appropriate for the current state
# of the arg parser
while [ $word_index -ne $COMP_CWORD ]; do
local this_word="${COMP_WORDS[$word_index]}"
if [[ -n $sub_parsers && " ${sub_parsers[@]} " =~ " ${this_word} " ]]; then
# valid subcommand: add it to the prefix & reset the current action
prefix="${prefix}_$(_shtab_replace_nonword $this_word)"
_set_parser_defaults
fi
if [[ " ${current_option_strings[@]} " =~ " ${this_word} " ]]; then
# a new action should be acquired (due to recognised option string or
# no more input expected from current action);
# the next positional action can fill in here
_set_new_action $this_word false
fi
if [[ "$current_action_nargs" != "*" ]] && \
[[ "$current_action_nargs" != "+" ]] && \
[[ "$current_action_nargs" != *"..." ]] && \
(( $word_index + 1 - $current_action_args_start_index >= \
$current_action_nargs )); then
$current_action_is_positional && let "completed_positional_actions += 1"
_set_new_action "pos_${completed_positional_actions}" true
fi
let "word_index+=1"
done
# Generate the completions
if [[ "${completing_word}" == -* ]]; then
# optional argument started: use option strings
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
else
# use choices & compgen
local IFS=$'\n'
COMPREPLY=( $(compgen -W "${current_action_choices}" -- "${completing_word}") \
$([ -n "${current_action_compgen}" ] \
&& "${current_action_compgen}" "${completing_word}") )
fi
return 0
}
complete -o filenames -F _shtab_edm_tool edm

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
set(ENV{EVEREST_EDM_WORKSPACE} {{ workspace["workspace"] }})
set(CPM_USE_NAMED_CACHE_DIRECTORIES ON)
{% for dep in checkout %}
set(CPM_{{ dep["name"] }}_SOURCE "{{ dep["path"] }}")
{% endfor %}
{% for name, value in dependencies.items() %}
if("{{name}}" IN_LIST EVEREST_EXCLUDE_DEPENDENCIES)
message(STATUS "Excluding dependency {{name}}")
{% if "cmake_condition" in value and value["cmake_condition"]|length > 0 %}
elseif({{ value["cmake_condition"] }})
{% else %}
else()
{% endif %}
{% if value and "git" in value %}
CPMAddPackage(
NAME {{ name }}
GIT_REPOSITORY {{ value["git"] }}
{% if "git_tag" in value %}
GIT_TAG {{ value["git_tag"] }}
{% endif %}
{% if "options" in value and value["options"]|length > 0 %}
OPTIONS
{{value["options"]|quote|join(" ")}}
{% endif %}
{% if "prevent_install" in value and value["prevent_install"] %}
EXCLUDE_FROM_ALL YES
{% endif %}
)
{% else %}
find_package(
{{ name }}
{% if value and "components" in value and value["components"]|length > 0 %}
COMPONENTS
{{value["components"]|quote|join(" ")}}
{% endif %}
{% if not value or "optional" not in value or not value["optional"] %}
REQUIRED
{% endif %}
)
{% endif %}
{% if value and "alias" in value %}
if({{name}}_ADDED)
add_library({{value["alias"]["name"]}} ALIAS {{value["alias"]["target"]}})
endif()
{% endif %}
{% if "cmake_condition" in value and value["cmake_condition"]|length > 0 %}
else()
message(STATUS "Excluding dependency {{name}} based on cmake_condition")
{% endif %}
endif()
{% endfor %}
execute_process(
COMMAND "${EVEREST_DEPENDENCY_MANAGER}" release --everest-dir ${PROJECT_SOURCE_DIR} --build-dir ${CMAKE_BINARY_DIR} --out ${CMAKE_BINARY_DIR}/release.json
)
install(
FILES "${CMAKE_BINARY_DIR}/release.json"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/everest"
)

View File

@@ -0,0 +1,690 @@
#!/usr/bin/env bash
set -e
# Default values
EVEREST_TOOL_BRANCH="main"
# Script directory - where devrd is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# .devcontainer directory is always relative to the script location
DEVCONTAINER_DIR="${SCRIPT_DIR}/../../.devcontainer"
# .env file is always in the .devcontainer directory (relative to script)
ENV_FILE="${DEVCONTAINER_DIR}/.env"
# Function to load HOST_WORKSPACE_FOLDER from .env file
# Usage: load_workspace_from_env [fallback]
# If fallback is provided and workspace not found in .env, returns fallback
# If no fallback provided, returns empty string (for use with ${var:-default} syntax)
load_workspace_from_env() {
local fallback="$1"
if [ -f "$ENV_FILE" ]; then
local workspace=$(grep "^HOST_WORKSPACE_FOLDER=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$workspace" ]; then
echo "$workspace"
return
fi
fi
# If fallback provided and workspace not found, return fallback
if [ -n "$fallback" ]; then
echo "$fallback"
fi
}
# HOST_WORKSPACE_FOLDER is the folder that is mapped to /workspace in the container
# Priority: 1) Command line/env var, 2) .env file, 3) Current directory
HOST_WORKSPACE_FOLDER="${HOST_WORKSPACE_FOLDER:-$(load_workspace_from_env)}"
HOST_WORKSPACE_FOLDER="${HOST_WORKSPACE_FOLDER:-$(pwd)}"
# Docker Compose project name (defaults to workspace folder name with _devcontainer suffix, can be overridden)
# This matches VSC's naming convention: {workspace-folder-name}_devcontainer-{service-name}-1
# If needed (and not running in VSCode), can be changed by setting the DOCKER_COMPOSE_PROJECT_NAME environment variable.
DOCKER_COMPOSE_PROJECT_NAME="${DOCKER_COMPOSE_PROJECT_NAME:-$(basename "$HOST_WORKSPACE_FOLDER" | tr \"A-Z\" \"a-z\")_devcontainer}"
# Function to detect if running inside container
is_inside_container() {
# Check for /.dockerenv (standard Docker indicator)
[ -f /.dockerenv ] && return 0
# Check if /workspace exists and is mounted (devcontainer specific)
[ -d /workspace ] && [ -f /workspace/.devcontainer/devrd ] && return 0
return 1
}
# Function to show error when command is run from inside container
show_inside_container_error() {
local cmd_name="${1:-this command}"
echo "✖ Error: This command cannot be run from inside the container"
echo ""
echo "You are currently inside the development container."
echo "Please run this command from the host system instead:"
echo ""
echo " 1. Exit the container (type 'exit' or press Ctrl+D)"
echo " 2. Run the command from your host terminal:"
echo " ./devrd $cmd_name"
echo ""
exit 1
}
# Function to run docker compose with static project name
# Compose files are always relative to the script's .devcontainer directory
docker_compose() {
docker compose -p "$DOCKER_COMPOSE_PROJECT_NAME" \
-f "${DEVCONTAINER_DIR}/docker-compose.yml" \
-f "${DEVCONTAINER_DIR}/general-devcontainer/docker-compose.devcontainer.yml" "$@"
}
# Function to validate folder path
validate_folder() {
local folder="$1"
# Convert relative path to absolute
case "$folder" in
/*) ;; # Already absolute
*) folder="$(cd "$folder" && pwd)" ;; # Convert relative to absolute
esac
# Check if folder exists
if [ ! -d "$folder" ]; then
echo "Error: Folder '$folder' does not exist"
exit 1
fi
# Check if folder is readable
if [ ! -r "$folder" ]; then
echo "Error: Folder '$folder' is not accessible (permission denied)"
exit 1
fi
echo "$folder"
}
# Function to generate .env file
generate_env() {
if is_inside_container; then
show_inside_container_error "env"
fi
# Process command line options
if [ -n "$ENV_OPTIONS" ]; then
set -- $ENV_OPTIONS
while [ $# -gt 0 ]; do
case "$1" in
-v|--version)
EVEREST_TOOL_BRANCH="$2"
shift 2
;;
-w|--workspace)
HOST_WORKSPACE_FOLDER="$2"
shift 2
;;
*)
shift
;;
esac
done
fi
# Set workspace folder
if [ -n "$HOST_WORKSPACE_FOLDER" ]; then
HOST_WORKSPACE_FOLDER=$(validate_folder "$HOST_WORKSPACE_FOLDER")
else
HOST_WORKSPACE_FOLDER="$(pwd)"
fi
# Get commit hash
COMMIT_HASH=$(git ls-remote https://github.com/EVerest/everest-dev-environment.git ${EVEREST_TOOL_BRANCH} | cut -f1 2>/dev/null || echo "")
# Check if we need to update existing file
local needs_update=false
if [ -f "$ENV_FILE" ] && [ -s "$ENV_FILE" ]; then
# File exists, check if we have options that require updates
if [ -n "$ENV_OPTIONS" ]; then
needs_update=true
fi
fi
if [ ! -f "$ENV_FILE" ] || [ ! -s "$ENV_FILE" ] || [ "$needs_update" = true ]; then
cat > "$ENV_FILE" << EOF
# Auto-generated by devrd script
ORGANIZATION_ARG=EVerest
REPOSITORY_HOST=github.com
REPOSITORY_USER=git
COMMIT_HASH=$COMMIT_HASH
EVEREST_TOOL_BRANCH=$EVEREST_TOOL_BRANCH
UID=$(id -u)
GID=$(id -g)
HOST_WORKSPACE_FOLDER=$HOST_WORKSPACE_FOLDER
EOF
if [ "$needs_update" = true ]; then
echo "Updated .env file"
else
echo "Generated .env file"
fi
else
echo "Found existing .env file"
cat "$ENV_FILE"
fi
}
# Function to build the container
build_container() {
if is_inside_container; then
show_inside_container_error "build"
fi
echo "Building development container..."
docker_compose --profile all build
}
# Function to get actual port mapping from docker compose
get_port_mapping() {
local service_name=$1
local internal_port=$2
# Get the actual port mapping from docker compose
local port_mapping=$(docker_compose port $service_name $internal_port 2>/dev/null)
if [ -n "$port_mapping" ]; then
# Extract just the host port (remove the host part)
echo "$port_mapping" | sed 's/.*://'
else
echo ""
fi
}
# Function to display container links and tips
display_container_status() {
echo ""
echo "Container Services Summary:"
echo "=============================="
# Get actual port mappings from docker compose
local mqtt_explorer_port=$(get_port_mapping mqtt-explorer 4000)
local steve_http_port=$(get_port_mapping steve 8180)
# Display links with actual ports
if [ -n "$mqtt_explorer_port" ]; then
echo "MQTT Explorer: http://localhost:$mqtt_explorer_port"
else
echo "MQTT Explorer: currently not running"
fi
if [ -n "$steve_http_port" ]; then
echo "Steve (HTTP): http://localhost:$steve_http_port"
else
echo "Steve (HTTP): currently not running"
fi
# Check if Node-RED is running
if docker_compose ps | grep -q "nodered"; then
echo "Node-RED UI: http://localhost:1880/ui"
else
echo "Node-RED UI: currently not running"
fi
echo ""
echo "Tips:"
echo " • MQTT Explorer: Browse and debug MQTT topics"
echo " • Steve: OCPP backend management interface"
echo " • Node-RED: Web-based UI for SIL simulations"
echo " • Use './devrd prompt' to access the container shell"
echo " • Use './devrd nodered-flows' to see available flows"
echo ""
}
# Function to start containers using profiles
start_compose_profile() {
if is_inside_container; then
show_inside_container_error "start"
fi
local profile_or_service="$1"
if [ -n "$profile_or_service" ]; then
echo "Starting containers for profile/service: $profile_or_service..."
docker_compose --profile "$profile_or_service" up -d
else
echo "Starting the development container and all services..."
docker_compose --profile all up -d
fi
# Display workspace mapping
echo "Workspace mapping: $HOST_WORKSPACE_FOLDER → /workspace"
echo ""
# Display container links
display_container_status
}
# Function to stop containers using profiles or container name pattern
stop_compose_profile() {
if is_inside_container; then
show_inside_container_error "stop"
fi
local profile_or_pattern="$1"
if [ -n "$profile_or_pattern" ]; then
# Check if it's a valid profile name
case "$profile_or_pattern" in
mqtt|ocpp|sil|all)
echo "Stopping containers for profile: $profile_or_pattern..."
docker_compose --profile "$profile_or_pattern" stop
;;
*)
# Treat as container name pattern
echo "Stopping containers matching pattern: $profile_or_pattern..."
local containers=$(docker ps --format "{{.Names}}" | grep -E "($profile_or_pattern)" || true)
if [ -z "$containers" ]; then
echo "No running containers found matching pattern: $profile_or_pattern"
return 1
fi
echo "$containers" | while read container; do
echo "Stopping container: $container"
docker stop "$container" 2>/dev/null || echo "Failed to stop container: $container"
done
;;
esac
else
echo "Stopping the development container and all services..."
docker_compose --profile all stop
fi
}
# Function to purge everything
purge_everything() {
if is_inside_container; then
show_inside_container_error "purge"
fi
local purge_pattern="${1:-$(basename "$HOST_WORKSPACE_FOLDER")}"
local current_project="$(basename "$HOST_WORKSPACE_FOLDER")"
echo "Purging all devcontainer resources for pattern: $purge_pattern..."
# Only use docker_compose down if purging the current project
if [ "$purge_pattern" = "$current_project" ]; then
echo "Stopping and removing containers for current project..."
docker_compose down -v --remove-orphans
else
echo "Purging resources for different project pattern: $purge_pattern"
echo "Skipping docker-compose cleanup (not current project)"
fi
# Remove all images related to the project
echo "Removing devcontainer images..."
docker images --format "table {{.Repository}}:{{.Tag}}" | grep -E "($purge_pattern)" | awk '{print $1}' | xargs -r docker rmi -f
# Remove all volumes related to the project (with force if needed)
echo "Removing devcontainer volumes..."
docker volume ls --format "{{.Name}}" | grep -E "($purge_pattern)" | while read volume; do
echo "Removing volume: $volume"
docker volume rm -f "$volume" 2>/dev/null || echo "Volume $volume could not be removed (may be in use)"
done
# Ask user if they want to purge CPM cache volume
echo ""
echo "CPM source cache volume (everest-cpm-source-cache) is shared across all workspaces."
read -p "Do you want to purge the CPM cache volume as well? [y/N]: " purge_cache
purge_cache="${purge_cache:-N}"
if [[ "$purge_cache" =~ ^[Yy]$ ]]; then
echo "Removing CPM cache volume..."
if docker volume rm everest-cpm-source-cache 2>/dev/null; then
echo "✔ CPM cache volume removed"
else
echo "⚠ CPM cache volume could not be removed (may be in use or not exist)"
fi
else
echo "Keeping CPM cache volume (will be reused for faster builds)"
fi
# Remove any dangling images and containers
echo ""
echo "Cleaning up dangling resources..."
docker system prune -f
echo ""
echo "✔ Purge complete! All devcontainer resources have been removed."
}
# Function to check if SSH agent is running
check_ssh_agent() {
if [ -z "$SSH_AUTH_SOCK" ] || ! ssh-add -l >/dev/null 2>&1; then
echo "Error: SSH agent is not running or no keys are loaded."
echo "Please start the SSH agent and add your keys:"
echo " eval \$(ssh-agent)"
echo " ssh-add ~/.ssh/id_rsa # or your private key"
echo "Or if you're using a different key:"
echo " ssh-add ~/.ssh/your_private_key"
exit 1
fi
}
# Function to execute a command in the container
exec_devcontainer() {
if is_inside_container; then
echo "✖ You're already inside the container."
echo ""
echo "To run a command, just execute it directly:"
if [ $# -gt 0 ]; then
echo " $@"
else
echo " <your-command>"
fi
exit 1
fi
echo "Checking if development container is running..."
# Check if the devcontainer service is running
if ! docker_compose ps devcontainer | grep -q "Up"; then
echo "Error: Development container is not running."
echo "Please start the container first with: ./devrd start"
echo "Or build and start with: ./devrd build && ./devrd start"
exit 1
fi
echo "Executing command in development container..."
run_in_devcontainer "$@"
}
# Function to get a shell prompt in the container
prompt_devcontainer() {
if is_inside_container; then
echo "✖ You're already inside the container shell."
exit 1
fi
echo "Starting shell in development container..."
exec_devcontainer /bin/bash
}
# Helper function to check if Node-RED is running and get the URL
# Sets nodered_url variable and returns 0 if running, 1 if not
check_nodered_running() {
if is_inside_container; then
nodered_url="http://nodered:1880"
curl -s "$nodered_url/flows" >/dev/null 2>&1 && return 0
else
nodered_url="http://localhost:1880"
docker_compose ps | grep -q "nodered" && return 0
fi
return 1
}
# Helper function to execute a command in the container
# Usage: run_in_devcontainer [--no-tty] <command> [args...]
# Executes directly if inside container, via docker_compose exec if on host
# No error checking - assumes container is running when called from host
# Use --no-tty for non-interactive commands that need output capture
run_in_devcontainer() {
local no_tty=false
if [ "$1" = "--no-tty" ]; then
no_tty=true
shift
fi
if is_inside_container; then
"$@"
else
if [ "$no_tty" = true ]; then
docker_compose exec -T devcontainer "$@"
else
docker_compose exec devcontainer "$@"
fi
fi
}
# Function to list available flows
list_nodered_flows() {
echo ""
echo "Available Node-RED Flows:"
echo "============================="
# Check if Node-RED is running
if ! check_nodered_running; then
echo "✖ Node-RED container is not running"
echo "Please start with './devrd start' first"
return 1
fi
# Find all flow files in the workspace
local flows
if is_inside_container; then
flows=$(find /workspace -name "*-flow.json" -type f 2>/dev/null | sort)
else
flows=$(docker_compose exec -T devcontainer find /workspace -name "*-flow.json" -type f 2>/dev/null | sort)
fi
if [ -z "$flows" ]; then
echo "No flow files found in workspace"
echo ""
echo "Expected pattern: *-flow.json"
echo "Search location: /workspace"
return 1
fi
echo "Found $(echo "$flows" | wc -l) flow file(s):"
echo ""
for flow in $flows; do
# Remove /workspace/ prefix to get relative path from workspace root
local relative_path=$(echo "$flow" | sed 's|^/workspace/||')
echo " Path: $relative_path"
done
echo ""
echo "Usage: ./devrd flow <flow-file-path>"
echo "Example: ./devrd flow EVerest/config/nodered/config-sil-dc-flow.json"
echo ""
}
# Function to switch flow using REST API
switch_nodered_flow() {
local flow_path="$1"
if [ -z "$flow_path" ]; then
echo "Error: Please specify a flow file path"
echo ""
echo "Available flows:"
list_nodered_flows
return 1
fi
# Check if Node-RED is running
if ! check_nodered_running; then
echo "✖ Node-RED container is not running"
echo "Please start with './devrd start' first"
return 1
fi
# Construct full path in container
local full_path="/workspace/$flow_path"
# Check if file exists and is readable, then copy to temp file
if ! run_in_devcontainer --no-tty test -r "$full_path"; then
echo "✖ Flow file not found or not readable: $flow_path"
echo ""
echo "Available flows:"
list_nodered_flows
return 1
fi
# Copy flow to temporary file
run_in_devcontainer --no-tty cat "$full_path" > /tmp/flows.json
echo "Switching Node-RED to flow: $(basename "$flow_path")"
echo "Source: $flow_path"
# Process environment variables in the flow JSON
# Replace "broker": "localhost" with "broker": "mqtt-server"
sed -i 's/"broker": "localhost"/"broker": "mqtt-server"/g' /tmp/flows.json
# Deploy flow via Node-RED REST API
echo "Deploying flow via Node-RED API..."
local response=$(curl -s -w "%{http_code}" -X POST "$nodered_url/flows" \
-H "Content-Type: application/json" \
-d @/tmp/flows.json)
local http_code="${response: -3}"
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
echo "✔ Node-RED flow deployed successfully via API!"
if is_inside_container; then
echo "Access at: http://nodered:1880/ui (from container) or http://localhost:1880/ui (from host)"
else
echo "Access at: http://localhost:1880/ui"
fi
else
echo "✖ Failed to deploy flow via API (HTTP $http_code)"
echo "Response: ${response%???}"
return 1
fi
# Clean up temporary file
rm -f /tmp/flows.json
}
# Function to display help
show_help() {
echo "Usage: $0 [COMMAND] [OPTIONS]"
echo ""
echo "Commands:"
echo " env Generate .env file with repository information (default)"
echo " build Build the development container"
echo " start [profile] Start containers (profiles: mqtt, ocpp, sil, all)"
echo " stop [profile|pattern] Stop containers by profile (mqtt, ocpp, sil, all) or container name pattern"
echo " purge [pattern] Remove all devcontainer resources (containers, images, volumes)"
echo " Optional pattern to match (default: current folder name)"
echo " exec <command> Execute a command in the development container (requires the container to be running)"
echo " prompt Get a shell prompt in the development container (requires the container to be running)"
echo " flows List available flows"
echo " flow <path> Switch to specific flow file"
echo ""
echo "Options (for env command only):"
echo " -v, --version VERSION Everest tool branch (default: $EVEREST_TOOL_BRANCH, preserves existing if not specified)"
echo " -w, --workspace DIR Workspace directory to map to /workspace in container (default: current directory)"
echo " --help Display this help message"
echo ""
echo "Examples:"
echo " $0 env # Generate .env file with repository information"
echo " $0 build # Build container"
echo " $0 start # Start all containers"
echo " $0 start sil # Start SIL simulation tools (Node-RED, MQTT Explorer)"
echo " $0 start ocpp # Start OCPP services (Steve, OCPP DB, MQTT)"
echo " $0 start mqtt # Start only MQTT server"
echo " $0 stop sil # Stop SIL simulation tools"
echo " $0 stop ev-ws # Stop all containers matching pattern 'ev-ws'"
echo " $0 purge # Remove all devcontainer resources for current folder"
echo " $0 purge my-project # Remove all devcontainer resources matching 'my-project'"
echo " $0 exec ls -la # Execute command in container"
echo " $0 prompt # Get shell prompt in container"
echo " $0 flows # List available flows"
echo " $0 flow <path> # Switch to specific Node-RED flow file"
echo " $0 -w ~/Documents # Map Documents folder to /workspace"
echo " $0 --workspace /opt/tools # Map tools folder to /workspace"
exit 0
}
# Parse command line arguments
COMMAND="env"
ENV_OPTIONS=""
# First pass: collect all options
while [ $# -gt 0 ]; do
case $1 in
-v|--version|-w|--workspace)
# Store env-specific options for later use
ENV_OPTIONS="$ENV_OPTIONS $1 $2"
shift 2
;;
--help)
show_help
;;
exec)
COMMAND="$1"
shift
# For exec, pass all remaining arguments to the exec function
break
;;
env|build|prompt|flows)
COMMAND="$1"
shift
# Don't break here, continue to collect more options
;;
flow)
COMMAND="$1"
shift
# For flow, pass any remaining arguments as flow path
break
;;
purge)
COMMAND="$1"
shift
# For purge, pass any remaining arguments as pattern
break
;;
start|stop)
COMMAND="$1"
shift
# For start/stop, pass any remaining arguments as container name
break
;;
*)
echo "Unknown option: $1"
show_help
;;
esac
done
# Execute the command
case $COMMAND in
env)
# Check SSH agent for Git operations
check_ssh_agent
generate_env
;;
build)
# Only generate env if .env file doesn't exist or is empty
if [ ! -f "$ENV_FILE" ] || [ ! -s "$ENV_FILE" ]; then
# Check SSH agent for Git operations
check_ssh_agent
generate_env
fi
build_container
;;
start)
# Only generate env if .env file doesn't exist or is empty
if [ ! -f "$ENV_FILE" ] || [ ! -s "$ENV_FILE" ]; then
# Check SSH agent for Git operations
check_ssh_agent
generate_env
fi
start_compose_profile "$@"
;;
stop)
stop_compose_profile "$@"
;;
purge)
purge_everything "$@"
;;
exec)
if [ $# -eq 0 ]; then
echo "Error: exec command requires arguments"
show_help
fi
exec_devcontainer "$@"
;;
prompt)
prompt_devcontainer
;;
flows)
list_nodered_flows
;;
flow)
if [ $# -eq 0 ]; then
echo "Error: flow command requires a flow file path"
show_help
fi
switch_nodered_flow "$1"
;;
*)
echo "Unknown command: $COMMAND"
show_help
;;
esac

View File

@@ -0,0 +1,97 @@
#!/bin/bash
# Bash completion for devrd script
# Source this file or add to your .bashrc to enable completion
_devrd_completion() {
local cur prev opts cmds
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Available commands
cmds="install env build start stop prompt purge exec flows flow"
# Available options
opts="-v --version -w --workspace --help"
# Function to get available Node-RED flows dynamically
_get_nodered_flows() {
# Get the current project name (same logic as devrd script)
local project_name="${DOCKER_COMPOSE_PROJECT_NAME:-$(basename "$(pwd)")_devcontainer}"
# Check if we're in the right directory and container is running
if [ -f "devrd" ] && docker compose -p "$project_name" -f .devcontainer/docker-compose.yml -f .devcontainer/general-devcontainer/docker-compose.devcontainer.yml ps devcontainer | grep -q "Up"; then
# Get flows from the container and return full paths (relative to workspace)
docker compose -p "$project_name" -f .devcontainer/docker-compose.yml -f .devcontainer/general-devcontainer/docker-compose.devcontainer.yml exec -T devcontainer find /workspace -name "*-flow.json" -type f 2>/dev/null | sed 's|/workspace/||' | sort
else
# Fallback to common flow file paths
echo "EVerest/config/nodered/config-sil-dc-flow.json"
echo "EVerest/config/nodered/config-sil-dc-bpt-flow.json"
echo "EVerest/config/nodered/config-sil-energy-management-flow.json"
echo "EVerest/config/nodered/config-sil-two-evse-flow.json"
echo "EVerest/config/nodered/config-sil-flow.json"
fi
}
# Function to get available container names
_get_container_names() {
echo "mqtt ocpp sil"
}
# If the previous word is an option that takes an argument, complete based on the option
case "$prev" in
-v|--version)
# Complete with common version patterns
COMPREPLY=( $(compgen -W "main master develop release/1.0 release/1.1" -- "$cur") )
return 0
;;
-w|--workspace)
# Complete directories
COMPREPLY=( $(compgen -d -- "$cur") )
return 0
;;
flow)
# Complete with available flow file paths dynamically
local flows
flows=$(_get_nodered_flows)
COMPREPLY=( $(compgen -W "$flows" -- "$cur") )
return 0
;;
start|stop)
# Complete with available container names
local containers
containers=$(_get_container_names)
COMPREPLY=( $(compgen -W "$containers" -- "$cur") )
return 0
;;
exec)
# For exec command, complete with common commands
COMPREPLY=( $(compgen -W "ls pwd cd cmake ninja make" -- "$cur") )
return 0
;;
esac
# If we're completing the first word (command), show commands
if [ $COMP_CWORD -eq 1 ]; then
COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
return 0
fi
# If we're completing an option, show options
if [[ "$cur" == -* ]]; then
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
return 0
fi
# For other cases, complete with files/directories
COMPREPLY=( $(compgen -f -- "$cur") )
return 0
}
# Register the completion function
complete -F _devrd_completion devrd
complete -F _devrd_completion ./devrd
complete -F _devrd_completion ../devrd
complete -F _devrd_completion ./applications/devrd/devrd

View File

@@ -0,0 +1,90 @@
#!/bin/zsh
# Zsh completion for devrd script
# Source this file or add to your .zshrc to enable completion
_devrd_completion() {
local context state line
typeset -A opt_args
# Available commands
local commands=(
'env:Generate .env file with repository information'
'build:Build the development container'
'start:Start containers (profiles: mqtt, ocpp, sil)'
'stop:Stop containers (profiles: mqtt, ocpp, sil)'
'purge:Remove all devcontainer resources (containers, images, volumes)'
'exec:Execute a command in the container'
'prompt:Get a shell prompt in the container'
'flows:List available flows'
'flow:Switch to specific flow file'
)
# Available options
local options=(
'-v[Everest tool branch]:version:'
'--version[Everest tool branch]:version:'
'-w[Workspace directory]:directory:_files -/'
'--workspace[Workspace directory]:directory:_files -/'
'--help[Display help message]'
)
# Function to get available Node-RED flows dynamically
_get_nodered_flows() {
# Get the current project name (same logic as devrd script)
local project_name="${DOCKER_COMPOSE_PROJECT_NAME:-$(basename "$(pwd)")_devcontainer}"
# Check if we're in the right directory and container is running
if [ -f "devrd" ] && docker compose -p "$project_name" -f .devcontainer/docker-compose.yml -f .devcontainer/general-devcontainer/docker-compose.devcontainer.yml ps devcontainer | grep -q "Up"; then
# Get flows from the container and return full paths (relative to workspace)
docker compose -p "$project_name" -f .devcontainer/docker-compose.yml -f .devcontainer/general-devcontainer/docker-compose.devcontainer.yml exec -T devcontainer find /workspace -name "*-flow.json" -type f 2>/dev/null | sed 's|/workspace/||' | sort
else
# Fallback to common flow file paths
echo "EVerest/config/nodered/config-sil-dc-flow.json"
echo "EVerest/config/nodered/config-sil-dc-bpt-flow.json"
echo "EVerest/config/nodered/config-sil-energy-management-flow.json"
echo "EVerest/config/nodered/config-sil-two-evse-flow.json"
echo "EVerest/config/nodered/config-sil-flow.json"
fi
}
# Function to get available container names
_get_container_names() {
echo "mqtt ocpp sil"
}
# Main completion logic
_arguments -C \
"$options[@]" \
"1: :{_describe 'commands' commands}" \
"*::arg:->args"
case $state in
args)
case $line[1] in
flow)
_values 'flow files' $(_get_nodered_flows)
;;
start|stop)
_values 'profiles' $(_get_container_names)
;;
exec)
_values 'commands' 'ls' 'pwd' 'cd' 'cmake' 'ninja' 'make'
;;
purge)
_files
;;
esac
;;
esac
}
# Register the completion function
if command -v compdef >/dev/null 2>&1; then
compdef _devrd_completion devrd
compdef _devrd_completion ./devrd
compdef _devrd_completion ../devrd
compdef _devrd_completion ./applications/devrd/devrd
else
echo "Warning: zsh completion system not loaded. Add 'autoload -U compinit && compinit' to your .zshrc"
fi

View File

@@ -0,0 +1,3 @@
build
__pycache__
*.egg-info

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.

View File

@@ -0,0 +1,9 @@
[project]
name = "everest_dev_tool"
version = "0.1.0"
description = "This tool provides helpful commands to setup/control your dev environment"
license = { text="Apache-2.0" }
dependencies = []
[project.scripts]
everest = "everest_dev_tool:main"

View File

@@ -0,0 +1,9 @@
__version__ = "0.1.0"
from . import parser
def get_parser():
return parser.get_parser(__version__)
def main():
parser.main(get_parser())

View File

@@ -0,0 +1,30 @@
import argparse
import subprocess
def clone_handler(args: argparse.Namespace):
log = args.logger
log.info(
f"Cloning repository:\n"
f" Method: {args.method}\n"
f" Host: {args.host}\n"
f" SSH User (if ssh): {args.ssh_user}\n"
f" Organization: {args.organization}\n"
f" Repository Name: {args.repository_name}\n"
f" Branch: {args.branch}\n"
)
repository_url = ""
if args.method == 'https':
repository_url = f"https://{args.host}/"
else:
repository_url = f"{args.ssh_user}@{args.host}:"
repository_url = repository_url + f"{ args.organization }/{ args.repository_name }.git"
cmd_args = ["git", "clone", "-b", args.branch, repository_url]
log.debug(f"Command to execute: {' '.join(cmd_args)}")
if args.dry:
log.info(f"Dry run: Would execute: {' '.join(cmd_args)}")
else:
subprocess.run(cmd_args, check=True)

View File

@@ -0,0 +1,83 @@
import argparse
import logging
import os
from . import git_handlers
log = logging.getLogger("EVerest's Development Tool")
def get_parser(version: str) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
description="EVerest's Development Tool",)
parser.add_argument('--version', action='version', version=f'%(prog)s { version }')
parser.add_argument('-v', '--verbose', action='store_true', help="Verbose output")
parser.set_defaults(action_handler=lambda _: parser.print_help())
subparsers = parser.add_subparsers(help="available commands")
# Git related commands
clone_parser = subparsers.add_parser("clone", help="Clone a repository", add_help=True)
clone_parser.add_argument('-v', '--verbose', action='store_true', help="Verbose output")
default_git_host = os.environ.get("EVEREST_DEV_TOOL_DEFAULT_GIT_HOST", "github.com")
clone_parser.add_argument(
'--host',
default=default_git_host,
help=(
"Git host to use, default is 'github.com' "
"(can be overridden by the environment variable "
"EVEREST_DEV_TOOL_DEFAULT_GIT_HOST)"
),
)
default_git_method = os.environ.get("EVEREST_DEV_TOOL_DEFAULT_GIT_METHOD", "ssh")
clone_parser.add_argument(
'--method',
default=default_git_method,
choices=['https', 'ssh'],
help=(
"Git method to use, default is 'ssh' "
"(can be overridden by the environment variable "
"EVEREST_DEV_TOOL_DEFAULT_GIT_METHOD)"
)
)
default_git_ssh_user = os.environ.get("EVEREST_DEV_TOOL_DEFAULT_GIT_SSH_USER", "git")
clone_parser.add_argument(
'--ssh-user',
default=default_git_ssh_user,
help=(
"SSH user to use, default is 'git' "
"(can be overridden by the environment variable "
"EVEREST_DEV_TOOL_DEFAULT_GIT_SSH_USER)"
)
)
default_git_organization = os.environ.get("EVEREST_DEV_TOOL_DEFAULT_GIT_ORGANIZATION", "EVerest")
clone_parser.add_argument(
'--organization', '--org',
default=default_git_organization,
help=(
"Github Organization name, default is 'EVerest'"
" (can be overridden by the environment variable "
"EVEREST_DEV_TOOL_DEFAULT_GIT_ORGANIZATION)"
)
)
clone_parser.add_argument('--branch', '-b', default="main", help="Branch to checkout, default is 'main'")
clone_parser.add_argument('--dry', action='store_true', help="Dry run, do not execute the clone command")
clone_parser.add_argument("repository_name", help="Name of the repository to clone")
clone_parser.set_defaults(action_handler=git_handlers.clone_handler)
return parser
def setup_logging(verbose: bool):
if verbose:
log.setLevel(logging.DEBUG)
else:
log.setLevel(logging.INFO)
console_handler = logging.StreamHandler()
log.addHandler(console_handler)
def main(parser: argparse.ArgumentParser):
args = parser.parse_args()
args.logger = log
setup_logging(args.verbose)
args.action_handler(args)

View File

@@ -0,0 +1,73 @@
cmake_minimum_required(VERSION 3.16)
if(DEFINED EVEREST_IO_WITH_MQTT AND NOT EVEREST_IO_WITH_MQTT)
message(FATAL_ERROR "pionix_chargebridge requires MQTT support in everest::io. "
"Set EVEREST_IO_WITH_MQTT=ON or disable EVEREST_BUILD_APPLICATIONS.")
endif()
find_package(ryml QUIET)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_executable(pionix_chargebridge
src/everest_api/api_connector.cpp
src/everest_api/evse_bsp_api.cpp
src/everest_api/ovm_api.cpp
src/everest_api/ev_bsp_api.cpp
src/firmware_update/sync_fw_updater.cpp
src/utilities/filesystem.cpp
src/utilities/logging.cpp
src/utilities/parse_config.cpp
src/utilities/print_config.cpp
src/utilities/string.cpp
src/utilities/symlink.cpp
src/utilities/sync_udp_client.cpp
src/utilities/type_converters.cpp
src/can_bridge.cpp
src/charge_bridge.cpp
src/bsp_bridge.cpp
src/gpio_bridge.cpp
src/heartbeat_service.cpp
src/plc_bridge.cpp
src/serial_bridge.cpp
src/discovery.cpp
main.cpp
)
target_link_libraries(pionix_chargebridge
everest::io
everest::everest_api_types
nlohmann_json::nlohmann_json
ryml::ryml
)
target_include_directories(pionix_chargebridge
PRIVATE include
PRIVATE shared
)
set(cb_firmware_binary config/firmware/charge-bridge-fw_complete.cbfw)
add_custom_command(
TARGET pionix_chargebridge
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/${cb_firmware_binary}"
"$<TARGET_FILE_DIR:pionix_chargebridge>/"
COMMENT "Copying Pionix ChargeBridge firmware binary..."
)
install (TARGETS pionix_chargebridge)
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/${cb_firmware_binary}" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge/firmware)
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-EVAL.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-EVAL.yaml-example")
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-SAT-AC.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-SAT-AC.yaml-example")
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-EVAL-EV.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-EVAL-EV.yaml-example")
install (FILES "${CMAKE_CURRENT_SOURCE_DIR}/config/config-CB-EVAL-SIM.yaml" DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/chargebridge RENAME "config-CB-EVAL-SIM.yaml-example")

View File

@@ -0,0 +1,154 @@
charge_bridge:
name: cb_eval_ev
ip: ANY_EV
#ip: chargebridge-44b7d0c99629.local
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
fw_update_on_start: true
mdns_name: ""
heartbeat:
interval_s: 1
connection_to_s: 10
safety:
pp_mode: "disabled"
cp_avg_ms: 10
inverted_emergency_input: 0
relay_1:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 200
feedback_inverted: true
# PWM not supported yet
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_2:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 200
feedback_inverted: true
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_3:
relay_mode: "UserRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: false
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
can_0:
enable: true
local: "cb_ev_can"
baudrate: 125000
serial_1:
enable: true
local: "/dev/cb_ev_uart"
baudrate: 115200
stopbits: OneStopBit
parity: None
serial_2:
enable: true
local: "/dev/cb_ev_rs485"
baudrate: 19200
stopbits: OneStopBit
parity: Even
plc:
enable: true
tap: "cb_ev_plc"
ip: 172.25.6.1
netmask: 255.255.255.0
mtu: 1518
powersaving_mode: 1
ev_bsp:
enable: true
module_id: "ev_bsp_1"
mqtt_remote: 127.0.0.1
mqtt_port: 1883
mqtt_bind: 127.0.0.1
ovm_enabled: false
ovm_module_id: "ovm_1"
evse_bsp:
enable: false
module_id: "bsp_1"
mqtt_remote: 127.0.0.1
mqtt_port: 1883
mqtt_bind: 127.0.0.1
capabilities:
max_current_A_import: 16
min_current_A_import: 6
max_phase_count_import: 3
min_phase_count_import: 3
max_current_A_export: 16
min_current_A_export: 6
max_phase_count_export: 3
min_phase_count_export: 3
supports_changing_phases_during_charging: false
connector_type: "IEC62196Type2Cable"
max_plug_temperature_C: 250
ovm_enabled: true
ovm_module_id: "ovm_1"
gpio:
enable: true
interval_s: 1
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_ping_interval_ms: 5000
gpio_0:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_1:
mode: "Input"
pulls: "NoPull"
config: 32767
gpio_2:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_3:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_4:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 1000
gpio_5:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_6:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_7:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_8:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_9:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767

View File

@@ -0,0 +1,153 @@
charge_bridge:
name: cb_eval
ip: ANY_EVSE
#ip: chargebridge-44b7d0c99629.local
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
fw_update_on_start: true
mdns_name: ""
heartbeat:
interval_s: 1
connection_to_s: 10
safety:
pp_mode: "disabled"
cp_avg_ms: 10
inverted_emergency_input: 0
relay_1:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 200
feedback_inverted: true
# PWM not supported yet
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_2:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 200
feedback_inverted: true
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_3:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: true
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
can_0:
enable: true
local: "cb_can"
baudrate: 125000
serial_1:
enable: true
local: "/dev/cb_uart"
baudrate: 115200
stopbits: OneStopBit
parity: None
serial_2:
enable: true
local: "/dev/cb_rs485"
baudrate: 19200
stopbits: OneStopBit
parity: Even
plc:
enable: true
tap: "cb_plc"
ip: 172.25.6.1
netmask: 255.255.255.0
mtu: 1518
powersaving_mode: 1
ev_bsp:
enable: false
module_id: "ev_bsp_1"
mqtt_remote: 127.0.0.1
mqtt_port: 1883
mqtt_bind: 127.0.0.1
ovm_enabled: false
ovm_module_id: "ovm_1"
evse_bsp:
enable: true
module_id: "cb_bsp"
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_ping_interval_ms: 5000
ovm_enabled: true
ovm_module_id: "cb_ovm"
capabilities:
max_current_A_import: 16
min_current_A_import: 0
max_phase_count_import: 3
min_phase_count_import: 3
max_current_A_export: 16
min_current_A_export: 0
max_phase_count_export: 3
min_phase_count_export: 3
supports_changing_phases_during_charging: false
connector_type: "IEC62196Type2Cable"
gpio:
enable: true
interval_s: 1
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_ping_interval_ms: 5000
gpio_0:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_1:
mode: "Input"
pulls: "NoPull"
config: 32767
gpio_2:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_3:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_4:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 1000
gpio_5:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_6:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_7:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_8:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_9:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767

View File

@@ -0,0 +1,147 @@
charge_bridge:
name: cb_eval
ip: ANY_EVSE
#ip: chargebridge-44b7d0c9bcc0.local^
#ip: chargebridge-44b7d0c99629.local
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
fw_update_on_start: true
mdns_name: ""
heartbeat:
interval_s: 1
connection_to_s: 10
safety:
pp_mode: "disabled"
cp_avg_ms: 10
inverted_emergency_input: 0
relay_1:
relay_mode: "PowerRelay"
feedback_enabled: true
feedback_delay_ms: 200
feedback_inverted: true
# PWM not supported yet
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_2:
relay_mode: "PowerRelay"
feedback_enabled: true
feedback_delay_ms: 200
feedback_inverted: true
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_3:
relay_mode: "UserRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: false
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
can_0:
enable: true
local: "cb_can"
baudrate: 125000
serial_1:
enable: true
local: "/dev/cb_uart"
baudrate: 115200
stopbits: OneStopBit
parity: None
serial_2:
enable: true
local: "/dev/cb_rs485"
baudrate: 19200
stopbits: OneStopBit
parity: Even
plc:
enable: true
tap: "cb_plc"
ip: 172.25.6.2
netmask: 255.255.255.0
mtu: 1518
powersaving_mode: 1
evse_bsp:
enable: true
module_id: "cb_bsp"
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_bind: 127.0.0.1
mqtt_ping_interval_ms: 5000
ovm_enabled: true
ovm_module_id: "cb_ovm"
capabilities:
max_current_A_import: 16
min_current_A_import: 0
max_phase_count_import: 3
min_phase_count_import: 3
max_current_A_export: 16
min_current_A_export: 0
max_phase_count_export: 3
min_phase_count_export: 3
supports_changing_phases_during_charging: false
connector_type: "IEC62196Type2Cable"
gpio:
enable: true
interval_s: 1
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_bind: 127.0.0.1
mqtt_ping_interval_ms: 5000
gpio_0:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_1:
mode: "Input"
pulls: "NoPull"
config: 32767
gpio_2:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_3:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_4:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 1000
gpio_5:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_6:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_7:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_8:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_9:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767

View File

@@ -0,0 +1,169 @@
charge_bridge:
name: cb_sat_ac
ip: ANY_EVSE
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
fw_update_on_start: true
mdns_name: ""
heartbeat:
interval_s: 1
connection_to_s: 10
safety:
pp_mode: "disabled"
cp_avg_ms: 10
inverted_emergency_input: 0
relay_1:
relay_mode: "PowerRelay"
# Auxilary contact is connected from the OMRON relay
feedback_enabled: true
# The Omron relay switches in less than 100ms, use 200ms here
feedback_delay_ms: 200
# Only for PCB version 1.1, set to false for PCB version 1.2 and up
feedback_inverted: true
# PWM not supported yet
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_2:
# Not connected on PCB
relay_mode: "UserRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: false
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_3:
# Not connected on PCB
relay_mode: "UserRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: false
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
can_0:
enable: true
local: "cb_can"
baudrate: 250000
serial_1:
enable: true
local: "/dev/cb_uart"
baudrate: 115200
stopbits: OneStopBit
parity: None
serial_2:
enable: true
local: "/dev/cb_rs485"
baudrate: 9600
stopbits: OneStopBit
parity: None
plc:
enable: true
tap: "cb_plc"
ip: 172.25.6.1
netmask: 255.255.255.0
mtu: 1518
powersaving_mode: 1
evse_bsp:
enable: true
module_id: "cb_bsp"
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_bind: 127.0.0.1
mqtt_ping_interval_ms: 1000
capabilities:
max_current_A_import: 16
min_current_A_import: 6
max_phase_count_import: 3
min_phase_count_import: 3
max_current_A_export: 16
min_current_A_export: 6
max_phase_count_export: 3
min_phase_count_export: 3
supports_changing_phases_during_charging: false
connector_type: "IEC62196Type2Cable"
ovm_enabled: false
ovm_module_id: cb_ovm
gpio:
enable: true
interval_s: 1
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_bind: 127.0.0.1
mqtt_ping_interval_ms: 1000
gpio_0:
# RCD.TEST
#mode: "Rcd_Selftest_Output"
# Self test not fully supported yet
mode: "Input"
pulls: "NoPull"
mdns: false
# RCD self test duration (ignore emergency input signals for this time after self test)
# and show reason of safety decision on host somehow to simplify debugging
config: 1000
gpio_1:
# RCD.ERROR
mode: "Input"
pulls: "NoPull"
mdns: false
config: 0
gpio_2:
# MOTOR_1
# simple Motor lock with only 2 wires (no feedback contacts)
mode: "MotorLock_1"
pulls: "NoPull"
mdns: false
# 1000 ms motor drive time for locking/unlocking
config: 1000
gpio_3:
# MOTOR_2
# simple Motor lock with only 2 wires (no feedback contacts)
mode: "MotorLock_2"
pulls: "NoPull"
mdns: false
config: 1000
gpio_4:
# RCD.PWM
# not supported yet
mode: "Input"
pulls: "NoPull"
mdns: false
config: 0
gpio_5:
# External GPIO on connector J4 pin 9 (10kOhm I/O)
mode: "Input"
pulls: "NoPull"
mdns: false
config: 0
gpio_6:
# External GPIO on connector J4 pin 10 (10kOhm I/O)
mode: "Input"
pulls: "PullUp"
mdns: false
config: 0
gpio_7:
# External GPIO on connector J3 pin 11 (10kOhm I/O)
mode: "Input"
pulls: "NoPull"
mdns: false
config: 0
gpio_8:
# External GPIO on connector J3 pin 12 (10kOhm I/O)
mode: "Input"
pulls: "PullDown"
mdns: false
config: 0
gpio_9:
# Not connected
mode: "Input"
pulls: "PullDown"
mdns: false
config: 0

View File

@@ -0,0 +1,143 @@
charge_bridge_ip_list : [ "192.168.188.65", "192.168.188.65"]
charge_bridge:
name: cb_##
ip: ""
fw_file: ./firmware/charge-bridge-fw_complete.cbfw
fw_update_on_start: false
mdns_name: "pionix_cb_##"
heartbeat:
interval_s: 1
connection_to_s: 10
safety:
pp_mode: "disabled"
cp_avg_ms: 10
relay_1:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: false
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_2:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: false
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
relay_3:
relay_mode: "PowerRelay"
feedback_enabled: false
feedback_delay_ms: 10
feedback_inverted: false
pwm_dc: 100
pwm_delay_ms: 0
switchoff_delay_ms: 10
can_0:
enable: true
local: "cb_##_can0"
baudrate: 250000
serial_1:
enable: true
local: "/dev/cb_##_serial_1"
baudrate: 19200
stopbits: OneStopBit
parity: None
serial_2:
enable: false
local: "/dev/cb_##_serial_2"
baudrate: 19200
stopbits: OneStopBit
parity: None
plc:
enable: false
tap: "cb_##_tap0"
ip: 172.25.6.1
netmask: 255.255.255.0
mtu: 1518
powersaving_mode: 1
evse_bsp:
enable: false
module_id: "bsp_##"
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_ping_interval_ms: 1000
capabilities:
max_current_A_import: 16
min_current_A_import: 6
max_phase_count_import: 3
min_phase_count_import: 3
max_current_A_export: 16
min_current_A_export: 6
max_phase_count_export: 3
min_phase_count_export: 3
supports_changing_phases_during_charging: false
connector_type: "IEC62196Type2Cable"
ovm_enabled: true
ovm_module_id: "ovm_1"
gpio:
enable: false
interval_s: 4
mqtt_remote: "localhost"
mqtt_port: 1883
mqtt_ping_interval_ms: 1000
gpio_0:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_1:
mode: "Input"
pulls: "NoPull"
config: 32767
gpio_2:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_3:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767
gpio_4:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_5:
mode: "Output"
pulls: "NoPull"
mdns: false
config: 32767
gpio_6:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_7:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_8:
mode: "Output"
pulls: "PullUp"
mdns: false
config: 32767
gpio_9:
mode: "Input"
pulls: "PullUp"
mdns: false
config: 32767

View File

@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <charge_bridge/everest_api/api_connector.hpp>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/udp/udp_client.hpp>
#include <everest_api_types/evse_board_support/API.hpp>
namespace charge_bridge {
struct bsp_bridge_config {
std::string cb;
std::string item;
std::uint16_t cb_port;
std::string cb_remote;
evse_bsp::everest_api_config api;
};
class bsp_bridge : public everest::lib::io::event::fd_event_register_interface {
public:
bsp_bridge(bsp_bridge_config const& config);
~bsp_bridge() = default;
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
private:
void handle_timer_event();
evse_bsp::api_connector m_api;
everest::lib::io::udp::udp_client m_udp;
everest::lib::io::event::timer_fd m_timer;
bool m_udp_on_error{false};
};
} // namespace charge_bridge

View File

@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <chrono>
#include <everest/io/can/can_payload.hpp>
#include <everest/io/can/socket_can.hpp>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest/io/udp/udp_client.hpp>
#include <memory>
extern "C" struct cb_can_message;
namespace charge_bridge {
struct can_bridge_config {
std::string cb;
std::string item;
std::uint16_t cb_port;
std::string cb_remote;
std::string can_device;
};
class can_bridge : public everest::lib::io::event::fd_event_register_interface {
public:
can_bridge(can_bridge_config const& config);
~can_bridge();
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
private:
void handle_error_timer();
void handle_heartbeat_timer();
void send_can_to_udp(cb_can_message const& pl);
std::unique_ptr<everest::lib::io::can::socket_can> m_can;
everest::lib::io::udp::udp_client m_udp;
std::string m_can_device;
std::string m_identifier;
everest::lib::io::event::timer_fd m_heartbeat_timer;
std::chrono::steady_clock::time_point m_last_msg_to_cb;
};
} // namespace charge_bridge

View File

@@ -0,0 +1,90 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <atomic>
#include <charge_bridge/bsp_bridge.hpp>
#include <charge_bridge/can_bridge.hpp>
#include <charge_bridge/discovery.hpp>
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
#include <charge_bridge/gpio_bridge.hpp>
#include <charge_bridge/heartbeat_service.hpp>
#include <charge_bridge/plc_bridge.hpp>
#include <charge_bridge/serial_bridge.hpp>
#include <charge_bridge/utilities/symlink.hpp>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/serial/event_pty.hpp>
#include <everest/io/tun_tap/tap_client.hpp>
#include <everest/util/async/monitor.hpp>
#include <memory>
#include <optional>
namespace charge_bridge {
struct charge_bridge_status {
bool is_connected{false};
bool discovery_pending{false};
};
struct charge_bridge_config {
std::string cb_name;
std::uint16_t cb_port;
std::string cb_remote;
std::optional<can_bridge_config> can0;
std::optional<serial_bridge_config> serial1;
std::optional<serial_bridge_config> serial2;
std::optional<serial_bridge_config> serial3;
std::optional<plc_bridge_config> plc;
std::optional<bsp_bridge_config> bsp;
std::optional<heartbeat_config> heartbeat;
std::optional<gpio_config> gpio;
firmware_update::fw_update_config firmware;
};
void print_charge_bridge_config(charge_bridge_config const& config);
class charge_bridge : public everest::lib::io::event::fd_event_register_interface {
public:
charge_bridge(charge_bridge_config const& config);
~charge_bridge();
bool update_firmware(bool force);
std::string get_pty_1_slave_path();
std::string get_pty_2_slave_path();
std::string get_pty_3_slave_path();
void print_config();
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
void manage(everest::lib::io::event::fd_event_handler& handler, std::atomic_bool const& exit, bool force_update);
private:
void init();
void init_discovery(discovery_device_type type, std::set<std::string> const& interfaces, bool excluding);
void handle_discovery(std::string const& ip);
private:
std::unique_ptr<can_bridge> m_can_0_client;
std::unique_ptr<serial_bridge> m_pty_1;
std::unique_ptr<serial_bridge> m_pty_2;
std::unique_ptr<serial_bridge> m_pty_3;
std::unique_ptr<bsp_bridge> m_bsp;
std::unique_ptr<plc_bridge> m_plc;
std::unique_ptr<heartbeat_service> m_heartbeat;
std::unique_ptr<gpio_bridge> m_gpio;
std::unique_ptr<discovery> m_discovery;
everest::lib::io::event::fd_event_handler* m_event_handler{nullptr};
bool m_force_firmware_update{false};
everest::lib::util::monitor<charge_bridge_status> m_cb_status;
bool m_was_connected{false};
bool m_discovery_active{false};
charge_bridge_config m_config;
};
} // namespace charge_bridge

View File

@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <chrono>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest/io/mdns/mdns.hpp>
#include <everest/io/mdns/mdns_client.hpp>
#include <functional>
#include <memory>
#include <set>
namespace charge_bridge {
enum class discovery_device_type {
CB_EVSE,
CB_EV
};
class discovery : public everest::lib::io::event::fd_event_register_interface {
public:
using discovery_cb = std::function<void(std::string const&)>;
discovery(discovery_device_type type);
discovery(discovery_device_type type, std::set<std::string> const& interfaces, bool excluding);
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
void set_discovery_callback(discovery_cb const& cb);
private:
void add_client(std::string const& interface);
void query_registry();
std::vector<std::unique_ptr<everest::lib::io::mdns::mdns_client>> m_mdns;
everest::lib::io::event::timer_fd m_timer;
discovery_cb m_on_discover;
everest::lib::io::mdns::mDNS_registry m_registry;
discovery_device_type m_type;
static const std::string discovery_id;
};
} // namespace charge_bridge

View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <charge_bridge/everest_api/ev_bsp_api.hpp>
#include <charge_bridge/everest_api/evse_bsp_api.hpp>
#include <charge_bridge/everest_api/ovm_api.hpp>
#include <chrono>
#include <cstdint>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest/io/mqtt/mqtt_client.hpp>
#include <everest_api_types/evse_board_support/API.hpp>
#include <everest_api_types/evse_manager/API.hpp>
#include <everest_api_types/utilities/Topics.hpp>
#include <functional>
#include <protocol/cb_common.h>
#include <protocol/evse_bsp_cb_to_host.h>
#include <protocol/evse_bsp_host_to_cb.h>
#include <string>
namespace charge_bridge::evse_bsp {
namespace API_BSP = everest::lib::API::V1_0::types::evse_board_support;
struct everest_api_config {
std::string mqtt_remote;
std::string mqtt_bind;
uint16_t mqtt_port;
uint32_t mqtt_ping_interval_ms;
evse_bsp_config evse;
evse_ovm_config ovm;
evse_ev_bsp_config ev;
};
class api_connector : public everest::lib::io::event::fd_event_register_interface {
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
public:
api_connector(everest_api_config const& config, std::string const& cb_identifier);
void set_cb_tx(tx_ftor const& handler);
void set_cb_message(evse_bsp_cb_to_host const& msg);
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
private:
void handle_mqtt_connect();
void handle_cb_connection_state();
bool check_cb_heartbeat();
std::string m_cb_identifier;
everest::lib::io::mqtt::mqtt_client m_mqtt;
tx_ftor m_tx;
std::chrono::steady_clock::time_point m_last_cb_heartbeat;
everest::lib::io::event::timer_fd m_sync_timer;
std::string m_evse_bsp_receive_topic;
std::string m_evse_bsp_send_topic;
std::string m_ovm_receive_topic;
std::string m_ovm_send_topic;
std::string m_ev_bsp_receive_topic;
std::string m_ev_bsp_send_topic;
bool m_evse_bsp_enabled{false};
bool m_ovm_enabled{false};
bool m_ev_bsp_enabled{false};
bool m_cb_initial_comm_check{true};
bool m_cb_connected{false};
evse_bsp_host_to_cb m_host_status;
evse_bsp_api m_evse_bsp;
ovm_api m_ovm;
ev_bsp_api m_ev_bsp;
};
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <chrono>
#include <cstdint>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest_api_types/ev_board_support/API.hpp>
#include <everest_api_types/evse_board_support/API.hpp>
#include <everest_api_types/evse_manager/API.hpp>
#include <everest_api_types/generic/API.hpp>
#include <everest_api_types/utilities/Topics.hpp>
#include <functional>
#include <protocol/cb_common.h>
#include <protocol/evse_bsp_cb_to_host.h>
#include <protocol/evse_bsp_host_to_cb.h>
#include <string>
namespace charge_bridge::evse_bsp {
namespace API_EVSE_BSP = everest::lib::API::V1_0::types::evse_board_support;
namespace API_EV_BSP = everest::lib::API::V1_0::types::ev_board_support;
namespace API_EVM = everest::lib::API::V1_0::types::evse_manager;
namespace API_GENERIC = everest::lib::API::V1_0::types::generic;
// namespace API_OVM = everest::lib::API::V1_0::types::over_voltage_monitor;
struct evse_ev_bsp_config {
bool enabled{false};
std::string module_id;
};
class ev_bsp_api : public everest::lib::io::event::fd_event_register_interface {
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
using mqtt_ftor = std::function<void(std::string const&, std::string const&)>;
public:
ev_bsp_api(evse_ev_bsp_config const& config, std::string const& cb_identifier, evse_bsp_host_to_cb& host_status);
void set_cb_tx(tx_ftor const& handler);
void set_cb_message(evse_bsp_cb_to_host const& msg);
void set_mqtt_tx(mqtt_ftor const& tx);
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
void dispatch(std::string const& operation, std::string const& payload);
void raise_comm_fault();
void clear_comm_fault();
void sync(bool cb_connected);
private:
void tx(evse_bsp_host_to_cb const& msg);
void send_bsp_event(API_EVSE_BSP::Event data);
void send_bsp_measurement(API_EV_BSP::BspMeasurement data);
void send_ev_info(API_EVM::EVInfo data);
void send_raise_error(API_GENERIC::ErrorEnum error, std::string const& subtype, std::string const& msg);
void send_clear_error(API_GENERIC::ErrorEnum error, std::string const& subtype);
void send_communication_check();
void send_mqtt(std::string const& topic, std::string const& message);
void send_event(API_EVSE_BSP::Event data);
void receive_enable(std::string const& payload);
void receive_set_cp_state(std::string const& payload);
void receive_allow_power_on(std::string const& payload);
void receive_diode_fail(std::string const& payload);
void receive_set_ac_max_current(std::string const& payload);
void receive_set_three_phases(std::string const& payload);
void receive_set_rcd_error(std::string const& payload);
void receive_heartbeat(std::string const& pl);
void handle_error(const SafetyErrorFlags& data);
void handle_event_cp(std::uint8_t cp);
void handle_event_relay(std::uint8_t relay);
void handle_bsp_measurement(uint16_t cp, uint8_t pp_1, uint8_t pp2);
bool check_everest_heartbeat();
void handle_everest_connection_state();
evse_bsp_host_to_cb& host_status;
evse_bsp_cb_to_host m_cb_status;
tx_ftor m_tx;
bool m_everest_connected{false};
bool m_cb_connected{false};
bool m_cb_initial_comm_check{true};
bool m_bc_initial_comm_check{true};
std::string m_cb_identifier;
std::chrono::steady_clock::time_point last_everest_heartbeat;
mqtt_ftor m_mqtt_tx;
std::size_t m_last_hb_id{0};
everest::lib::API::V1_0::types::evse_board_support::Event last_cp_event{
everest::lib::API::V1_0::types::evse_board_support::Event::Disconnected};
};
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <chrono>
#include <cstdint>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest_api_types/evse_board_support/API.hpp>
#include <everest_api_types/evse_manager/API.hpp>
#include <everest_api_types/utilities/Topics.hpp>
#include <functional>
#include <protocol/cb_common.h>
#include <protocol/evse_bsp_cb_to_host.h>
#include <protocol/evse_bsp_host_to_cb.h>
#include <string>
namespace charge_bridge::evse_bsp {
namespace API_BSP = everest::lib::API::V1_0::types::evse_board_support;
namespace API_EVM = everest::lib::API::V1_0::types::evse_manager;
struct evse_bsp_config {
std::string module_id;
bool enabled{false};
API_BSP::HardwareCapabilities capabilities;
};
class evse_bsp_api : public everest::lib::io::event::fd_event_register_interface {
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
using mqtt_ftor = std::function<void(std::string const&, std::string const&)>;
public:
evse_bsp_api(evse_bsp_config const& config, std::string const& cb_identifier, evse_bsp_host_to_cb& host_status);
void set_cb_tx(tx_ftor const& handler);
void set_cb_message(evse_bsp_cb_to_host const& msg);
void set_mqtt_tx(mqtt_ftor const& tx);
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
void dispatch(std::string const& operation, std::string const& payload);
void raise_comm_fault();
void clear_comm_fault();
void sync(bool cb_connected);
private:
void tx(evse_bsp_host_to_cb const& msg);
void handle_event_cp(std::uint8_t cp);
void handle_event_relay(std::uint8_t relay);
void handle_error(const SafetyErrorFlags& data);
void handle_pp_type1(std::uint8_t data);
void handle_pp_type2(std::uint8_t data);
void handle_stop_button(std::uint8_t data);
void send_event(API_BSP::Event data);
void send_ac_nr_of_phases(std::uint8_t data);
void send_capabilities();
void send_ac_pp_amapcity(API_BSP::Ampacity data);
void send_request_stop_transaction(API_EVM::StopTransactionReason data);
void send_rcd_current(std::uint8_t data);
void send_raise_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg);
void send_clear_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg);
void send_communication_check();
void send_reply_reset(std::string const& replyTo);
void send_mqtt(std::string const& topic, std::string const& message);
void receive_enable(std::string const& payload);
void receive_pwm_on(std::string const& payload);
void receive_cp_state_X1(std::string const& payload);
void receive_cp_state_F(std::string const& payload);
void receive_allow_power_on(std::string const& payload);
void receive_ac_switch_three_phases_while_charging(std::string const& payload);
void receive_ac_overcurrent_limit(std::string const& payload);
void receive_lock();
void receive_unlock();
void receive_self_test(std::string const& payload);
void receive_request_reset(std::string const& payload);
void receive_heartbeat(std::string const& pl);
bool check_everest_heartbeat();
void handle_everest_connection_state();
evse_bsp_host_to_cb& host_status;
evse_bsp_cb_to_host cb_status;
tx_ftor m_tx;
everest::lib::io::event::timer_fd m_capabilities_timer;
API_BSP::HardwareCapabilities m_capabilities;
bool m_enabled{false};
bool everest_connected{false};
bool m_cb_connected{false};
bool m_bc_initial_comm_check{true};
std::string m_cb_identifier;
std::chrono::steady_clock::time_point last_everest_heartbeat;
mqtt_ftor m_mqtt_tx;
std::size_t m_last_hb_id{0};
};
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,85 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <chrono>
#include <cstdint>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest_api_types/evse_manager/API.hpp>
#include <everest_api_types/over_voltage_monitor/API.hpp>
#include <everest_api_types/utilities/Topics.hpp>
#include <functional>
#include <protocol/cb_common.h>
#include <protocol/evse_bsp_cb_to_host.h>
#include <protocol/evse_bsp_host_to_cb.h>
#include <string>
namespace charge_bridge::evse_bsp {
namespace API_OVM = everest::lib::API::V1_0::types::over_voltage_monitor;
struct evse_ovm_config {
bool enabled{false};
std::string module_id;
};
class ovm_api : public everest::lib::io::event::fd_event_register_interface {
using tx_ftor = std::function<void(evse_bsp_host_to_cb const&)>;
using rx_ftor = std::function<void(evse_bsp_cb_to_host const&)>;
using mqtt_ftor = std::function<void(std::string const&, std::string const&)>;
public:
ovm_api(evse_ovm_config const& config, std::string const& cb_identifier, evse_bsp_host_to_cb& host_status);
void set_cb_tx(tx_ftor const& handler);
void set_cb_message(evse_bsp_cb_to_host const& msg);
void set_mqtt_tx(mqtt_ftor const& tx);
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
void dispatch(std::string const& operation, std::string const& payload);
void raise_comm_fault();
void clear_comm_fault();
void sync(bool cb_connected);
private:
void tx(evse_bsp_host_to_cb const& msg);
void send_voltage_measurement_V(double data);
void send_raise_error(API_OVM::ErrorEnum error, std::string const& subtype, std::string const& msg,
API_OVM::ErrorSeverityEnum severity);
void send_clear_error(API_OVM::ErrorEnum error, std::string const& subtype);
void send_communication_check();
void send_mqtt(std::string const& topic, std::string const& message);
void handle_dc_hv_ov_emergency(bool high);
void handle_dc_hv_ov_error(bool high);
void handle_cp_state(CpState state);
void receive_set_limits(std::string const& payload);
void receive_start();
void receive_stop();
void receive_reset_over_voltage_error();
void receive_heartbeat(std::string const& pl);
bool check_everest_heartbeat();
void handle_everest_connection_state();
evse_bsp_host_to_cb& host_status;
evse_bsp_cb_to_host m_cb_status;
tx_ftor m_tx;
bool m_everest_connected{false};
bool m_cb_connected{false};
bool m_cb_initial_comm_check{true};
bool m_bc_initial_comm_check{true};
std::string m_cb_identifier;
std::chrono::steady_clock::time_point last_everest_heartbeat;
API_OVM::OverVoltageLimits m_limits{0, 0};
mqtt_ftor m_mqtt_tx;
std::size_t m_last_hb_id{0};
};
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <charge_bridge/utilities/filesystem.hpp>
#include <charge_bridge/utilities/sync_udp_client.hpp>
#include <fstream>
namespace charge_bridge::firmware_update {
struct fw_update_config {
std::string cb;
std::uint16_t cb_port;
std::string cb_remote;
std::string fw_path;
bool fw_update_on_start;
};
class sync_fw_updater {
public:
sync_fw_updater(fw_update_config const& config);
~sync_fw_updater() = default;
std::optional<std::string> get_fw_version();
bool switch_bank();
bool ping();
bool upload_fw();
void print_fw_version();
bool print_switch_bank();
bool quick_check_connection();
bool check_connection();
bool check_if_correct_fw_installed();
private:
bool check_reply(utilities::sync_udp_client::reply const& val);
bool upload_firmware();
bool upload_init(const fs::path& file_path, std::uint32_t& offset,
charge_bridge::filesystem_utils::CryptSignedHeader& hdr);
bool upload_transfer(const fs::path& file_path, std::uint16_t& sector, std::uint32_t offset,
std::uint32_t& total_bytes);
bool upload_finish(const fs::path& file_path, std::uint32_t total_bytes,
const charge_bridge::filesystem_utils::CryptSignedHeader& hdr);
everest::lib::io::udp::udp_payload make_fw_chunk(std::uint16_t sector, std::uint8_t last_chunk,
std::vector<std::uint8_t> const& data);
utilities::sync_udp_client m_udp;
fw_update_config m_config;
static const std::uint32_t app_udp_sector_size;
static const std::uint16_t sub_chunk_size;
};
} // namespace charge_bridge::firmware_update

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include "everest/io/mqtt/mosquitto_cpp.hpp"
#include <array>
#include <everest/io/can/can_payload.hpp>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest/io/mqtt/mqtt_client.hpp>
#include <everest/io/udp/udp_client.hpp>
#include <protocol/cb_management.h>
namespace charge_bridge {
struct gpio_config {
std::string cb;
std::string item;
std::uint16_t cb_port;
std::string cb_remote;
std::uint16_t interval_s;
std::string mqtt_remote;
std::string mqtt_bind;
std::uint16_t mqtt_port;
std::uint32_t mqtt_ping_interval_ms;
};
class gpio_bridge : public everest::lib::io::event::fd_event_register_interface {
public:
gpio_bridge(gpio_config const& config);
~gpio_bridge();
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
private:
void handle_error_timer();
void handle_heartbeat_timer();
void handle_udp_rx(everest::lib::io::udp::udp_payload const& payload);
void dispatch(everest::lib::io::mqtt::mqtt_client::message const& data);
void send_mqtt(std::string const& topic, std::string const& message);
void send_udp();
everest::lib::io::udp::udp_client m_udp;
bool m_udp_on_error{false};
everest::lib::io::event::timer_fd m_heartbeat_timer;
std::chrono::steady_clock::time_point last_heartbeat;
CbManagementPacket<CbGpioPacket> m_message;
std::string m_identifier;
bool m_mqtt_on_error{false};
everest::lib::io::mqtt::mqtt_client m_mqtt;
std::string m_receive_topic;
std::string m_send_topic;
};
} // namespace charge_bridge

View File

@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include "protocol/cb_management.h"
#include <chrono>
#include <everest/io/can/can_payload.hpp>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest/io/udp/udp_client.hpp>
#include <memory>
#include <protocol/cb_config.h>
namespace charge_bridge {
struct heartbeat_config {
std::string cb;
std::string item;
std::uint16_t cb_port;
std::string cb_remote;
std::uint16_t interval_s;
std::uint16_t connection_to_s;
CbConfig cb_config;
};
class heartbeat_service : public everest::lib::io::event::fd_event_register_interface {
public:
heartbeat_service(heartbeat_config const& config, std::function<void(bool)> const& publish_connection_status);
~heartbeat_service();
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
private:
void handle_error_timer();
void handle_heartbeat_timer();
void handle_udp_rx(everest::lib::io::udp::udp_payload const& payload);
everest::lib::io::udp::udp_client m_udp;
bool m_udp_on_error{false};
everest::lib::io::event::timer_fd m_heartbeat_timer;
std::string m_identifier;
CbManagementPacket<CbConfig> m_config_message;
std::chrono::steady_clock::time_point m_last_heartbeat_reply;
bool m_cb_connected{false};
bool m_inital_cb_commcheck{true};
std::chrono::milliseconds m_heartbeat_interval;
std::chrono::milliseconds m_connection_to;
std::function<void(bool)> m_publish_connection_status;
std::uint32_t m_mcu_timestamp{0};
int m_mcu_reset_count{0};
};
} // namespace charge_bridge

View File

@@ -0,0 +1,41 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest/io/tun_tap/tap_client.hpp>
#include <everest/io/udp/udp_client.hpp>
namespace charge_bridge {
struct plc_bridge_config {
std::string cb;
std::string item;
std::uint16_t cb_port;
std::string cb_remote;
std::string plc_tap;
std::string plc_ip;
std::string plc_netmaks;
int plc_mtu;
};
class plc_bridge : public everest::lib::io::event::fd_event_register_interface {
public:
plc_bridge(plc_bridge_config const& config);
~plc_bridge() = default;
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
private:
void handle_timer_event();
everest::lib::io::tun_tap::tap_client m_tap;
everest::lib::io::udp::udp_client m_udp;
everest::lib::io::event::timer_fd m_timer;
bool m_udp_on_error{false};
bool m_tap_on_error{false};
};
} // namespace charge_bridge

View File

@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <charge_bridge/utilities/symlink.hpp>
#include <everest/io/event/fd_event_register_interface.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <everest/io/serial/event_pty.hpp>
#include <everest/io/tcp/tcp_client.hpp>
namespace charge_bridge {
struct serial_bridge_config {
std::string cb;
std::string item;
std::uint16_t cb_port;
std::string cb_remote;
std::string serial_device;
};
class serial_bridge : public everest::lib::io::event::fd_event_register_interface {
public:
serial_bridge(serial_bridge_config const& config);
~serial_bridge() = default;
bool register_events(everest::lib::io::event::fd_event_handler& handler) override;
bool unregister_events(everest::lib::io::event::fd_event_handler& handler) override;
std::string get_slave_path();
private:
void reset_tcp();
everest::lib::io::serial::event_pty m_pty;
everest::lib::io::tcp::tcp_client m_tcp;
utilities::symlink m_symlink;
int m_tcp_last_error_id = -1;
};
} // namespace charge_bridge

View File

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <functional>
#include <vector>
namespace fs = std::filesystem;
namespace charge_bridge::filesystem_utils {
bool read_from_file_partial(const fs::path& file_path, const std::size_t byte_count, std::string& out_data);
bool read_from_file(const fs::path& file_path, std::string& out_data);
/// @brief Process the file in chunks with the provided function. If the process function
/// returns false, this function will also immediately return
/// @return True if the file was properly opened false otherwise
bool process_file(const fs::path& file_path, std::size_t buffer_size,
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func);
bool process_file(std::ifstream& file, std::size_t buffer_size,
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func);
struct CryptSignedHeader {
std::string firmware_version; // max 32 bytes long string describing the fw version
std::uint8_t sig_len = 0;
std::vector<std::uint8_t> signature; // length = sig_len
std::uint8_t num_sectors = 0; // global one-byte value
std::array<std::uint8_t, 16> iv{}; // 16-byte IV from file #2
};
bool read_crypt_signed_header(const fs::path& path, CryptSignedHeader& hdr, std::uint32_t& image_offset);
} // namespace charge_bridge::filesystem_utils

View File

@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <iostream>
namespace charge_bridge::utilities {
std::ostream& print_error(std::string const& device, std::string const& unit, int status);
}

View File

@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <charge_bridge/charge_bridge.hpp>
namespace charge_bridge::utilities {
std::vector<charge_bridge_config> parse_config_multi(std::string const& config_file);
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <cstdint>
#include <cstring>
#include <vector>
namespace charge_bridge::utilities {
// Converts a struct to raw bytes
template <typename T> static inline void struct_to_vector(const T& data_struct, std::vector<std::uint8_t>& buffer) {
static constexpr auto struct_size = sizeof(T);
buffer.resize(struct_size);
std::memcpy(buffer.data(), &data_struct, struct_size);
}
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <protocol/cb_config.h>
#include <string>
namespace charge_bridge::utilities {
std::string to_string(CbCanBaudrate value);
std::string to_string(CbUartBaudrate value);
std::string to_string(CbUartParity value);
std::string to_string(CbUartStopbits value);
std::string to_string(CbUartConfig const& value);
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#pragma once
#include <set>
#include <string>
namespace charge_bridge::utilities {
bool string_starts_with(std::string_view const& str, std::string_view const& pattern);
bool string_ends_with(std::string const& str, std::string const& pattern);
std::string string_after_pattern(std::string_view const& str, std::string_view const& pattern);
std::string& replace_all_in_place(std::string& source, std::string const& placeholder, std::string const& substitute);
std::string replace_all(std::string const& source, std::string const& placeholder, std::string const& substitute);
std::set<std::string> csv_to_set(std::string const& str);
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <string>
namespace charge_bridge::utilities {
class symlink {
public:
symlink(std::string const& src, std::string const& tar);
symlink();
bool set_link(std::string const& src, std::string const& tar);
bool del_link();
~symlink();
private:
std::string m_tar;
};
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include "everest/io/udp/udp_payload.hpp"
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/udp/udp_socket.hpp>
#include <optional>
namespace charge_bridge::utilities {
class sync_udp_client {
public:
using udp_payload = everest::lib::io::udp::udp_payload;
using reply = std::optional<udp_payload>;
sync_udp_client(std::string const& remote, std::uint16_t port);
sync_udp_client(std::string const& remote, std::uint16_t port, std::uint16_t retries, std::uint16_t timeout_ms);
reply request_reply(udp_payload const& payload);
reply request_reply(udp_payload const& payload, std::uint16_t timeout_ms, std::uint16_t retries);
bool tx(udp_payload const& payload);
reply rx();
reply rx(std::uint16_t timeout_ms);
bool is_open();
private:
void init(std::string const& remote, std::uint16_t port);
void clear_socket();
std::uint16_t m_retries;
std::uint16_t m_timeout_ms;
everest::lib::io::udp::udp_client_socket m_udp;
everest::lib::io::event::fd_event_handler m_handler;
};
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <everest_api_types/evse_board_support/API.hpp>
#include <protocol/cb_config.h>
#include <protocol/cb_management.h>
#include <ryml.hpp>
#include <string>
namespace charge_bridge::utilities {
class yml_node_error {
public:
yml_node_error(c4::yml::ConstNodeRef node);
yml_node_error(c4::yml::ConstNodeRef node, std::string const& msg);
c4::yml::ConstNodeRef m_node;
std::string m_msg;
};
namespace EXT_API = everest::lib::API;
namespace EXT_API_BSP = EXT_API::V1_0::types::evse_board_support;
bool decode_CbGpioMode(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs);
bool decode_CbGpioPulls(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs);
bool decode_CbUartBaudrate(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs);
bool decode_CbUartStopbits(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs);
bool decode_CbUartParity(c4::yml::ConstNodeRef const& node, CbUartParity& rhs);
bool decode_CbCanBaudrate(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs);
bool decode_CbRelayMode(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs);
bool decode_CbSafetyMode(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs);
bool decode_RelayConfig(c4::yml::ConstNodeRef const& node, RelayConfig& rhs);
bool decode_SafetyConfig(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs);
bool decode_CbGpioConfig(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs);
bool decode_CbUartConfig(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs);
bool decode_CbCanConfig(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs);
bool decode_CbNetworkConfig(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs);
bool decode_Connector_type(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs);
bool decode_HardwareCapabilities(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioMode& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioPulls& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartBaudrate& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartStopbits& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartParity& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanBaudrate& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbRelayMode& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbSafetyMode& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, RelayConfig& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, SafetyConfig& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbGpioConfig& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbUartConfig& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbCanConfig& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, CbNetworkConfig& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::Connector_type& rhs);
c4::yml::ConstNodeRef const& operator>>(c4::yml::ConstNodeRef const& node, EXT_API_BSP::HardwareCapabilities& rhs);
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include "charge_bridge/charge_bridge.hpp"
#include "charge_bridge/utilities/string.hpp"
#include <algorithm>
#include <atomic>
#include <charge_bridge/utilities/parse_config.hpp>
#include <chrono>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/event/timer_fd.hpp>
#include <iostream>
#include <numeric>
using namespace everest::lib::io::event;
using namespace everest::lib::API::V1_0::types;
using namespace charge_bridge;
enum class mode {
error,
connector,
update,
update_only,
};
mode parse_args(int argc, char* argv[], std::vector<std::string>& config_files) {
// clang-format off
auto print_msg = []() {
std::cout << "\nUSAGE: \n";
std::cout << "pionix_chargebridge [--update][--update_only] {config_file [config_file_2 ....]} \n";
std::cout << "\n";
std::cout << "--update use this flag to execute an update at start and continue operation after\n";
std::cout << "--update_only use this flag to execute an update and stop the application after\n";
std::cout << "config_file use this configuration file\n";
std::cout << "config_file_x add more configuration files for each additional ChargeBridge group\n";
std::cout << "\n";
};
// clang-format on
auto mode = mode::connector;
for (int i = 1; i < argc; ++i) {
std::string current_arg = argv[i];
if (current_arg == "--update_only") {
mode = mode::update_only;
} else if (current_arg == "--update") {
mode = mode::update;
} else if (utilities::string_starts_with(current_arg, "--")) {
mode = mode::error;
break;
} else {
config_files.push_back(current_arg);
}
}
if (config_files.size() == 0) {
mode = mode::error;
}
if (mode == mode::error) {
print_msg();
}
return mode;
}
std::atomic<bool> g_run_application(true);
void signal_handler(int signum) {
std::cout << "\nSignal " << signum << " received. Initiating graceful shutdown." << std::endl;
g_run_application = false;
}
int main(int argc, char* argv[]) {
std::cout << "PIONIX ChargeBridge (C) 2025-2026\n" << std::endl;
std::signal(SIGINT, signal_handler);
std::signal(SIGHUP, signal_handler);
std::signal(SIGTERM, signal_handler);
std::vector<std::string> config_files;
std::vector<charge_bridge_config> cb_configs;
std::vector<std::unique_ptr<::charge_bridge::charge_bridge>> cb_handler;
auto mode_of_operation = parse_args(argc, argv, config_files);
if (mode_of_operation == mode::error) {
return EXIT_FAILURE;
}
fd_event_handler ev_handler;
std::set<std::string> cb_ids_in_use;
for (auto const& elem : config_files) {
auto config_list = utilities::parse_config_multi(elem);
if (config_list.empty()) {
g_run_application.store(false);
break;
}
for (auto const& config : config_list) {
print_charge_bridge_config(config);
if (cb_ids_in_use.count(config.cb_name) > 0) {
std::cerr << "Duplicate charge_bridge::name '" << config.cb_name << "'" << std::endl;
return EXIT_FAILURE;
}
cb_ids_in_use.insert(config.cb_name);
cb_handler.push_back(std::make_unique<::charge_bridge::charge_bridge>(config));
auto& cb = *cb_handler.rbegin();
if (mode_of_operation == mode::update_only) {
cb->update_firmware(true);
}
auto force_update = mode_of_operation == mode::update;
cb->manage(ev_handler, g_run_application, force_update);
}
}
ev_handler.run(g_run_application);
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,7 @@
# Chargebridge Protocol
Contains the header definitions for the raw C structs that will be used for comms between the Linux system and the
development board. Will also include various assertions and size/bounds checks to determine the that the client
systems are compatible and have the same memory layout.
The headers are C compliant for both C and C++ user code.

View File

@@ -0,0 +1,87 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include "cb_platform.h"
#include <stdint.h>
/* Enum definitions */
typedef enum _CanErrorState {
CanErrorState_Error_Active = 0,
CanErrorState_Error_Passive = 1,
CanErrorState_ForceSize = 0xFFFFFFFF,
} CanErrorState;
typedef enum _CanBitrate {
CanBitrate_125kbps = 0,
CanBitrate_250kbps = 1,
CanBitrate_500kbps = 2,
CanBitrate_1000kbps = 3,
CanBitrate_ForceSize = 0xFFFFFFFF,
} CanBitrate;
typedef enum _CanFDBitrate {
CanFDBitrate_1MBps = 0,
CanFDBitrate_2MBps = 1,
CanFDBitrate_3MBps = 2,
CanFDBitrate_4MBps = 3,
CanFDBitrate_5MBps = 4,
CanFDBitrate_6MBps = 5,
CanFDBitrate_7MBps = 6,
CanFDBitrate_8MBps = 7,
CanFDBitrate_ForceSize = 0xFFFFFFFF,
} CanFDBitrate;
typedef enum _CanFlags {
CanFlags_EFF = 1,
CanFlags_RTR = 1 << 1,
CanFlags_ERR = 1 << 2,
} CanFlags;
typedef struct CB_COMPILER_ATTR_PACK _CanStatistics {
// tx: direction is from host to bus
// rx: direction is from bus to host
uint32_t frames_tx;
uint32_t frames_rx;
uint32_t event_rx_buf_full;
uint32_t event_tx_buf_full;
} CanStatistics;
typedef enum _CanPacketType : uint8_t{
CanPacketType_Regular = 0,
CanPacketType_Keep_Alive = 1,
} CanPacketType;
struct CB_COMPILER_ATTR_PACK cb_can_message {
uint8_t version;
CanPacketType packet_type; // 0: regular CAN packet, 1: dummy keep-alive packet
CanErrorState error_state;
CanStatistics statistics;
CanBitrate bitrate;
CanFDBitrate fd_bitrate; /* integer in MBit (1-8) */
uint8_t can_flags; // EFF, RTR, ERR
uint32_t can_id;
/* dlc 0..8: standard CAN frame with up to 8 bytes
* FDCAN dlc:
* 9: 12 bytes
* 10: 16 bytes
* 11: 20 bytes
* 12: 24 bytes
* 13: 32 bytes
* 14: 48 bytes
* 15: 64 bytes
*/
uint8_t dlc;
// Note: in UDP transmission, data bytes at the end may be omitted in the message.
// Always check dlc first before accessing the data
uint8_t data[64];
};
#define cb_can_message_set_zero \
{0, CanPacketType_Regular, CanErrorState_Error_Active, {0, 0, 0, 0}, CanBitrate_125kbps, CanFDBitrate_1MBps, 0, 0, \
0, {0, 0, 0, 0, 0, 0, 0, 0}};
#include "test/cb_can_message_test.h"

View File

@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include "cb_platform.h"
#include <stdint.h>
// Structs
typedef union _SafetyErrorFlags {
struct _flags {
uint32_t cp_not_state_c : 1;
uint32_t pwm_not_enabled : 1;
uint32_t pp_invalid : 1;
uint32_t plug_temperature_too_high : 1;
uint32_t internal_temperature_too_high : 1;
uint32_t emergency_input_latched : 1;
uint32_t relay_health_latched : 1;
uint32_t vdd_3v3_out_of_range : 1;
uint32_t vdd_core_out_of_range : 1;
uint32_t vdd_12V_out_of_range : 1;
uint32_t vdd_N12V_out_of_range : 1;
uint32_t vdd_refint_out_of_range : 1;
uint32_t external_allow_power_on : 1;
uint32_t config_mem_error : 1;
uint32_t dc_hv_ov_emergency : 1;
uint32_t dc_hv_ov_error : 1;
uint32_t reserved : 17;
} flags;
uint32_t raw;
} SafetyErrorFlags;
typedef enum _CpState : uint8_t {
CpState_A,
CpState_B,
CpState_C,
CpState_D,
CpState_E,
CpState_F,
CpState_DF,
CpState_INVALID
} CpState;

View File

@@ -0,0 +1,127 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include "cb_platform.h"
#include <stdint.h>
#define CB_NUMBER_OF_GPIOS 10
#define CB_NUMBER_OF_UARTS 3
// enums
typedef enum _CbGpioMode : uint8_t {
CBG_Input = 0x00,
CBG_Output = 0x01,
CBG_Pwm_Input = 0x02,
CBG_Pwm_Output = 0x03,
CBG_RS485_2_DE = 0x04,
CBG_Rcd_Selftest_Output = 0x05,
CBG_Rcd_Error_Input= 0x06,
CBG_Rcd_PWM_Input= 0x07,
CBG_MotorLock_1 = 0x08,
CBG_MotorLock_2 = 0x09,
} CbGpioMode;
typedef enum _CbRelayMode : uint8_t {
CBR_PowerRelay = 0x00, CBR_UserRelay = 0x01,
} CbRelayMode;
typedef enum _CbGpioPulls : uint8_t {
CBGP_NoPull = 0x00, CBGP_PullUp = 0x01, CBGP_PullDown = 0x02,
} CbGpioPulls;
typedef enum _CbUartBaudrate : uint8_t {
CBUBR_9600 = 0x00,
CBUBR_19200 = 0x01,
CBUBR_38400 = 0x02,
CBUBR_57600 = 0x03,
CBUBR_115200 = 0x04,
CBUBR_230400 = 0x05,
CBUBR_250000 = 0x06,
CBUBR_460800 = 0x07,
CBUBR_500000 = 0x08,
CBUBR_1000000 = 0x09,
CBUBR_2000000 = 0x0A,
CBUBR_3000000 = 0x0B,
CBUBR_4000000 = 0x0C,
CBUBR_6000000 = 0x0D,
CBUBR_8000000 = 0x0E,
CBUBR_10000000 = 0x0F,
} CbUartBaudrate;
typedef enum _CbUartStopbits : uint8_t {
CBUS_OneStopBit = 0x00, CBUS_TwoStopBits = 0x01,
} CbUartStopbits;
typedef enum _CbUartParity : uint8_t {
CBUP_None = 0x00, CBUP_Odd = 0x01, CBUP_Even = 0x02,
} CbUartParity;
typedef enum _CbCanBaudrate : uint8_t {
CBCBR_125000 = 0x00,
CBCBR_250000 = 0x01,
CBCBR_500000 = 0x02,
CBCBR_1000000 = 0x03,
} CbCanBaudrate;
typedef enum _CbSafetyMode : uint8_t {
CBSM_disabled = 0x00, CBSM_US = 0x01, CBSM_EU = 0x02,
} CbSafetyMode;
// Structs
typedef struct CB_COMPILER_ATTR_PACK _relay_config {
CbRelayMode relay_mode;
uint8_t feedback_enabled; // 0: feedback unused, 1: feedback expected
uint16_t feedback_delay_ms; // After switching, wait for this delay before evaluating feedback pin
uint8_t feedback_inverted; // 0: feedback normal (mirror contact, high when relay is off), 1: inverted
uint8_t pwm_dc; // 100: Do not use PWM. 1-99: Set PWM Duty cycle after delay
uint16_t pwm_delay_ms; // Delay in ms after which the PWM starts
uint16_t switchoff_delay_ms; // Delay before switching relay off. Can be used to set a small delay between EMG_OUT
// and relays off [SR-SL-2]
} RelayConfig;
typedef struct CB_COMPILER_ATTR_PACK _safety_config {
CbSafetyMode pp_mode; // set to 0: disabled 1: US type 1, 2: EU type 2
uint8_t cp_avg_ms; // default is 10ms / pulses
RelayConfig relays[3]; // Config for the 3 relay I/Os
uint8_t inverted_emergency_input; // 0: normal operation, 1: emergency input is inverted
uint8_t temperature_limit_pt1000_C; // Temperature limit for the PT1000 inputs. Relays will switch off if temperature is above the limit. Setting this to 0 will disable the feature.
} SafetyConfig;
typedef struct CB_COMPILER_ATTR_PACK _CbGpioConfig {
CbGpioMode mode;
CbGpioPulls pulls;
uint8_t strap_option_mdns_naming; // sample as bit for mdns id;
uint16_t mode_config; // Config value for the mode, e.g. frequency of PWM
} CbGpioConfig;
typedef struct CB_COMPILER_ATTR_PACK _CbUartConfig {
CbUartBaudrate baudrate;
CbUartStopbits stopbits;
CbUartParity parity;
} CbUartConfig;
typedef struct CB_COMPILER_ATTR_PACK _CbCanConfig {
CbCanBaudrate baudrate;
} CbCanConfig;
typedef struct CB_COMPILER_ATTR_PACK _CbNetworkConfig {
char mdns_name[20]; // custom MDNS name
} CbNetworkConfig;
// Final complete config struct
#define CB_CONFIG_VERSION 0
typedef struct CB_COMPILER_ATTR_PACK _cb_config {
uint32_t config_version;
SafetyConfig safety;
CbGpioConfig gpios[CB_NUMBER_OF_GPIOS];
CbUartConfig uarts[CB_NUMBER_OF_UARTS];
CbCanConfig can;
CbNetworkConfig network;
uint8_t plc_powersaving_mode;
} CbConfig;

View File

@@ -0,0 +1,151 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
/**
* \file Common structs used between linux and the STM32 dev board. Data
* will be sent raw over the UDP socket, with the sender using
* \ref send(struct, sizeof(CbStruct))
* and the receiver using
* \ref receive(raw_bytes, sizeof(CbStruct))
* CbStruct *struct = reinterpret_cast<CbStruct *>(raw_bytes);
*
* Notes:
* 1) After V1 structs will not be able to remove fields, only add fields
* after the existing fields
* 2) There can be problems with variable length structs, for example
* CbFirmwarePacket that can have a payload with lengths 0-1024
*
* Test files are added at the end that check the sizes of the structs
* at compile time, to determine the fact that they are consistent
* across platforms.
*/
#pragma once
#include <stdint.h>
#include "cb_config.h"
#ifndef __cplusplus
#error "This header is C++ only"
#endif
enum class AppUDPResponse : uint32_t {
AUR_Ok = 0x500D500D, AUR_Bad = 0xBADBAD00,
};
enum class CbType : uint8_t {
CCS_EVSE = 0, CCS_EV = 1,
};
/*
* What type of message is on the socket
*/
enum class CbStructType : uint16_t {
// track IP with timeout and port
// Housekeepig, heartbeat/config (safety_config, serial port(fixed)/CAN bitrate, gpio config, mdns module name), fw version, softreset,
CST_HostToCb_Heartbeat = 1,
CST_CbToHost_Heartbeat = 2,
// track IP with timeout and port
// GPIO client
CST_HostToCb_Gpio = 3,
CST_CbToHost_Gpio = 4,
// FW update
CST_CbFirmwareReply = 0xFFF9,
CST_CbFirmwareStart = 0xFFFA,
CST_CbFirmwarePacket = 0xFFFB,
CST_CbFirmwareFinish = 0xFFFC,
CST_CbFirmwareUpdateCancel = 0xFFFD,
CST_CbFirmwarePing = 0xFFFE,
CST_CbFirmwareGetVersion = 0xFFFF,
};
/*
This container message is used for all generic module management packets
*/
template<typename T>
struct CB_COMPILER_ATTR_PACK CbManagementPacket {
CbStructType type;
T data;
};
template<> struct CB_COMPILER_ATTR_PACK CbManagementPacket<void> {
CbStructType type;
};
struct CB_COMPILER_ATTR_PACK CbGpioPacket {
uint8_t number_of_gpios; // Just to check compatibility
uint16_t gpio_values[CB_NUMBER_OF_GPIOS]; // Actual value, 0: low, 1: high, or duty cycle for PWM
};
struct CB_COMPILER_ATTR_PACK CbHeartbeatPacket {
CbConfig module_config;
};
struct CB_COMPILER_ATTR_PACK CbHeartbeatReplyPacket {
int32_t cp_hi_mV;
int32_t cp_lo_mV;
int32_t vdd_core;
int32_t vdd_3v3;
int32_t vdd_refint;
int32_t vdd_12V;
int32_t vdd_N12V;
int32_t pp_mOhm;
int32_t pp_voltage_mV;
uint8_t relay_state_feedback[3];
int16_t temperature_mcu_C;
int16_t temperature_pcb_C;
int16_t temperature_modem_C;
int16_t temperature_PT1000_C[2];
int32_t uptime_ms;
};
struct CB_COMPILER_ATTR_PACK CbFirmwareStart {
uint8_t is_secure_fw :1;
uint8_t requires_crc_verification :1;
uint8_t requires_sha256_verification :1;
uint8_t requires_signature_verification :1;
uint8_t requires_decryption :1;
// IV in case we require decryption
uint8_t iv[16];
};
struct CB_COMPILER_ATTR_PACK CbFirmwarePacket {
// If it is the last packet sent, used when we need to take care of
// the padding bytes in case of decryption or other operations
uint8_t last_packet :1;
uint16_t sector;
uint16_t data_len;
uint8_t data[1024];
};
struct CB_COMPILER_ATTR_PACK CbFirmwareEnd {
uint32_t firmware_len;
// Signature for the firmware in the faw format
uint8_t fw_signature[128];
uint8_t fw_signature_len;
uint8_t watermark_secure_end;
};
struct CB_COMPILER_ATTR_PACK CbFirmwareUpdateCancel {
uint8_t dummy;
};
struct CB_COMPILER_ATTR_PACK CbFirmwarePing {
uint8_t dummy;
};
struct CB_COMPILER_ATTR_PACK CbFirmwareGetVersion {
uint8_t dummy;
};
#include "test/cb_management_test.h"

View File

@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
/**
* \file compiler utilities and Pionix defines for
* cross-compiling
*/
#pragma once
#if defined(__cplusplus)
#define CB_STATIC_ASSERT(cond, msg) static_assert(cond, msg)
#else
#define CB_STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
#endif
#define CB_COMPILER_ATTR_PACK __attribute__((packed))
// Should be < MTU (defined as #define NX_DRIVER_ETHERNET_MTU 1514)
#define CB_MAX_UDP_PACKET_SIZE (1024 + 256)
// -128 since we might want some non-struct metadata
#define CB_MAX_CB_STRUCT_SIZE (CB_MAX_UDP_PACKET_SIZE - 128)
#define CB_MAX_STRING_SIZE 64
#define cb_string(name) int8_t name[CB_MAX_STRING_SIZE]

View File

@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef EVSE_BSP_CB_TO_HOST_H
#define EVSE_BSP_CB_TO_HOST_H
#include <stdint.h>
#include "cb_platform.h"
#include "cb_common.h"
struct CB_COMPILER_ATTR_PACK evse_bsp_cb_to_host {
// add version number as first uint16 ????
// potentially unused, ignore for now
uint8_t reset_reason;
CpState cp_state;
uint8_t relay_state;
SafetyErrorFlags error_flags;
uint8_t pp_state_type1;
uint8_t pp_state_type2;
uint8_t lock_state;
uint32_t hv_mV;
// still define handling set for
uint8_t stop_charging;
uint16_t cp_duty_cycle;
};
/* Enum definitions */
typedef enum _RelayState {
RelayState_Open = 0,
RelayState_Closed = 1
} RelaiseState;
typedef enum _ResetReason {
ResetReason_USER = 0,
ResetReason_WATCHDOG = 1
} ResetReason;
typedef enum _PpState_Type2 {
PpState_Type2_STATE_NC = 0,
PpState_Type2_STATE_13A = 1,
PpState_Type2_STATE_20A = 2,
PpState_Type2_STATE_32A = 3,
PpState_Type2_STATE_70A = 4,
PpState_Type2_STATE_FAULT = 5
} PpState_Type2;
typedef enum _PpState_Type1 {
PpState_Type1_STATE_NotConnected,
PpState_Type1_STATE_Connected_Button_Pressed,
PpState_Type1_STATE_Connected,
PpState_Type1_STATE_Invalid
} PpState_Type1;
typedef enum _LockState {
LockState_UNDEFINED = 0,
LockState_UNLOCKED = 1,
LockState_LOCKED = 2
} LockState;
#include "test/evse_bsp_cb_to_host_test.h"
#endif // EVSE_BSP_CB_TO_HOST_H

View File

@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef EVSE_BSP_HOST_TO_CB_H
#define EVSE_BSP_HOST_TO_CB_H
#include <stdint.h>
#include "cb_platform.h"
struct CB_COMPILER_ATTR_PACK evse_bsp_host_to_cb {
uint8_t connector_lock; /* 0: unlock, otherwise: lock */
uint32_t pwm_duty_cycle; /* in 0.01 %, 0 = State F, 10000 = X1 */
uint8_t allow_power_on; /* 0 false, true otherwise */
uint8_t reset; /* 0 false, true otherwise */
uint8_t ovm_enable; /* 0 disabled, 1: enabled */
uint8_t ovm_reset_errors; /* 0 leave errors untouched, 1: clear error bits for OVM */
uint32_t ovm_limit_emergency_mV; /* 9ms limit in mV */
uint32_t ovm_limit_error_mV; /* 400ms limit in mV */
CpState ev_set_cp_state; /* Set CP state (EV side only) */
uint8_t ev_set_diodefault; /* Set/Clear DF state (EV side only) */
};
#include "test/evse_bsp_host_to_cb_test.h"
#endif // EVSE_BSP_H

View File

@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include "protocol/cb_can_message.h"
#include "protocol/cb_platform.h"
CB_STATIC_ASSERT(sizeof(CanErrorState) == 4, "CanErrorState data size!!");
CB_STATIC_ASSERT(sizeof(CanBitrate) == 4, "CanBitrate data size!!");
CB_STATIC_ASSERT(sizeof(CanFDBitrate) == 4, "CanFDBitrate data size!!");
CB_STATIC_ASSERT(sizeof(CanStatistics) == 4+4+4+4, "CanStatistics data size!!");
CB_STATIC_ASSERT(sizeof(struct cb_can_message) == 1+4+16+4+4+1+4+1+64+1, "cb_can_message type size!!");

View File

@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
/**
* \file Test utilities used to determine that the used types
* have the same sizes independent of the platforms that we
* are using. Added to make sure that reinterpret_cast or other
* types of cast will yield the same types across platform
*/
#pragma once
CB_STATIC_ASSERT(sizeof(AppUDPResponse) == 4, "Wrong AppUDPReponse type size!");
CB_STATIC_ASSERT(sizeof(CbType) == 1, "Wrong CB type size!");
CB_STATIC_ASSERT(sizeof(CbStructType) == 2, "Wrong CB type size!");
CB_STATIC_ASSERT((sizeof(CbFirmwareStart) == 16 + 1 && sizeof(CbFirmwareStart) <= CB_MAX_CB_STRUCT_SIZE),
"Wrong CB type size!");
CB_STATIC_ASSERT((sizeof(CbFirmwarePacket) == 1 + 2 + 2 + 1024 && sizeof(CbFirmwarePacket) <= CB_MAX_CB_STRUCT_SIZE),
"Wrong CB type size!");
CB_STATIC_ASSERT((sizeof(CbFirmwareEnd) == 4 + 1 + (128 + 1) && sizeof(CbFirmwareEnd) <= CB_MAX_CB_STRUCT_SIZE),
"Wrong CB type size!");
CB_STATIC_ASSERT((sizeof(CbHeartbeatPacket) == 119 && sizeof(CbHeartbeatPacket) <= CB_MAX_CB_STRUCT_SIZE),
"Wrong CB type size!");

View File

@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
CB_STATIC_ASSERT(sizeof(struct evse_bsp_cb_to_host)== 11+4+4+2, "Wrong evse_bsp_cb_to_host size!!!");

View File

@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
CB_STATIC_ASSERT (sizeof(struct evse_bsp_host_to_cb) == 7+9+1+1+1, "Wrong evse_bsp_host_to_cb size!");

View File

@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/bsp_bridge.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <cstring>
#include <everest/io/udp/udp_payload.hpp>
#include <iostream>
#include <protocol/evse_bsp_cb_to_host.h>
#include <protocol/evse_bsp_host_to_cb.h>
namespace {
const int default_udp_timeout_ms = 1000;
}
namespace charge_bridge {
bsp_bridge::bsp_bridge(bsp_bridge_config const& config) :
m_api(config.api, config.cb + "/" + config.item), m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
using namespace std::chrono_literals;
m_timer.set_timeout(5s);
m_api.set_cb_tx([this](auto& data) {
everest::lib::io::udp::udp_payload pl;
pl.set_message(&data, sizeof(data));
m_udp.tx(pl);
});
m_udp.set_rx_handler([this](auto const& data, auto&) {
evse_bsp_cb_to_host msg;
std::memcpy(&msg, data.buffer.data(), data.size());
m_api.set_cb_message(msg);
});
auto identifier = config.cb + "/" + config.item;
m_udp.set_error_handler([this, identifier](auto id, auto const& msg) {
utilities::print_error(identifier, "BSP/UDP", id) << msg << std::endl;
m_udp_on_error = id not_eq 0;
});
}
void bsp_bridge::handle_timer_event() {
if (m_udp_on_error) {
m_udp.reset();
}
}
bool bsp_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
result = handler.register_event_handler(&m_api) && result;
result = handler.register_event_handler(&m_udp) && result;
result = handler.register_event_handler(&m_timer, [this](auto&) { handle_timer_event(); }) && result;
return result;
}
bool bsp_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
result = handler.unregister_event_handler(&m_api) && result;
result = handler.unregister_event_handler(&m_udp) && result;
result = handler.unregister_event_handler(&m_timer) && result;
return result;
}
} // namespace charge_bridge

View File

@@ -0,0 +1,162 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/can_bridge.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <chrono>
#include <cstring>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/netlink/vcan_netlink_manager.hpp>
#include <memory>
#include <protocol/cb_can_message.h>
namespace {
const int default_udp_timeout_ms = 1000;
}
namespace charge_bridge {
using namespace std::chrono_literals;
namespace {
void msg_cb_to_host(cb_can_message const& src, everest::lib::io::can::socket_can::ClientPayloadT& tar) {
tar.set_can_id_with_flags(src.can_id, src.can_flags & CanFlags_EFF, src.can_flags & CanFlags_RTR,
src.can_flags & CanFlags_ERR);
tar.len8_dlc = 0;
tar.payload.resize(src.dlc);
std::memcpy(tar.payload.data(), src.data, src.dlc);
}
void msg_host_to_cb(everest::lib::io::can::socket_can::ClientPayloadT const& src, cb_can_message& tar) {
tar = cb_can_message_set_zero;
tar.can_id = src.get_can_id();
tar.can_flags = 0;
if (src.eff_flag()) {
tar.can_flags |= CanFlags_EFF;
}
if (src.rtr_flag()) {
tar.can_flags |= CanFlags_RTR;
}
if (src.err_flag()) {
tar.can_flags |= CanFlags_ERR;
}
tar.dlc = std::min<uint8_t>(src.payload.size(), sizeof(tar.data));
std::memcpy(tar.data, src.payload.data(), src.payload.size());
}
bool is_data_msg([[maybe_unused]] cb_can_message const& msg) {
return true;
}
} // namespace
can_bridge::can_bridge(can_bridge_config const& config) :
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
m_can_device(config.can_device),
m_last_msg_to_cb(std::chrono::steady_clock::time_point()) {
auto& manager = everest::lib::io::netlink::vcan_netlink_manager::Instance();
auto success = manager.create(config.can_device) && manager.bring_up(config.can_device);
if (success) {
m_can = std::make_unique<everest::lib::io::can::socket_can>(config.can_device);
} else {
manager.destroy(config.can_device);
success = manager.create(config.can_device) && manager.bring_up(config.can_device);
if (success) {
m_can = std::make_unique<everest::lib::io::can::socket_can>(config.can_device);
} else {
manager.destroy(config.can_device);
throw std::runtime_error("Failed to setup virtual CAN device: " + config.can_device);
}
}
m_can->set_rx_handler([this](auto const& data, auto&) {
everest::lib::io::udp::udp_client::ClientPayloadT pl;
cb_can_message msg;
msg_host_to_cb(data, msg);
send_can_to_udp(msg);
});
m_udp.set_rx_handler([this](auto const& data, auto&) {
everest::lib::io::can::socket_can::ClientPayloadT pl;
cb_can_message msg;
std::memcpy(&msg, data.buffer.data(), sizeof(cb_can_message));
msg_cb_to_host(msg, pl);
if (is_data_msg(msg)) {
m_can->tx(pl);
}
});
m_identifier = config.cb + "/" + config.item;
m_can->set_error_handler([this](auto id, auto const& msg) {
utilities::print_error(m_identifier, "CAN/HW", id) << msg << std::endl;
if (id not_eq 0) {
// This is a smart pointer!! Using .reset() would delete the obj!
m_can->reset();
}
});
m_udp.set_error_handler([this](auto id, auto const& msg) {
utilities::print_error(m_identifier, "CAN/UDP", id) << msg << std::endl;
if (id not_eq 0) {
m_udp.reset();
}
});
m_heartbeat_timer.set_timeout(10s);
}
can_bridge::~can_bridge() {
auto& manager = everest::lib::io::netlink::vcan_netlink_manager::Instance();
if (m_can) {
m_can.reset();
manager.destroy(m_can_device);
}
}
bool can_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = handler.register_event_handler(m_can.get());
result = handler.register_event_handler(&m_udp) && result;
result = handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); }) && result;
if (result) {
handler.add_action([this]() { handle_heartbeat_timer(); });
}
return result;
}
bool can_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = handler.unregister_event_handler(m_can.get());
result = handler.unregister_event_handler(&m_udp) && result;
result = handler.unregister_event_handler(&m_heartbeat_timer) && result;
return result;
}
void can_bridge::send_can_to_udp(cb_can_message const& msg) {
everest::lib::io::udp::udp_client::ClientPayloadT udp_pl;
udp_pl.buffer.resize(sizeof(cb_can_message));
std::memcpy(udp_pl.buffer.data(), &msg, sizeof(cb_can_message));
m_udp.tx(udp_pl);
m_last_msg_to_cb = std::chrono::steady_clock::now();
}
void can_bridge::handle_heartbeat_timer() {
if (m_udp.on_error()) {
// If the connection is not available, retry soon and invalidate last hearbeat
m_heartbeat_timer.set_timeout(250ms);
m_last_msg_to_cb = std::chrono::steady_clock::time_point();
return;
} else {
// otherwise go back to regular interval
m_heartbeat_timer.set_timeout(10s);
}
auto delta = std::chrono::steady_clock::now() - m_last_msg_to_cb;
if (delta > 10s) {
cb_can_message msg = cb_can_message_set_zero;
msg.packet_type = CanPacketType_Keep_Alive;
send_can_to_udp(msg);
}
}
} // namespace charge_bridge

View File

@@ -0,0 +1,388 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include "protocol/cb_config.h"
#include <charge_bridge/charge_bridge.hpp>
#include <charge_bridge/discovery.hpp>
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
#include <charge_bridge/gpio_bridge.hpp>
#include <charge_bridge/heartbeat_service.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <charge_bridge/utilities/print_config.hpp>
#include <charge_bridge/utilities/string.hpp>
#include <charge_bridge/utilities/sync_udp_client.hpp>
#include <everest/io/event/fd_event_sync_interface.hpp>
#include <everest/io/netlink/vcan_netlink_manager.hpp>
#include <everest/util/misc/bind.hpp>
#include <iostream>
#include <memory>
#include <thread>
namespace charge_bridge {
namespace {
std::pair<bool, std::set<std::string>> make_interface_list(std::string const& str, std::string const& pattern) {
if (str == pattern) {
return {false, {}};
};
auto raw = utilities::string_after_pattern(str, pattern).substr(1);
if (raw.size() <= 2) {
return {false, {}};
}
auto exclude = raw.substr(0, 1) == "!";
auto items = utilities::csv_to_set(raw.substr(exclude ? 1 : 0));
for (auto const& elem : items) {
std::cout << elem << ", ";
}
std::cout << std::endl;
return {exclude, items};
}
} // namespace
charge_bridge::charge_bridge(charge_bridge_config const& config) : m_config(config) {
if (utilities::string_starts_with(config.cb_remote, "ANY_EVSE")) {
auto params = make_interface_list(config.cb_remote, "ANY_EVSE");
init_discovery(discovery_device_type::CB_EVSE, params.second, params.first);
} else if (utilities::string_starts_with(config.cb_remote, "ANY_EV")) {
auto params = make_interface_list(config.cb_remote, "ANY_EV");
init_discovery(discovery_device_type::CB_EV, params.second, params.first);
} else {
init();
}
}
void charge_bridge::init_discovery(discovery_device_type type, std::set<std::string> const& interfaces,
bool excluding) {
using namespace everest::lib::util;
utilities::print_error(m_config.cb_name, "DISCOVERY", -1) << "Discovery pending" << std::endl;
m_discovery = std::make_unique<discovery>(type, interfaces, excluding);
m_discovery->set_discovery_callback(bind_obj(&charge_bridge::handle_discovery, this));
{
auto handle = m_cb_status.handle();
handle->discovery_pending = true;
}
m_cb_status.notify_one();
}
void charge_bridge::handle_discovery(std::string const& ip) {
utilities::print_error(m_config.cb_name, "DISCOVERY", 0) << "Discovered at: " + ip << std::endl;
m_config.cb_remote = ip;
if (m_config.can0) {
m_config.can0->cb_remote = ip;
}
if (m_config.serial1) {
m_config.serial1->cb_remote = ip;
}
if (m_config.serial2) {
m_config.serial2->cb_remote = ip;
}
if (m_config.serial3) {
m_config.serial3->cb_remote = ip;
}
if (m_config.plc) {
m_config.plc->cb_remote = ip;
}
if (m_config.bsp) {
m_config.bsp->cb_remote = ip;
}
if (m_config.heartbeat) {
m_config.heartbeat->cb_remote = ip;
}
if (m_config.gpio) {
m_config.gpio->cb_remote = ip;
}
m_config.firmware.cb_remote = ip;
m_event_handler->add_action([this]() {
std::unique_ptr<discovery> tmp;
std::swap(m_discovery, tmp);
init();
{
auto handle = m_cb_status.handle();
handle->discovery_pending = false;
}
m_cb_status.notify_one();
});
}
void charge_bridge::init() {
if (m_config.can0.has_value()) {
m_can_0_client = std::make_unique<can_bridge>(m_config.can0.value());
}
if (m_config.serial1.has_value()) {
m_pty_1 = std::make_unique<serial_bridge>(m_config.serial1.value());
}
if (m_config.serial2.has_value()) {
m_pty_2 = std::make_unique<serial_bridge>(m_config.serial2.value());
}
if (m_config.serial3.has_value()) {
m_pty_3 = std::make_unique<serial_bridge>(m_config.serial3.value());
}
if (m_config.plc.has_value()) {
m_plc = std::make_unique<plc_bridge>(m_config.plc.value());
}
if (m_config.bsp.has_value()) {
m_bsp = std::make_unique<bsp_bridge>(m_config.bsp.value());
}
if (m_config.heartbeat.has_value()) {
m_heartbeat = std::make_unique<heartbeat_service>(m_config.heartbeat.value(), [this](bool connected) {
{
auto handle = m_cb_status.handle();
handle->is_connected = connected;
}
m_cb_status.notify_one();
});
}
if (m_config.gpio.has_value()) {
m_gpio = std::make_unique<gpio_bridge>(m_config.gpio.value());
}
}
charge_bridge::~charge_bridge() {
m_cb_status.notify_one();
}
void charge_bridge::manage(everest::lib::io::event::fd_event_handler& handler, std::atomic_bool const& run,
bool force_update) {
using namespace std::chrono_literals;
m_event_handler = &handler;
m_force_firmware_update = force_update;
auto action = [this](bool is_connected, bool discovery_pending, int& error_count) {
if (discovery_pending) {
if (m_discovery_active) {
return;
}
m_discovery_active = true;
m_event_handler->add_action([this]() { register_events(*m_event_handler); });
return;
}
if (m_was_connected and not is_connected) {
if (error_count > 1) {
m_event_handler->add_action([this]() { unregister_events(*m_event_handler); });
m_was_connected = false;
} else {
error_count++;
}
}
if (not m_was_connected) {
if (update_firmware(m_force_firmware_update)) {
m_event_handler->add_action([this]() { register_events(*m_event_handler); });
m_was_connected = true;
error_count = 0;
}
}
};
std::thread manager([&run, action, this]() {
auto handle = m_cb_status.handle();
bool last_is_connected = handle->is_connected;
bool last_discovery_pending = handle->discovery_pending;
int error_count = 0;
auto condition = [&] {
if (handle->is_connected not_eq last_is_connected) {
return true;
}
if (handle->discovery_pending not_eq last_discovery_pending) {
return true;
}
if (not run.load()) {
return true;
}
return false;
};
while (run.load()) {
action(handle->is_connected, handle->discovery_pending, error_count);
handle.wait_for(condition, 10s);
last_is_connected = handle->is_connected;
last_discovery_pending = handle->discovery_pending;
}
});
manager.detach();
}
bool charge_bridge::update_firmware(bool force) {
firmware_update::sync_fw_updater updater(m_config.firmware);
auto is_connected = updater.quick_check_connection();
if (not is_connected) {
return false;
}
updater.print_fw_version();
auto do_update = force or (m_config.firmware.fw_update_on_start and not updater.check_if_correct_fw_installed());
if (not do_update) {
return true;
}
auto result = updater.upload_fw() && updater.check_connection();
if (not result) {
std::cout << "Error: could not install correct firmware version" << std::endl;
}
return result;
}
std::string charge_bridge::get_pty_1_slave_path() {
if (m_pty_1) {
return m_pty_1->get_slave_path();
}
return "";
}
std::string charge_bridge::get_pty_2_slave_path() {
if (m_pty_2) {
return m_pty_2->get_slave_path();
}
return "";
}
std::string charge_bridge::get_pty_3_slave_path() {
if (m_pty_3) {
return m_pty_3->get_slave_path();
}
return "";
}
bool charge_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
if (m_can_0_client) {
result = handler.register_event_handler(m_can_0_client.get()) && result;
}
if (m_pty_1) {
result = handler.register_event_handler(m_pty_1.get()) && result;
}
if (m_pty_2) {
result = handler.register_event_handler(m_pty_2.get()) && result;
}
if (m_pty_3) {
result = handler.register_event_handler(m_pty_3.get()) && result;
}
if (m_bsp) {
result = handler.register_event_handler(m_bsp.get()) && result;
}
if (m_plc) {
result = handler.register_event_handler(m_plc.get()) && result;
}
if (m_heartbeat) {
result = handler.register_event_handler(m_heartbeat.get()) && result;
}
if (m_gpio) {
result = handler.register_event_handler(m_gpio.get()) && result;
}
if (m_discovery) {
result = handler.register_event_handler(m_discovery.get()) && result;
}
return result;
}
bool charge_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
if (m_can_0_client) {
result = handler.unregister_event_handler(m_can_0_client.get()) && result;
}
if (m_pty_1) {
result = handler.unregister_event_handler(m_pty_1.get()) && result;
}
if (m_pty_2) {
result = handler.unregister_event_handler(m_pty_2.get()) && result;
}
if (m_pty_3) {
result = handler.unregister_event_handler(m_pty_3.get()) && result;
}
if (m_bsp) {
result = handler.unregister_event_handler(m_bsp.get()) && result;
}
if (m_plc) {
result = handler.unregister_event_handler(m_plc.get()) && result;
}
if (m_heartbeat) {
result = handler.unregister_event_handler(m_heartbeat.get()) && result;
}
if (m_gpio) {
result = handler.unregister_event_handler(m_gpio.get()) && result;
}
if (m_discovery) {
result = handler.unregister_event_handler(m_discovery.get()) && result;
}
return result;
}
void charge_bridge::print_config() {
print_charge_bridge_config(m_config);
}
void print_charge_bridge_config(charge_bridge_config const& c) {
using namespace utilities;
std::cout << "ChargeBridge: " << c.cb_name << std::endl;
std::cout << " * remote: " << c.cb_remote << std::endl;
if (c.serial1) {
std::cout << " * serial 1: " << c.serial1->serial_device;
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 1) {
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[0]);
}
std::cout << std::endl;
}
if (c.serial2) {
std::cout << " * serial 2: " << c.serial2->serial_device;
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 2) {
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[1]);
}
std::cout << std::endl;
}
if (c.serial3) {
std::cout << " * serial 3: " << c.serial3->serial_device;
if (c.heartbeat.has_value() && CB_NUMBER_OF_UARTS >= 3) {
std::cout << " " << to_string(c.heartbeat->cb_config.uarts[2]);
}
std::cout << std::endl;
}
if (c.can0) {
std::cout << " * can 0: " << c.can0->can_device;
if (c.heartbeat.has_value()) {
std::cout << " " << to_string(c.heartbeat->cb_config.can.baudrate) << "bps" << std::endl;
}
}
if (c.plc) {
std::cout << " * plc: " << c.plc->plc_tap << std::flush;
std::cout << " " << c.cb_remote << ":" << c.plc->cb_port;
std::cout << " adress " << c.plc->plc_ip;
std::cout << " netmask " << c.plc->plc_netmaks;
std::cout << " MTU " << c.plc->plc_mtu << std::endl;
}
if (c.bsp) {
if (c.bsp->api.evse.enabled) {
std::cout << " * evse_bsp: ";
} else if (c.bsp->api.ev.enabled) {
std::cout << " * ev_bsp: ";
}
std::cout << c.bsp->cb_remote << ":" << c.bsp->cb_port;
std::cout << " module " << c.bsp->api.evse.module_id;
std::cout << " MQTT " << c.bsp->api.mqtt_remote << ":" << c.bsp->api.mqtt_port;
if (not c.bsp->api.mqtt_bind.empty()) {
std::cout << " on " << c.bsp->api.mqtt_bind;
}
std::cout << " ping " << c.bsp->api.mqtt_ping_interval_ms << "ms";
if (c.bsp->api.ovm.enabled) {
std::cout << " OVM module " << c.bsp->api.ovm.module_id;
}
std::cout << std::endl;
}
if (c.heartbeat) {
std::cout << " * heartbeat: " << c.cb_remote << ":" << c.cb_port;
std::cout << " heartbeat interval " << c.heartbeat->interval_s << "s" << std::endl;
}
if (c.gpio) {
std::cout << " * gpio: " << c.cb_remote << ":" << c.cb_port;
std::cout << " MQTT " << c.gpio->mqtt_remote << ":" << c.gpio->mqtt_port;
if (not c.gpio->mqtt_bind.empty()) {
std::cout << " on " << c.gpio->mqtt_bind;
}
std::cout << " send interval " << c.gpio->interval_s << "s" << std::endl;
}
std::cout << "\n" << std::endl;
}
} // namespace charge_bridge

View File

@@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include "charge_bridge/utilities/string.hpp"
#include <charge_bridge/discovery.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <everest/io/event/fd_event_handler.hpp>
#include <type_traits>
namespace charge_bridge {
namespace {
std::string to_string(discovery_device_type val) {
switch (val) {
case discovery_device_type::CB_EV:
return "CB-CCS-EV-LU";
case discovery_device_type::CB_EVSE:
return "CB-CCS-EVSE-LU";
default:
return "INVALID";
}
}
bool is_cb_match(std::string const& board_type, discovery_device_type discriminator) {
auto result = board_type == to_string(discriminator);
return result;
}
} // namespace
const std::string discovery::discovery_id = "_chargebridge._udp.local";
discovery::discovery(discovery_device_type type) : m_type(type) {
using namespace std::chrono_literals;
m_timer.set_timeout(1s);
for (auto const& item : everest::lib::io::socket::get_all_interaces()) {
add_client(item.name);
}
}
discovery::discovery(discovery_device_type type, std::set<std::string> const& interfaces, bool excluding) :
m_type(type) {
using namespace std::chrono_literals;
m_timer.set_timeout(1s);
for (auto const& item : everest::lib::io::socket::get_all_interaces()) {
if (not interfaces.empty()) {
if (interfaces.count(item.name) == 1 and excluding) {
continue;
}
if (interfaces.count(item.name) == 0 and not excluding) {
continue;
}
}
std::cout << " using interface: " << item.name << std::endl;
add_client(item.name);
}
}
void discovery::add_client(std::string const& interface) {
auto client = std::make_unique<everest::lib::io::mdns::mdns_client>(interface);
client->set_rx_handler([&](auto const& data, auto&) {
auto discovery = everest::lib::io::mdns::parse_mdns_packet(data.buffer);
if (discovery.has_value()) {
if (m_registry.update(discovery.value())) {
query_registry();
}
}
});
m_mdns.push_back(std::move(client));
}
void discovery::query_registry() {
auto obj = m_registry.get();
for (auto const& [key, value] : obj) {
if (not utilities::string_ends_with(key, discovery_id)) {
continue;
}
if (not value.txt.count("board_type") or not is_cb_match(value.txt.at("board_type"), m_type)) {
continue;
}
if (not m_on_discover) {
continue;
}
m_on_discover(value.ip);
return;
}
}
void discovery::set_discovery_callback(discovery_cb const& cb) {
m_on_discover = cb;
}
bool discovery::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
for (auto& item : m_mdns) {
if (item) {
result = handler.register_event_handler(item.get()) && result;
}
}
handler.register_event_handler(&m_timer, [&](auto) {
for (auto& item : m_mdns) {
item->get_raw_handler()->query(discovery_id);
}
});
return result;
}
bool discovery::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
for (auto& item : m_mdns) {
if (item) {
result = handler.unregister_event_handler(item.get()) && result;
}
}
handler.unregister_event_handler(&m_timer);
return result;
}
} // namespace charge_bridge

View File

@@ -0,0 +1,214 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "charge_bridge/utilities/string.hpp"
#include "everest/io/mqtt/mosquitto_cpp.hpp"
#include "protocol/evse_bsp_cb_to_host.h"
#include <charge_bridge/everest_api/api_connector.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <cstring>
#include <exception>
#include <stdexcept>
using namespace std::chrono_literals;
namespace mqtt = everest::lib::io::mqtt;
namespace {
const int mqtt_reconnect_to_ms = 1000;
}
namespace charge_bridge::evse_bsp {
api_connector::api_connector(everest_api_config const& config, std::string const& cb_identifier) :
m_cb_identifier(cb_identifier),
m_mqtt(mqtt_reconnect_to_ms),
m_evse_bsp(config.evse, cb_identifier, m_host_status),
m_ovm(config.ovm, cb_identifier, m_host_status),
m_ev_bsp(config.ev, cb_identifier, m_host_status) {
everest::lib::API::Topics api_topics;
m_evse_bsp_enabled = config.evse.enabled;
m_ovm_enabled = config.ovm.enabled;
m_ev_bsp_enabled = config.ev.enabled;
if (m_evse_bsp_enabled && m_ev_bsp_enabled) {
throw std::runtime_error("Configuration error: Cannot enable EV and EVSE BSP at the same time");
}
utilities::print_error(m_cb_identifier, "BSP/CB", 0) << "ChargeBridge connected." << std::endl;
if (m_evse_bsp_enabled) {
api_topics.setup(config.evse.module_id, "evse_board_support", 1);
m_evse_bsp_receive_topic = api_topics.everest_to_extern("");
m_evse_bsp_send_topic = api_topics.extern_to_everest("");
m_evse_bsp.set_mqtt_tx(
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_evse_bsp_send_topic + topic, payload); });
}
if (m_ovm_enabled) {
api_topics.setup(config.ovm.module_id, "over_voltage_monitor", 1);
m_ovm_receive_topic = api_topics.everest_to_extern("");
m_ovm_send_topic = api_topics.extern_to_everest("");
m_ovm.set_mqtt_tx(
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_ovm_send_topic + topic, payload); });
}
if (m_ev_bsp_enabled) {
api_topics.setup(config.ev.module_id, "ev_board_support", 1);
m_ev_bsp_receive_topic = api_topics.everest_to_extern("");
m_ev_bsp_send_topic = api_topics.extern_to_everest("");
m_ev_bsp.set_mqtt_tx(
[this](auto const& topic, auto const& payload) { m_mqtt.publish(m_ev_bsp_send_topic + topic, payload); });
}
m_mqtt.set_error_handler([this](int code, std::string const& msg) {
auto is_error = code == 0 ? 0 : 1;
utilities::print_error(m_cb_identifier, "BSP/MQTT", is_error) << msg << std::endl;
});
m_mqtt.set_callback_connect([this, config](auto&, auto, auto, auto const&) { handle_mqtt_connect(); });
m_mqtt.connect(config.mqtt_bind, config.mqtt_remote, config.mqtt_port, config.mqtt_ping_interval_ms);
m_sync_timer.set_timeout(1s);
std::memset(&m_host_status, 0, sizeof(m_host_status));
}
bool api_connector::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
if (m_evse_bsp_enabled) {
result = handler.register_event_handler(&m_evse_bsp) && result;
}
if (m_ovm_enabled) {
result = handler.register_event_handler(&m_ovm) && result;
}
if (m_ev_bsp_enabled) {
result = handler.register_event_handler(&m_ev_bsp) && result;
}
result = handler.register_event_handler(&m_mqtt) && result;
result = handler.register_event_handler(&m_sync_timer, [this](auto&) {
if (m_evse_bsp_enabled) {
m_evse_bsp.sync(m_cb_connected);
}
if (m_ovm_enabled) {
m_ovm.sync(m_cb_connected);
}
if (m_ev_bsp_enabled) {
m_ev_bsp.sync(m_cb_connected);
}
handle_cb_connection_state();
}) && result;
return result;
}
bool api_connector::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
if (m_evse_bsp_enabled) {
result = handler.unregister_event_handler(&m_evse_bsp) && result;
}
if (m_ovm_enabled) {
result = handler.unregister_event_handler(&m_ovm) && result;
}
if (m_ev_bsp_enabled) {
result = handler.unregister_event_handler(&m_ev_bsp) && result;
}
result = handler.unregister_event_handler(&m_mqtt) && result;
result = handler.unregister_event_handler(&m_sync_timer) && result;
return result;
}
void api_connector::set_cb_tx(tx_ftor const& handler) {
m_tx = handler;
m_evse_bsp.set_cb_tx(handler);
m_ev_bsp.set_cb_tx(handler);
}
void api_connector::set_cb_message(evse_bsp_cb_to_host const& msg) {
m_last_cb_heartbeat = std::chrono::steady_clock::now();
if (m_evse_bsp_enabled) {
m_evse_bsp.set_cb_message(msg);
}
if (m_ev_bsp_enabled) {
m_ev_bsp.set_cb_message(msg);
}
if (m_ovm_enabled) {
m_ovm.set_cb_message(msg);
}
}
bool api_connector::check_cb_heartbeat() {
if (m_last_cb_heartbeat == std::chrono::steady_clock::time_point::max()) {
return false;
}
return std::chrono::steady_clock::now() - m_last_cb_heartbeat < 2s;
}
void api_connector::handle_mqtt_connect() {
if (m_evse_bsp_enabled) {
m_mqtt.subscribe(m_evse_bsp_receive_topic + "#",
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
auto operation = utilities::string_after_pattern(topic, m_evse_bsp_receive_topic);
if (not operation.empty()) {
m_evse_bsp.dispatch(operation, static_cast<std::string>(payload));
}
});
}
if (m_ovm_enabled) {
m_mqtt.subscribe(m_ovm_receive_topic + "#",
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
auto operation = utilities::string_after_pattern(topic, m_ovm_receive_topic);
if (not operation.empty()) {
m_ovm.dispatch(operation, static_cast<std::string>(payload));
}
});
}
if (m_ev_bsp_enabled) {
m_mqtt.subscribe(m_ev_bsp_receive_topic + "#",
[this](auto&, auto const& topic, auto const& payload, auto, auto const&) {
auto operation = utilities::string_after_pattern(topic, m_ev_bsp_receive_topic);
if (not operation.empty()) {
m_ev_bsp.dispatch(operation, static_cast<std::string>(payload));
}
});
}
}
void api_connector::handle_cb_connection_state() {
m_tx(m_host_status);
auto current = check_cb_heartbeat();
auto handle_status = [this](bool status) {
if (status) {
utilities::print_error(m_cb_identifier, "BSP/CB", 0) << "ChargeBridge connected." << std::endl;
if (m_evse_bsp_enabled) {
m_evse_bsp.clear_comm_fault();
}
if (m_ovm_enabled) {
m_ovm.clear_comm_fault();
}
if (m_ev_bsp_enabled) {
m_ev_bsp.clear_comm_fault();
}
} else {
if (m_evse_bsp_enabled) {
m_evse_bsp.raise_comm_fault();
}
if (m_ovm_enabled) {
m_ovm.raise_comm_fault();
}
if (m_ev_bsp_enabled) {
m_ev_bsp.raise_comm_fault();
}
utilities::print_error(m_cb_identifier, "BSP/CB", 1) << "Waiting for ChargeBridge...." << std::endl;
}
};
if (m_cb_initial_comm_check) {
handle_status(current);
m_cb_initial_comm_check = false;
}
if (m_cb_connected != current) {
handle_status(not m_cb_connected);
}
m_cb_connected = current;
}
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,418 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include "protocol/cb_common.h"
#include "protocol/evse_bsp_cb_to_host.h"
#include <charge_bridge/everest_api/ev_bsp_api.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <charge_bridge/utilities/string.hpp>
#include <chrono>
#include <cstring>
#include <everest_api_types/ev_board_support/codec.hpp>
#include <everest_api_types/evse_board_support/codec.hpp>
#include <everest_api_types/generic/codec.hpp>
#include <everest_api_types/utilities/codec.hpp>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
using namespace std::chrono_literals;
using namespace everest::lib::API::V1_0::types::generic;
using namespace everest::lib::API;
namespace charge_bridge::evse_bsp {
ev_bsp_api::ev_bsp_api([[maybe_unused]] evse_ev_bsp_config const& config, std::string const& cb_identifier,
evse_bsp_host_to_cb& host_status) :
host_status(host_status), m_cb_identifier(cb_identifier) {
last_everest_heartbeat = std::chrono::steady_clock::time_point();
}
void ev_bsp_api::sync(bool cb_connected) {
m_cb_connected = cb_connected;
handle_everest_connection_state();
}
bool ev_bsp_api::register_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
return true;
}
bool ev_bsp_api::unregister_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
return true;
}
void ev_bsp_api::set_cb_tx(tx_ftor const& handler) {
m_tx = handler;
}
void ev_bsp_api::tx(evse_bsp_host_to_cb const& msg) {
if (m_tx) {
m_tx(msg);
}
}
void ev_bsp_api::set_mqtt_tx(mqtt_ftor const& handler) {
m_mqtt_tx = handler;
}
void ev_bsp_api::send_bsp_event(API_EVSE_BSP::Event data) {
API_EVSE_BSP::BspEvent event{data};
send_mqtt("bsp_event", serialize(event));
}
void ev_bsp_api::send_bsp_measurement(API_EV_BSP::BspMeasurement data) {
API_EV_BSP::BspMeasurement measurement{data};
send_mqtt("bsp_measurement", serialize(measurement));
}
void ev_bsp_api::handle_event_relay(std::uint8_t relay) {
using bc_event = API_EVSE_BSP::Event;
bc_event relaise_event;
bool relaise_state_valid = true;
switch (relay) {
case RelaiseState::RelayState_Open:
relaise_event = bc_event::PowerOff;
break;
case RelaiseState::RelayState_Closed:
relaise_event = bc_event::PowerOn;
break;
default:
relaise_state_valid = false;
}
if (relaise_state_valid) {
send_bsp_event(relaise_event);
}
}
void ev_bsp_api::handle_event_cp(std::uint8_t cp) {
using bc_event = API_EVSE_BSP::Event;
bc_event cp_event;
bool cp_state_valid = true;
switch (cp) {
case CpState_A:
cp_event = bc_event::A;
break;
case CpState_B:
cp_event = bc_event::B;
break;
case CpState_C:
cp_event = bc_event::C;
break;
case CpState_D:
cp_event = bc_event::D;
break;
case CpState_E:
cp_event = bc_event::Disconnected;
break;
case CpState_F:
cp_event = bc_event::F;
break;
case CpState_DF:
cp_event = bc_event::E;
break;
case CpState::CpState_INVALID:
cp_event = bc_event::E;
break;
default:
cp_state_valid = false;
}
if (cp_state_valid) {
last_cp_event = cp_event;
send_bsp_event(cp_event);
}
}
void ev_bsp_api::handle_bsp_measurement(uint16_t cp, [[maybe_unused]] uint8_t pp_1, [[maybe_unused]] uint8_t pp2) {
// FIXME implement PP correctly
API_EV_BSP::BspMeasurement data;
data.cp_pwm_duty_cycle = cp / 65536. * 100.;
API_EVSE_BSP::ProximityPilot pp;
API_EVSE_BSP::Ampacity amp;
amp = API_EVSE_BSP::Ampacity::None;
pp.ampacity = amp;
data.proximity_pilot = pp;
send_bsp_measurement(data);
}
inline static bool operator==(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
return a.raw == b.raw;
}
inline static bool operator!=(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
return a.raw != b.raw;
}
void ev_bsp_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
if (m_cb_status.cp_state not_eq msg.cp_state) {
handle_event_cp(msg.cp_state);
}
if (m_cb_status.relay_state != msg.relay_state) {
handle_event_relay(msg.relay_state);
}
if (m_cb_status.cp_duty_cycle not_eq msg.cp_duty_cycle or m_cb_status.pp_state_type1 not_eq msg.pp_state_type1 or
m_cb_status.pp_state_type2 not_eq msg.pp_state_type2) {
handle_bsp_measurement(msg.cp_duty_cycle, m_cb_status.pp_state_type1, m_cb_status.pp_state_type2);
}
if (m_cb_status.error_flags not_eq msg.error_flags) {
handle_error(msg.error_flags);
}
// This is not supported in EVerest yet but should be added at some point
/*
if (cb_status.stop_charging not_eq msg.stop_charging) {
handle_stop_button(msg.stop_charging);
}*/
// The ev_board_support interface in EVerest does not yet have proper errors defined, so we do not handle errors
// here yet
/*
if (m_cb_status.error_flags not_eq msg.error_flags) {
handle_error(msg.error_flags);
}*/
m_cb_status = msg;
}
enum class SafetyErrorMask : std::uint32_t {
cp_not_state_c = (1 << 0),
pwm_not_enabled = (1 << 1),
pp_invalid = (1 << 2),
plug_temperature_too_high = (1 << 3),
internal_temperature_too_high = (1 << 4),
emergency_input_latched = (1 << 5),
relay_health_latched = (1 << 6),
vdd_3v3_out_of_range = (1 << 7),
vdd_core_out_of_range = (1 << 8),
vdd_12V_out_of_range = (1 << 9),
vdd_N12V_out_of_range = (1 << 10),
vdd_refint_out_of_range = (1 << 11),
external_allow_power_on = (1 << 12),
config_mem_error = (1 << 13),
dc_hv_ov = (1 << 14),
};
// Table that maps a mask to our API error + message
struct FlagSpec {
SafetyErrorMask mask;
const char* message;
};
static constexpr FlagSpec error_specs[] = {
{SafetyErrorMask::pp_invalid, "PP invalid"},
{SafetyErrorMask::plug_temperature_too_high, "Plug temperature too high"},
{SafetyErrorMask::internal_temperature_too_high, "ChargeBridge internal over temperature"},
{SafetyErrorMask::emergency_input_latched, "Emergency input latched"},
{SafetyErrorMask::relay_health_latched, "Relay welded error"},
{SafetyErrorMask::vdd_3v3_out_of_range, "Supply voltage 3.3V out of range"},
{SafetyErrorMask::vdd_core_out_of_range, "Internal supply core voltage out of range"},
{SafetyErrorMask::vdd_12V_out_of_range, "Internal supply 12V voltage out of range"},
{SafetyErrorMask::vdd_N12V_out_of_range, "Internal supply -12V voltage out of range"},
{SafetyErrorMask::vdd_refint_out_of_range, "Internal supply VREF voltage out of range"},
{SafetyErrorMask::config_mem_error, "Internal config memory error"},
{SafetyErrorMask::dc_hv_ov, "DC HV OVM. FIXME: This should be on OVM not EVSE interface"},
};
static constexpr FlagSpec print_warning_specs[] = {
{SafetyErrorMask::cp_not_state_c, "CP is not state C"},
{SafetyErrorMask::pwm_not_enabled, "PWM not enabled"},
{SafetyErrorMask::external_allow_power_on, "Allow power on from EVerest missing"},
};
void ev_bsp_api::handle_error(const SafetyErrorFlags& data) {
std::uint32_t next = data.raw; // current raw value
std::stringstream log;
for (const auto& s : print_warning_specs) {
if (next & static_cast<std::uint32_t>(s.mask)) {
log << "[" << s.message << "] ";
}
}
for (const auto& s : error_specs) {
if (next & static_cast<std::uint32_t>(s.mask)) {
log << "[" << s.message << "] ";
}
}
if (m_everest_connected && m_cb_connected) {
if (log.str().empty()) {
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "Relays can be switched on." << std::endl;
} else {
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "Relays off due to:" << log.str() << std::endl;
}
}
}
void ev_bsp_api::dispatch(std::string const& operation, std::string const& payload) {
if (operation == "enable") {
receive_enable(payload);
} else if (operation == "set_cp_state") {
receive_set_cp_state(payload);
} else if (operation == "allow_power_on") {
receive_allow_power_on(payload);
} else if (operation == "diode_fail") {
receive_diode_fail(payload);
} else if (operation == "set_ac_max_current") {
receive_set_ac_max_current(payload);
} else if (operation == "set_three_phases") {
receive_set_three_phases(payload);
} else if (operation == "set_rcd_error") {
receive_set_rcd_error(payload);
} else if (operation == "heartbeat") {
receive_heartbeat(payload);
} else {
std::cerr << "ev_bsp_api: RECEIVE invalid operation: " << operation << std::endl;
}
}
void ev_bsp_api::raise_comm_fault() {
send_raise_error(API_GENERIC::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
}
void ev_bsp_api::clear_comm_fault() {
send_clear_error(API_GENERIC::ErrorEnum::CommunicationFault, "ChargeBridge not available");
}
void ev_bsp_api::receive_enable([[maybe_unused]] std::string const& payload) {
// Not implemented
}
static CpState evcpstate_to_cpstate(API_EV_BSP::EvCpState s) {
switch (s) {
case API_EV_BSP::EvCpState::A:
return CpState::CpState_A;
case API_EV_BSP::EvCpState::B:
return CpState::CpState_B;
case API_EV_BSP::EvCpState::C:
return CpState::CpState_C;
case API_EV_BSP::EvCpState::D:
return CpState::CpState_D;
case API_EV_BSP::EvCpState::E:
return CpState::CpState_E;
default:
return CpState::CpState_INVALID;
}
}
void ev_bsp_api::receive_set_cp_state(std::string const& payload) {
API_EV_BSP::EvCpState cp;
if (everest::lib::API::deserialize(payload, cp)) {
host_status.ev_set_cp_state = evcpstate_to_cpstate(cp);
tx(host_status);
} else {
std::cerr << "ev_bsp_api::receive_set_cp_state: payload invalid -> " << payload << std::endl;
}
}
void ev_bsp_api::receive_allow_power_on(std::string const& payload) {
bool on;
if (everest::lib::API::deserialize(payload, on)) {
host_status.allow_power_on = static_cast<std::uint8_t>(on);
tx(host_status);
} else {
std::cerr << "ev_bsp_api::receive_allow_power_on: payload invalid -> " << payload << std::endl;
}
}
void ev_bsp_api::receive_diode_fail(std::string const& payload) {
bool on;
if (everest::lib::API::deserialize(payload, on)) {
host_status.ev_set_diodefault = static_cast<std::uint8_t>(on);
tx(host_status);
} else {
std::cerr << "ev_bsp_api::receive_diode_fail: payload invalid -> " << payload << std::endl;
}
}
void ev_bsp_api::receive_set_ac_max_current([[maybe_unused]] std::string const& payload) {
// Not implemented
}
void ev_bsp_api::receive_set_three_phases([[maybe_unused]] std::string const& payload) {
// Not implemented
}
void ev_bsp_api::receive_set_rcd_error([[maybe_unused]] std::string const& payload) {
// Not implemented
}
void ev_bsp_api::receive_heartbeat(std::string const& pl) {
last_everest_heartbeat = std::chrono::steady_clock::now();
std::size_t id = 0;
if (deserialize(pl, id)) {
auto delta = id - m_last_hb_id;
if (delta > 1) {
utilities::print_error(m_cb_identifier, "EV_BSP/EVEREST", -1)
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
}
m_last_hb_id = id;
} else {
utilities::print_error(m_cb_identifier, "EV_BSP/EVEREST", -1)
<< "EVerest invalid heartbeat message: " << pl << std::endl;
}
}
void ev_bsp_api::send_communication_check() {
send_mqtt("communication_check", serialize(true));
}
void ev_bsp_api::send_mqtt(std::string const& topic, std::string const& message) {
if (m_mqtt_tx) {
m_mqtt_tx(topic, message);
}
}
bool ev_bsp_api::check_everest_heartbeat() {
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
}
void ev_bsp_api::handle_everest_connection_state() {
send_communication_check();
auto current = check_everest_heartbeat();
auto handle_status = [this](bool status) {
if (status) {
utilities::print_error(m_cb_identifier, "EV/EVEREST", 0) << "EVerest connected" << std::endl;
// re-send last CP state event
send_bsp_event(last_cp_event);
} else {
utilities::print_error(m_cb_identifier, "EV/EVEREST", 1) << "Waiting for EVerest..." << std::endl;
// unplug CP if EVerest disconnects
host_status.ev_set_cp_state = CpState_A;
tx(host_status);
}
};
if (m_bc_initial_comm_check) {
handle_status(current);
m_bc_initial_comm_check = false;
} else if (m_everest_connected != current) {
handle_status(not m_everest_connected);
}
m_everest_connected = current;
}
void ev_bsp_api::send_raise_error(API_GENERIC::ErrorEnum error, std::string const& subtype, std::string const& msg) {
API_GENERIC::Error error_msg;
error_msg.type = error;
error_msg.sub_type = subtype;
error_msg.message = msg;
send_mqtt("raise_error", serialize(error_msg));
}
void ev_bsp_api::send_clear_error(API_GENERIC::ErrorEnum error, std::string const& subtype) {
API_GENERIC::Error error_msg;
error_msg.type = error;
error_msg.sub_type = subtype;
send_mqtt("clear_error", serialize(error_msg));
}
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,514 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "protocol/evse_bsp_cb_to_host.h"
#include <charge_bridge/everest_api/evse_bsp_api.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <charge_bridge/utilities/string.hpp>
#include <chrono>
#include <cstring>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest_api_types/evse_board_support/API.hpp>
#include <everest_api_types/evse_board_support/codec.hpp>
#include <everest_api_types/evse_manager/codec.hpp>
#include <everest_api_types/generic/codec.hpp>
#include <everest_api_types/utilities/codec.hpp>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
using namespace std::chrono_literals;
using namespace everest::lib::API::V1_0::types::generic;
using namespace everest::lib::API;
namespace charge_bridge::evse_bsp {
evse_bsp_api::evse_bsp_api(evse_bsp_config const& config, std::string const& cb_identifier,
evse_bsp_host_to_cb& host_status) :
host_status(host_status), m_capabilities(config.capabilities), m_cb_identifier(cb_identifier) {
last_everest_heartbeat = std::chrono::steady_clock::time_point();
m_capabilities_timer.set_timeout(10s);
std::memset(&cb_status, 0, sizeof(cb_status));
m_enabled = true;
}
void evse_bsp_api::sync(bool cb_connected) {
m_cb_connected = cb_connected;
handle_everest_connection_state();
}
bool evse_bsp_api::register_events(everest::lib::io::event::fd_event_handler& handler) {
// clang-format off
return
handler.register_event_handler(&m_capabilities_timer, [this](auto&) {
send_capabilities();
});
// clang-format on
}
bool evse_bsp_api::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
// clang-format off
return
handler.unregister_event_handler(&m_capabilities_timer);
// clang-format on
}
void evse_bsp_api::set_cb_tx(tx_ftor const& handler) {
m_tx = handler;
}
void evse_bsp_api::tx(evse_bsp_host_to_cb const& msg) {
if (m_tx) {
m_tx(msg);
}
}
void evse_bsp_api::set_mqtt_tx(mqtt_ftor const& handler) {
m_mqtt_tx = handler;
}
inline static bool operator==(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
return a.raw == b.raw;
}
inline static bool operator!=(const SafetyErrorFlags& a, const SafetyErrorFlags& b) {
return a.raw != b.raw;
}
void evse_bsp_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
if (cb_status.reset_reason not_eq msg.reset_reason) {
}
if (cb_status.cp_state not_eq msg.cp_state) {
handle_event_cp(msg.cp_state);
}
if (cb_status.relay_state != msg.relay_state) {
handle_event_relay(msg.relay_state);
}
if (cb_status.error_flags not_eq msg.error_flags) {
handle_error(msg.error_flags);
}
if (cb_status.pp_state_type1 not_eq msg.pp_state_type1) {
handle_pp_type1(msg.pp_state_type1);
}
if (cb_status.pp_state_type2 not_eq msg.pp_state_type2) {
handle_pp_type2(msg.pp_state_type2);
}
if (cb_status.stop_charging not_eq msg.stop_charging) {
handle_stop_button(msg.stop_charging);
}
// cb_status.lock_state is not checked here as it cannot be reported to EVerest.
cb_status = msg;
}
void evse_bsp_api::dispatch(std::string const& operation, std::string const& payload) {
if (operation == "enable") {
receive_enable(payload);
} else if (operation == "pwm_on") {
receive_pwm_on(payload);
} else if (operation == "cp_state_X1") {
receive_cp_state_X1(payload);
} else if (operation == "cp_state_F") {
receive_cp_state_F(payload);
} else if (operation == "allow_power_on") {
receive_allow_power_on(payload);
} else if (operation == "ac_switch_three_phases_while_charging") {
receive_ac_switch_three_phases_while_charging(payload);
} else if (operation == "ac_overcurrent_limit") {
receive_ac_overcurrent_limit(payload);
} else if (operation == "lock") {
receive_lock();
} else if (operation == "unlock") {
receive_unlock();
} else if (operation == "self_test") {
receive_self_test(payload);
} else if (operation == "reset") {
receive_request_reset(payload);
} else if (operation == "heartbeat") {
receive_heartbeat(payload);
} else {
std::cerr << "evse_bsp: RECEIVE invalid operation: " << operation << std::endl;
}
}
void evse_bsp_api::raise_comm_fault() {
send_raise_error(API_BSP::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
}
void evse_bsp_api::clear_comm_fault() {
send_clear_error(API_BSP::ErrorEnum::CommunicationFault, "ChargeBridge not available", "");
}
void evse_bsp_api::handle_event_cp(std::uint8_t cp) {
using bc_event = API_BSP::Event;
bc_event cp_event;
bool cp_state_valid = true;
switch (cp) {
case CpState_A:
cp_event = bc_event::A;
send_clear_error(API_BSP::ErrorEnum::MREC14PilotFault, "", "");
send_clear_error(API_BSP::ErrorEnum::DiodeFault, "", "");
break;
case CpState_B:
cp_event = bc_event::B;
break;
case CpState_C:
cp_event = bc_event::C;
break;
case CpState_D:
cp_event = bc_event::D;
break;
case CpState_E:
cp_event = bc_event::E;
break;
case CpState_F:
cp_event = bc_event::F;
break;
case CpState_DF:
cp_event = bc_event::E;
send_raise_error(API_BSP::ErrorEnum::DiodeFault, "", "Diode Fault");
break;
case CpState::CpState_INVALID:
cp_event = bc_event::E;
send_raise_error(API_BSP::ErrorEnum::MREC14PilotFault, "", "Pilot Fault");
break;
default:
cp_state_valid = false;
}
if (cp_state_valid and m_enabled) {
send_event(cp_event);
}
}
void evse_bsp_api::handle_event_relay(std::uint8_t relay) {
using bc_event = API_BSP::Event;
bc_event relaise_event;
bool relaise_state_valid = true;
switch (relay) {
case RelaiseState::RelayState_Open:
relaise_event = bc_event::PowerOff;
break;
case RelaiseState::RelayState_Closed:
relaise_event = bc_event::PowerOn;
break;
default:
relaise_state_valid = false;
}
if (relaise_state_valid) {
send_event(relaise_event);
}
}
void evse_bsp_api::handle_pp_type2(std::uint8_t data) {
API_BSP::Ampacity bc_ampacity;
bool bc_ampacity_valid = true;
switch (data) {
case PpState_Type2_STATE_NC:
bc_ampacity = API_BSP::Ampacity::None;
break;
case PpState_Type2_STATE_13A:
bc_ampacity = API_BSP::Ampacity::A_13;
break;
case PpState_Type2_STATE_20A:
bc_ampacity = API_BSP::Ampacity::A_20;
break;
case PpState_Type2_STATE_32A:
bc_ampacity = API_BSP::Ampacity::A_32;
break;
case PpState_Type2_STATE_70A:
bc_ampacity = API_BSP::Ampacity::A_63_3ph_70_1ph;
break;
case PpState_Type2_STATE_FAULT:
// Raise error check state
bc_ampacity_valid = false;
send_raise_error(API_BSP::ErrorEnum::MREC23ProximityFault, "", "Proximity Pilot Fault State");
break;
default:
bc_ampacity_valid = false;
}
if (bc_ampacity_valid) {
send_ac_pp_amapcity(bc_ampacity);
}
}
void evse_bsp_api::handle_pp_type1(std::uint8_t data) {
// EVerest does not really have support for type 1 PP.
// We just send a stop charging if some one presses the button,
// for everything else the PP state does not really matter.
if (data == PpState_Type1_STATE_Connected_Button_Pressed) {
auto reason = API_EVM::StopTransactionReason::EVDisconnected;
send_request_stop_transaction(reason);
}
}
// Error handling
// Define bit masks
enum class SafetyErrorMask : std::uint32_t {
cp_not_state_c = (1 << 0),
pwm_not_enabled = (1 << 1),
pp_invalid = (1 << 2),
plug_temperature_too_high = (1 << 3),
internal_temperature_too_high = (1 << 4),
emergency_input_latched = (1 << 5),
relay_health_latched = (1 << 6),
vdd_3v3_out_of_range = (1 << 7),
vdd_core_out_of_range = (1 << 8),
vdd_12V_out_of_range = (1 << 9),
vdd_N12V_out_of_range = (1 << 10),
vdd_refint_out_of_range = (1 << 11),
external_allow_power_on = (1 << 12),
config_mem_error = (1 << 13),
dc_hv_ov = (1 << 14),
};
// Table that maps a mask to our API error + message
struct FlagSpec {
SafetyErrorMask mask;
API_BSP::ErrorEnum error;
const char* subtype;
const char* message;
};
static constexpr FlagSpec error_specs[] = {
{SafetyErrorMask::pp_invalid, API_BSP::ErrorEnum::MREC23ProximityFault, "", "PP invalid"},
{SafetyErrorMask::plug_temperature_too_high, API_BSP::ErrorEnum::MREC19CableOverTempStop, "",
"Plug temperature too high"},
{SafetyErrorMask::internal_temperature_too_high, API_BSP::ErrorEnum::VendorError, "INTTEMP",
"ChargeBridge internal over temperature"},
{SafetyErrorMask::emergency_input_latched, API_BSP::ErrorEnum::VendorError, "EMGINPUT", "Emergency input latched"},
{SafetyErrorMask::relay_health_latched, API_BSP::ErrorEnum::VendorError, "RELAYS", "Relay welded error"},
{SafetyErrorMask::vdd_3v3_out_of_range, API_BSP::ErrorEnum::VendorError, "3V3", "Supply voltage 3.3V out of range"},
{SafetyErrorMask::vdd_core_out_of_range, API_BSP::ErrorEnum::VendorError, "VDDCORE",
"Internal supply core voltage out of range"},
{SafetyErrorMask::vdd_12V_out_of_range, API_BSP::ErrorEnum::VendorError, "VCC12",
"Internal supply 12V voltage out of range"},
{SafetyErrorMask::vdd_N12V_out_of_range, API_BSP::ErrorEnum::VendorError, "VCCN12",
"Internal supply -12V voltage out of range"},
{SafetyErrorMask::vdd_refint_out_of_range, API_BSP::ErrorEnum::VendorError, "VCCREF",
"Internal supply VREF voltage out of range"},
{SafetyErrorMask::config_mem_error, API_BSP::ErrorEnum::VendorError, "CONFIGMEM", "Internal config memory error"},
{SafetyErrorMask::dc_hv_ov, API_BSP::ErrorEnum::VendorError, "DV_HV",
"DC HV OVM. FIXME: This should be on OVM not EVSE interface"},
};
static constexpr FlagSpec print_warning_specs[] = {
{SafetyErrorMask::cp_not_state_c, API_BSP::ErrorEnum::VendorWarning, "", "CP is not state C"},
{SafetyErrorMask::pwm_not_enabled, API_BSP::ErrorEnum::VendorWarning, "", "PWM not enabled"},
{SafetyErrorMask::external_allow_power_on, API_BSP::ErrorEnum::VendorWarning, "",
"Allow power on from EVerest missing"},
};
// 4) Edge-driven handler
void evse_bsp_api::handle_error(const SafetyErrorFlags& data) {
std::uint32_t prev = cb_status.error_flags.raw; // cached raw value from before
std::uint32_t next = data.raw; // current raw value
std::uint32_t became_active = next & ~prev; // rising edges
std::uint32_t became_inactive = prev & ~next; // falling edges
for (const auto& s : error_specs) {
if (became_active & static_cast<std::uint32_t>(s.mask)) {
send_raise_error(s.error, s.subtype, s.message);
}
if (became_inactive & static_cast<std::uint32_t>(s.mask)) {
send_clear_error(s.error, s.subtype, "");
}
}
std::stringstream log;
for (const auto& s : print_warning_specs) {
if (next & static_cast<std::uint32_t>(s.mask)) {
log << "[" << s.message << "] ";
}
}
for (const auto& s : error_specs) {
if (next & static_cast<std::uint32_t>(s.mask)) {
log << "[" << s.message << "] ";
}
}
if (everest_connected && m_cb_connected) {
if (log.str().empty()) {
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0) << "Relays can be switched on." << std::endl;
} else {
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0)
<< "Relays off due to:" << log.str() << std::endl;
}
}
}
void evse_bsp_api::handle_stop_button([[maybe_unused]] std::uint8_t data) {
auto reason = API_EVM::StopTransactionReason::Local;
send_request_stop_transaction(reason);
}
void evse_bsp_api::receive_enable(std::string const& payload) {
if (everest::lib::API::deserialize(payload, m_enabled)) {
handle_event_cp(cb_status.cp_state);
handle_event_relay(cb_status.relay_state);
} else {
std::cerr << "evse_bsp_api::receive_enabled: payload invalid -> " << payload << std::endl;
}
}
void evse_bsp_api::receive_pwm_on(std::string const& payload) {
double pwm = 0;
if (everest::lib::API::deserialize(payload, pwm)) {
host_status.pwm_duty_cycle = pwm * 100;
tx(host_status);
} else {
std::cerr << "evse_bsp_api::receive_pwm_on: payload invalid -> " << payload << std::endl;
}
}
void evse_bsp_api::receive_cp_state_X1([[maybe_unused]] std::string const& payload) {
host_status.pwm_duty_cycle = 10001;
tx(host_status);
}
void evse_bsp_api::receive_cp_state_F([[maybe_unused]] std::string const& payload) {
host_status.pwm_duty_cycle = 0;
tx(host_status);
}
void evse_bsp_api::receive_allow_power_on(std::string const& payload) {
API_BSP::PowerOnOff obj;
if (everest::lib::API::deserialize(payload, obj)) {
host_status.allow_power_on = obj.allow_power_on;
tx(host_status);
} else {
std::cerr << "evse_bsp_api::receive_allow_power_on: payload invalid -> " << payload << std::endl;
}
}
void evse_bsp_api::receive_ac_switch_three_phases_while_charging(std::string const&) {
}
void evse_bsp_api::receive_ac_overcurrent_limit(std::string const&) {
}
void evse_bsp_api::receive_lock() {
host_status.connector_lock = 1;
tx(host_status);
}
void evse_bsp_api::receive_unlock() {
host_status.connector_lock = 0;
tx(host_status);
}
void evse_bsp_api::receive_self_test([[maybe_unused]] std::string const& payload) {
}
void evse_bsp_api::receive_request_reset(std::string const&) {
}
void evse_bsp_api::receive_heartbeat(std::string const& pl) {
last_everest_heartbeat = std::chrono::steady_clock::now();
std::size_t id = 0;
if (deserialize(pl, id)) {
auto delta = id - m_last_hb_id;
if (delta > 1) {
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
}
m_last_hb_id = id;
} else {
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
<< "EVerest invalid heartbeat message: " << pl << std::endl;
}
}
void evse_bsp_api::send_event(API_BSP::Event data) {
API_BSP::BspEvent event{data};
send_mqtt("event", serialize(event));
}
void evse_bsp_api::send_ac_nr_of_phases(std::uint8_t data) {
auto phases = static_cast<int>(data);
if (phases > 0 && phases <= 3) {
send_mqtt("ac_nr_of_phases", serialize(phases));
}
}
void evse_bsp_api::send_capabilities() {
send_mqtt("capabilities", serialize(m_capabilities));
}
void evse_bsp_api::send_ac_pp_amapcity(API_BSP::Ampacity data) {
API_BSP::ProximityPilot msg{data};
send_mqtt("ac_pp_ampacity", serialize(msg));
}
void evse_bsp_api::send_request_stop_transaction(API_EVM::StopTransactionReason data) {
API_EVM::StopTransactionRequest request;
request.reason = data;
send_mqtt("request_stop_transaction", serialize(request));
}
void evse_bsp_api::send_rcd_current(std::uint8_t) {
}
void evse_bsp_api::send_raise_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg) {
API_BSP::Error error_msg;
error_msg.type = error;
error_msg.sub_type = subtype;
error_msg.message = msg;
send_mqtt("raise_error", serialize(error_msg));
}
void evse_bsp_api::send_clear_error(API_BSP::ErrorEnum error, std::string const& subtype, std::string const& msg) {
API_BSP::Error error_msg;
error_msg.type = error;
error_msg.sub_type = subtype;
error_msg.message = msg;
send_mqtt("clear_error", serialize(error_msg));
}
void evse_bsp_api::send_communication_check() {
send_mqtt("communication_check", serialize(true));
}
void evse_bsp_api::send_reply_reset([[maybe_unused]] std::string const& replyTo) {
}
void evse_bsp_api::send_mqtt(std::string const& topic, std::string const& message) {
m_mqtt_tx(topic, message);
}
bool evse_bsp_api::check_everest_heartbeat() {
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
}
void evse_bsp_api::handle_everest_connection_state() {
send_communication_check();
auto current = check_everest_heartbeat();
auto handle_status = [this](bool status) {
if (status) {
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 0) << "EVerest connected" << std::endl;
send_capabilities();
} else {
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", 1) << "Waiting for EVerest...." << std::endl;
host_status.allow_power_on = 0;
host_status.pwm_duty_cycle = 65535;
tx(host_status);
}
};
if (m_bc_initial_comm_check) {
handle_status(current);
m_bc_initial_comm_check = false;
} else if (everest_connected != current) {
handle_status(not everest_connected);
}
everest_connected = current;
}
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,226 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "protocol/cb_common.h"
#include "protocol/evse_bsp_cb_to_host.h"
#include <charge_bridge/everest_api/ovm_api.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <charge_bridge/utilities/string.hpp>
#include <chrono>
#include <cstring>
#include <everest_api_types/generic/codec.hpp>
#include <everest_api_types/over_voltage_monitor/API.hpp>
#include <everest_api_types/over_voltage_monitor/codec.hpp>
#include <everest_api_types/utilities/codec.hpp>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
using namespace std::chrono_literals;
using namespace everest::lib::API::V1_0::types::generic;
using namespace everest::lib::API;
namespace charge_bridge::evse_bsp {
ovm_api::ovm_api([[maybe_unused]] evse_ovm_config const& config, std::string const& cb_identifier,
evse_bsp_host_to_cb& host_status) :
host_status(host_status), m_cb_identifier(cb_identifier) {
last_everest_heartbeat = std::chrono::steady_clock::time_point();
}
void ovm_api::sync(bool cb_connected) {
m_cb_connected = cb_connected;
handle_everest_connection_state();
}
bool ovm_api::register_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
return true;
}
bool ovm_api::unregister_events([[maybe_unused]] everest::lib::io::event::fd_event_handler& handler) {
return true;
}
void ovm_api::set_cb_tx(tx_ftor const& handler) {
m_tx = handler;
}
void ovm_api::tx(evse_bsp_host_to_cb const& msg) {
if (m_tx) {
m_tx(msg);
}
}
void ovm_api::set_mqtt_tx(mqtt_ftor const& handler) {
m_mqtt_tx = handler;
}
void ovm_api::set_cb_message(evse_bsp_cb_to_host const& msg) {
const double voltage_V = msg.hv_mV * 0.001;
send_voltage_measurement_V(voltage_V);
if (msg.error_flags.flags.dc_hv_ov_emergency not_eq m_cb_status.error_flags.flags.dc_hv_ov_emergency) {
handle_dc_hv_ov_emergency(msg.error_flags.flags.dc_hv_ov_emergency not_eq 0);
}
if (msg.error_flags.flags.dc_hv_ov_error not_eq m_cb_status.error_flags.flags.dc_hv_ov_error) {
handle_dc_hv_ov_error(msg.error_flags.flags.dc_hv_ov_error not_eq 0);
}
if (msg.cp_state not_eq m_cb_status.cp_state) {
handle_cp_state(static_cast<CpState>(msg.cp_state));
}
m_cb_status = msg;
}
void ovm_api::dispatch(std::string const& operation, std::string const& payload) {
if (operation == "set_limits") {
receive_set_limits(payload);
} else if (operation == "start") {
receive_start();
} else if (operation == "stop") {
receive_stop();
} else if (operation == "reset_over_voltage_error") {
receive_reset_over_voltage_error();
} else if (operation == "heartbeat") {
receive_heartbeat(payload);
} else {
std::cerr << "ovm_api: RECEIVE invalid operation: " << operation << std::endl;
}
}
void ovm_api::raise_comm_fault() {
send_raise_error(API_OVM::ErrorEnum::CommunicationFault, "ChargeBridge not available", "",
API_OVM::ErrorSeverityEnum::High);
}
void ovm_api::clear_comm_fault() {
send_clear_error(API_OVM::ErrorEnum::CommunicationFault, "ChargeBridge not available");
}
void ovm_api::handle_dc_hv_ov_emergency(bool high) {
static const std::string subtype = "Emergency";
if (high) {
send_raise_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype, "", API_OVM::ErrorSeverityEnum::High);
} else {
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype);
}
}
void ovm_api::handle_dc_hv_ov_error(bool high) {
static const std::string subtype = "Error";
if (high) {
send_raise_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype, "", API_OVM::ErrorSeverityEnum::Medium);
} else {
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, subtype);
}
}
void ovm_api::handle_cp_state(CpState state) {
if (state == CpState_A) {
send_clear_error(API_OVM::ErrorEnum::MREC5OverVoltage, "");
}
}
void ovm_api::receive_set_limits(std::string const& payload) {
static auto const V_to_mV_factor = 1000;
if (everest::lib::API::deserialize(payload, m_limits)) {
host_status.ovm_limit_emergency_mV = static_cast<std::uint32_t>(m_limits.emergency_limit_V * V_to_mV_factor);
host_status.ovm_limit_error_mV = static_cast<std::uint32_t>(m_limits.error_limit_V * V_to_mV_factor);
tx(host_status);
} else {
std::cerr << "ovm_api::receive_set_limits: payload invalid -> " << payload << std::endl;
}
}
void ovm_api::receive_start() {
host_status.ovm_enable = 1;
host_status.ovm_reset_errors = 0;
tx(host_status);
}
void ovm_api::receive_stop() {
host_status.ovm_enable = 0;
tx(host_status);
}
void ovm_api::receive_reset_over_voltage_error() {
host_status.ovm_reset_errors = 1;
tx(host_status);
}
void ovm_api::receive_heartbeat(std::string const& pl) {
last_everest_heartbeat = std::chrono::steady_clock::now();
std::size_t id = 0;
if (deserialize(pl, id)) {
auto delta = id - m_last_hb_id;
if (delta > 1) {
utilities::print_error(m_cb_identifier, "OVM/EVEREST", -1)
<< "EVerest heartbeat missmatch: " << m_last_hb_id << "<->" << id << std::endl;
}
m_last_hb_id = id;
} else {
utilities::print_error(m_cb_identifier, "EVSE/EVEREST", -1)
<< "EVerest invalid heartbeat message: " << pl << std::endl;
}
}
void ovm_api::send_voltage_measurement_V(double data) {
send_mqtt("voltage_measurement_V", serialize(data));
}
void ovm_api::send_raise_error(API_OVM::ErrorEnum error, std::string const& subtype, std::string const& msg,
API_OVM::ErrorSeverityEnum severity) {
API_OVM::Error error_msg;
error_msg.type = error;
error_msg.sub_type = subtype;
error_msg.message = msg;
error_msg.severity = severity;
send_mqtt("raise_error", serialize(error_msg));
}
void ovm_api::send_clear_error(API_OVM::ErrorEnum error, std::string const& subtype) {
API_OVM::Error error_msg;
error_msg.type = error;
error_msg.sub_type = subtype;
send_mqtt("clear_error", serialize(error_msg));
}
void ovm_api::send_communication_check() {
send_mqtt("communication_check", serialize(true));
}
void ovm_api::send_mqtt(std::string const& topic, std::string const& message) {
if (m_mqtt_tx) {
m_mqtt_tx(topic, message);
}
}
bool ovm_api::check_everest_heartbeat() {
return std::chrono::steady_clock::now() - last_everest_heartbeat < 2s;
}
void ovm_api::handle_everest_connection_state() {
send_communication_check();
auto current = check_everest_heartbeat();
auto handle_status = [this](bool status) {
if (status) {
utilities::print_error(m_cb_identifier, "OVM/EVEREST", 0) << "EVerest connected" << std::endl;
} else {
utilities::print_error(m_cb_identifier, "OVM/EVEREST", 1) << "Waiting for EVerest...." << std::endl;
}
};
if (m_bc_initial_comm_check) {
handle_status(current);
m_bc_initial_comm_check = false;
} else if (m_everest_connected != current) {
handle_status(not m_everest_connected);
}
m_everest_connected = current;
}
} // namespace charge_bridge::evse_bsp

View File

@@ -0,0 +1,295 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "everest/io/udp/udp_payload.hpp"
#include <charge_bridge/firmware_update/sync_fw_updater.hpp>
#include <charge_bridge/utilities/filesystem.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <charge_bridge/utilities/platform_utils.hpp>
#include <protocol/cb_management.h>
#include <cstring>
#include <fstream>
namespace {
const int default_udp_timeout_ms = 3000;
}
namespace charge_bridge::firmware_update {
const std::uint32_t sync_fw_updater::app_udp_sector_size = 0x2000;
const std::uint16_t sync_fw_updater::sub_chunk_size = 1024;
using namespace everest::lib::io::udp;
static everest::lib::io::udp::udp_payload make_ping_command() {
everest::lib::io::udp::udp_payload payload;
CbManagementPacket<CbFirmwarePing> packet;
packet.type = CbStructType::CST_CbFirmwarePing;
packet.data.dummy = 0;
utilities::struct_to_vector(packet, payload.buffer);
return payload;
}
static everest::lib::io::udp::udp_payload make_get_version_command() {
everest::lib::io::udp::udp_payload payload;
CbManagementPacket<CbFirmwareGetVersion> packet;
packet.type = CbStructType::CST_CbFirmwareGetVersion;
packet.data.dummy = 0;
utilities::struct_to_vector(packet, payload.buffer);
return payload;
}
sync_fw_updater::sync_fw_updater(fw_update_config const& config) :
m_udp(config.cb_remote, config.cb_port, 3, default_udp_timeout_ms), m_config(config) {
}
std::optional<std::string> sync_fw_updater::get_fw_version() {
auto pl = make_get_version_command();
auto result = m_udp.request_reply(pl);
if (not result) {
return std::nullopt;
}
result->buffer[result->buffer.size() - 1] = 0x00; // ensure it is actually a 0 terminated string
auto* str_ptr = reinterpret_cast<char*>(result->buffer.data()); // reinterpret for string conversion
return std::string(str_ptr + 2); // skip 2 byte header
}
void sync_fw_updater::print_fw_version() {
auto result = get_fw_version();
utilities::print_error(m_config.cb, "FIRMWARE", not result.has_value())
<< "Firmware version " << result.value_or("ERROR") << std::endl;
}
bool sync_fw_updater::check_if_correct_fw_installed() {
auto installed_fw = get_fw_version();
if (not installed_fw.has_value()) {
return true;
}
charge_bridge::filesystem_utils::CryptSignedHeader hdr;
std::uint32_t offset;
if (not read_crypt_signed_header(m_config.fw_path, hdr, offset)) {
utilities::print_error(m_config.cb, "FIRMWARE", 1)
<< "Could not read header for file: " << m_config.fw_path << std::endl;
return false;
}
auto available_fw = hdr.firmware_version;
utilities::print_error(m_config.cb, "FIRMWARE", 0)
<< "Firmware installed: \"" << installed_fw.value() << "\" Firmware available: \"" << available_fw << "\""
<< std::endl;
if (installed_fw.value() == available_fw) {
return true;
} else {
return false;
}
}
bool sync_fw_updater::quick_check_connection() {
static const std::uint16_t rr_timeout_ms = 200;
static const std::uint16_t rr_retires_ms = 10;
everest::lib::io::udp::udp_payload pl = make_ping_command();
auto result = m_udp.request_reply(pl, rr_timeout_ms, rr_retires_ms).has_value();
utilities::print_error(m_config.cb, "FIRMWARE", not result)
<< (result ? "ChargeBride Connected" : "No connection to ChargeBridge") << std::endl;
return result;
}
bool sync_fw_updater::check_connection() {
static const std::uint16_t rr_timeout_ms = 150;
static const std::uint16_t rr_retires_ms = 100;
everest::lib::io::udp::udp_payload pl = make_ping_command();
auto result = m_udp.request_reply(pl, rr_timeout_ms, rr_retires_ms).has_value();
utilities::print_error(m_config.cb, "FIRMWARE", not result)
<< (result ? "ChargeBride Connected" : "No connection to ChargeBridge") << std::endl;
return result;
}
bool sync_fw_updater::ping() {
everest::lib::io::udp::udp_payload pl = make_ping_command();
return m_udp.request_reply(pl).has_value();
}
bool sync_fw_updater::check_reply(utilities::sync_udp_client::reply const& val) {
if (val && val->size() == (sizeof(AppUDPResponse) + 2)) {
AppUDPResponse reply;
memcpy(&reply, val->buffer.data() + 2, sizeof(AppUDPResponse));
return (reply == AppUDPResponse::AUR_Ok);
}
return false;
}
bool sync_fw_updater::upload_fw() {
utilities::print_error(m_config.cb, "FIRMWARE", 0) << "Upload in progress" << std::endl;
if (not upload_firmware()) {
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "Upload of firmware image: " << std::endl;
return false;
}
utilities::print_error(m_config.cb, "FIRMWARE", 0) << "Upload completed" << std::endl;
return true;
}
bool sync_fw_updater::upload_firmware() {
auto path = m_config.fw_path;
utilities::print_error(m_config.cb, "FIRMWARE", 0) << path << std::endl;
if (not fs::exists(path) || not fs::is_regular_file(path)) {
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "firmware file not found: " << path << std::endl;
return false;
}
std::uint32_t offset;
charge_bridge::filesystem_utils::CryptSignedHeader hdr;
if (not upload_init(path, offset, hdr)) {
return false;
}
std::uint32_t total_bytes = 0;
std::uint16_t sector = 0;
if (not upload_transfer(path, sector, offset, total_bytes)) {
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "Upload failed at sector: " << sector << std::endl;
return false;
}
if (not upload_finish(path, total_bytes, hdr)) {
return false;
}
return true;
}
/*
# File format for the binary update bundle:
# 32 byte header [reserved]
# 1 byte length of signature
# signature binary
# 1 byte NUM_SECTORS: This is the number of secure sectors
# 16 byte IV
# ... rest of the file is assembled firmware image: secure part...padding...non secure part (encrypted)
*/
bool sync_fw_updater::upload_init(const fs::path& file_path, std::uint32_t& offset,
charge_bridge::filesystem_utils::CryptSignedHeader& hdr) {
everest::lib::io::udp::udp_payload payload;
if (not read_crypt_signed_header(file_path, hdr, offset)) {
utilities::print_error(m_config.cb, "FIRMWARE", 1)
<< "Could not read header for file: " << file_path << std::endl;
return false;
}
utilities::print_error(m_config.cb, "FIRMWARE", 0)
<< "Loaded firmware version file: " << file_path << " Version: " << hdr.firmware_version << std::endl;
CbManagementPacket<CbFirmwareStart> msg;
msg.type = CbStructType::CST_CbFirmwareStart;
msg.data.is_secure_fw = true;
msg.data.requires_crc_verification = true;
msg.data.requires_sha256_verification = true;
msg.data.requires_signature_verification = true;
msg.data.requires_decryption = true;
// Copy the IV from the header
std::memcpy(msg.data.iv, hdr.iv.data(), sizeof(msg.data.iv));
utilities::struct_to_vector(msg, payload.buffer);
auto result = m_udp.request_reply(payload);
return check_reply(result);
}
bool sync_fw_updater::upload_transfer(const fs::path& file_path, std::uint16_t& sector, std::uint32_t offset,
std::uint32_t& total_bytes) {
bool send_failed = false;
std::ifstream file(file_path, std::ios::binary);
if (!file) {
return false;
}
// Skip the header
file.seekg(offset, std::ios::beg);
bool processed_file = filesystem_utils::process_file(
file, sub_chunk_size, [&](const std::vector<std::uint8_t>& buffer, bool last_chunk) -> bool {
total_bytes += buffer.size();
// Care must be taken when sending this over, since on the
// receiving end we must remove the PKCS#7 added bytes
auto block = make_fw_chunk(sector, last_chunk, buffer);
auto result = m_udp.request_reply(block);
if (not check_reply(result)) {
utilities::print_error(m_config.cb, "FIRMWARE", 1) << "chunk could not be sent" << std::endl;
send_failed = true;
return true; // Interrupt
}
sector++;
return false; // Continue
});
return (processed_file) && (send_failed == false);
}
bool sync_fw_updater::upload_finish([[maybe_unused]] const fs::path& file_path, std::uint32_t total_bytes,
const charge_bridge::filesystem_utils::CryptSignedHeader& hdr) {
CbManagementPacket<CbFirmwareEnd> fw_check_packet;
fw_check_packet.type = CbStructType::CST_CbFirmwareFinish;
fw_check_packet.data.firmware_len = total_bytes;
fw_check_packet.data.watermark_secure_end = hdr.num_sectors;
if (hdr.sig_len > sizeof(fw_check_packet.data.fw_signature) || hdr.sig_len > hdr.signature.size()) {
return false;
}
memcpy(fw_check_packet.data.fw_signature, hdr.signature.data(), hdr.sig_len);
fw_check_packet.data.fw_signature_len = hdr.sig_len;
udp_payload payload;
utilities::struct_to_vector(fw_check_packet, payload.buffer);
// The final check can be a very slow operation due to the cryptography involved
static const std::uint16_t rr_timeout_ms = 10000;
static const std::uint16_t rr_retires_ms = 1;
auto result = m_udp.request_reply(payload, rr_timeout_ms, rr_retires_ms);
return check_reply(result);
}
udp_payload sync_fw_updater::make_fw_chunk(std::uint16_t sector, std::uint8_t last_chunk,
std::vector<std::uint8_t> const& data) {
CbManagementPacket<CbFirmwarePacket> fw_data_packet;
fw_data_packet.type = CbStructType::CST_CbFirmwarePacket;
fw_data_packet.data.last_packet = last_chunk;
fw_data_packet.data.sector = sector;
fw_data_packet.data.data_len = data.size();
std::memcpy(fw_data_packet.data.data, data.data(), data.size());
udp_payload result;
utilities::struct_to_vector(fw_data_packet, result.buffer);
return result;
}
} // namespace charge_bridge::firmware_update

View File

@@ -0,0 +1,156 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/gpio_bridge.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <charge_bridge/utilities/platform_utils.hpp>
#include <charge_bridge/utilities/string.hpp>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/mqtt/mqtt_client.hpp>
#include <iostream>
#include <limits>
#include <memory>
#include <protocol/cb_management.h>
#include <stdexcept>
#include <string>
namespace charge_bridge {
using namespace std::chrono_literals;
namespace mqtt = everest::lib::io::mqtt;
namespace {
const int default_udp_timeout_ms = 1000;
const int mqtt_reconnect_timeout_ms = 1000;
} // namespace
gpio_bridge::gpio_bridge(gpio_config const& config) :
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
m_mqtt(mqtt_reconnect_timeout_ms)
{
m_identifier = config.cb + "/" + config.item;
m_heartbeat_timer.set_timeout(std::chrono::seconds(config.interval_s));
m_udp.set_rx_handler([this](auto const& data, auto&) { handle_udp_rx(data); });
m_udp.set_error_handler([this](auto id, auto const& msg) {
utilities::print_error(m_identifier, "GPIO/UDP", id) << msg << std::endl;
m_udp_on_error = id not_eq 0;
});
m_receive_topic = "pionix/chargebridge/" + config.cb + "/gpio/output/";
m_send_topic = "pionix/chargebridge/" + config.cb + "/gpio/input/";
m_mqtt.set_error_handler([this, config](int id, std::string const& msg) {
utilities::print_error(m_identifier, "GPIO/MQTT", id) << msg << std::endl;
m_mqtt_on_error = id not_eq 0;
});
m_mqtt.set_callback_connect([this](auto&, auto, auto, auto const&) {
m_mqtt.subscribe(
m_receive_topic + "#", [this](auto&, auto const& payload) { dispatch(payload); },
everest::lib::io::mqtt::mqtt_client::QoS::at_most_once);
});
m_mqtt.connect(config.mqtt_bind, config.mqtt_remote, config.mqtt_port, config.mqtt_ping_interval_ms);
m_message.type = CbStructType::CST_HostToCb_Gpio;
m_message.data.number_of_gpios = CB_NUMBER_OF_GPIOS;
std::memset(m_message.data.gpio_values, 0, sizeof(m_message.data.gpio_values));
}
gpio_bridge::~gpio_bridge() {
}
bool gpio_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = handler.register_event_handler(&m_udp);
result = handler.register_event_handler(&m_mqtt) && result;
result = handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); }) && result;
return result;
}
bool gpio_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = handler.unregister_event_handler(&m_udp);
result = handler.unregister_event_handler(&m_mqtt) && result;
result = handler.unregister_event_handler(&m_heartbeat_timer) && result;
return result;
}
void gpio_bridge::dispatch(everest::lib::io::mqtt::mqtt_client::message const& data) {
auto& topic = data.topic;
auto& payload = data.payload;
auto operation = utilities::string_after_pattern(topic, m_receive_topic);
uint16_t value = 0;
int id = 0;
auto stous = [](std::string const& data) {
auto val = stoi(data);
if (val < 0 or val > std::numeric_limits<uint16_t>::max()) {
throw std::range_error("");
}
return static_cast<uint16_t>(val);
};
try {
value = stous(payload);
} catch (...) {
std::cout << "INVALID DATA on MQTT for GPIO DATA" << std::endl;
return;
}
try {
id = std::stoi(operation);
} catch (...) {
std::cout << "INVALID DATA on MQTT for GPIO ID" << std::endl;
return;
}
if (id < 0 or id >= CB_NUMBER_OF_GPIOS) {
std::cout << "INVALID GPIO ID" << std::endl;
return;
}
m_message.data.gpio_values[id] = value;
send_udp();
}
void gpio_bridge::send_mqtt(std::string const& topic, std::string const& message) {
everest::lib::io::mqtt::mqtt_client::message payload;
payload.topic = m_send_topic + topic;
payload.payload = message;
m_mqtt.publish(payload);
}
void gpio_bridge::send_udp() {
if (not m_udp_on_error) {
everest::lib::io::udp::udp_payload payload;
utilities::struct_to_vector(m_message, payload.buffer);
m_udp.tx(payload);
}
}
void gpio_bridge::handle_error_timer() {
if (m_udp_on_error) {
m_udp.reset();
}
}
void gpio_bridge::handle_heartbeat_timer() {
send_udp();
}
void gpio_bridge::handle_udp_rx(everest::lib::io::udp::udp_payload const& payload) {
CbManagementPacket<CbGpioPacket> data;
if (payload.size() == sizeof(data)) {
std::memcpy(&data, payload.buffer.data(), sizeof(data));
for (std::size_t i = 0; i < sizeof(data.data.gpio_values) / sizeof(data.data.gpio_values[0]); ++i) {
send_mqtt(std::to_string(i), std::to_string(data.data.gpio_values[i]));
}
} else {
std::cout << "INVALID DATA SIZE in UDP RX of GPIO: " << payload.size() << " vs " << sizeof(data) << std::endl;
}
}
} // namespace charge_bridge

View File

@@ -0,0 +1,117 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/heartbeat_service.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <charge_bridge/utilities/platform_utils.hpp>
#include <chrono>
#include <cstring>
#include <everest/io/event/fd_event_handler.hpp>
#include <iostream>
#include <memory>
#include <protocol/cb_management.h>
namespace {
const int default_udp_timeout_ms = 1000;
const std::uint16_t s_to_ms_factor = 1000;
} // namespace
namespace charge_bridge {
using namespace std::chrono_literals;
heartbeat_service::heartbeat_service(heartbeat_config const& config,
std::function<void(bool)> const& publish_connection_status) :
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms),
m_publish_connection_status(publish_connection_status) {
m_identifier = config.cb + "/" + config.item;
std::memcpy(&m_config_message.data, &config.cb_config, sizeof(CbConfig));
m_config_message.type = CbStructType::CST_HostToCb_Heartbeat;
m_heartbeat_interval = std::chrono::milliseconds(config.interval_s * s_to_ms_factor);
m_connection_to = std::chrono::milliseconds(config.connection_to_s * s_to_ms_factor);
m_heartbeat_timer.set_timeout(m_heartbeat_interval);
m_last_heartbeat_reply = std::chrono::steady_clock::time_point();
m_udp.set_rx_handler([this](auto const& data, auto&) { handle_udp_rx(data); });
m_udp.set_error_handler([this](auto id, auto const& msg) {
if (m_inital_cb_commcheck and id == 0) {
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 1) << "Waiting for ChargeBridge" << std::endl;
} else {
utilities::print_error(m_identifier, "HEARTBEAT/UDP", id) << msg << std::endl;
}
m_udp_on_error = id not_eq 0;
});
}
heartbeat_service::~heartbeat_service() {
}
bool heartbeat_service::register_events(everest::lib::io::event::fd_event_handler& handler) {
// clang-format off
return
handler.register_event_handler(&m_udp) &&
handler.register_event_handler(&m_heartbeat_timer, [this](auto&) { handle_heartbeat_timer(); });
// clang-format on
}
bool heartbeat_service::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
// clang-format off
return
handler.unregister_event_handler(&m_udp) &&
handler.unregister_event_handler(&m_heartbeat_timer);
// clang-format on
}
void heartbeat_service::handle_error_timer() {
if (m_udp_on_error) {
m_udp.reset();
}
}
void heartbeat_service::handle_heartbeat_timer() {
if (not m_udp_on_error) {
everest::lib::io::udp::udp_payload payload;
utilities::struct_to_vector(m_config_message, payload.buffer);
m_udp.tx(payload);
}
auto timeout = std::chrono::steady_clock::now() - m_last_heartbeat_reply > m_connection_to;
if (timeout and m_cb_connected) {
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 1) << "ChargeBridge connection lost" << std::endl;
m_cb_connected = false;
}
else if (not timeout and not m_cb_connected) {
utilities::print_error(m_identifier, "HEARTBEAT/UDP", 0) << "ChargeBridge connected" << std::endl;
m_cb_connected = true;
}
if (m_publish_connection_status) {
m_publish_connection_status(m_cb_connected);
}
}
void heartbeat_service::handle_udp_rx(everest::lib::io::udp::udp_payload const& payload) {
CbManagementPacket<CbHeartbeatReplyPacket> data;
if (payload.size() == sizeof(data)) {
std::memcpy(&data, payload.buffer.data(), sizeof(data));
m_last_heartbeat_reply = std::chrono::steady_clock::now();
auto mcu_current = static_cast<uint32_t>(data.data.uptime_ms);
if (mcu_current <= m_mcu_timestamp) {
m_mcu_reset_count++;
utilities::print_error(m_identifier, "HEARTBEAT/UDP", -1)
<< "ChargeBridge reset count " << m_mcu_reset_count << std::endl;
}
m_mcu_timestamp = mcu_current;
// TODO: Once we have the telemetry framework in EVerest, we should publish those values.
/*printf(
"CP: %.2f/%.2f PP: %i MCU_temp %i degC\nVoltages: 12V: %.2f, -12V: %.2f, ref %.3f, 3.3V: %.3f, core:
%.3f\n", data.data.cp_hi_mV / 1000., data.data.cp_lo_mV / 1000., (int)data.data.pp_mOhm / 1000,
data.data.temperature_mcu_C, data.data.vdd_12V/1000., data.data.vdd_N12V/1000., data.data.vdd_refint/1000.,
data.data.vdd_3v3/1000., data.data.vdd_core/1000.);*/
} else {
std::cout << "INVALID DATA SIZE in UDP RX of HEARTBEAT: " << payload.size() << " vs " << sizeof(data)
<< std::endl;
}
}
} // namespace charge_bridge

View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/plc_bridge.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/udp/udp_payload.hpp>
#include <iostream>
namespace {
const int default_udp_timeout_ms = 1000;
} // namespace
namespace charge_bridge {
plc_bridge::plc_bridge(plc_bridge_config const& config) :
m_tap(config.plc_tap, config.plc_ip, config.plc_netmaks, config.plc_mtu),
m_udp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
using namespace std::chrono_literals;
m_timer.set_timeout(5s);
m_tap.set_rx_handler([this](auto const& data, auto&) {
everest::lib::io::udp::udp_payload pl;
pl.buffer = data;
m_udp.tx(pl);
});
m_udp.set_rx_handler([this](auto const& data, auto&) { m_tap.tx(data.buffer); });
auto identifier = config.cb + "/" + config.item;
m_tap.set_error_handler([this, identifier](auto id, auto const& msg) {
utilities::print_error(identifier, "PLC/TAP", id) << msg << std::endl;
m_tap_on_error = id not_eq 0;
});
m_udp.set_error_handler([this, identifier](auto id, auto const& msg) {
utilities::print_error(identifier, "PLC/UDP", id) << msg << std::endl;
m_udp_on_error = id not_eq 0;
});
}
void plc_bridge::handle_timer_event() {
if (m_udp_on_error) {
m_udp.reset();
}
if (m_tap_on_error) {
m_tap.reset();
}
}
bool plc_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
result = handler.register_event_handler(&m_tap) && result;
result = handler.register_event_handler(&m_udp) && result;
result = handler.register_event_handler(&m_timer, [this](auto) { handle_timer_event(); }) && result;
return result;
}
bool plc_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
result = handler.unregister_event_handler(&m_tap) && result;
result = handler.unregister_event_handler(&m_udp) && result;
result = handler.unregister_event_handler(&m_timer) && result;
return result;
}
} // namespace charge_bridge

View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "everest/io/serial/event_pty.hpp"
#include <charge_bridge/serial_bridge.hpp>
#include <charge_bridge/utilities/logging.hpp>
#include <cstring>
#include <everest/io/event/fd_event_handler.hpp>
#include <protocol/cb_can_message.h>
namespace {
const int default_udp_timeout_ms = 1000;
const std::uint32_t tcp_user_timeout_ms = 4000;
} // namespace
namespace charge_bridge {
serial_bridge::serial_bridge(serial_bridge_config const& config) :
m_pty(), m_tcp(config.cb_remote, config.cb_port, default_udp_timeout_ms) {
using namespace std::chrono_literals;
auto link_ok = m_symlink.set_link(m_pty.get_slave_path(), config.serial_device);
if (not link_ok) {
throw std::runtime_error("Failed to setup symbolic links for serial ports");
}
m_tcp.set_on_ready_action([this]() {
m_tcp.get_raw_handler()->set_keep_alive(3, 1, 1);
m_tcp.get_raw_handler()->set_user_timeout(tcp_user_timeout_ms);
});
m_pty.set_data_handler([this](auto const& data, auto&) { m_tcp.tx(data); });
m_tcp.set_rx_handler([this](auto const& data, auto&) { m_pty.tx(data); });
auto identifier = config.cb + "/" + config.item;
m_pty.set_error_handler([this, identifier](auto id, auto const& msg) {
utilities::print_error(identifier, "SERIAL/PTY", id) << msg << std::endl;
if (id not_eq 0) {
m_pty.reset();
}
});
m_tcp.set_error_handler([this, identifier](auto id, auto const& msg) {
if (m_tcp_last_error_id not_eq id) {
utilities::print_error(identifier, "SERIAL/TCP", id) << msg << std::endl;
m_tcp_last_error_id = id;
}
if (id not_eq 0) {
m_tcp.reset();
}
});
}
void serial_bridge::reset_tcp() {
m_tcp.reset();
}
std::string serial_bridge::get_slave_path() {
return m_pty.get_slave_path();
}
bool serial_bridge::register_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
result = handler.register_event_handler(&m_pty) && result;
result = handler.register_event_handler(&m_tcp) && result;
return result;
}
bool serial_bridge::unregister_events(everest::lib::io::event::fd_event_handler& handler) {
auto result = true;
result = handler.unregister_event_handler(&m_pty) && result;
result = handler.unregister_event_handler(&m_tcp) && result;
return result;
}
} // namespace charge_bridge

View File

@@ -0,0 +1,147 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/utilities/filesystem.hpp>
#include <cstring>
#include <fstream>
#include <iostream>
#include <limits>
#include <random>
namespace charge_bridge::filesystem_utils {
bool read_from_file_partial(const fs::path& file_path, const std::size_t byte_count, std::string& out_data) {
try {
if (fs::is_regular_file(file_path)) {
std::ifstream file(file_path, std::ios::binary);
if (file.is_open()) {
std::vector<char> buffer(byte_count);
file.read(buffer.data(), byte_count);
std::size_t read_bytes = file.gcount();
if (read_bytes == byte_count) {
out_data.assign(buffer.data(), read_bytes);
return true;
}
}
}
} catch (const std::exception& e) {
return false;
}
return false;
}
bool read_from_file(const fs::path& file_path, std::string& out_data) {
try {
if (fs::is_regular_file(file_path)) {
std::ifstream file(file_path, std::ios::binary);
if (file.is_open()) {
out_data = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
return true;
}
}
} catch (const std::exception& e) {
return false;
}
return false;
}
bool process_file(const fs::path& file_path, std::size_t buffer_size,
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func) {
std::ifstream file(file_path, std::ios::binary);
return process_file(file, buffer_size, std::move(func));
}
bool process_file(std::ifstream& file, std::size_t buffer_size,
std::function<bool(const std::vector<std::uint8_t>&, bool last_chunk)>&& func) {
if (!file) {
return false;
}
std::vector<std::uint8_t> buffer(buffer_size);
bool interupted = false;
while (file.read(reinterpret_cast<char*>(buffer.data()), buffer_size)) {
interupted = func(buffer, false);
if (interupted) {
break;
}
}
// Process the remaining bytes
if (interupted == false) {
std::size_t remaining = file.gcount();
// Keep only remaining elements
buffer.resize(remaining);
func(buffer, true);
}
return true;
}
// Returns true on success, fills `hdr`, and sets `image_offset` to the byte
// position (from start of file) where the firmware image begins.
bool read_crypt_signed_header(const fs::path& path, CryptSignedHeader& hdr, std::uint32_t& image_offset) {
std::ifstream f(path, std::ios::binary);
if (!f) {
return false;
}
auto read_exact = [&](void* dst, std::size_t n) -> bool {
f.read(reinterpret_cast<char*>(dst), static_cast<std::streamsize>(n));
return f.good() || (f.eof() && static_cast<std::size_t>(f.gcount()) == n);
};
char firmware_version_str[32];
std::memset(firmware_version_str, 0, sizeof(firmware_version_str)); // all zeros
// 32-byte reserved header
if (!read_exact(firmware_version_str, sizeof(firmware_version_str))) {
return false;
}
hdr.firmware_version = std::string(firmware_version_str);
// 1-byte signature length
if (!read_exact(&hdr.sig_len, 1)) {
return false;
}
// L-byte signature
hdr.signature.resize(hdr.sig_len);
if (hdr.sig_len > 0) {
if (!read_exact(hdr.signature.data(), hdr.signature.size())) {
return false;
}
}
// 1-byte NUM_SECTORS
if (!read_exact(&hdr.num_sectors, 1)) {
return false;
}
// 16-byte IV
if (!read_exact(hdr.iv.data(), hdr.iv.size())) {
return false;
}
// Where the firmware image starts:
// offset = 32 + 1 + L + 1 + 16
image_offset = static_cast<std::uint32_t>(f.tellg());
// As a sanity fallback, compute if tellg() failed:
// Disabled, since it is always false
// if (static_cast<std::streamoff>(image_offset)< 0) {
// image_offset = 32u + 1u + static_cast<std::uint64_t>(hdr.sig_len) + 1u + 16u;
// }
return true;
}
} // namespace charge_bridge::filesystem_utils

View File

@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/utilities/logging.hpp>
#include <iomanip>
namespace charge_bridge::utilities {
enum class color {
error,
success,
warning,
message,
unit,
standard,
terminal,
};
std::ostream& operator<<(std::ostream& s, color c) {
switch (c) {
case color::error:
s << "\033[31m";
break;
case color::success:
s << "\033[32m";
break;
case color::warning:
s << "\033[33m";
break;
case color::message:
s << "\033[37m";
break;
case color::unit:
s << "\033[1;37m";
break;
case color::terminal:
s << "\033[m";
break;
case color::standard:
default:
s << "\033[39;49m";
}
return s;
}
std::ostream& print_error(std::string const& device, std::string const& unit, int status) {
// clang-format off
auto ctrl =
status == 0 ? color::success :
status == -1 ? color::warning:
color::error;
std::cout << "[ " << ctrl << std::setw(13) << std::left << unit << color::terminal << " ] "
<< color::unit << std::setw(20) << device << color::terminal << " ";
if(status not_eq 0){
if(status == -1){
std::cout << color::standard << "WARNING ";
}
else{
std::cout << color::standard << "ERROR ( " << status << " ) ";
}
}
return std::cout << color::standard;
// clang-format on
}
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,415 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include "c4/yml/node.hpp"
#include <charge_bridge/utilities/parse_config.hpp>
#include <charge_bridge/utilities/string.hpp>
#include <charge_bridge/utilities/type_converters.hpp>
#include <everest_api_types/evse_board_support/API.hpp>
#include <everest_api_types/evse_board_support/codec.hpp>
#include <iostream>
#include <filesystem>
// clang-format off
#include <ryml_std.hpp>
#include <ryml.hpp>
// clang-format on
using namespace everest::lib::API::V1_0::types;
namespace {
static const int g_cb_port_management = 6000;
static const int g_cb_port_evse_bsp = 6001;
static const int g_cb_port_plc = 6002;
static const int g_cb_port_can0 = 6003;
static const int g_cb_port_serial_1 = 6004;
static const int g_cb_port_serial_2 = 6005;
static const std::uint16_t default_mqtt_ping_interval_ms = 1000;
std::string print_yaml_location(ryml::Location const& loc) {
std::stringstream error_msg;
if (loc) {
if (not loc.name.empty()) {
auto tmp = std::string(loc.name.str, loc.name.len);
if (charge_bridge::utilities::string_ends_with(tmp, ".hpp")) {
return "";
}
error_msg << "\n file ";
error_msg << tmp;
}
error_msg << "\n line " << loc.line;
if (loc.col) {
error_msg << " column " << loc.col;
}
if (loc.offset) {
error_msg << " offset " << loc.offset << "B";
}
error_msg << "\n";
}
return error_msg.str();
}
void yaml_error_handler(const char* msg, std::size_t len, ryml::Location loc, void*) {
std::stringstream error_msg;
error_msg << "YAML parsing error: ";
error_msg << print_yaml_location(loc);
error_msg.write(msg, len);
std::cerr << error_msg.str() << std::endl;
throw std::runtime_error(error_msg.str());
}
void print_location(ryml::ConstNodeRef node, ryml::Parser& parser) {
std::cerr << print_yaml_location(node.location(parser)) << std::endl;
}
void load_yaml_file(const std::string& filename, ryml::Parser* parser, ryml::Tree* t) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Could not open file: " + filename);
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string file_content = buffer.str();
parse_in_arena(parser, ryml::to_csubstr(filename), ryml::to_csubstr(file_content), t);
}
template <class T> c4::yml::ConstNodeRef decode(c4::yml::ConstNodeRef const& node, T& rhs) {
using namespace charge_bridge::utilities;
node >> rhs;
return node;
}
std::pair<std::string, c4::yml::ConstNodeRef> find_node(c4::yml::NodeRef& config, std::string const& main,
std::string const& sub) {
auto main_str = ryml::to_csubstr(main);
auto node_str = main;
c4::yml::ConstNodeRef node;
if (not sub.empty()) {
node_str = node_str + "::" + sub;
auto sub_str = ryml::to_csubstr(sub);
node = config.find_child(main_str);
if (not node.invalid()) {
node = config.find_child(main_str).find_child(sub_str);
}
} else {
node = config[main_str];
}
return {node_str, node};
}
template <class DataT>
bool get_node_impl(c4::yml::ConstNodeRef node, ryml::Parser& parser, std::string const& node_str, DataT& data) {
if (node.invalid()) {
std::cerr << "Node not found: " << node_str << std::endl;
throw std::runtime_error("");
}
try {
decode(node, data);
return true;
} catch (std::exception const& e) {
std::cerr << "Cannot parse config: " << node_str << std::endl;
std::cerr << e.what() << std::endl;
} catch (charge_bridge::utilities::yml_node_error const& e) {
std::cerr << "Error source: \n"
<< " parent " << node_str << "\n"
<< " data " << e.m_msg << std::flush;
print_location(e.m_node, parser);
}
throw std::runtime_error("");
}
struct RymlCallbackInitializer {
RymlCallbackInitializer() {
ryml::set_callbacks({nullptr, nullptr, nullptr, yaml_error_handler});
}
};
} // namespace
namespace charge_bridge::utilities {
void parse_config_impl(c4::yml::NodeRef& config, charge_bridge_config& c, std::filesystem::path const& config_path,
ryml::Parser& parser) {
auto get_node = [&config, &parser](auto& data, std::string const& main, std::string const& sub = "") {
auto [node_str, node] = find_node(config, main, sub);
get_node_impl(node, parser, node_str, data);
};
auto get_node_or_default = [&get_node, &config](auto& data, std::string const& main, std::string const& sub,
auto fallback) {
auto [node_str, node] = find_node(config, main, sub);
if (node.invalid()) {
data = fallback;
return;
}
try {
get_node(data, main, sub);
} catch (...) {
data = fallback;
}
};
auto get_block = [&config, &c](std::string const& block, auto& block_cfg, auto const& ftor) {
bool enable = false;
auto block_str = ryml::to_csubstr(block);
if (not config.find_child(block_str).invalid()) {
if (config[block_str].find_child("enable").invalid()) {
enable = true;
} else {
decode(config[block_str]["enable"], enable);
}
}
if (enable) {
block_cfg.emplace();
ftor(*block_cfg, block);
block_cfg->cb = c.cb_name;
block_cfg->item = block;
}
};
get_node(c.cb_name, "charge_bridge", "name");
get_node(c.cb_remote, "charge_bridge", "ip");
c.cb_port = g_cb_port_management;
get_block("can_0", c.can0, [&](auto& cfg, auto const& main) {
get_node(cfg.can_device, main, "local");
cfg.cb_port = g_cb_port_can0;
cfg.cb_remote = c.cb_remote;
});
get_block("serial_1", c.serial1, [&](auto& cfg, auto const& main) {
get_node(cfg.serial_device, main, "local");
cfg.cb_port = g_cb_port_serial_1;
cfg.cb_remote = c.cb_remote;
});
get_block("serial_2", c.serial2, [&](auto& cfg, auto const& main) {
get_node(cfg.serial_device, main, "local");
cfg.cb_port = g_cb_port_serial_2;
cfg.cb_remote = c.cb_remote;
});
// FIXME (JH) serial3 not availabe in first release
// get_block("serial_3", c.serial3, [&](auto& cfg, auto const& main) {
// get_node(main, "local", cfg.serial_device);
// get_node(main, "port", cfg.cb_port);
// cfg.cb_remote = c.cb_remote;
// });
get_block("plc", c.plc, [&](auto& cfg, auto const& main) {
get_node(cfg.plc_tap, main, "tap");
get_node(cfg.plc_ip, main, "ip");
get_node(cfg.plc_netmaks, main, "netmask");
get_node(cfg.plc_mtu, main, "mtu");
cfg.cb_port = g_cb_port_plc;
cfg.cb_remote = c.cb_remote;
});
{
bool wants_ev = false;
bool wants_evse = false;
get_node_or_default(wants_ev, "ev_bsp", "enable", false);
get_node_or_default(wants_evse, "evse_bsp", "enable", false);
if (wants_ev && wants_evse) {
std::cerr << "Configuration error: Cannot enable EVSE and EV BSP at the same time" << std::endl;
throw std::exception();
}
}
get_block("evse_bsp", c.bsp, [&](auto& cfg, auto const& main) {
cfg.cb_port = g_cb_port_evse_bsp;
cfg.api.evse.enabled = true;
get_node(cfg.api.evse.module_id, main, "module_id");
get_node(cfg.api.mqtt_remote, main, "mqtt_remote");
get_node_or_default(cfg.api.mqtt_bind, main, "mqtt_bind", "");
get_node(cfg.api.mqtt_port, main, "mqtt_port");
get_node_or_default(cfg.api.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms",
default_mqtt_ping_interval_ms);
cfg.cb_remote = c.cb_remote;
get_node(cfg.api.evse.capabilities, main, "capabilities");
get_node(cfg.api.ovm.enabled, main, "ovm_enabled");
get_node(cfg.api.ovm.module_id, main, "ovm_module_id");
});
if (not c.bsp.has_value()) {
get_block("ev_bsp", c.bsp, [&](auto& cfg, auto const& main) {
cfg.cb_port = g_cb_port_evse_bsp;
cfg.api.ev.enabled = true;
get_node(cfg.api.ev.module_id, main, "module_id");
get_node(cfg.api.mqtt_remote, main, "mqtt_remote");
get_node_or_default(cfg.api.mqtt_bind, main, "mqtt_bind", "");
get_node(cfg.api.mqtt_port, main, "mqtt_port");
get_node_or_default(cfg.api.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms",
default_mqtt_ping_interval_ms);
cfg.cb_remote = c.cb_remote;
get_node(cfg.api.ovm.enabled, main, "ovm_enabled");
get_node(cfg.api.ovm.module_id, main, "ovm_module_id");
});
}
get_block("gpio", c.gpio, [&](auto& cfg, auto const& main) {
get_node(cfg.interval_s, main, "interval_s");
get_node(cfg.mqtt_remote, main, "mqtt_remote");
get_node_or_default(cfg.mqtt_bind, main, "mqtt_bind", "");
get_node(cfg.mqtt_port, main, "mqtt_port");
get_node_or_default(cfg.mqtt_ping_interval_ms, main, "mqtt_ping_interval_ms", default_mqtt_ping_interval_ms);
cfg.cb_remote = c.cb_remote;
cfg.cb_port = c.cb_port;
});
get_block("heartbeat", c.heartbeat, [&](auto& cfg, auto const& main) {
get_node_or_default(cfg.interval_s, main, "interval_s", 1);
get_node_or_default(cfg.connection_to_s, main, "connection_to_s", 3 * cfg.interval_s);
cfg.cb_remote = c.cb_remote;
cfg.cb_port = c.cb_port;
get_node(cfg.cb_config.network, "charge_bridge");
get_node(cfg.cb_config.safety, "safety");
std::memset(cfg.cb_config.gpios, 0, CB_NUMBER_OF_GPIOS * sizeof(CbGpioConfig));
std::memset(cfg.cb_config.uarts, 0, CB_NUMBER_OF_UARTS * sizeof(CbUartConfig));
if (c.serial1) {
get_node(cfg.cb_config.uarts[0], "serial_1");
}
if (c.serial2) {
get_node(cfg.cb_config.uarts[1], "serial_2");
}
// FIXME (JH) serial 3 not available in first release
// if (c.serial3) {
// get_main_node("serial_3", cfg.cb_config.uarts[2]);
// }
if (c.gpio) {
for (auto i = 0; i < CB_NUMBER_OF_GPIOS; ++i) {
get_node(cfg.cb_config.gpios[i], "gpio", "gpio_" + std::to_string(i));
}
}
if (c.can0) {
get_node(cfg.cb_config.can, "can_0");
}
get_node(cfg.cb_config.plc_powersaving_mode, "plc", "powersaving_mode");
cfg.cb_config.config_version = CB_CONFIG_VERSION;
});
get_node(c.firmware.fw_path, "charge_bridge", "fw_file");
get_node(c.firmware.fw_update_on_start, "charge_bridge", "fw_update_on_start");
// If the path to the firmware file is relative, make it relative to the config file
std::filesystem::path fw_path = c.firmware.fw_path;
if (fw_path.is_relative()) {
c.firmware.fw_path = config_path.parent_path().append(c.firmware.fw_path);
}
c.firmware.cb_remote = c.cb_remote;
c.firmware.cb_port = c.cb_port;
c.firmware.cb = c.cb_name;
}
charge_bridge_config set_config_placeholders(charge_bridge_config const& src, charge_bridge_config& result,
std::string const& ip, std::size_t index) {
auto index_str = std::to_string(index);
result = src;
auto replace = [index_str](std::string& src) { replace_all_in_place(src, "##", index_str); };
result.cb_remote = ip;
result.firmware.cb_remote = ip;
replace(result.cb_name);
result.firmware.cb = result.cb_name;
if (result.can0.has_value()) {
result.can0->cb_remote = ip;
result.can0->cb = result.cb_name;
replace(result.can0->can_device);
}
if (result.serial1.has_value()) {
result.serial1->cb_remote = ip;
result.serial1->cb = result.cb_name;
replace(result.serial1->serial_device);
}
if (result.serial2.has_value()) {
result.serial2->cb_remote = ip;
result.serial2->cb = result.cb_name;
replace(result.serial2->serial_device);
}
if (result.serial3.has_value()) {
result.serial3->cb_remote = ip;
result.serial3->cb = result.cb_name;
replace(result.serial3->serial_device);
}
if (result.plc.has_value()) {
result.plc->cb_remote = ip;
result.plc->cb = result.cb_name;
replace(result.plc->plc_tap);
}
if (result.bsp.has_value()) {
result.bsp->cb_remote = ip;
result.bsp->cb = result.cb_name;
replace(result.bsp->api.evse.module_id);
replace(result.bsp->api.ev.module_id);
replace(result.bsp->api.ovm.module_id);
}
if (result.heartbeat.has_value()) {
result.heartbeat->cb = result.cb_name;
result.heartbeat->cb_remote = ip;
}
if (result.gpio.has_value()) {
result.gpio->cb = result.cb_name;
result.gpio->cb_remote = ip;
}
if (result.heartbeat.has_value()) {
auto& raw = result.heartbeat->cb_config.network.mdns_name;
std::string item = raw;
replace(item);
auto limit = sizeof(raw);
if (item.size() > limit) {
item = "cb_" + index_str;
std::cout << "WARNING: Replacement for mdns_name is too long. Fallback to '" + item + "'" << std::endl;
}
std::memset(raw, 0, limit);
std::memcpy(raw, item.c_str(), std::min(item.size(), limit));
result.heartbeat->cb_remote = ip;
result.heartbeat->cb = result.cb_name;
}
return result;
}
std::vector<charge_bridge_config> parse_config_multi(std::string const& config_file) {
const static RymlCallbackInitializer ryml_callback_initializer;
try {
ryml::EventHandlerTree evt_handler = {};
ryml::Parser parser(&evt_handler, ryml::ParserOptions().locations(true));
ryml::Tree config_tree;
load_yaml_file(config_file, &parser, &config_tree);
c4::yml::NodeRef config = config_tree.rootref();
if (config.invalid()) {
std::cerr << "Config file not found: " << config_file << std::endl;
return {};
}
charge_bridge_config base_config;
parse_config_impl(config, base_config, config_file, parser);
auto ip_list_node = config.find_child("charge_bridge_ip_list");
if (ip_list_node.invalid()) {
return {base_config};
}
std::vector<std::string> ip_list;
ip_list_node >> ip_list;
std::vector<charge_bridge_config> cb_config_list(ip_list.size());
for (std::size_t i = 0; i < ip_list.size(); ++i) {
set_config_placeholders(base_config, cb_config_list[i], ip_list[i], i);
}
return cb_config_list;
} catch (...) {
std::cerr << "FAILED to parse configuration!" << std::endl;
}
return {};
}
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,97 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "protocol/cb_config.h"
#include <charge_bridge/charge_bridge.hpp>
#include <sstream>
namespace charge_bridge::utilities {
std::string to_string(CbCanBaudrate value) {
switch (value) {
case CBCBR_125000:
return "125000";
case CBCBR_250000:
return "250000";
case CBCBR_500000:
return "500000";
case CBCBR_1000000:
return "1000000";
default:
break;
}
return "Invalid bitrate";
}
std::string to_string(CbUartBaudrate value) {
switch (value) {
case CBUBR_9600:
return "9600";
case CBUBR_19200:
return "19200";
case CBUBR_38400:
return "38400";
case CBUBR_57600:
return "57600";
case CBUBR_115200:
return "115200";
case CBUBR_230400:
return "230400";
case CBUBR_250000:
return "250000";
case CBUBR_460800:
return "460800";
case CBUBR_500000:
return "500000";
case CBUBR_1000000:
return "1000000";
case CBUBR_2000000:
return "2000000";
case CBUBR_3000000:
return "3000000";
case CBUBR_4000000:
return "4000000";
case CBUBR_6000000:
return "6000000";
case CBUBR_8000000:
return "8000000";
case CBUBR_10000000:
return "10000000";
default:
break;
}
return "Invalid baudrate";
}
std::string to_string(CbUartParity value) {
switch (value) {
case CBUP_None:
return "N";
case CBUP_Odd:
return "O";
case CBUP_Even:
return "E";
default:
break;
}
return "Invalid parity";
}
std::string to_string(CbUartStopbits value) {
switch (value) {
case CBUS_OneStopBit:
return "1";
case CBUS_TwoStopBits:
return "2";
default:
break;
}
return "Invalid parity";
}
std::string to_string(CbUartConfig const& value) {
std::stringstream data;
data << to_string(value.baudrate) << " 8" << to_string(value.parity) << to_string(value.stopbits);
return data.str();
}
} // namespace charge_bridge::utilities

View File

@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include <charge_bridge/utilities/string.hpp>
#include <sstream>
namespace charge_bridge::utilities {
bool string_starts_with(std::string_view const& str, std::string_view const& pattern) {
return str.rfind(pattern, 0) == 0;
}
bool string_ends_with(std::string const& str, std::string const& pattern) {
if (pattern.size() > str.size())
return false;
return std::equal(pattern.rbegin(), pattern.rend(), str.rbegin());
}
std::string string_after_pattern(std::string_view const& str, std::string_view const& pattern) {
if (charge_bridge::utilities::string_starts_with(str, pattern)) {
return static_cast<std::string>(str.substr(pattern.length()));
}
return "";
}
std::string& replace_all_in_place(std::string& source, std::string const& placeholder, std::string const& substitute) {
if (placeholder.empty()) {
return source;
}
std::size_t start_pos = 0;
while ((start_pos = source.find(placeholder, start_pos)) != std::string::npos) {
source.replace(start_pos, placeholder.length(), substitute);
start_pos += substitute.length();
}
return source;
}
std::string replace_all(std::string const& source, std::string const& placeholder, std::string const& substitute) {
std::string result = source;
return replace_all_in_place(result, placeholder, substitute);
}
std::set<std::string> csv_to_set(std::string const& str) {
std::set<std::string> result;
std::stringstream ss(str);
std::string item;
while (std::getline(ss, item, ',')) {
if (!item.empty()) {
result.insert(item);
}
}
return result;
}
} // namespace charge_bridge::utilities

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