Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ jobs:
# partition, so it proves codegen requests the partition exactly once
# — add_partition() refuses a duplicate name.
- tests/build/uds/test-partition.esp32-c6-idf.yaml
- tests/build/ccp/test.esp32-c6-idf.yaml
- tests/build/ccp/test-write.esp32-c6-idf.yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand All @@ -111,3 +113,19 @@ jobs:
pip install clang-format==13.0.1
find components -name '*.cpp' -o -name '*.h' -o -name '*.tcc' | \
xargs clang-format --dry-run --Werror

required-checks:
name: Required checks
if: always()
needs: [validate, host-tests, compile, clang-format]
runs-on: ubuntu-latest
steps:
# One stable context for branch protection to require, instead of
# pinning to every individual compile-matrix leg by name — a build
# fixture can be renamed or added without touching the ruleset.
- name: All required jobs passed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" || "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs did not succeed."
exit 1
fi
196 changes: 196 additions & 0 deletions components/ccp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# CCP 2.1 single-frame master ESPHome codegen and automation registration.
"""CCP 2.1 single-frame master over a can_gateway observation port."""

from __future__ import annotations

from esphome import automation
import esphome.codegen as cg
from esphome.components.can_gateway import GatewayPort
import esphome.config_validation as cv
from esphome.const import CONF_ID
import esphome.final_validate as fv

CODEOWNERS = ["@kipp-ing"]
DEPENDENCIES = ["can_gateway"]
MULTI_CONF = True

ccp_ns = cg.esphome_ns.namespace("ccp")
CcpHub = ccp_ns.class_("CcpHub", cg.Component)
CcpConnectAction = ccp_ns.class_("CcpConnectAction", automation.Action)
CcpDisconnectAction = ccp_ns.class_("CcpDisconnectAction", automation.Action)
CcpGetVersionAction = ccp_ns.class_("CcpGetVersionAction", automation.Action)
CcpExchangeIdAction = ccp_ns.class_("CcpExchangeIdAction", automation.Action)
CcpSetMtaAction = ccp_ns.class_("CcpSetMtaAction", automation.Action)
CcpUploadAction = ccp_ns.class_("CcpUploadAction", automation.Action)
CcpShortUploadAction = ccp_ns.class_("CcpShortUploadAction", automation.Action)
CcpDownloadAction = ccp_ns.class_("CcpDownloadAction", automation.Action)
CcpSelectCalPageAction = ccp_ns.class_("CcpSelectCalPageAction", automation.Action)
CcpReadMemoryAction = ccp_ns.class_("CcpReadMemoryAction", automation.Action)
CcpWriteMemoryAction = ccp_ns.class_("CcpWriteMemoryAction", automation.Action)
CcpDaqAction = ccp_ns.class_("CcpDaqAction", automation.Action)
CcpRawAction = ccp_ns.class_("CcpRawAction", automation.Action)
CcpSetEnabledAction = ccp_ns.class_("CcpSetEnabledAction", automation.Action)

CONF_CAN_GATEWAY_ID = "can_gateway_id"
CONF_COMMAND_ID = "command_id"
CONF_RESPONSE_ID = "response_id"
CONF_STATION_ADDRESS = "station_address"
CONF_BYTE_ORDER = "byte_order"
CONF_RESPONSE_TIMEOUT = "response_timeout"
CONF_ALLOW_WRITE = "allow_write"
CONF_ON_CONNECTED = "on_connected"
CONF_ON_RESPONSE = "on_response"
CONF_ON_ERROR = "on_error"
CONF_ON_DAQ = "on_daq"
CONF_DATA = "data"
CONF_END = "end"
CONF_MTA = "mta"
CONF_EXT = "ext"
CONF_ADDRESS = "address"
CONF_SIZE = "size"
CONF_LENGTH = "length"
CONF_ENABLED = "enabled"


def _can_id(value):
value = cv.hex_uint32_t(value)
if value > 0x7FF:
raise cv.Invalid("must be an 11-bit CAN identifier (0x000..0x7FF)")
return value


def _byte_order(value):
return cv.one_of("little", "big", lower=True)(value)


CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(CcpHub),
cv.Required(CONF_CAN_GATEWAY_ID): cv.use_id(GatewayPort),
cv.Optional(CONF_COMMAND_ID, default=0x700): _can_id,
cv.Optional(CONF_RESPONSE_ID, default=0x701): _can_id,
cv.Optional(CONF_STATION_ADDRESS, default=0x0001): cv.hex_uint16_t,
cv.Optional(CONF_BYTE_ORDER, default="little"): _byte_order,
cv.Optional(CONF_RESPONSE_TIMEOUT, default="100ms"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ALLOW_WRITE, default=False): cv.boolean,
cv.Optional(CONF_ON_CONNECTED): automation.validate_automation(),
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(),
cv.Optional(CONF_ON_DAQ): automation.validate_automation(),
}
).extend(cv.COMPONENT_SCHEMA)


