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,34 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
# add_subdirectory(test-tool)
target_sources(${MODULE_NAME}
PRIVATE
"can_driver_acdc/CanBus.cpp"
"can_driver_acdc/CanPackets.cpp"
"can_driver_acdc/InfyCanDevice.cpp"
)
target_link_libraries(${MODULE_NAME}
PRIVATE
Pal::Sigslot
everest::io
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/power_supply_DCImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "InfyPower.hpp"
namespace module {
void InfyPower::init() {
acdc = std::make_unique<InfyCanDevice>();
acdc->set_can_device(config.can_device);
acdc->set_config_values(config.module_addresses, config.group_address, config.device_connection_timeout_s,
config.controller_address);
invoke_init(*p_main);
}
void InfyPower::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,65 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef INFY_POWER_HPP
#define INFY_POWER_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
#include "can_driver_acdc/InfyCanDevice.hpp"
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
std::string can_device;
std::string module_addresses;
int group_address;
int device_connection_timeout_s;
double conversion_efficiency_export;
int controller_address;
};
class InfyPower : public Everest::ModuleBase {
public:
InfyPower() = delete;
InfyPower(const ModuleInfo& info, std::unique_ptr<power_supply_DCImplBase> p_main, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), config(config){};
const std::unique_ptr<power_supply_DCImplBase> p_main;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
std::unique_ptr<InfyCanDevice> acdc;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // INFY_POWER_HPP

View File

@@ -0,0 +1,408 @@
# InfyPower ACDC Power Supply Driver
Driver module for InfyPower ACDC power supplies supporting the V1.13 CAN protocol. This module can control multiple InfyPower modules (up to 60 modules theoretically, **recommended maximum 4-6 devices** for reliable operation, typically 2-6 in field installations) using either fixed addressing or automatic group discovery.
## Overview
This is an **EVerest Hardware Driver** module that implements the standardized `power_supply_DC` interface. It translates EVerest's generic power supply commands into InfyPower-specific CAN protocol messages, enabling seamless integration of InfyPower hardware into EVerest-based charging systems.
**Key Integration Points:**
- **Implements**: `power_supply_DC` interface for standardized power supply control
- **Communicates**: Via CAN bus using InfyPower V1.13 protocol
- **Provides**: Voltage/current control, telemetry, error reporting, and capability detection
- **Integrates**: With EVerest's error management, logging, and configuration systems
## Features
- **Multi-Module Support**: Control multiple InfyPower modules simultaneously with current sharing (up to 60 modules theoretically)
- **Dual Operating Modes**: Fixed address mode or automatic group discovery mode
- **Comprehensive Error Handling**: Maps all InfyPower errors to standardized power_supply_DC framework errors
- **Serial Number Identification**: Uses Command 0x0B barcode reading for unique device identification
- **Thread-Safe Operation**: Robust concurrent access protection for telemetry data
- **Automatic Recovery**: Detects and recovers from offline modules automatically
## Configuration
### Basic Configuration
```yaml
# Required: CAN interface
can_device: "can0"
# Choose ONE of the following addressing modes:
# Option 1: Fixed Address Mode (recommended for known setups)
module_addresses: "0,1,2" # Comma-separated list of module addresses
# Option 2: Group Discovery Mode (for automatic allocation)
group_address: 1 # Group number matching PSU dial settings
module_addresses: "" # Must be empty for group discovery
# Optional settings
device_connection_timeout_s: 10 # Module offline timeout
conversion_efficiency_export: 0.95 # Power conversion efficiency
controller_address: 240 # CAN controller address (0xF0)
```
### Configuration Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `can_device` | string | `"can0"` | CAN interface name (e.g., "can0", "vcan0") |
| `module_addresses` | string | `""` | Fixed addresses: "0,1,2" or empty for group discovery |
| `group_address` | integer | `0` | Group number for discovery mode (matches PSU dial) |
| `device_connection_timeout_s` | integer | `15` | **CRITICAL**: Must be > 10s module timeout (see Safety Notes) |
| `conversion_efficiency_export` | number | `0.95` | Power conversion efficiency (0.0-1.0) |
| `controller_address` | integer | `240` | CAN controller address (0xF0 = 240 decimal) |
## ⚠️ Safety Notes
### Critical Timeout Configuration
**IMPORTANT**: The `device_connection_timeout_s` parameter has critical safety implications and must be configured correctly.
#### InfyPower Module Internal Timeout
Per InfyPower V1.13 protocol specification: *"If the charger module did not receive the message for 10 seconds from the controller, the charger module will off."*
This means each module has a **10-second internal timeout** and will **automatically turn OFF its power output** if it doesn't receive CAN messages within this period.
#### EVerest Driver Timeout Requirement
The EVerest `device_connection_timeout_s` **MUST be longer** than the module's internal timeout. Here's why:
**❌ DANGEROUS - Driver timeout < Module timeout:**
```
Time: 0s → 8s → 10s → 12s
Driver: ✓ → ✓ → ✗ (removes module from capabilities) → ✗
Module: ✓ → ✓ → ✓ (still delivering power!) → ✗ (finally turns off)
```
**Problem**: Driver advertises reduced capability while module still delivers full power → **overcurrent risk**
**✅ SAFE - Driver timeout > Module timeout:**
```
Time: 0s → 8s → 10s → 12s → 15s
Driver: ✓ → ✓ → ✓ (still includes in capabilities) → ✓ → ✗ (removes module)
Module: ✓ → ✓ → ✗ (safely turned off) → ✗ → ✗
```
**Result**: Driver keeps module in capabilities until certain it's OFF → **no overcurrent risk**
#### Configuration Recommendation
```yaml
# InfyPower modules have 10s internal timeout (per V1.13 protocol):
device_connection_timeout_s: 15 # Safe default: 15s > 10s module timeout ✓
device_connection_timeout_s: 11 # Minimum safe: 11s > 10s module timeout
# NEVER configure equal to or shorter than module timeout:
device_connection_timeout_s: 10 # DANGEROUS: Equal to module timeout
device_connection_timeout_s: 8 # DANGEROUS: 8s < 10s module timeout
```
#### Safety Principle
- **Overestimating capabilities is safe**: System delivers less than advertised
- **Underestimating capabilities is dangerous**: Could cause overcurrent conditions
- **Always err on the side of longer timeouts** for safety
## Operating Modes
### 1. Fixed Address Mode (Recommended)
Use when you know the exact addresses of your InfyPower modules.
**Configuration:**
```yaml
module_addresses: "0,1,2" # List specific module addresses
group_address: 0 # Ignored in this mode
```
**Advantages:**
- Predictable, deterministic addressing
- Faster startup (no discovery phase)
- Direct control over which modules are used
**Use Cases:**
- Production installations with known hardware
- Testing with specific module configurations
- When you need guaranteed addressing consistency
### 2. Group Discovery Mode
Use when modules are configured for automatic address allocation.
**Configuration:**
```yaml
module_addresses: "" # Must be empty
group_address: 1 # Must match PSU group dial setting
```
**Advantages:**
- Automatic module detection
- Hot-plugging support
- Easier setup for dynamic configurations
- Adapts to hardware changes
**Requirements:**
- PSU dial switches must be set to matching group number
- All modules in group must be powered on during discovery
**Use Cases:**
- Development and testing environments
- Field installations with variable hardware
- When module addresses may change
## CAN Protocol Details
### Protocol Specification
- **Standard**: InfyPower V1.13 CAN Communication Protocol
- **Bus Type**: CAN 2.0B Extended Frame Format (29-bit identifiers)
- **Baud Rate**: 125 kbps
- **Message Length**: 8 bytes (fixed)
- **Termination**: 120Ω resistors at both ends
### CAN ID Structure (29-bit)
```
Bits 28-26: Error Code (3 bits)
Bits 25-22: Device Number (4 bits) - 0x0A=single, 0x0B=group
Bits 21-16: Command Number (6 bits)
Bits 15-8: Destination Address (8 bits)
Bits 7-0: Source Address (8 bits) - Controller address
```
### Key Commands Used
| Command | ID | Description | Usage |
|---------|----|---------|----|
| Module Count | 0x02 | Get number of modules in group | Group discovery |
| Module VI | 0x03 | Read voltage/current | Regular telemetry |
| Module Status | 0x04 | Read status flags | Error monitoring |
| Module Capabilities | 0x0A | Read voltage/current/power limits | Capability detection |
| Module Barcode | 0x0B | Read serial number | Device identification |
| Module VI After Diode | 0x0C | Read external voltage/available current | Advanced telemetry |
| Set Module On/Off | 0x1A | Enable/disable module | Control |
| Set Module VI | 0x1C | Set voltage/current | Control |
### Communication Flow
#### Initialization
1. **Discovery** (Group mode): Send Module Count request to detect modules
2. **Capabilities**: Read max voltage/current/power from each module
3. **Serial Numbers**: Read barcode for device identification
4. **Safety**: Ensure all modules start in OFF state
#### Regular Operation (1-second intervals)
1. **Telemetry**: Read voltage, current, status from all modules
2. **Error Monitoring**: Check status flags for alarm conditions
3. **Timeout Detection**: Remove unresponsive modules from active list
4. **Recovery**: Automatically re-add modules that come back online
## Error Handling
### Error Mapping
InfyPower errors are automatically mapped to standardized power_supply_DC errors:
| InfyPower Error | power_supply_DC Error | Severity | Description |
|-----------------|----------------------|----------|-------------|
| OverVoltage | OverVoltageDC | High | Output voltage too high |
| UnderVoltage | UnderVoltageDC | High | Output voltage too low |
| OverTemperature | OverTemperature | High | Module overheating |
| OverCurrent | OverCurrentDC | High | Output current too high |
| InputVoltage | UnderVoltageAC | High | AC input voltage issues |
| InternalFault | HardwareFault | High | Internal module fault |
| InputPhaseLoss | VendorError | High | AC phase loss |
| FanFault | VendorWarning | Medium | Cooling fan failure |
| CommunicationFault | CommunicationFault | High | Lost communication |
### Error Recovery
- **Individual Module Errors**: Other modules continue operating
- **Communication Timeout**: Modules automatically removed from active list
- **Communication Recovery**: Modules automatically re-added when responding
- **System-Wide Faults**: All modules forced off for safety
### Logging Format
Error messages follow standardized format for easy identification:
```
Infy[0x00/SN_130C15A3FB06A8_V]: Module fault alarm activated
Infy[0x01]: Communication fault detected, FORCE mode to off
```
## Current Sharing
When multiple modules are active, current is automatically shared equally:
**Example with 3 modules:**
- Request: 150A total
- Per-module: 50A each
- Voltage: Same for all modules
**Load Balancing:**
- Automatic equal current distribution
- Voltage synchronized across all modules
- Individual module limits respected
- Failed modules automatically excluded
## Troubleshooting
### Common Issues
#### 0. ⚠️ SAFETY: Always check timeout configuration first
**Before troubleshooting**: Verify `device_connection_timeout_s` > module internal timeout (see Safety Notes above)
#### 1. No modules detected
**Symptoms**: "No active modules" warnings
**Causes**:
- CAN interface down: `sudo ip link set can0 up type can bitrate 125000`
- Wrong CAN device name: Check `can_device` configuration
- Modules not powered or wrong group setting
**Solutions**:
```bash
# Check CAN interface
ip link show can0
# Bring up CAN interface
sudo ip link set can0 up type can bitrate 125000
# Monitor CAN traffic
candump can0
```
#### 2. Modules going offline
**Symptoms**: "module communication expired" messages
**Causes**:
- CAN bus errors or noise
- Loose connections
- Power supply issues
**Solutions**:
- Check CAN bus termination (120Ω resistors)
- Verify all connections are secure
- Monitor for CAN errors in system logs
#### 3. Current not shared properly
**Symptoms**: Uneven current distribution
**Causes**:
- Modules have different capabilities
- Some modules in fault state
- Configuration mismatch
**Solutions**:
- Check module status in logs
- Verify all modules have same specifications
- Check for error conditions
#### 4. Group discovery not working
**Symptoms**: No modules found in group mode
**Causes**:
- Wrong group_address setting
- PSU dial switches not matching
- Modules not ready during discovery
**Solutions**:
- Verify PSU dial switch settings match group_address
- Ensure all modules powered on before starting
### Debug Information
Enable debug logging to see detailed protocol information:
```yaml
# In your EVerest configuration
logging:
InfyPower: debug
```
Monitor CAN traffic:
```bash
# Raw CAN monitoring
candump can0
# With timestamp
candump -t z can0
# Filter InfyPower traffic (controller address 0xF0)
candump can0 | grep F0
```
## Hardware Requirements
### CAN Bus Setup
- **Topology**: Linear bus with 120Ω termination at both ends
- **Cable**: Twisted pair CAN cable (CAN_H, CAN_L)
- **Max Distance**: 40m at 125 kbps
- **Max Devices**: Up to 60 InfyPower modules + 1 controller (typically 2-6 in field installations)
### PSU Configuration
- Set module addresses or group dial switches before powering on
- Ensure all modules use same firmware version
- Configure appropriate current/voltage limits
## Development Notes
### Architecture Constraints (Critical Implementation Details)
#### Single-Threaded CAN Communication
**IMPORTANT**: The entire CAN communication system operates in a single thread:
- `rx_handler()` and `poll_status_handler()` execute in the same thread
- No concurrent CAN operations - all message processing is sequential
- This design ensures deterministic message handling and eliminates race conditions
- **Implication**: Blocking operations in handlers affect entire CAN communication
#### Configuration Immutability
**CRITICAL**: Configuration is set exactly once during initialization:
- `set_config_values()` called only once at startup
- No runtime configuration changes supported
- Module addresses and operating mode cannot be changed after initialization
- **Implication**: Configuration errors require full module restart
### Security Model
#### No CAN Bus Security
**IMPORTANT**: InfyPower CAN protocol has no built-in security:
- **No encryption**: All CAN messages transmitted in plain text
- **No authentication**: No verification of message sender identity
- **No message integrity**: Only basic CRC provided by CAN hardware
- **Physical security required**: Secure the CAN bus physically
#### Message Validation Strategy
**Minimal Validation by Design**:
- **Only validation**: Controller address verification (`destination_address == controller_address`)
- **Only malformation check**: Message must be exactly 8 bytes
- **No payload validation**: Content validity checking beyond packet structure
- **Rationale**: Keep protocol simple and fast for real-time operation
### Adding New Commands
1. Define packet structure in `CanPackets.hpp`
2. Implement constructor and conversion in `CanPackets.cpp`
3. Add command handling in `InfyCanDevice::rx_handler()`
4. Add sending method if needed
### Thread Safety
- `telemetries` map: Protected by implicit single-thread access
- `active_module_addresses`: Protected by `active_modules_mutex`
- All signals are thread-safe via sigslot library
### Testing
- Use `vcan0` virtual CAN interface for development
- Mock modules can be simulated with `cansend` commands
- Unit tests recommended for packet encoding/decoding
## References
- [InfyPower V1.13 CAN Protocol Documentation](https://drive.google.com/file/d/1bZ9Pr7JD_ichvKmLmh8eVnZi0CdSr0xF/view?usp=drive_link) - Complete protocol specification
- [EVerest power_supply_DC Interface](https://everest.github.io/) - Standardized interface documentation
- [Linux SocketCAN Documentation](https://www.kernel.org/doc/html/latest/networking/can.html) - CAN bus programming guide
## Hardware Compatibility
### Supported InfyPower Models
- **REG Series**: REG1K0100G and compatible models
- **CEG Series**: CEG1K0100G and compatible models (includes MPPT mode support)
- **LRG Series**: LRG series charger modules
- **Protocol Version**: V1.13 and compatible versions
### Manufacturer Information
- **Manufacturer**: Shenzhen Infypower Co., Ltd
- **Website**: http://www.infypower.com
- **Protocol Version**: V1.13 (this driver implementation)
- **Document Reference**: "REG/CEG/LRG series Charger Module CAN Communication Protocol V1.13"
**Note**: Ensure your InfyPower modules support V1.13 protocol or compatible versions. Older protocol versions may not be fully compatible with this driver.

View File

@@ -0,0 +1,183 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <iostream>
#include <vector>
#include "CanBus.hpp"
#include <everest/io/can/can_recv_filter.hpp>
#include <everest/logging.hpp>
using namespace std::chrono_literals;
namespace {
constexpr auto CAN_RECOVERY_TIMER_INTERVAL = 1000ms;
constexpr auto CAN_POLL_STATUS_TIMER_INTERVAL = 1000ms;
// InfyPower protocol: space controller commands 50200 ms apart.
constexpr auto CAN_PACE_TX_INTERVAL = 50ms;
constexpr uint32_t INFY_INNER_FRAME_ID = 0x0757F800;
constexpr uint32_t INFY_INNER_FRAME_MASK = 0x1FFFF800;
std::vector<everest::lib::io::can::can_recv_filter> infy_kernel_recv_filters() {
return {everest::lib::io::can::can_recv_filter::reject_match(INFY_INNER_FRAME_ID, INFY_INNER_FRAME_MASK)};
}
} // namespace
CanBus::CanBus() : rx_thread_online{true}, can_bus(nullptr) {
}
CanBus::~CanBus() {
close_device();
}
bool CanBus::open_device(const std::string& dev) {
can_bus = std::make_unique<can::socket_can>(dev, infy_kernel_recv_filters());
can_bus->set_rx_handler([&](auto const& pl, auto&) {
uint32_t can_id = pl.get_can_id_with_flags();
this->rx_handler(can_id, pl.payload);
});
can_bus->set_error_handler([&](auto err, auto msg) {
if (err != 0) {
EVLOG_error << "CAN error: " << err << " - " << msg << std::endl;
on_error.store(true);
} else {
EVLOG_info << "CAN error cleared: " << msg << std::endl;
on_error.store(false);
}
});
ev_handler.register_event_handler(can_bus.get());
recovery_timer.set_timeout(CAN_RECOVERY_TIMER_INTERVAL);
ev_handler.register_event_handler(&recovery_timer, [&](event::fd_event_handler::event_list const& events) {
if (on_error.load()) {
EVLOG_error << "CAN error detected, attempting recovery";
can_bus->reset();
}
});
poll_status_timer.set_timeout(CAN_POLL_STATUS_TIMER_INTERVAL);
ev_handler.register_event_handler(
&poll_status_timer, [&](event::fd_event_handler::event_list const& events) { poll_status_handler(); });
pace_tx_timer.set_single_shot(true);
pace_tx_timer.disarm();
ev_handler.register_event_handler(&pace_tx_timer,
[&](event::fd_event_handler::event_list const& events) { pace_tx_handler(); });
rx_thread_handle = std::thread(&CanBus::rx_thread, this);
return true;
}
bool CanBus::close_device() {
if (!can_bus) {
return true;
}
EVLOG_info << "Closing CAN device";
rx_thread_online = false;
if (rx_thread_handle.joinable()) {
rx_thread_handle.join();
}
ev_handler.unregister_event_handler(&recovery_timer);
ev_handler.unregister_event_handler(&poll_status_timer);
ev_handler.unregister_event_handler(&pace_tx_timer);
ev_handler.unregister_event_handler(can_bus.get());
clear_paced_tx_queue();
can_bus.reset();
on_error.store(false);
EVLOG_info << "CAN device closed successfully";
return true;
}
void CanBus::rx_thread() {
EVLOG_info << "Starting CAN RX thread" << std::endl;
ev_handler.run(rx_thread_online);
}
void CanBus::enqueue_paced_tx(uint32_t can_id, std::vector<uint8_t> payload) {
m_pace_tx_queue.push_back(paced_tx_frame{can_id, std::move(payload)});
}
void CanBus::clear_paced_tx_queue() {
m_pace_tx_queue.clear();
disarm_pace_tx_timer();
}
void CanBus::start_paced_tx_cycle() {
disarm_pace_tx_timer();
if (m_pace_tx_queue.empty()) {
return;
}
auto frame = std::move(m_pace_tx_queue.front());
m_pace_tx_queue.pop_front();
_tx(frame.can_id, frame.payload);
if (!m_pace_tx_queue.empty()) {
arm_pace_tx_one_shot();
}
}
void CanBus::pace_tx_handler() {
pace_tx_timer.read();
if (m_pace_tx_queue.empty()) {
disarm_pace_tx_timer();
return;
}
auto frame = std::move(m_pace_tx_queue.front());
m_pace_tx_queue.pop_front();
_tx(frame.can_id, frame.payload);
if (!m_pace_tx_queue.empty()) {
arm_pace_tx_one_shot();
} else {
disarm_pace_tx_timer();
}
}
bool CanBus::arm_pace_tx_one_shot() {
return pace_tx_timer.set_timeout(CAN_PACE_TX_INTERVAL);
}
void CanBus::disarm_pace_tx_timer() {
pace_tx_timer.disarm();
}
bool CanBus::_tx(uint32_t can_id, const std::vector<uint8_t>& payload) {
if (payload.size() > 8) {
EVLOG_error << "CAN payload too large (" << payload.size() << " bytes), max 8 bytes allowed";
return false;
}
everest::lib::io::can::can_dataset data;
// Use plain 29-bit id only; EFF must not be OR'd into the arbitration field.
data.set_can_id_with_flags(can_id & CAN_EFF_MASK, true, false, false);
data.payload = payload;
if (on_error.load()) {
EVLOG_error << "CAN error detected, not sending frame";
return false;
}
return can_bus->tx(data);
}

View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef CAN_BUS_HPP
#define CAN_BUS_HPP
#include <atomic>
#include <condition_variable>
#include <deque>
#include <linux/can.h>
#include <mutex>
#include <thread>
#include <vector>
#include <everest/io/can/socket_can.hpp>
#include <everest/io/event/fd_event_handler.hpp>
#include <everest/io/event/timer_fd.hpp>
using namespace everest::lib::io;
class CanBus {
public:
CanBus();
virtual ~CanBus();
bool open_device(const std::string& dev);
bool close_device();
protected:
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) = 0;
virtual void poll_status_handler() = 0;
bool _tx(uint32_t can_id, const std::vector<uint8_t>& payload);
/**
* @brief Queue a frame for paced transmission on the CAN event thread.
* @details Call \ref start_paced_tx_cycle after enqueueing a poll batch.
*/
void enqueue_paced_tx(uint32_t can_id, std::vector<uint8_t> payload);
/** @brief Discard pending paced frames and disarm the pace timer. */
void clear_paced_tx_queue();
/**
* @brief Send the first queued frame immediately and schedule the rest at the pace interval.
*/
void start_paced_tx_cycle();
private:
struct paced_tx_frame {
uint32_t can_id{0};
std::vector<uint8_t> payload;
};
void pace_tx_handler();
bool arm_pace_tx_one_shot();
void disarm_pace_tx_timer();
std::unique_ptr<can::socket_can> can_bus;
std::atomic_bool on_error{false};
event::fd_event_handler ev_handler;
event::timer_fd recovery_timer;
event::timer_fd poll_status_timer;
event::timer_fd pace_tx_timer;
std::deque<paced_tx_frame> m_pace_tx_queue;
std::atomic_bool rx_thread_online;
std::thread rx_thread_handle;
void rx_thread();
};
#endif // CAN_BUS_HPP

View File

@@ -0,0 +1,220 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "CanPackets.hpp"
#include "Conversions.hpp"
#include <cstring>
#include <everest/logging.hpp>
#include <iomanip>
#include <iostream>
#include <sstream>
namespace can_packet_acdc {
namespace {
// CAN ID bit positions (as per InfyPower V1.13 protocol)
constexpr uint8_t CAN_ID_DESTINATION_ADDRESS_SHIFT = 8; // Bits 15-8
constexpr uint8_t CAN_ID_COMMAND_NUMBER_SHIFT = 16; // Bits 21-16
constexpr uint8_t CAN_ID_DEVICE_NUMBER_SHIFT = 22; // Bits 25-22
constexpr uint8_t CAN_ID_ERROR_CODE_SHIFT = 26; // Bits 28-26
} // namespace
// helper functions
uint32_t encode_can_id(uint8_t source_address, uint8_t destination_address, uint8_t command_number,
uint8_t device_number, uint8_t error_code) {
// Bits 28-26: Error code (0x00 for normal)
// Bits 25-22: Device No (4 bits)
// Bits 21-16: Command No (6 bits)
// Bits 15-8: Destination Address (8 bits)
// Bits 7-0: Source Address (8 bits) - configurable controller address (default 0xF0)
uint32_t id = source_address;
id |= destination_address << CAN_ID_DESTINATION_ADDRESS_SHIFT;
command_number &= InfyProtocol::COMMAND_MASK;
id |= command_number << CAN_ID_COMMAND_NUMBER_SHIFT;
device_number &= InfyProtocol::DEVICE_NUMBER_MASK;
id |= device_number << CAN_ID_DEVICE_NUMBER_SHIFT;
error_code &= InfyProtocol::ERROR_CODE_MASK;
id |= error_code << CAN_ID_ERROR_CODE_SHIFT;
return id;
}
uint8_t destination_address_from_can_id(uint32_t id) {
return ((id & CAN_EFF_MASK) >> CAN_ID_DESTINATION_ADDRESS_SHIFT) & 0xFF;
}
uint8_t source_address_from_can_id(uint32_t id) {
return id & CAN_EFF_MASK & 0xFF;
}
uint8_t command_number_from_can_id(uint32_t id) {
return ((id & CAN_EFF_MASK) >> CAN_ID_COMMAND_NUMBER_SHIFT) & InfyProtocol::COMMAND_MASK;
}
uint8_t error_code_from_can_id(uint32_t id) {
return ((id & CAN_EFF_MASK) >> CAN_ID_ERROR_CODE_SHIFT) & InfyProtocol::ERROR_CODE_MASK;
}
// packet definitions
PowerModuleStatus::PowerModuleStatus() {
}
PowerModuleStatus::PowerModuleStatus(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
uint8_t status0 = from_raw<uint8_t>(raw, 7);
uint8_t status1 = from_raw<uint8_t>(raw, 6);
uint8_t status2 = from_raw<uint8_t>(raw, 5);
// PowerModuleStatus bit mapping per InfyPower V1.13 protocol documentation
// status0 (byte 7): Bit0=output_short_current, Bit4=sleeping, Bit5=discharge_abnormal
// status1 (byte 6): Bit0=dc_side_off, Bit1=fault_alarm, Bit2=protection_alarm, Bit3=fan_fault_alarm,
// Bit4=over_temperature_alarm, Bit5=output_over_voltage_alarm, Bit6=walk_in_enable,
// Bit7=communication_interrupt_alarm
// status2 (byte 5): Bit0=power_limit_status, Bit1=id_repeat_alarm, Bit2=load_sharing_alarm,
// Bit3=input_phase_lost_alarm,
// Bit4=input_unbalanced_alarm, Bit5=input_low_voltage_alarm, Bit6=input_over_voltage_protection,
// Bit7=pfc_side_off
output_short_current = status0 & (1 << 0);
sleeping = status0 & (1 << 4);
discharge_abnormal = status0 & (1 << 5);
dc_side_off = status1 & (1 << 0);
fault_alarm = status1 & (1 << 1);
protection_alarm = status1 & (1 << 2);
fan_fault_alarm = status1 & (1 << 3);
over_temperature_alarm = status1 & (1 << 4);
output_over_voltage_alarm = status1 & (1 << 5);
walk_in_enable = status1 & (1 << 6);
communication_interrupt_alarm = status1 & (1 << 7);
power_limit_status = status2 & (1 << 0);
id_repeat_alarm = status2 & (1 << 1);
load_sharing_alarm = status2 & (1 << 2);
input_phase_lost_alarm = status2 & (1 << 3);
input_unbalanced_alarm = status2 & (1 << 4);
input_low_voltage_alarm = status2 & (1 << 5);
input_over_voltage_protection = status2 & (1 << 6);
pfc_side_off = status2 & (1 << 7);
}
std::ostream& operator<<(std::ostream& out, const PowerModuleStatus& self) {
out << "PowerModuleStatus: " << (self.output_short_current ? "output_short_current " : "")
<< (self.sleeping ? "sleeping " : "") << (self.discharge_abnormal ? "discharge_abnormal " : "")
<< (self.dc_side_off ? "dc_side_off " : "") << (self.fault_alarm ? "fault_alarm " : "")
<< (self.protection_alarm ? "protection_alarm " : "") << (self.fan_fault_alarm ? "fan_fault_alarm " : "")
<< (self.over_temperature_alarm ? "over_temperature_alarm " : "")
<< (self.output_over_voltage_alarm ? "output_over_voltage_alarm " : "")
<< (self.walk_in_enable ? "walk_in_enable " : "")
<< (self.communication_interrupt_alarm ? "communication_interrupt_alarm " : "")
<< (self.power_limit_status ? "power_limit_status " : "") << (self.id_repeat_alarm ? "id_repeat_alarm " : "")
<< (self.load_sharing_alarm ? "load_sharing_alarm " : "")
<< (self.input_phase_lost_alarm ? "input_phase_lost_alarm " : "")
<< (self.input_unbalanced_alarm ? "input_unbalanced_alarm " : "")
<< (self.input_low_voltage_alarm ? "input_low_voltage_alarm " : "")
<< (self.input_over_voltage_protection ? "input_over_voltage_protection " : "")
<< (self.pfc_side_off ? "pfc_side_off " : "");
return out;
}
PowerModuleStatus::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data;
return data;
}
// New packet class implementations for V1.13 protocol
ReadModuleCount::ReadModuleCount() : count(0) {
}
ReadModuleCount::ReadModuleCount(const std::vector<uint8_t>& raw) {
if (raw.size() < 3) {
EVLOG_warning << "Received invalid ReadModuleCount packet with size " << raw.size();
return;
}
count = from_raw<uint8_t>(raw, 2);
}
ReadModuleCount::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data(8);
return data;
}
ReadModuleVI::ReadModuleVI(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
voltage = from_raw<float>(raw, 0);
current = from_raw<float>(raw, 4);
}
ReadModuleVI::operator std::vector<uint8_t>() const {
return {};
}
ReadModuleCapabilities::ReadModuleCapabilities(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
max_voltage = from_raw<uint16_t>(raw, 0) * InfyProtocol::SCALING_FACTOR_1_0;
min_voltage = from_raw<uint16_t>(raw, 2) * InfyProtocol::SCALING_FACTOR_1_0;
max_current = from_raw<uint16_t>(raw, 4) * InfyProtocol::SCALING_FACTOR_0_1;
rated_power = from_raw<uint16_t>(raw, 6) * InfyProtocol::SCALING_FACTOR_10_0;
}
ReadModuleCapabilities::operator std::vector<uint8_t>() const {
return {};
}
ReadModuleVIAfterDiode::ReadModuleVIAfterDiode(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
v_ext = from_raw<uint16_t>(raw, 0) * InfyProtocol::SCALING_FACTOR_0_1;
i_avail = from_raw<uint16_t>(raw, 2) * InfyProtocol::SCALING_FACTOR_0_1;
}
ReadModuleVIAfterDiode::operator std::vector<uint8_t>() const {
return {};
}
ReadModuleBarcode::ReadModuleBarcode(const std::vector<uint8_t>& raw) {
// Size validation is handled at rx_handler level
// According to protocol documentation for Command 0x0B:
// Example: Barcode "081807123451V1704" -> Response: 56 13 0C 15 A3 FB 06 A8
// Byte 0: 13th character (ASCII) - 0x56 = 'V'
// Bytes 1-7: Encoded first 12 characters + last 4 characters in complex HEX format
// For now, we'll extract what we can and create a readable serial number
char thirteenth_char = static_cast<char>(from_raw<uint8_t>(raw, 0));
// Extract the remaining bytes as hex values for debugging/identification
std::stringstream ss;
ss << "SN_" << std::hex << std::setfill('0');
for (size_t i = 1; i < 8 && i < raw.size(); ++i) {
ss << std::setw(2) << static_cast<unsigned>(from_raw<uint8_t>(raw, i));
}
ss << "_" << thirteenth_char;
serial_number = ss.str();
}
ReadModuleBarcode::operator std::vector<uint8_t>() const {
return {};
}
SetModuleVI::SetModuleVI(float v, float c) : voltage(v), current(c) {
}
SetModuleVI::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data;
to_raw(static_cast<uint32_t>(voltage * InfyProtocol::VOLTAGE_TO_MV), data); // we need to transform V to mV
to_raw(static_cast<uint32_t>(current * InfyProtocol::CURRENT_TO_MA), data); // we need to transform A to mA
return data;
}
SetModuleOnOff::SetModuleOnOff(bool o) : on(o) {
}
SetModuleOnOff::operator std::vector<uint8_t>() const {
std::vector<uint8_t> data;
to_raw(static_cast<uint8_t>(on ? 0x00 : 0x01), data);
// The rest of the payload is reserved and should be 0
for (int i = 0; i < 7; ++i) {
to_raw(static_cast<uint8_t>(0), data);
}
return data;
}
} // namespace can_packet_acdc

View File

@@ -0,0 +1,139 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef CAN_PACKETS_HPP
#define CAN_PACKETS_HPP
#include <linux/can.h>
#include <ostream>
#include <stdint.h>
#include <vector>
namespace InfyProtocol {
// CAN Protocol Constants
constexpr uint8_t DEVICE_SINGLE_MODULE = 0x0A;
constexpr uint8_t DEVICE_GROUP_MODULE = 0x0B;
constexpr uint32_t CAN_EXTENDED_FLAG = 0x80000000U;
// Bit Masks for CAN ID encoding
constexpr uint8_t COMMAND_MASK = 0x3F; // 6-bit mask for command number
constexpr uint8_t DEVICE_NUMBER_MASK = 0x0F; // 4-bit mask for device number
constexpr uint8_t ERROR_CODE_MASK = 0x07; // 3-bit mask for error code
// Unit Conversion Constants
constexpr uint32_t VOLTAGE_TO_MV = 1000U; // Volts to millivolts (V * 1000 = mV)
constexpr uint32_t CURRENT_TO_MA = 1000U; // Amperes to milliamperes (A * 1000 = mA)
// Scaling Factors for Raw Data Conversion
constexpr float SCALING_FACTOR_0_1 = 0.1f; // 0.1 scaling factor for voltage/current raw data
constexpr float SCALING_FACTOR_1_0 = 1.0f; // 1.0 scaling factor for voltage raw data
constexpr float SCALING_FACTOR_10_0 = 10.0f; // 10.0 scaling factor for power raw data
} // namespace InfyProtocol
namespace can_packet_acdc {
uint32_t encode_can_id(uint8_t source_address, uint8_t destination_address, uint8_t command_number,
uint8_t device_number, uint8_t error_code);
uint8_t destination_address_from_can_id(uint32_t id);
uint8_t source_address_from_can_id(uint32_t id);
uint8_t command_number_from_can_id(uint32_t id);
uint8_t error_code_from_can_id(uint32_t id);
struct PowerModuleStatus {
static constexpr uint8_t CMD_ID = 0x04;
PowerModuleStatus();
PowerModuleStatus(const std::vector<uint8_t>& raw);
friend std::ostream& operator<<(std::ostream& out, const PowerModuleStatus& self);
operator std::vector<uint8_t>() const;
bool output_short_current{false}; // Error if all modules have this state
bool sleeping{false}; // Status
bool discharge_abnormal{false}; // Vendor Warning
bool dc_side_off{false}; // Status
bool fault_alarm{false}; // Error if all modules have this state
bool protection_alarm{false}; // Vendor Warning
bool fan_fault_alarm{false}; // Vendor Warning
bool over_temperature_alarm{false}; // Vendor Warning
bool output_over_voltage_alarm{false}; // Vendor Warning
bool walk_in_enable{false}; // Status
bool communication_interrupt_alarm{false};
bool power_limit_status{false}; // Status
bool id_repeat_alarm{false}; // Status
bool load_sharing_alarm{false}; // Vendor Warning
bool input_phase_lost_alarm{false}; // Vendor Warning
bool input_unbalanced_alarm{false}; // Vendor Warning
bool input_low_voltage_alarm{false}; // Vendor Warning
bool input_over_voltage_protection{false}; // Vendor Warning
bool pfc_side_off{false}; // Status
};
// New packet classes for V1.13 protocol
struct ReadModuleCount {
static constexpr uint8_t CMD_ID = 0x02;
ReadModuleCount();
ReadModuleCount(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
uint8_t count{0};
};
struct ReadModuleVI {
static constexpr uint8_t CMD_ID = 0x03;
ReadModuleVI(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
float voltage{0.0f};
float current{0.0f};
};
struct ReadModuleCapabilities {
static constexpr uint8_t CMD_ID = 0x0A;
ReadModuleCapabilities(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
float max_voltage{0.0f};
float min_voltage{0.0f};
float max_current{0.0f};
float rated_power{0.0f};
};
struct ReadModuleVIAfterDiode {
static constexpr uint8_t CMD_ID = 0x0C;
ReadModuleVIAfterDiode(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
float v_ext{0.0f};
float i_avail{0.0f};
};
struct ReadModuleBarcode {
static constexpr uint8_t CMD_ID = 0x0B;
ReadModuleBarcode(const std::vector<uint8_t>& raw);
operator std::vector<uint8_t>() const;
std::string serial_number; // Full barcode string (e.g., "081807123451V1704")
};
struct SetModuleVI {
static constexpr uint8_t CMD_ID = 0x1C;
SetModuleVI(float voltage, float current);
operator std::vector<uint8_t>() const;
float voltage{0.0f};
float current{0.0f};
};
struct SetModuleOnOff {
static constexpr uint8_t CMD_ID = 0x1A;
SetModuleOnOff(bool on);
operator std::vector<uint8_t>() const;
bool on{false};
};
} // namespace can_packet_acdc
#endif // CAN_PACKETS_HPP

View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef CONVERSIONS_HPP
#define CONVERSIONS_HPP
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <type_traits>
#include <vector>
#include <endian.h>
// Helper template to ensure type safety for conversion operations
template <typename T> struct is_conversion_safe {
static constexpr bool value =
std::is_trivially_copyable_v<T> && std::is_standard_layout_v<T> && !std::is_pointer_v<T>;
};
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint8_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
T ret;
memcpy(&ret, &raw[idx], 1);
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint16_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
uint16_t tmp;
memcpy(&tmp, raw.data() + idx, sizeof(uint16_t)); // Safe copy from buffer
tmp = be16toh(tmp); // Convert endianness
T ret;
memcpy(&ret, &tmp, sizeof(T));
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint32_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
uint32_t tmp;
memcpy(&tmp, raw.data() + idx, sizeof(uint32_t)); // Safe copy from buffer
tmp = be32toh(tmp); // Convert endianness
T ret;
memcpy(&ret, &tmp, sizeof(T));
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint64_t) && is_conversion_safe<T>::value, T>
from_raw(const std::vector<uint8_t>& raw, int idx) {
if (idx + sizeof(T) > raw.size()) {
throw std::out_of_range("from_raw: buffer access out of bounds");
}
uint64_t tmp;
memcpy(&tmp, raw.data() + idx, sizeof(uint64_t)); // Safe copy from buffer
tmp = be64toh(tmp); // Convert endianness
T ret;
memcpy(&ret, &tmp, sizeof(T));
return ret;
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint8_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint8_t tmp;
memcpy(&tmp, &src, sizeof(T));
dest.push_back(tmp);
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint16_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint16_t tmp;
memcpy(&tmp, &src, sizeof(T));
tmp = htobe16(tmp);
// Use array for better alignment guarantees
alignas(uint16_t) uint8_t ret[sizeof(uint16_t)];
memcpy(ret, &tmp, sizeof(uint16_t));
dest.insert(dest.end(), {ret[0], ret[1]});
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint32_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint32_t tmp;
memcpy(&tmp, &src, sizeof(T));
tmp = htobe32(tmp);
// Use array for better alignment guarantees
alignas(uint32_t) uint8_t ret[sizeof(uint32_t)];
memcpy(ret, &tmp, sizeof(uint32_t));
dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3]});
}
template <class T>
typename std::enable_if_t<sizeof(T) == sizeof(uint64_t) && is_conversion_safe<T>::value>
to_raw(T src, std::vector<uint8_t>& dest) {
uint64_t tmp;
memcpy(&tmp, &src, sizeof(T));
tmp = htobe64(tmp);
// Use array for better alignment guarantees
alignas(uint64_t) uint8_t ret[sizeof(uint64_t)];
memcpy(ret, &tmp, sizeof(uint64_t));
dest.insert(dest.end(), {ret[0], ret[1], ret[2], ret[3], ret[4], ret[5], ret[6], ret[7]});
}
#endif // CONVERSIONS_HPP

View File

@@ -0,0 +1,368 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "InfyCanDevice.hpp"
#include "CanPackets.hpp"
#include <everest/logging.hpp>
#include <algorithm>
#include <iomanip>
#include <regex>
static std::vector<std::string> split_by_delimiters(const std::string& s, const std::string& delimiters) {
std::regex re("[" + delimiters + "]");
std::sregex_token_iterator first{s.begin(), s.end(), re, -1}, last;
return {first, last};
}
static std::vector<uint8_t> parse_module_addresses(const std::string& a) {
std::vector<uint8_t> addresses;
auto adr = split_by_delimiters(a, ",");
addresses.reserve(adr.size()); // Pre-allocate memory for efficiency
for (const auto& ad : adr) {
try {
addresses.push_back(std::stoi(ad));
} catch (const std::exception& e) {
EVLOG_error << "Infy: Invalid module address '" << ad << "': " << e.what();
}
}
return addresses;
}
InfyCanDevice::InfyCanDevice() : CanBus() {
}
InfyCanDevice::~InfyCanDevice() {
}
void InfyCanDevice::initial_ping() {
if (operating_mode == OperatingMode::GROUP_DISCOVERY) {
send_command<can_packet_acdc::ReadModuleCount>(group_address, true);
} else {
EVLOG_info << "Infy: Operating in FIXED_ADDRESS mode. No need to ping.";
initialized = true;
switch_on_off(false);
}
}
void InfyCanDevice::set_can_device(const std::string& dev) {
can_device = dev;
EVLOG_info << "Infy: Setting config values: CAN device: " << dev;
open_device(can_device.c_str());
}
void InfyCanDevice::set_config_values(const std::string& addrs, int group_addr, int timeout, int controller_address) {
this->device_connection_timeout_s = timeout;
this->group_address = group_addr;
this->controller_address = controller_address;
EVLOG_info << "Infy: Operating with controller address: 0x" << std::hex << controller_address;
if (!addrs.empty()) {
operating_mode = OperatingMode::FIXED_ADDRESS;
active_module_addresses = parse_module_addresses(addrs);
expected_module_count = active_module_addresses.size();
EVLOG_info << "Infy: Operating in FIXED_ADDRESS mode with " << expected_module_count << " addresses: " << addrs;
} else {
operating_mode = OperatingMode::GROUP_DISCOVERY;
EVLOG_info << "Infy: Operating in GROUP_DISCOVERY mode for group address: " << group_address;
}
EVLOG_info << "Infy: module communication timeout: " << device_connection_timeout_s << "s";
}
void InfyCanDevice::rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload) {
if (!(can_id & CAN_EFF_FLAG)) {
return;
}
// Ignore messages not addressed to us (the controller)
if (can_packet_acdc::destination_address_from_can_id(can_id) != controller_address) {
return;
}
// Discard malformed CAN frames with insufficient data
if (payload.size() < 8) {
EVLOG_error << "Infy: Received malformed CAN frame with size " << payload.size()
<< " (expected 8 bytes). Discarding frame.";
return;
}
const uint8_t source_address = can_packet_acdc::source_address_from_can_id(can_id);
const uint8_t command_number = can_packet_acdc::command_number_from_can_id(can_id);
switch (command_number) {
case can_packet_acdc::ReadModuleCount::CMD_ID: {
handle_module_count_packet(payload);
} break;
case can_packet_acdc::ReadModuleVI::CMD_ID: {
handle_simple_telemetry_update(source_address, payload, command_number);
} break;
case can_packet_acdc::PowerModuleStatus::CMD_ID: {
can_packet_acdc::PowerModuleStatus status(payload);
signalModuleStatus(status);
// Signal error status changes (excluding fields marked as "Status")
auto& telemetry = telemetries[source_address];
check_and_signal_error_status_change(source_address, status, telemetry.status);
telemetry.status = status;
// using status message to set the last_update time
telemetry.last_update = std::chrono::steady_clock::now();
} break;
case can_packet_acdc::ReadModuleVIAfterDiode::CMD_ID: {
handle_simple_telemetry_update(source_address, payload, command_number);
} break;
case can_packet_acdc::ReadModuleCapabilities::CMD_ID: {
handle_simple_telemetry_update(source_address, payload, command_number);
} break;
case can_packet_acdc::ReadModuleBarcode::CMD_ID: {
can_packet_acdc::ReadModuleBarcode barcode(payload);
auto& telemetry = telemetries[source_address];
telemetry.serial_number = barcode.serial_number;
EVLOG_info << format_module_id(source_address) << ": serial number: " << barcode.serial_number;
} break;
default: {
// Not implemented yet
}
}
}
size_t InfyCanDevice::remove_expired_telemetry_entries() {
auto now = std::chrono::steady_clock::now();
auto timeout_duration = std::chrono::seconds(device_connection_timeout_s);
size_t removed_count = 0;
// Remove expired telemetry entries
for (auto it = telemetries.begin(); it != telemetries.end();) {
const auto& [address, telemetry] = *it;
if (now - telemetry.last_update > timeout_duration) {
EVLOG_warning << format_module_id(address, telemetry.serial_number)
<< ": module communication expired (timeout: " << device_connection_timeout_s
<< "s). Removing from active modules.";
it = telemetries.erase(it);
{
std::lock_guard<std::mutex> lock(active_modules_mutex);
active_module_addresses.erase(
std::remove(active_module_addresses.begin(), active_module_addresses.end(), address),
active_module_addresses.end());
}
++removed_count;
} else {
++it;
}
}
// Update active_module_addresses to match current telemetries keys
{
// Check CommunicationFault state: trigger if no active modules but we expect some, clear otherwise
if (removed_count != 0 && telemetries.empty()) {
// No modules responding - trigger CommunicationFault
signalError(0xFF, Error::CommunicationFault, true); // Use address 0xFF for system-wide fault
} else if (!telemetries.empty()) {
// At least one module responding - clear CommunicationFault
signalError(0xFF, Error::CommunicationFault, false); // Use address 0xFF for system-wide fault
}
}
return removed_count;
}
void InfyCanDevice::poll_status_handler() {
// Remove expired telemetry entries
size_t removed_count = remove_expired_telemetry_entries();
if (removed_count > 0) {
EVLOG_info << "Infy: Removed " << removed_count << " expired modules. "
<< "Active modules remaining: " << active_module_addresses.size();
// signal the telemetry updates
signalCapabilitiesUpdate(telemetries);
signalVoltageCurrent(telemetries);
}
// --- Telemetry Polling (paced on CAN event thread) ---
static const std::vector<uint8_t> empty_read_payload(8, 0);
clear_paced_tx_queue();
if (operating_mode == OperatingMode::GROUP_DISCOVERY) {
enqueue_poll_command(group_address, can_packet_acdc::ReadModuleCount::CMD_ID, empty_read_payload, true);
}
std::vector<uint8_t> poll_addresses;
{
std::lock_guard<std::mutex> lock(active_modules_mutex);
poll_addresses = active_module_addresses;
}
for (const auto& addr : poll_addresses) {
enqueue_poll_command(addr, can_packet_acdc::ReadModuleVI::CMD_ID, empty_read_payload);
enqueue_poll_command(addr, can_packet_acdc::PowerModuleStatus::CMD_ID, empty_read_payload);
enqueue_poll_command(addr, can_packet_acdc::ReadModuleVIAfterDiode::CMD_ID, empty_read_payload);
auto it = telemetries.find(addr);
if (it == telemetries.end() || it->second.serial_number.empty()) {
enqueue_poll_command(addr, can_packet_acdc::ReadModuleCapabilities::CMD_ID, empty_read_payload);
enqueue_poll_command(addr, can_packet_acdc::ReadModuleBarcode::CMD_ID, empty_read_payload);
}
}
start_paced_tx_cycle();
}
bool InfyCanDevice::switch_on_off(bool on) {
std::lock_guard<std::mutex> lock(active_modules_mutex);
EVLOG_info << "Infy: switch_on_off(" << on << ") - active modules: " << active_module_addresses.size();
bool success = true;
if (active_module_addresses.empty()) {
EVLOG_warning << "Infy: No active modules to send switch_on_off command to.";
return false;
}
for (const auto& addr : active_module_addresses) {
bool tx_result = send_command(addr, can_packet_acdc::SetModuleOnOff(on));
success &= tx_result;
}
return success;
}
bool InfyCanDevice::set_voltage_current(float voltage, float current) {
std::lock_guard<std::mutex> lock(active_modules_mutex);
EVLOG_info << "Infy: set_voltage_current(" << voltage << "V, " << current
<< "A) - active modules: " << active_module_addresses.size();
// Validate that we have active modules before attempting to divide current
const size_t module_count = active_module_addresses.size();
if (module_count == 0) {
EVLOG_warning << "Infy: No active modules to set voltage/current.";
return false;
}
// Current is shared between all modules - safe division guaranteed by check above
const float current_per_module = current / static_cast<float>(module_count);
bool success = true;
for (const auto& addr : active_module_addresses) {
bool tx_result = send_command(addr, can_packet_acdc::SetModuleVI(voltage, current_per_module));
success &= tx_result;
}
return success;
}
bool InfyCanDevice::send_command_impl(uint8_t destination_address, uint8_t command_number,
const std::vector<uint8_t>& payload, bool group) {
uint32_t can_id = can_packet_acdc::encode_can_id(
controller_address, destination_address, command_number,
group ? InfyProtocol::DEVICE_GROUP_MODULE : InfyProtocol::DEVICE_SINGLE_MODULE, 0x00);
auto result = _tx(can_id, payload);
if (not result) {
EVLOG_warning << "Infy: CAN transmission failed for can_id: 0x" << std::hex << std::uppercase << can_id;
}
return result;
}
void InfyCanDevice::enqueue_poll_command(uint8_t destination_address, uint8_t command_number,
const std::vector<uint8_t>& payload, bool group) {
uint32_t can_id = can_packet_acdc::encode_can_id(
controller_address, destination_address, command_number,
group ? InfyProtocol::DEVICE_GROUP_MODULE : InfyProtocol::DEVICE_SINGLE_MODULE, 0x00);
enqueue_paced_tx(can_id, payload);
}
void InfyCanDevice::handle_module_count_packet(const std::vector<uint8_t>& payload) {
can_packet_acdc::ReadModuleCount n(payload);
if (operating_mode != OperatingMode::GROUP_DISCOVERY) {
return;
}
// n count must be at least 1, it is the module that it is answering (the group master)
expected_module_count = n.count;
if (expected_module_count != telemetries.size()) {
EVLOG_info << "Infy: System reports " << expected_module_count
<< " total modules in group, we might have lost some modules, waiting for timeout or recovery";
}
// Initially assume all configured modules are active (will be updated based on responses)
{
std::lock_guard<std::mutex> lock(active_modules_mutex);
active_module_addresses.clear();
active_module_addresses.reserve(n.count); // Pre-allocate before assignment
for (uint8_t i = 0; i < n.count; ++i) {
active_module_addresses.push_back(i);
}
}
if (!initialized) {
initialized = true;
EVLOG_info << "Infy: Received first module count packet. Make sure that the modules are off";
switch_on_off(false);
}
}
void InfyCanDevice::handle_simple_telemetry_update(uint8_t source_address, const std::vector<uint8_t>& payload,
uint8_t command_number) {
auto& telemetry = telemetries[source_address];
switch (command_number) {
case can_packet_acdc::ReadModuleVI::CMD_ID: {
can_packet_acdc::ReadModuleVI vi(payload);
telemetry.voltage = vi.voltage;
telemetry.current = vi.current;
signalVoltageCurrent(telemetries);
} break;
case can_packet_acdc::ReadModuleVIAfterDiode::CMD_ID: {
can_packet_acdc::ReadModuleVIAfterDiode v_after_diode(payload);
telemetry.v_ext = v_after_diode.v_ext;
telemetry.i_avail = v_after_diode.i_avail;
signalVoltageCurrent(telemetries);
} break;
case can_packet_acdc::ReadModuleCapabilities::CMD_ID: {
can_packet_acdc::ReadModuleCapabilities caps(payload);
telemetry.valid_caps = true;
telemetry.dc_max_output_voltage = caps.max_voltage;
telemetry.dc_min_output_voltage = caps.min_voltage;
telemetry.dc_max_output_current = caps.max_current;
telemetry.dc_rated_output_power = caps.rated_power;
EVLOG_info << format_module_id(source_address) << ": capabilities: " << caps.max_voltage << "V / "
<< caps.min_voltage << "V, " << caps.max_current << "A, power " << caps.rated_power << "W";
signalCapabilitiesUpdate(telemetries);
} break;
}
}
void InfyCanDevice::check_and_signal_error_status_change(uint8_t source_address,
const can_packet_acdc::PowerModuleStatus& new_status,
const can_packet_acdc::PowerModuleStatus& old_status) {
// Helper lambda to reduce repetition in error status checking
auto check_status_change = [this, source_address](bool new_val, bool old_val, Error error_type) {
if (new_val != old_val) {
signalError(source_address, error_type, new_val);
}
};
// Check all error status changes (excluding fields marked as "Status")
check_status_change(new_status.fault_alarm, old_status.fault_alarm, Error::InternalFault);
check_status_change(new_status.over_temperature_alarm, old_status.over_temperature_alarm, Error::OverTemperature);
check_status_change(new_status.output_over_voltage_alarm, old_status.output_over_voltage_alarm, Error::OverVoltage);
check_status_change(new_status.fan_fault_alarm, old_status.fan_fault_alarm, Error::FanFault);
check_status_change(new_status.input_phase_lost_alarm, old_status.input_phase_lost_alarm, Error::InputPhaseLoss);
check_status_change(new_status.output_short_current, old_status.output_short_current, Error::OverCurrent);
check_status_change(new_status.communication_interrupt_alarm, old_status.communication_interrupt_alarm,
Error::CommunicationFault);
check_status_change(new_status.input_low_voltage_alarm, old_status.input_low_voltage_alarm, Error::UnderVoltage);
check_status_change(new_status.input_unbalanced_alarm, old_status.input_unbalanced_alarm, Error::InputVoltage);
check_status_change(new_status.input_over_voltage_protection, old_status.input_over_voltage_protection,
Error::InputVoltage);
check_status_change(new_status.protection_alarm, old_status.protection_alarm, Error::InternalFault);
check_status_change(new_status.load_sharing_alarm, old_status.load_sharing_alarm, Error::InternalFault);
check_status_change(new_status.discharge_abnormal, old_status.discharge_abnormal, Error::InternalFault);
}
std::string InfyCanDevice::format_module_id(uint8_t address, const std::string& serial_number) const {
std::stringstream ss;
ss << "Infy[0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << static_cast<int>(address);
if (!serial_number.empty()) {
ss << "/" << serial_number;
}
ss << "]";
return ss.str();
}

