Skip to content

feat: add CAN support #3

Open
nberlette wants to merge 2 commits intomainfrom
feat/can
Open

feat: add CAN support #3
nberlette wants to merge 2 commits intomainfrom
feat/can

Conversation

@nberlette
Copy link
Owner

Closes #2.

Adds opt-in CAN bus output to the ECA firmware with compile-time feature gating, centralizes all configuration, and documents the full system including error condition mappings.

New files

  • src/eca_config.h — single config header replacing scattered #defines. All pins, voltages, error thresholds, and CAN parameters in one place.
  • src/eca_can.h — header-only MCP2515 CAN module. EcaStatus enum + bit flags map 1:1 to existing analog error voltages. Compiles out entirely when ECA_ENABLE_CAN is 0 (default).
  • eca.dbc — canonical CAN message schema. Defines ECA_Data (ID 0x0EC, 8 bytes) with signals for ethanol %, frequency, voltage, fuel temp, status code, and fault flags.
  • CHANGELOG.md — v1.0.0 reconstructed from repo history; v2.0.0 for this work.

Modified files

  • src/eca.ino — uses config header; #ifdef/defined() replaced with #if guards; CAN status tracked through getEthanol() error paths and transmitted in loop(); serial output switched to F() flash strings; local variables properly scoped in loop()
  • README.md — full rewrite: pinout, voltage mapping tables, analog error conditions, CAN payload layout, status/error code cross-reference, DBC usage, integration examples
  • LICENSE — copyright years → 2021–2026

CAN error mapping

Analog Voltage CAN Status Code Condition
0.10 V 0x01 Disconnected ≤ 0 Hz
4.80 V 0x02 Contaminated < 50 Hz
4.90 V 0x03 HighWater > 150 Hz
0.50–4.50 V 0x00 OK Normal

Enabling CAN

// src/eca_config.h
#define ECA_ENABLE_CAN        1       // opt-in
#define ECA_CAN_ARBID         0xEC    // configurable arb ID
#define ECA_CAN_TX_INTERVAL_MS 100    // 10 Hz

When ECA_ENABLE_CAN remains 0, no CAN headers are included and the binary is identical to pre-change behavior.

Original prompt

You are working in the GitHub repository nberlette/eca, a C++ Arduino project for an Ethanol Content Analyzer.

Project context:

  • The device currently converts a digital FlexFuel sensor input (50–150 Hz) into an analog output for tuners, datalogs, and in-dash gauges.
  • Current documented behavior maps:
    • 50 Hz -> 0% ethanol -> 0.50V
    • 100 Hz -> 50% ethanol -> 2.25V
    • 150 Hz -> 100% ethanol -> 4.50V
  • The current 0–5V output path also uses distinct analog voltages to represent error states:
    • < 50 Hz -> contaminated -> 4.80V
    • > 150 Hz -> high water level -> 4.90V
    • <= 0 Hz -> disconnected -> 0.10V
  • The README is currently very minimal and needs to be expanded substantially.

Your task is to implement a clean, production-quality feature update with the following goals:

Primary feature: optional CAN support

Add CAN bus output support as a strictly opt-in feature. This must not break existing analog-output-only behavior.

Requirements:

  • CAN support must be configurable at compile time and/or via a central config header.
  • Preserve the current default behavior for users who do not enable CAN.
  • Introduce a clear feature flag or config option such as ECA_ENABLE_CAN.
  • Keep the implementation lightweight and Arduino-friendly.

CAN configuration options

Support configurable CAN-related settings, including at minimum:

  • Custom CAN arbitration ID (ARBID)
    • Default: 0xEC (the de facto standard for this project/use case)
  • Output scaling parameters
    • Allow users to configure how ethanol content and any related values are encoded into CAN payload bytes
    • Use sensible defaults
  • Any additional practical CAN settings that make sense for embedded use, such as:
    • update interval / transmission cadence
    • byte order / payload layout constants
    • optional inclusion of diagnostic/status/error flags