def _final_validate(config):
"""One CCP master owns a port, avoiding independent strict-response slots on it."""
hubs = fv.full_config.get().get("ccp", [])
this_id = str(config[CONF_ID])
port = str(config[CONF_CAN_GATEWAY_ID])
for hub in hubs:
if str(hub[CONF_ID]) != this_id and str(hub[CONF_CAN_GATEWAY_ID]) == port:
raise cv.Invalid(f"CCP hub port '{port}' is already used by another CCP hub")
return config


FINAL_VALIDATE_SCHEMA = _final_validate


async def to_code(config):
port = await cg.get_variable(config[CONF_CAN_GATEWAY_ID])
var = cg.new_Pvariable(config[CONF_ID], port)
await cg.register_component(var, config)
cg.add_define("USE_CAN_GATEWAY_OBSERVE")
if config[CONF_ALLOW_WRITE]:
cg.add_define("USE_CCP_WRITE")
cg.add(var.set_command_id(config[CONF_COMMAND_ID]))
cg.add(var.set_response_id(config[CONF_RESPONSE_ID]))
cg.add(var.set_station_address(config[CONF_STATION_ADDRESS]))
cg.add(var.set_byte_order(cg.RawExpression("ccp::ByteOrder::LITTLE" if config[CONF_BYTE_ORDER] == "little" else "ccp::ByteOrder::BIG")))
cg.add(var.set_response_timeout(config[CONF_RESPONSE_TIMEOUT].total_milliseconds))
cg.add(var.set_allow_write(config[CONF_ALLOW_WRITE]))
for conf in config.get(CONF_ON_CONNECTED, []):
await automation.build_callback_automation(var, "add_on_connected_callback", [], conf)
for conf in config.get(CONF_ON_RESPONSE, []):
await automation.build_callback_automation(var, "add_on_response_callback", [(cg.uint8, "command"), (cg.uint8, "return_code"), (cg.std_vector.template(cg.uint8), "data")], conf)
for conf in config.get(CONF_ON_ERROR, []):
await automation.build_callback_automation(var, "add_on_error_callback", [(cg.uint8, "command"), (cg.uint8, "return_code")], conf)
for conf in config.get(CONF_ON_DAQ, []):
await automation.build_callback_automation(var, "add_on_daq_callback", [(cg.uint8, "pid"), (cg.std_vector.template(cg.uint8), "data")], conf)


_HUB = {cv.Required(CONF_ID): cv.use_id(CcpHub)}
_BYTES = cv.templatable(cv.ensure_list(cv.hex_uint8_t))
_U8 = lambda default=None: cv.templatable(cv.int_range(min=0, max=0xFF))
_U16 = lambda: cv.templatable(cv.int_range(min=0, max=0xFFFF))
_U32 = lambda: cv.templatable(cv.int_range(min=0, max=0xFFFFFFFF))


async def _action(config, action_id, template_arg, cls):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var


@automation.register_action("ccp.connect", CcpConnectAction, cv.Schema(_HUB), synchronous=True)
async def ccp_connect(config, action_id, template_arg, args): return await _action(config, action_id, template_arg, CcpConnectAction)

@automation.register_action("ccp.disconnect", CcpDisconnectAction, cv.Schema({**_HUB, cv.Optional(CONF_END, default=False): cv.templatable(cv.boolean)}), synchronous=True)
async def ccp_disconnect(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpDisconnectAction); cg.add(var.set_end(await cg.templatable(config[CONF_END], args, bool))); return var

@automation.register_action("ccp.get_version", CcpGetVersionAction, cv.Schema(_HUB), synchronous=True)
async def ccp_version(config, action_id, template_arg, args): return await _action(config, action_id, template_arg, CcpGetVersionAction)

@automation.register_action("ccp.exchange_id", CcpExchangeIdAction, cv.Schema(_HUB), synchronous=True)
async def ccp_exchange_id(config, action_id, template_arg, args): return await _action(config, action_id, template_arg, CcpExchangeIdAction)