View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef INFY_CAN_DEVICE_HPP
#define INFY_CAN_DEVICE_HPP
#include "CanBus.hpp"
#include "CanPackets.hpp"
#include <chrono>
#include <linux/can.h>
#include <map>
#include <mutex>
#include <sigslot/signal.hpp>
#include <vector>
class InfyCanDevice : public CanBus {
public:
InfyCanDevice();
~InfyCanDevice();
enum class Error {
OverVoltage,
UnderVoltage,
OverTemperature,
FanFault,
InputPhaseLoss,
CommunicationFault,
InternalFault,
OverCurrent,
InputVoltage
};
enum class OperatingMode {
FIXED_ADDRESS,
GROUP_DISCOVERY
};
void set_can_device(const std::string& dev);
void set_config_values(const std::string& addrs, int group_address, int timeout, int controller_address);
void initial_ping();
// Commands
bool switch_on_off(bool on);
bool set_voltage_current(float voltage, float current);
// Template overloads for type-safe command sending
template <typename PacketType> bool send_command(uint8_t destination_address, bool group = false) {
// Use static const vector to avoid repeated allocations
static const std::vector<uint8_t> empty_payload(
8, 0); // 8 zero bytes for read commands, otherwise the device returns an error
return send_command_impl(destination_address, PacketType::CMD_ID, empty_payload, group);
}
template <typename PacketType>
bool send_command(uint8_t destination_address, const PacketType& packet, bool group = false) {
return send_command_impl(destination_address, PacketType::CMD_ID, packet.operator std::vector<uint8_t>(),
group);
}
struct Telemetry {
float voltage{0.};
float current{0.};
float v_ext{0.};
float i_avail{0.};
bool valid_caps{false};
float dc_max_output_voltage{0.};
float dc_min_output_voltage{0.};
float dc_max_output_current{0.};
float dc_rated_output_power{0.};
can_packet_acdc::PowerModuleStatus status;
std::string serial_number; // Module barcode/serial number for identification
std::chrono::time_point<std::chrono::steady_clock> last_update;
};
typedef std::map<uint8_t, Telemetry> TelemetryMap;
TelemetryMap telemetries;
// Data out
sigslot::signal<TelemetryMap> signalVoltageCurrent;
sigslot::signal<can_packet_acdc::PowerModuleStatus> signalModuleStatus;
sigslot::signal<uint8_t, Error, bool> signalError;
sigslot::signal<TelemetryMap> signalCapabilitiesUpdate;
protected:
virtual void rx_handler(uint32_t can_id, const std::vector<uint8_t>& payload);
private:
bool initialized{false}; // Set to true when we have received the very first module count packet
uint8_t controller_address{0};
std::string can_device{""};
int group_address{0};
size_t expected_module_count{0};
int device_connection_timeout_s{0};
OperatingMode operating_mode{OperatingMode::FIXED_ADDRESS};
std::vector<uint8_t> active_module_addresses;
std::mutex active_modules_mutex;
void poll_status_handler() override;
size_t remove_expired_telemetry_entries();
// Helper methods to reduce code duplication in packet handling
void handle_module_count_packet(const std::vector<uint8_t>& payload);
void handle_simple_telemetry_update(uint8_t source_address, const std::vector<uint8_t>& payload,
uint8_t command_number);
void check_and_signal_error_status_change(uint8_t source_address,
const can_packet_acdc::PowerModuleStatus& new_status,
const can_packet_acdc::PowerModuleStatus& old_status);
// Helper for standardized module identification in logging
std::string format_module_id(uint8_t address, const std::string& serial_number = "") const;
// Private implementation for template methods
bool send_command_impl(uint8_t destination_address, uint8_t command_number, const std::vector<uint8_t>& payload,
bool group = false);
void enqueue_poll_command(uint8_t destination_address, uint8_t command_number, const std::vector<uint8_t>& payload,
bool group = false);
};
#endif // INFY_CAN_DEVICE_HPP