If the project does not already include a CAN library abstraction, add one in the least invasive way possible. Keep the code modular so CAN support can be compiled out cleanly.

CAN error/status support

The CAN output must include support for the same error conditions currently represented by the analog 0–5V output.

Requirements:

  • Preserve the existing analog error signaling exactly as-is
  • Define explicit CAN-side status/error codes that correspond to the existing analog error conditions
  • At minimum, represent:
    • contaminated / below valid frequency range (< 50 Hz, currently 4.80V)
    • high water level / above valid frequency range (> 150 Hz, currently 4.90V)
    • disconnected / no signal (<= 0 Hz, currently 0.10V)
  • Keep these mappings centralized and easy to maintain
  • Ensure README and DBC documentation clearly explain how analog error voltages map to CAN error/status values
  • If useful, expose both:
    • a compact enumerated error/status code signal, and/or
    • individual bit flags for fault conditions
  • Normal valid operation should also have a clearly defined CAN status value

Design this in a way that downstream ECUs, dashboards, loggers, or tuning software can reliably distinguish valid ethanol readings from sensor fault conditions.

CAN message schema / DBC

Create a small DBC file and add it to the repository as the canonical message schema for the new CAN output.

Requirements:

  • The DBC should define at least one message for the ECA output
  • Use the default arbitration ID 0xEC
  • Define signals for the values the firmware publishes, such as:
    • ethanol content
    • frequency input
    • output voltage and/or calculated analog target if appropriate
    • status / error code(s), including the custom error conditions already supported by the analog output
  • Use practical scaling and units
  • Keep the DBC minimal but valid and useful
  • Add comments in the DBC where helpful

Also update the README to mention the DBC file and explain how it should be used by tuners, dashboards, loggers, or other downstream software.

Modernization / stale content cleanup

Audit the repo for anything obviously outdated and bring it up to date, including:

  • copyright years
  • stale wording
  • outdated feature descriptions
  • old examples or assumptions that no longer match the current code

Be conservative: do not rewrite things unnecessarily, but fix obvious drift.

CHANGELOG

Create a CHANGELOG.md in the repo root.

Requirements:

  • Use a clean, conventional structure
  • Inspect repo history before writing historical entries
  • Add entries for prior notable project changes only where they can be responsibly inferred from the repo history, existing files, tags, commits, or README evolution
  • Add a new entry for today’s work describing:
    • optional CAN support
    • configurable arbitration ID
    • configurable scaling/output encoding
    • CAN support for the existing analog error conditions
    • added DBC schema file
    • README improvements
    • copyright/year updates
  • If older historical changes cannot be fully reconstructed, include a brief honest note rather than ...

Copilot AI and others added 2 commits March 16, 2026 00:48
…EADME, and CHANGELOG

- Add src/eca_config.h: central compile-time configuration
- Add src/eca_can.h: opt-in CAN bus module with MCP2515 support
- Add eca.dbc: CAN message schema with all signals and error codes
- Update src/eca.ino: integrate config header, CAN support, fix preprocessor guards
- Expand README.md: full documentation with pinout, mapping tables, CAN guide, DBC usage
- Add CHANGELOG.md: version history for v1.0.0 and v2.0.0
- Update LICENSE copyright to 2021-2026

Co-authored-by: nberlette <11234104+nberlette@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 16, 2026 02:45
@nberlette nberlette mentioned this pull request Mar 16, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in CAN output path to the Arduino ECA firmware, centralizes compile-time configuration, and documents the full analog + CAN interface (including a DBC schema) for downstream integration.

Changes:

  • Introduces centralized configuration (src/eca_config.h) and a header-only MCP2515 CAN module (src/eca_can.h) gated by ECA_ENABLE_CAN.
  • Updates the main firmware (src/eca.ino) to use the config header, track status, and transmit CAN frames on an interval.
  • Adds/updates project documentation and artifacts: eca.dbc, expanded README.md, new CHANGELOG.md, and LICENSE year update.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/eca_config.h Centralizes pins, scaling, error voltages, and CAN settings/feature flags.