@automation.register_action("ccp.set_mta", CcpSetMtaAction, cv.Schema({**_HUB, cv.Optional(CONF_MTA, default=0): _U8(), cv.Required(CONF_EXT): _U8(), cv.Required(CONF_ADDRESS): _U32()}), synchronous=True)
async def ccp_set_mta(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpSetMtaAction)
cg.add(var.set_mta(await cg.templatable(config[CONF_MTA], args, cg.uint8))); cg.add(var.set_ext(await cg.templatable(config[CONF_EXT], args, cg.uint8))); cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint32))); return var

@automation.register_action("ccp.upload", CcpUploadAction, cv.Schema({**_HUB, cv.Required(CONF_SIZE): cv.templatable(cv.int_range(min=1, max=5))}), synchronous=True)
async def ccp_upload(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpUploadAction); cg.add(var.set_size(await cg.templatable(config[CONF_SIZE], args, cg.uint8))); return var

@automation.register_action("ccp.short_upload", CcpShortUploadAction, cv.Schema({**_HUB, cv.Required(CONF_SIZE): cv.templatable(cv.int_range(min=1, max=5)), cv.Required(CONF_EXT): _U8(), cv.Required(CONF_ADDRESS): _U32()}), synchronous=True)
async def ccp_short_upload(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpShortUploadAction)
cg.add(var.set_size(await cg.templatable(config[CONF_SIZE], args, cg.uint8))); cg.add(var.set_ext(await cg.templatable(config[CONF_EXT], args, cg.uint8))); cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint32))); return var

async def _data_action(config, action_id, template_arg, args, cls):
var = await _action(config, action_id, template_arg, cls)
if cg.is_template(config[CONF_DATA]): cg.add(var.set_data_template(await cg.templatable(config[CONF_DATA], args, cg.std_vector.template(cg.uint8))))
else: cg.add(var.set_data_static(config[CONF_DATA]))
return var

@automation.register_action("ccp.download", CcpDownloadAction, cv.Schema({**_HUB, cv.Required(CONF_DATA): _BYTES}), synchronous=True)
async def ccp_download(config, action_id, template_arg, args): return await _data_action(config, action_id, template_arg, args, CcpDownloadAction)

@automation.register_action("ccp.select_cal_page", CcpSelectCalPageAction, cv.Schema(_HUB), synchronous=True)
async def ccp_select_page(config, action_id, template_arg, args): return await _action(config, action_id, template_arg, CcpSelectCalPageAction)

@automation.register_action("ccp.read_memory", CcpReadMemoryAction, cv.Schema({**_HUB, cv.Required(CONF_EXT): _U8(), cv.Required(CONF_ADDRESS): _U32(), cv.Required(CONF_LENGTH): cv.templatable(cv.int_range(min=1, max=0xFFFF))}), synchronous=True)
async def ccp_read_memory(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpReadMemoryAction)
cg.add(var.set_ext(await cg.templatable(config[CONF_EXT], args, cg.uint8))); cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint32))); cg.add(var.set_length(await cg.templatable(config[CONF_LENGTH], args, cg.uint16))); return var

@automation.register_action("ccp.write_memory", CcpWriteMemoryAction, cv.Schema({**_HUB, cv.Required(CONF_EXT): _U8(), cv.Required(CONF_ADDRESS): _U32(), cv.Required(CONF_DATA): _BYTES}), synchronous=True)
async def ccp_write_memory(config, action_id, template_arg, args):
var = await _data_action(config, action_id, template_arg, args, CcpWriteMemoryAction)
cg.add(var.set_ext(await cg.templatable(config[CONF_EXT], args, cg.uint8))); cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint32))); return var

@automation.register_action("ccp.start_daq", CcpDaqAction, cv.Schema(_HUB), synchronous=True)
async def ccp_start_daq(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpDaqAction); cg.add(var.set_start(await cg.templatable(True, args, bool))); return var

@automation.register_action("ccp.stop_daq", CcpDaqAction, cv.Schema(_HUB), synchronous=True)
async def ccp_stop_daq(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpDaqAction); cg.add(var.set_start(await cg.templatable(False, args, bool))); return var

@automation.register_action("ccp.raw", CcpRawAction, cv.Schema({**_HUB, cv.Required(CONF_DATA): _BYTES}), synchronous=True)
async def ccp_raw(config, action_id, template_arg, args): return await _data_action(config, action_id, template_arg, args, CcpRawAction)

@automation.register_action("ccp.set_enabled", CcpSetEnabledAction, cv.Schema({**_HUB, cv.Required(CONF_ENABLED): cv.templatable(cv.boolean)}), synchronous=True)
async def ccp_enabled(config, action_id, template_arg, args):
var = await _action(config, action_id, template_arg, CcpSetEnabledAction); cg.add(var.set_enabled(await cg.templatable(config[CONF_ENABLED], args, bool))); return var
Loading