View File

@@ -0,0 +1,275 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "power_supply_DCImpl.hpp"
#include <iomanip>
#include <sstream>
namespace module {
namespace main {
void power_supply_DCImpl::init() {
constexpr int telemetry_update_interval = 5;
mod->acdc->initial_ping();
mod->acdc->signalVoltageCurrent.connect([this](InfyCanDevice::TelemetryMap telemetries) {
if (throttle_cnt++ % telemetry_update_interval == 0) {
float total_current = 0;
float ext_voltage = 0;
for (const auto& telemetry : telemetries) {
total_current += telemetry.second.current;
ext_voltage = telemetry.second.v_ext;
}
types::power_supply_DC::VoltageCurrent vc;
vc.current_A = total_current;
vc.voltage_V = ext_voltage;
publish_voltage_current(vc);
}
});
mod->acdc->signalModuleStatus.connect([this](can_packet_acdc::PowerModuleStatus status) {
// Publish mode changes
types::power_supply_DC::Mode mode;
if (status.fault_alarm) {
mode = types::power_supply_DC::Mode::Fault;
} else if (status.dc_side_off) {
mode = types::power_supply_DC::Mode::Off;
} else {
mode = types::power_supply_DC::Mode::Export;
}
if (this->mode.load() != mode || firsttime) {
publish_mode(mode);
firsttime = false;
}
});
mod->acdc->signalCapabilitiesUpdate.connect([this](InfyCanDevice::TelemetryMap telemetries) {
types::power_supply_DC::Capabilities new_caps;
new_caps.bidirectional = false;
// since the power supply is not bidirectional, we set the import capabilities to 0
new_caps.min_import_voltage_V = 0;
new_caps.max_import_voltage_V = 0;
new_caps.min_import_current_A = 0;
new_caps.max_import_current_A = 0;
// not working correctly if set to 0, the device will not allow setting the current to 0
new_caps.min_export_current_A = 1;
if (telemetries.size() == 0) {
EVLOG_info << "Infy: No telemetries received, setting default capabilities";
publish_capabilities(new_caps);
return;
}
float min_max_output_voltage = std::numeric_limits<float>::max();
float max_min_output_voltage = std::numeric_limits<float>::min();
for (const auto& telemetry : telemetries) {
// If the telemetry has a value for dc_max_output_voltage, we can use the values to update the capabilities
if (telemetry.second.valid_caps) {
if (telemetry.second.dc_max_output_voltage < min_max_output_voltage) {
min_max_output_voltage = telemetry.second.dc_max_output_voltage;
}
if (telemetry.second.dc_min_output_voltage > max_min_output_voltage) {
max_min_output_voltage = telemetry.second.dc_min_output_voltage;
}
new_caps.max_export_current_A += telemetry.second.dc_max_output_current;
new_caps.max_export_power_W += telemetry.second.dc_rated_output_power;
}
}
new_caps.max_export_voltage_V = min_max_output_voltage;
new_caps.min_export_voltage_V = max_min_output_voltage;
new_caps.conversion_efficiency_export = mod->config.conversion_efficiency_export;
caps = new_caps;
EVLOG_info << "Infy: Capabilities updated: " << new_caps.max_export_voltage_V << "V / "
<< new_caps.min_export_voltage_V << "V, " << new_caps.max_export_current_A << "A, power "
<< new_caps.max_export_power_W << "W";
publish_capabilities(new_caps);
if (last_module_count != telemetries.size() && telemetries.size() > 0) {
double voltage = exportVoltage.load();
double current = exportCurrentLimit.load();
types::power_supply_DC::Mode mode = this->mode.load();
types::power_supply_DC::ChargingPhase phase = this->phase.load();
if (telemetries.size() > last_module_count) {
EVLOG_info << "Infy: Hot plug detected - module count increased from "
<< static_cast<int>(last_module_count) << " to " << telemetries.size()
<< " modules, redistributing " << current << "A from " << static_cast<int>(last_module_count)
<< " to " << telemetries.size() << " modules";
} else if (telemetries.size() < last_module_count) {
EVLOG_info << "Infy: Hot unplug detected - module count decreased from "
<< static_cast<int>(last_module_count) << " to " << telemetries.size()
<< " modules, redistributing " << current << "A from " << static_cast<int>(last_module_count)
<< " to " << telemetries.size() << " modules";
}
EVLOG_info << "Infy: Restoring last settings: voltage=" << voltage << "V, current=" << current
<< "A, mode=" << mode << ", phase=" << phase;
last_module_count = telemetries.size();
handle_setExportVoltageCurrent(voltage, current);
handle_setMode(mode, phase);
}
last_module_count = telemetries.size();
});
mod->acdc->signalError.connect([this](uint8_t address, InfyCanDevice::Error error, bool active) {
const std::string error_type = map_infy_error_to_power_supply_dc(error);
const std::string error_message = create_error_message(address, error, active);
const bool is_error_active = error_state_monitor->is_error_active(error_type, "");
if (error == InfyCanDevice::Error::CommunicationFault && active) {
EVLOG_info << "Infy: Communication fault detected - all " << static_cast<int>(last_module_count)
<< " modules unresponsive, forcing system OFF for safety";
this->mode.store(types::power_supply_DC::Mode::Off);
}
if (active && !is_error_active) {
// New error detected - raise it
EVLOG_error << error_message;
auto severity = (error == InfyCanDevice::Error::FanFault) ? Everest::error::Severity::Medium
: Everest::error::Severity::High;
raise_error(error_factory->create_error(error_type, "", error_message, severity));
} else if (!active && is_error_active) {
// Error cleared - clear it
EVLOG_info << error_message;
clear_error(error_type);
}
});
}
void power_supply_DCImpl::ready() {
}
void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode,
types::power_supply_DC::ChargingPhase& phase) {
EVLOG_info << "Set mode via CAN: " << mode << " with phase " << phase;
if (mode == types::power_supply_DC::Mode::Off) {
mod->acdc->switch_on_off(false);
} else if (mode == types::power_supply_DC::Mode::Export) {
mod->acdc->switch_on_off(true);
} else if (mode == types::power_supply_DC::Mode::Import) {
mod->acdc->switch_on_off(true);
} else if (mode == types::power_supply_DC::Mode::Fault) {
mod->acdc->switch_on_off(false);
}
this->mode.store(mode);
this->phase.store(phase);
};
void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) {
if (voltage > caps.max_export_voltage_V)
voltage = caps.max_export_voltage_V;
else if (voltage < caps.min_export_voltage_V)
voltage = caps.min_export_voltage_V;
if (current > caps.max_export_current_A)
current = caps.max_export_current_A;
else if (current < caps.min_export_current_A)
current = caps.min_export_current_A;
exportVoltage.store(voltage);
exportCurrentLimit.store(current);
const size_t active_module_count = last_module_count;
if (active_module_count > 0) {
const double current_per_module = exportCurrentLimit.load() / static_cast<double>(active_module_count);
EVLOG_info << "Infy: Updating voltage/current via CAN: " << exportVoltage.load() << "V / "
<< exportCurrentLimit.load() << "A total → " << current_per_module
<< "A per module - active modules: " << active_module_count;
} else {
EVLOG_info << "Infy: Updating voltage/current via CAN: " << exportVoltage.load() << "V / "
<< exportCurrentLimit.load() << "A (but no active modules detected)";
}
mod->acdc->set_voltage_current(exportVoltage.load(), exportCurrentLimit.load());
};
void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) {
if (caps.min_import_voltage_V.has_value() && caps.max_import_voltage_V.has_value() &&
caps.min_import_current_A.has_value() && caps.max_import_current_A.has_value()) {
if (voltage > caps.max_import_voltage_V.value())
voltage = caps.max_import_voltage_V.value();
else if (voltage < caps.min_import_voltage_V.value())
voltage = caps.min_import_voltage_V.value();
if (current > caps.max_import_current_A.value())
current = caps.max_import_current_A.value();
else if (current < caps.min_import_current_A.value())
current = caps.min_import_current_A.value();
minImportVoltage.store(voltage);
importCurrentLimit.store(current);
EVLOG_info << "Infy: Updating voltage/current via CAN: " << minImportVoltage.load() << "V / "
<< importCurrentLimit.load() << "A";
mod->acdc->set_voltage_current(minImportVoltage.load(), importCurrentLimit.load());
}
}
std::string power_supply_DCImpl::map_infy_error_to_power_supply_dc(InfyCanDevice::Error error) {
switch (error) {
case InfyCanDevice::Error::OverVoltage:
return "power_supply_DC/OverVoltageDC";
case InfyCanDevice::Error::UnderVoltage:
return "power_supply_DC/UnderVoltageDC";
case InfyCanDevice::Error::OverTemperature:
return "power_supply_DC/OverTemperature";
case InfyCanDevice::Error::OverCurrent:
return "power_supply_DC/OverCurrentDC";
case InfyCanDevice::Error::InternalFault:
return "power_supply_DC/HardwareFault";
case InfyCanDevice::Error::CommunicationFault:
return "power_supply_DC/CommunicationFault";
case InfyCanDevice::Error::InputVoltage:
return "power_supply_DC/UnderVoltageAC"; // Most common case for input voltage issues
case InfyCanDevice::Error::FanFault:
return "power_supply_DC/VendorWarning"; // Non-critical vendor-specific warning
case InfyCanDevice::Error::InputPhaseLoss:
return "power_supply_DC/VendorError"; // Critical vendor-specific error
default:
return "power_supply_DC/VendorError"; // Fallback for unknown errors
}
}
std::string power_supply_DCImpl::create_error_message(uint8_t module_address, InfyCanDevice::Error error,
bool active) const {
std::string action = active ? "detected" : "cleared";
std::string error_name;
switch (error) {
case InfyCanDevice::Error::OverVoltage:
error_name = "overvoltage fault";
break;
case InfyCanDevice::Error::UnderVoltage:
error_name = "undervoltage fault";
break;
case InfyCanDevice::Error::OverTemperature:
error_name = "overtemperature fault";
break;
case InfyCanDevice::Error::OverCurrent:
error_name = "overcurrent fault";
break;
case InfyCanDevice::Error::InternalFault:
error_name = "internal fault";
break;
case InfyCanDevice::Error::CommunicationFault:
error_name = "communication fault";
break;
case InfyCanDevice::Error::InputVoltage:
error_name = "input voltage fault";
break;
case InfyCanDevice::Error::FanFault:
error_name = "fan fault";
break;
case InfyCanDevice::Error::InputPhaseLoss:
error_name = "input phase loss fault";
break;
default:
error_name = "unknown fault";
break;
}
std::stringstream ss;
ss << "Infy[0x" << std::hex << std::uppercase << static_cast<int>(module_address) << "]: " << error_name << " "
<< action;
return ss.str();
}
} // namespace main
} // namespace module