src/eca_can.h Adds MCP2515 CAN wrapper + status code/flag mapping.
src/eca.ino Wires config + CAN into the main loop and error paths; modernizes serial strings.
eca.dbc Defines the CAN message schema (ECA_Data) for tool-based decoding.
README.md Full documentation rewrite covering wiring, mappings, CAN payload, and DBC usage.
CHANGELOG.md Adds a Keep-a-Changelog formatted version history including v2.0.0 entry.
LICENSE Updates copyright years.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 219 to +236
unsigned long highTime = pulseIn(PIN_INPUT_SENSOR, HIGH);
unsigned long lowTime = pulseIn(PIN_INPUT_SENSOR, LOW);

unsigned long pulseTime = highTime + lowTime;
float frequency = float(1000000 / pulseTime);

eContent = getEthanol(pulseTime);
setVoltageFromEthanol(eContent);
int eContent = getEthanol(pulseTime);
float outputVoltage = setVoltageFromEthanol(eContent);

tempC = getTempC(highTime, lowTime);
tempF = cToF(tempC);
int tempC = getTempC(highTime, lowTime);
int tempF = cToF(tempC);

#if ECA_ENABLE_CAN
if (ecaCan.ready()) {
uint16_t voltageMv = (uint16_t)(outputVoltage * 1000);
ecaCan.send((uint8_t)eContent, frequency, voltageMv,
(int8_t)tempC, ecaStatus);
}
Comment on lines 13 to +60

| Type | Pin | Description |
| ---------- | -------- | ------------------- |
| Sensor In | `D8` | `TIMER1 / ICP1` |
| PWM Output | `D3/D11` | Built-in PWM driver |
| DAC Output | `A4/A5` | MCP4725 12bit DAC |
- **FlexFuel sensor input** — reads the 50–150 Hz square-wave from any
standard GM-style FlexFuel sensor via Timer1 input capture.
- **Dual analog output** — simultaneous PWM and 12-bit MCP4725 I2C DAC output
for maximum compatibility.
- **Configurable voltage range** — default 0.50–4.50 V maps linearly to
0–100% ethanol.
- **Error/fault signaling** — dedicated out-of-band voltages for sensor
disconnect, contaminated fuel, and high water content.
- **Optional CAN bus output** — opt-in at compile time; transmits ethanol
content, frequency, voltage, fuel temperature, and status over CAN using a
configurable arbitration ID.
- **Fuel temperature** — derived from the sensor's duty cycle, output via
serial and (optionally) CAN.
- **Serial debug output** — logs ethanol percentage and fuel temperature over
UART at configurable baud rate.
- **Single config header** — all compile-time options centralized in
`src/eca_config.h`.

## `Hz -> E % -> V`
---

| Input (Hz) | E (%) | Output (V) |
| :--------- | :----: | :------------------------- |
| `50 hz` | ` 0 %` | `0.50v` |
| `100 hz` | `50 %` | `2.25v` |
| `150 hz` | `100%` | `4.50v` |
| **Errors** | | |
| `< 50 hz` | `---` | `4.80v` - contaminated |
| `> 150 hz` | `---` | `4.90v` - high water level |
| `<= 0 hz` | `---` | `0.10v` - disconnected |
## Hardware

## `License`
### Supported Boards

