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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 50–200 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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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."
|
||||
Reference in New Issue
Block a user