View File

@@ -0,0 +1,80 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef MAIN_POWER_SUPPLY_DC_IMPL_HPP
#define MAIN_POWER_SUPPLY_DC_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/power_supply_DC/Implementation.hpp>
#include "../InfyPower.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include <atomic>
#include <memory>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class power_supply_DCImpl : public power_supply_DCImplBase {
public:
power_supply_DCImpl() = delete;
power_supply_DCImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<InfyPower>& mod, Conf& config) :
power_supply_DCImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual void handle_setMode(types::power_supply_DC::Mode& mode,
types::power_supply_DC::ChargingPhase& phase) override;
virtual void handle_setExportVoltageCurrent(double& voltage, double& current) override;
virtual void handle_setImportVoltageCurrent(double& voltage, double& current) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<InfyPower>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
std::atomic<types::power_supply_DC::Mode> mode{types::power_supply_DC::Mode::Off};
std::atomic<types::power_supply_DC::ChargingPhase> phase{types::power_supply_DC::ChargingPhase::CableCheck};
std::atomic<double> exportVoltage{0.};
std::atomic<double> exportCurrentLimit{0.};
std::atomic<double> minImportVoltage{0.};
std::atomic<double> importCurrentLimit{0.};
types::power_supply_DC::Capabilities caps;
bool firsttime{true};
uint8_t last_module_count{0};
uint8_t throttle_cnt{0};
// Error handling helpers
std::string map_infy_error_to_power_supply_dc(InfyCanDevice::Error error);
std::string create_error_message(uint8_t module_address, InfyCanDevice::Error error, bool active) const;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_POWER_SUPPLY_DC_IMPL_HPP