[MIT](https://mit-license.org) © [Nicholas Berlette](https://nick.berlette.com)
| Board | MCU | Notes |
|---|---|---|
| Arduino Nano | ATmega328P | Primary target |
| Arduino Uno | ATmega328P | Fully compatible |
| Other ATmega328P boards | ATmega328P | Should work with correct pin mapping |

### Required Components

| Component | Purpose |
|---|---|
| FlexFuel sensor (GM-style) | Ethanol content input (50–150 Hz square-wave) |
| MCP4725 breakout board | 12-bit I2C DAC for precision analog output |
| MCP2515 CAN module _(optional)_ | CAN bus transceiver (only needed if CAN is enabled) |

---

## Pinout

| Type | Pin(s) | Description |
|---|---|---|
| Sensor Input | `D10` | FlexFuel sensor signal (Timer1 / ICP1) |
| PWM Output | `D9` | Built-in PWM analog output |
| DAC Output | `A4 / A5` | MCP4725 I2C (SDA / SCL) |
Comment on lines 79 to +89
void setVoltage (double volts, bool init = false)
{
const int maxVolts = 5.0;
const int maxVolts = ECA_VOLTAGE_RAIL;

if (defined(ENABLE_PWM_OUT) && ENABLE_PWM_OUT == 1)
{
if (init) {
pinMode(PIN_OUTPUT_PWM, OUTPUT);
TCCR1B = TCCR1B & 0b11111000 | 0x01;
}
analogWrite(PIN_OUTPUT_PWM, int((PWM_MULTIPLIER * (volts / maxVolts))));
#if ECA_ENABLE_PWM_OUT
if (init) {
pinMode(PIN_OUTPUT_PWM, OUTPUT);
TCCR1B = TCCR1B & 0b11111000 | 0x01;
}
if (defined(ENABLE_DAC_OUT) && ENABLE_DAC_OUT == 1)
{
if (init) {
dac.begin(0x60);
}
dac.setVoltage(int(DAC_MULTIPLIER * (volts / maxVolts)), false);
analogWrite(PIN_OUTPUT_PWM, int((PWM_MULTIPLIER * (volts / maxVolts))));
#endif
Comment on lines +30 to +33
SG_ SensorFrequency : 8|16@1+ (0.1,0) [0|6553.5] "Hz" Vector__XXX

// Analog output voltage in millivolts
SG_ OutputVoltage : 24|16@1+ (1,0) [0|5000] "mV" Vector__XXX
// -------------------------------------------------------------------------
// Pin assignments
// -------------------------------------------------------------------------
#define PIN_INPUT_SENSOR 10 // FlexFuel sensor input (Timer1 / ICP1)
Comment on lines +86 to +88
// CAN chip-select pin for MCP2515 module
#define ECA_CAN_CS_PIN 10 // SPI chip select (change if shared)

Comment on lines +102 to +104
// CAN_500KBPS corresponds to ECA_CAN_BAUDRATE default of 500 kbps.
// MCP_ANY = accept all incoming messages (we only transmit, but set a sane default).
if (_can.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ) == CAN_OK) {
Comment on lines 112 to 145
int getEthanol (unsigned long pulseTime)
{
float frequency = float(1000000 / pulseTime);
// 20000 uS = 50 HZ - ~6667 uS = 150 HZ
if (pulseTime >= 20100 || pulseTime <= 6400)
{
if (pulseTime == 0)
{ // sensor disconnected / short circuit
setVoltage(0.1);
setVoltage(ECA_ERROR_V_DISCONNECTED);
#if ECA_ENABLE_CAN
ecaStatus = ECA_STATUS_DISCONNECTED;
#endif
}
else if (pulseTime >= 20100)
{ // contaminated fuel supply
setVoltage(4.8);
setVoltage(ECA_ERROR_V_CONTAMINATED);
#if ECA_ENABLE_CAN
ecaStatus = ECA_STATUS_CONTAMINATED;
#endif
}
else if ((pulseTime <= 6400) && (pulseTime >= 1))
{ // high water content in fuel
setVoltage(4.9);
setVoltage(ECA_ERROR_V_HIGH_WATER);
#if ECA_ENABLE_CAN
ecaStatus = ECA_STATUS_HIGH_WATER;
#endif
}
if (countTick < 2)
{
countTick++;
}
return;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: CAN support

3 participants