View File

@@ -0,0 +1,47 @@
description: Driver for InfyPower ACDC power supply. Supports multiple InfyPower modules using V1.13 CAN protocol either in fixed address mode or group discovery mode.
config:
can_device:
description: CAN interface name
type: string
default: can0
module_addresses:
description: >-
Module Addresses to use. Set to "0" for broadcast. Make sure to connect only one PSU on the CAN bus in broadcast mode, otherwise
it will charge at a too high current (as every module will produce the full current the EV requests!)
If you have multiple PSUs, use individual addresses and list them comma separated, e.g. "1,2". Then it will share the current between
these PSUs.
type: string
default: ""
group_address:
description: >-
Group address/dial setting. Use this if your PSUs using automatic allocation of the addresses.
If you have multiple PSUs, set this number in accordance with the group dial
setting of the PSUs you want to use. Check the PSU manual for details but
the DIAL is sometimes in format 8421 (left MSB and right LSB).
Can't use module addresses and group address at the same time, if both are set, the module addresses will be used.
type: integer
default: 0
device_connection_timeout_s:
description: >-
Timeout in seconds to wait for a module to respond before considering it offline.
CRITICAL SAFETY: Must be > 10s (module internal timeout) to prevent overcurrent risk.
Recommended: 15s.
type: integer
default: 15
conversion_efficiency_export:
description: Conversion efficiency of the export mode.
type: number
default: 0.95
controller_address:
description: Controller address defaults to 0xF0.
type: integer
default: 240
provides:
main:
description: Main interface
interface: power_supply_DC
config: {}
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Florin Mihut

View File

@@ -0,0 +1,532 @@
-- InfyPower V1.13 CAN protocol dissector (Lua)
-- SPDX-License-Identifier: Apache-2.0
--
-- Install:
-- mkdir -p ~/.local/lib/wireshark/plugins
-- cp infypower_can.lua ~/.local/lib/wireshark/plugins/
-- Reload: Analyze -> Reload Lua Plugins
--
-- Display filters:
-- infypower.src == 240 (controller 0xF0)
-- infypower.dest == 0 (module 0)
-- infypower.command == 0x03
-- infypower.src == 0 && infypower.dest == 240 (module -> controller)
-- infypower.internal (cmd 0x17 -> internal controller, default 0xF8)
-- infypower.command == 0x17 && infypower.dest == 248
-- infypower.pair contains "req" or "resp" (add custom column in Wireshark)
--
-- Optional row color: View -> Coloring Rules -> Import -> infypower_colorfilters
--
-- Request/response pairing uses FIFO order per (module, command). Reload once
-- after opening a capture so request frames also show response links.
local infypower_proto = Proto("infypower", "InfyPower V1.13 CAN")
infypower_proto.prefs.controller_addr = Pref.uint(
"Controller address (decimal)", 240, "Default EVerest controller: 0xF0")
infypower_proto.prefs.internal_controller_addr = Pref.uint(
"Internal controller address (decimal)", 248,
"Destination for internal cmd 0x17 traffic (default 0xF8)")
infypower_proto.prefs.highlight_internal = Pref.bool(
"Highlight internal cmd 0x17 frames", true,
"Label and colorize cmd 0x17 frames to internal controller")
-- Header fields (derived from 29-bit CAN ID, not from payload bytes)
local f_err = ProtoField.uint8("infypower.error_code", "Error Code", base.HEX)
local f_dev = ProtoField.uint8("infypower.device_number", "Device Number", base.HEX)
local f_cmd = ProtoField.uint8("infypower.command", "Command", base.HEX)
local f_dst = ProtoField.uint8("infypower.dest", "Destination Address", base.HEX)
local f_src = ProtoField.uint8("infypower.src", "Source Address", base.HEX)
-- Payload fields
local f_voltage = ProtoField.float("infypower.voltage_v", "Voltage (V)")
local f_current = ProtoField.float("infypower.current_a", "Current (A)")
local f_v_ext = ProtoField.float("infypower.v_ext_v", "External Voltage (V)")
local f_i_avail = ProtoField.float("infypower.i_avail_a", "Available Current (A)")
local f_max_v = ProtoField.uint16("infypower.max_voltage_v", "Max Voltage (V)", base.DEC)
local f_min_v = ProtoField.uint16("infypower.min_voltage_v", "Min Voltage (V)", base.DEC)
local f_max_i = ProtoField.float("infypower.max_current_a", "Max Current (A)")
local f_rated_pwr = ProtoField.uint32("infypower.rated_power_w", "Rated Power (W)", base.DEC)
local f_module_count = ProtoField.uint8("infypower.module_count", "Module Count", base.DEC)
local f_group_no = ProtoField.uint8("infypower.group_no", "Group Number", base.DEC)
local f_ambient_temp = ProtoField.int8("infypower.ambient_temp_c", "Ambient Temp (C)", base.DEC)
local f_voltage_mv = ProtoField.uint32("infypower.voltage_mv", "Voltage (mV)", base.DEC)
local f_current_ma = ProtoField.uint32("infypower.current_ma", "Current (mA)", base.DEC)
local f_on = ProtoField.bool("infypower.on", "Output On")
local f_raw = ProtoField.bytes("infypower.raw", "Raw Payload")
-- Wireshark draws request/response arrows in the packet list when these are set
local f_response_in = ProtoField.framenum(
"infypower.response_in", "Response in", base.NONE, frametype.RESPONSE)
local f_request_in = ProtoField.framenum(
"infypower.request_in", "Request in", base.NONE, frametype.REQUEST)
local f_pair = ProtoField.string("infypower.pair", "Pairing")
local f_internal = ProtoField.bool(
"infypower.internal", "Internal communication (cmd 0x17 to internal controller)")
infypower_proto.fields = {
f_err, f_dev, f_cmd, f_dst, f_src,
f_voltage, f_current, f_v_ext, f_i_avail,
f_max_v, f_min_v, f_max_i, f_rated_pwr,
f_module_count, f_group_no, f_ambient_temp,
f_voltage_mv, f_current_ma, f_on, f_raw,
f_response_in, f_request_in, f_pair, f_internal,
}
local can_id_field = Field.new("can.id")
-- Forward declarations (Listener callback must see these as locals, not globals)
local parse_can_id
local is_infypower_frame
local get_can_id
local CMD_NAMES = {
[0x02] = "ReadModuleCount",
[0x03] = "ReadModuleVI",
[0x04] = "PowerModuleStatus",
[0x0A] = "ReadModuleCapabilities",
[0x0B] = "ReadModuleBarcode",
[0x0C] = "ReadModuleVIAfterDiode",
[0x17] = "InternalComm",
[0x1A] = "SetModuleOnOff",
[0x1C] = "SetModuleVI",
}
local function field_uint(field)
if field == nil then
return nil
end
local v = field.value
if type(v) == "userdata" then
return tonumber(tostring(v))
end
return tonumber(v)
end
get_can_id = function()
local id_f = can_id_field()
if id_f == nil then
return nil
end
return field_uint(id_f)
end
parse_can_id = function(can_id)
local id = bit.band(can_id, 0x1FFFFFFF)
return bit.band(id, 0xFF),
bit.band(bit.rshift(id, 8), 0xFF),
bit.band(bit.rshift(id, 16), 0x3F),
bit.band(bit.rshift(id, 22), 0x0F),
bit.band(bit.rshift(id, 26), 0x07)
end
local function internal_controller_addr()
return infypower_proto.prefs.internal_controller_addr
end
local function is_internal_frame(src, dst, cmd)
return cmd == 0x17 and dst == internal_controller_addr()
end
is_infypower_frame = function(can_id)
if can_id == nil or can_id <= 0x7FF then
return false
end
local _, dst, cmd, dev = parse_can_id(can_id)
if is_internal_frame(nil, dst, cmd) then
return true
end
return (dev == 0x0A or dev == 0x0B) and CMD_NAMES[cmd] ~= nil
end
-- Request/response pairing state (filled by tap, read during dissection)
local pair_peer = {} -- frame number -> peer frame number
local pair_role = {} -- frame number -> "request" | "response"
local pending = {} -- pending[queue_key][cmd] = { frame, ... }
local last_group_req = {} -- last_group_req[cmd] = frame (for group reads, device 0x0B)
local tap_retap_done = false
local pairing_retap_in_progress = false -- guard: retap_packets() calls reset()
local function controller_addr()
return infypower_proto.prefs.controller_addr
end
local function pending_key_module(addr)
return "mod:" .. addr
end
local function pending_key_group(addr)
return "grp:" .. addr
end
local function pending_queue(key, cmd)
if pending[key] == nil then
pending[key] = {}
end
if pending[key][cmd] == nil then
pending[key][cmd] = {}
end
return pending[key][cmd]
end
local function link_pair(req_frame, resp_frame)
pair_peer[req_frame] = resp_frame
pair_peer[resp_frame] = req_frame
pair_role[req_frame] = "request"
pair_role[resp_frame] = "response"
end
local function is_request_frame(src, dst)
return src == controller_addr() and dst ~= controller_addr()
end
local function is_response_frame(src, dst)
return dst == controller_addr() and src ~= controller_addr()
end
local function pair_reset()
-- Reassign tables (avoid pairs() during Wireshark reset; also clears nested queues)
pair_peer = {}
pair_role = {}
pending = {}
last_group_req = {}
-- retap_packets() invokes reset(); do not re-arm retap in that case
if not pairing_retap_in_progress then
tap_retap_done = false
end
end
local function pair_process_frame(pinfo, can_id)
local src, dst, cmd, dev = parse_can_id(can_id)
if not CMD_NAMES[cmd] or is_internal_frame(src, dst, cmd) then
return
end
local fnum = pinfo.number
if is_request_frame(src, dst) then
if dev == 0x0B then
last_group_req[cmd] = fnum
table.insert(pending_queue(pending_key_group(dst), cmd), fnum)
else
table.insert(pending_queue(pending_key_module(dst), cmd), fnum)
end
elseif is_response_frame(src, dst) then
local req_frame
local mod_queue = pending_queue(pending_key_module(src), cmd)
if #mod_queue > 0 then
req_frame = table.remove(mod_queue, 1)
elseif last_group_req[cmd] ~= nil then
req_frame = last_group_req[cmd]
else
-- try any pending group queue entry for this cmd
for _, grp_queue in pairs(pending) do
if grp_queue[cmd] and #grp_queue[cmd] > 0 then
req_frame = table.remove(grp_queue[cmd], 1)
break
end
end
end
if req_frame ~= nil then
link_pair(req_frame, fnum)
end
end
end
local function add_pairing_to_tree(subtree, pinfo)
local fnum = pinfo.number
local peer = pair_peer[fnum]
if peer == nil then
return
end
local role = pair_role[fnum]
if role == "request" then
subtree:add(f_response_in, peer):set_text("Response in frame: " .. peer)
subtree:add(f_pair, "req"):set_generated()
pinfo.cols.info:append(string.format(" \xE2\x86\x92 #%d", peer))
elseif role == "response" then
subtree:add(f_request_in, peer):set_text("Request in frame: " .. peer)
subtree:add(f_pair, "resp"):set_generated()
pinfo.cols.info:append(string.format(" \xE2\x86\x90 #%d", peer))
end
end
local DEVICE_NAMES = {
[0x0A] = "SingleModule",
[0x0B] = "GroupModule",
}
local STATUS0_BITS = {
[0] = "output_short_current",
[4] = "sleeping",
[5] = "discharge_abnormal",
}
local STATUS1_BITS = {
[0] = "dc_side_off",
[1] = "fault_alarm",
[2] = "protection_alarm",
[3] = "fan_fault_alarm",
[4] = "over_temperature_alarm",
[5] = "output_over_voltage_alarm",
[6] = "walk_in_enable",
[7] = "communication_interrupt_alarm",
}
local STATUS2_BITS = {
[0] = "power_limit_status",
[1] = "id_repeat_alarm",
[2] = "load_sharing_alarm",
[3] = "input_phase_lost_alarm",
[4] = "input_unbalanced_alarm",
[5] = "input_low_voltage_alarm",
[6] = "input_over_voltage_protection",
[7] = "pfc_side_off",
}
local function be16(buf, offset)
if buf:len() < offset + 2 then
return 0
end
return buf(offset, 2):uint()
end
local function be32(buf, offset)
if buf:len() < offset + 4 then
return 0
end
return buf(offset, 4):uint()
end
local function tvb_float_be(buf, offset)
if buf:len() < offset + 4 then
return nil
end
return buf(offset, 4):float()
end
local function format_addr(addr)
return string.format("0x%02X", addr)
end
-- Add a field parsed from CAN ID (not from tvb) so it shows in the tree and filters work
local function add_header_field(tree, field, value, label_fmt, ...)
local ti = tree:add(field, value)
ti:set_generated()
if label_fmt then
ti:set_text(string.format(label_fmt, ...))
end
return ti
end
local function add_status_bits(tree, title, byte_val, bit_map)
local bits_tree = tree:add(infypower_proto, title .. string.format(" (0x%02X)", byte_val))
bits_tree:set_generated()
for bitpos, name in pairs(bit_map) do
if bit.band(byte_val, bit.lshift(1, bitpos)) ~= 0 then
local ti = bits_tree:add(infypower_proto, name .. ": set")
ti:set_generated()
end
end
end
local function payload_is_read_request(buf)
if buf:len() < 8 then
return false
end
for i = 0, 7 do
if buf(i, 1):uint() ~= 0 then
return false
end
end
return true
end
local function is_controller_to_module(src, dst)
local ctrl = controller_addr()
return src == ctrl and dst ~= ctrl
end
local function dissect_payload(buf, cmd, tree, src, dst)
local payload_tree = tree:add(infypower_proto, buf(), "Payload")
payload_tree:set_generated()
if buf:len() == 0 then
local ti = payload_tree:add(infypower_proto, "No payload bytes in dissector buffer")
ti:set_generated()
return
end
payload_tree:add(f_raw, buf())
if buf:len() < 8 then
local ti = payload_tree:add(infypower_proto,
string.format("Short payload (%d bytes, expected 8)", buf:len()))
ti:set_generated()
return
end
if payload_is_read_request(buf) and cmd ~= 0x1A and cmd ~= 0x1C then
if is_controller_to_module(src, dst) then
local ti = payload_tree:add(infypower_proto, "Read request (8 zero bytes)")
ti:set_generated()
else
local ti = payload_tree:add(infypower_proto, "Response with zero payload")
ti:set_generated()
end
return
end
if cmd == 0x03 then
local v = tvb_float_be(buf, 0)
local c = tvb_float_be(buf, 4)
if v ~= nil then
payload_tree:add(f_voltage, v):set_text(string.format("Output Voltage: %.3f V", v))
end
if c ~= nil then
payload_tree:add(f_current, c):set_text(string.format("Output Current: %.3f A", c))
end
elseif cmd == 0x0C then
payload_tree:add(f_v_ext, be16(buf, 0) * 0.1)
:set_text(string.format("V_ext: %.1f V", be16(buf, 0) * 0.1))
payload_tree:add(f_i_avail, be16(buf, 2) * 0.1)
:set_text(string.format("I_avail: %.1f A", be16(buf, 2) * 0.1))
elseif cmd == 0x0A then
payload_tree:add(f_max_v, be16(buf, 0))
payload_tree:add(f_min_v, be16(buf, 2))
payload_tree:add(f_max_i, be16(buf, 4) * 0.1)
:set_text(string.format("Max Current: %.1f A", be16(buf, 4) * 0.1))
payload_tree:add(f_rated_pwr, be16(buf, 6) * 10)
:set_text(string.format("Rated Power: %d W", be16(buf, 6) * 10))
elseif cmd == 0x02 then
payload_tree:add(f_module_count, buf(2, 1))
:set_text("Module Count: " .. buf(2, 1):uint())
elseif cmd == 0x1A then
local on = buf(0, 1):uint() == 0
payload_tree:add(f_on, on):set_text(on and "Output: ON" or "Output: OFF")
elseif cmd == 0x1C then
local mv = be32(buf, 0)
local ma = be32(buf, 4)
payload_tree:add(f_voltage_mv, mv)
:set_text(string.format("Voltage: %d mV (%.3f V)", mv, mv / 1000))
payload_tree:add(f_current_ma, ma)
:set_text(string.format("Current: %d mA (%.3f A)", ma, ma / 1000))
elseif cmd == 0x04 then
payload_tree:add(f_group_no, buf(2, 1))
:set_text("Group Number: " .. buf(2, 1):uint())
local amb = buf(4, 1):int()
payload_tree:add(f_ambient_temp, amb)
:set_text(string.format("Ambient Temp: %d C", amb))
add_status_bits(payload_tree, "Module State 0", buf(7, 1):uint(), STATUS0_BITS)
add_status_bits(payload_tree, "Module State 1", buf(6, 1):uint(), STATUS1_BITS)
add_status_bits(payload_tree, "Module State 2", buf(5, 1):uint(), STATUS2_BITS)
elseif cmd == 0x0B then
local ti = payload_tree:add(infypower_proto,
string.format("Barcode byte 0 (char): '%c' (0x%02X)", buf(0, 1):uint(), buf(0, 1):uint()))
ti:set_generated()
ti = payload_tree:add(f_raw, buf(1, 7))
ti:set_text("Barcode encoding: " .. buf(1, 7):bytes():tohex())
elseif cmd == 0x17 then
local ti = payload_tree:add(infypower_proto,
string.format("Byte0: 0x%02X Byte1: 0x%02X Byte2-3: 0x%04X Byte4-5: 0x%04X",
buf(0, 1):uint(), buf(1, 1):uint(), be16(buf, 2), be16(buf, 4)))
ti:set_generated()
ti = payload_tree:add(infypower_proto, "Bytes 6-7: " .. buf(6, 2):bytes():tohex())
ti:set_generated()
else
local ti = payload_tree:add(infypower_proto, "Payload not decoded for this command")
ti:set_generated()
end
end
local function highlight_internal_row(pinfo)
if not infypower_proto.prefs.highlight_internal then
return
end
if Color == nil then
return
end
-- Light amber background in packet list (Wireshark 4.x)
pinfo.cols.bg = Color.new(65535, 0xFFEE, 0xAA00)
pinfo.cols.fg = Color.new(0, 0, 0)
end
local function dissect_infypower(buf, pinfo, tree, can_id)
local src, dst, cmd, dev, err = parse_can_id(can_id)
local internal = is_internal_frame(src, dst, cmd)
if internal then
pinfo.cols.protocol:set("InfyPower-INT")
highlight_internal_row(pinfo)
else
pinfo.cols.protocol:set("InfyPower")
end
pinfo.cols.src:set(format_addr(src))
pinfo.cols.dst:set(format_addr(dst))
local subtree = tree:add(infypower_proto, buf(),
internal and "InfyPower V1.13 (internal)" or "InfyPower V1.13")
add_header_field(subtree, f_src, src, "Source Address: 0x%02X (%d)", src, src)
add_header_field(subtree, f_dst, dst, "Destination Address: 0x%02X (%d)", dst, dst)
add_header_field(subtree, f_cmd, cmd, "Command: 0x%02X (%s)", cmd, CMD_NAMES[cmd] or "unknown")
add_header_field(subtree, f_dev, dev, "Device Number: 0x%X (%s)", dev, DEVICE_NAMES[dev] or "unknown")
add_header_field(subtree, f_err, err, "Error Code: 0x%X", err)
if internal then
subtree:add(f_internal, true):set_text("Internal communication: cmd 0x17 -> internal controller")
end
local info_prefix = internal and "[INTERNAL 0x17] " or ""
pinfo.cols.info:set(string.format("%s%s %02X->%02X cmd=0x%02X",
info_prefix, CMD_NAMES[cmd] or "Infy", src, dst, cmd))
dissect_payload(buf, cmd, subtree, src, dst)
add_pairing_to_tree(subtree, pinfo)
return buf:len()
end
function infypower_proto.dissector(buf, pinfo, tree)
local can_id = get_can_id()
if not is_infypower_frame(can_id) then
return 0
end
return dissect_infypower(buf, pinfo, tree, can_id)
end
local function infypower_heuristic(buf, pinfo, tree)
local can_id = get_can_id()
if not is_infypower_frame(can_id) then
return false
end
dissect_infypower(buf, pinfo, tree, can_id)
return true
end
infypower_proto:register_heuristic("can", infypower_heuristic)
-- Pass 1: walk frames in order and build request/response pairs (FIFO per module+cmd)
local infy_pair_tap = Listener.new(nil, nil)
function infy_pair_tap.packet(pinfo, tvb)
local can_id = get_can_id()
if is_infypower_frame(can_id) then
pair_process_frame(pinfo, can_id)
end
end
function infy_pair_tap.reset()
pair_reset()
end
function infy_pair_tap.draw()
-- Pass 2: redissect so request frames also get Response-in links
if tap_retap_done or retap_packets == nil then
return
end
tap_retap_done = true
pairing_retap_in_progress = true
retap_packets()
pairing_retap_in_progress = false
end

View File

@@ -0,0 +1,13 @@
# Wireshark coloring rules for InfyPower internal traffic
# Import: View -> Coloring Rules -> Import... -> select this file
#
@InfyPower internal (cmd 0x17)
infypower.internal
True
65535
65535
43520
0
0
0
InfyPower internal cmd 0x17

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Simulate InfyPower module 0 responses on vcan0/can0.
#
# CAN ID: src (bits 7-0) | dst<<8 | cmd<<16 | dev(0x0A)<<22
#
# Request (controller 0xF0 -> module 0): ...00F0 (e.g. 028300F0 for cmd 0x03)
# Response (module 0 -> controller 0xF0): ...F000 (e.g. 0283F000 for cmd 0x03)
#
# Usage:
# ./sim_module0_responses.sh vcan0 once # one burst after driver polls
# ./sim_module0_responses.sh vcan0 loop # continuous (default)
set -euo pipefail
IFACE="${1:-vcan0}"
MODE="${2:-loop}"
if ! ip link show "$IFACE" &>/dev/null; then
echo "Interface $IFACE not found."
exit 1
fi
send() {
cansend "$IFACE" "$1"
}
send_responses() {
# Module 0 -> controller 0xF0 (note F000 suffix, NOT 00F0)
send "0283F000#43FA000040600000" # 0x03 ReadModuleVI: 500 V, 3.5 A
send "0284F000#000000021B004000" # 0x04 PowerModuleStatus
send "028CF000#0FA001F400000000" # 0x0C VI after diode: 400.0 V, 50.0 A
send "028AF000#01F400C803E803E8" # 0x0A capabilities
send "0282F000#0000010000000000" # 0x02 module count = 1
send "028BF000#5600000000000000" # 0x0B barcode (dummy)
}
if [[ "$MODE" == "once" ]]; then
send_responses
echo "Sent module-0 responses on $IFACE (src 0x00 -> dst 0xF0)"
exit 0
fi
echo "Looping module-0 responses on $IFACE every 0.5s (Ctrl+C to stop)"
while true; do
send_responses
sleep 0.5
done

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Create and bring up a **virtual** CAN interface (loopback / Wireshark testing).
#
# For physical USB CAN (can0, can1 @ 125 kbit/s) use:
# sudo ../setup_can.sh can0
#
# Usage:
# sudo ./vcan.sh # vcan0
# sudo ./vcan.sh vcan1
set -uo pipefail
IFACE="${1:-vcan0}"
if [[ "${IFACE}" == can0 || "${IFACE}" == can1 ]]; then
echo "Error: ${IFACE} is a physical interface, not virtual CAN." >&2
echo "Use: sudo $(dirname "$0")/../setup_can.sh ${IFACE}" >&2
exit 1
fi
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root (e.g. sudo $0)" >&2
exit 1
fi
modprobe vcan 2>/dev/null || true
if ip link show "${IFACE}" &>/dev/null; then
echo "${IFACE} already exists"
else
echo "Creating ${IFACE}"
ip link add dev "${IFACE}" type vcan
fi
ip link set "${IFACE}" up
ip -details link show "${IFACE}" | grep -E 'state|link/vcan' || true
echo "Done. Use can_device: ${IFACE} in Everest config for simulation."