Created
April 12, 2023 20:40
-
-
Save markamber/3c24759a1b5db58e1ca1836539a409be to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Project basic-spl-boron | |
* Description: CAN bus message receiver and battery discharge enable | |
* Author: Mark Amber | |
* Date: 2022.10.11 | |
*/ | |
#include "DebounceSwitchRK.h" | |
#include "mcp_can.h" | |
#include <SPI.h> // SPI library CANBUS | |
#if defined(DEBUG_BUILD) | |
BLE.off(); | |
#endif | |
SYSTEM_THREAD(ENABLED); | |
// Use primary serial over USB interface for logging output | |
// SerialLogHandler logHandler(115200, LOG_LEVEL_INFO, { // Logging level for non-application messages | |
// { "app", LOG_LEVEL_ALL } // Logging level for application messages | |
// }); | |
int buttonInputPin = D8; | |
int loadEnablePin = D2; | |
// LED drivers for buttons | |
int button1RLedPin = A0; | |
int button2RLedPin = A1; | |
int button1BLedPin = A2; | |
int button2BLedPin = A3; | |
// Output to BMS | |
int mpo1InputPin = D5; | |
int acPowerActive = D4; | |
int canIntCount; | |
int canErrCountSnd; | |
boolean powerGlobalState; | |
boolean powerDownFlicker; | |
boolean powerDownFlickerLast; | |
// Fade the LED | |
// define directions for LED fade | |
#define UP 0 | |
#define DOWN 1 | |
// constants for min and max PWM | |
const int minPWM = 0; | |
const int maxPWM = 255; | |
// State Variable for Fade Direction | |
byte fadeDirection = UP; | |
// Global Fade Value | |
// but be bigger than byte and signed, for rollover | |
int fadeValue = 0; | |
// How smooth to fade? | |
byte fadeIncrement = 5; | |
// millis() timing Variable, just for fading | |
unsigned long previousFadeMillis; | |
// How fast to increment? | |
int fadeInterval = 50; | |
// CANBUS Shield pins | |
#define CAN0_INT A4 // Set INT to pin 9 | |
MCP_CAN CAN0(A5); // Set CS to pin A5 | |
// MCP_CAN DATA | |
long unsigned int rxId; // Stores 4 bytes 32 bits | |
unsigned char len = 0; // Stores at least 1 byte | |
unsigned char rxBuf[8]; // Stores 8 bytes, 1 character = 1 byte | |
// MCP_CAN SEND DATA | |
byte mped[1] = {0x00}; // Multi-purpose output deactivation signal | |
byte mpee[1] = {0x01}; // Multi-purpose enable activation signal | |
// CANBUS data Identifier List, this is how I have our Orion BMS J. 2 setup | |
// ID 0x03B BYT0+1:INST_VOLT BYT2+3:INST_AMP BYT4+5:ABS_AMP BYT6:SOC **** ABS_AMP from OrionJr errendous **** | |
// ID 0x6B2 BYT0+1:LOW_CELL BYT2+3:HIGH_CELL BYT4:HEALTH BYT5+6:CYCLES | |
// ID 0x0A9 BYT0:RELAY_STATE BYT1:CCL BYT2:DCL BYT3+4:PACK_AH BYT5+6:AVG_AMP | |
// ID 0x0BD BYT0+1:CUSTOM_FLAGS BYT2:HI_TMP BYT3:LO_TMP BYT4:COUNTER BYT5:BMS_STATUS | |
// Variables | |
unsigned int rawU; // Voltage - multiplied by 10 | |
int rawI; // Current - multiplied by 10 - negative value indicates charge | |
byte soc; // State of charge - multiplied by 2 | |
int ssoc; // Scaled state of charge 18-175 | |
byte ang; // Needle angle | |
unsigned int p; // Watt reading | |
int m = 10; // Mapped values to fit needle | |
byte c = 255; // 8 bit unsigned integer range from 0-255 (low - high contrast) | |
byte dcl; // Discharge current limit (used on gauge and text page) | |
int fs; // Fault messages & status from CANBus | |
byte ry; // Relay status for determining when to show lightening bolt and sun icon respectively | |
int avgI; // Average current for clock and sun symbol calculations | |
int hC; // High Cell Voltage in 0.0001V | |
int lC; // Low Cell Voltage in 0.0001V | |
int h; // Health | |
int cc; // Total pack cycles | |
byte tH; // Highest cell temperature | |
byte tL; // Lowest cell temperature | |
byte ct; // Counter to observe data received | |
byte st; // BMS Status | |
float ah; // Amp hours | |
byte ccl; // Charge current limit | |
int fu; // BMS faults | |
float rt; // Runtime | |
// volatile bool canIntReq = false; // CAN bus interrupt request | |
// milis of the last time the print event was sent, we will be targeting every 1,000 ms | |
uint64_t lastPrintMillis = 0; | |
// millis of the last time the publish was sent, we will be targeting every 900,000 ms | |
uint64_t lastPublishMillis = 0; | |
// Cloud functions to set the state of the discharge (turn on and off system) | |
int powerState(String command) { | |
// message should be either "on" or "off" | |
if(command == "on") | |
{ | |
discharge(true); | |
} | |
if(command == "off") | |
{ | |
discharge(false); | |
} | |
return 0; | |
} | |
// The setup function is a standard part of any microcontroller program. | |
// It runs only once when the device boots up or is reset. | |
void setup() { | |
// Configure pin modes | |
pinMode(CAN0_INT, INPUT); | |
pinMode(A5, OUTPUT); | |
pinMode(loadEnablePin, OUTPUT); | |
pinMode(button1RLedPin, OUTPUT); | |
pinMode(button2RLedPin, OUTPUT); | |
pinMode(button1BLedPin, OUTPUT); | |
pinMode(button2BLedPin, OUTPUT); | |
pinMode(buttonInputPin, INPUT); | |
// Start CAN | |
CAN0.begin(MCP_ANY, CAN_250KBPS, MCP_16MHZ); | |
CAN0.setMode(MCP_NORMAL); | |
// attachInterrupt(CAN0_INT, addCanInt, FALLING); | |
//discharge(true); | |
// Register particle power on/off function called "powerState" | |
bool success = Particle.function("powerState", powerState); | |
analogWrite(button1RLedPin, fadeValue); | |
analogWrite(button1RLedPin, fadeValue); | |
analogWrite(button1BLedPin, fadeValue); | |
analogWrite(button2BLedPin, fadeValue); | |
debounceSetup(); | |
} | |
void calcCanValues(){ | |
// Set variables based on incoming CAN bus ID. | |
// This ALL relies heavilly on the BMS Can message settings | |
// in our case using prior art from https://github.com/tomstor82/arduino-Battery-Monitor | |
// to help set the messages in the BMS and also interpret them in code with bitwise ops | |
if(rxId == 0x03B) { | |
rawI = ((rxBuf[2] << 8) + rxBuf[3]); // Amps raw | |
rawU = ((rxBuf[0] << 8) + rxBuf[1]); // Volts raw | |
soc = (rxBuf[6]); // state of charge | |
} | |
// Calculate scaled state of charge | |
ssoc = ( (( int(soc) - 18 ) * 100 ) / 157); | |
ssoc = min(100 , ssoc); | |
ssoc = max(0, ssoc); | |
if(rxId == 0x0BD) { | |
fs = (rxBuf[0] + rxBuf[1] + rxBuf[5]); // Failure state | |
fu = ((rxBuf[0] << 8) + rxBuf[1]); | |
tH = (rxBuf[2]); | |
tL = (rxBuf[3]); | |
ct = (rxBuf[4]); | |
st = (rxBuf[5]); | |
} | |
if(rxId == 0x0A9) { | |
ry = (rxBuf[0]); // relay status | |
dcl = (rxBuf[2]); // discharge current limit | |
avgI = ((rxBuf[5] << 8) + rxBuf[6]); // Average current | |
ccl = (rxBuf[1]); | |
ah = ((rxBuf[3] << 8) + rxBuf[4]); | |
} | |
if(rxId == 0x6B2) { | |
lC = ((rxBuf[0] << 8) + rxBuf[1]); | |
hC = ((rxBuf[2] << 8) + rxBuf[3]); | |
h = (rxBuf[4]); | |
cc = ((rxBuf[5] << 8) + rxBuf[6]); | |
} | |
// Watt calculation | |
p = (abs(rawI)/10.0)*rawU/10.0; | |
// Runtime calc | |
rt = constrain ( ( ( ah / float(avgI) ) / 10 ) * 60 , 0, 10000); | |
} | |
// void addCanInt(){ | |
// canIntReq = true; | |
// } | |
void publishStats() { | |
char buf[255]; | |
JSONBufferWriter writer(buf, sizeof(buf)); | |
writer.beginObject(); | |
writer.name("r").value(rt); | |
writer.name("d").value(((ry & 0b00000001) == 0b00000001) ? "true" : "false"); | |
writer.name("v").value(ssoc); | |
writer.name("h").value("true"); | |
writer.name("cic").value(canIntCount); | |
writer.name("e").value(canErrCountSnd); | |
writer.endObject(); | |
writer.buffer()[std::min(writer.bufferSize(), writer.dataSize())] = 0; | |
Particle.publish("postdevice", buf, PRIVATE); | |
} | |
void calcStatus() { | |
//Log.trace("State of Charge is %d", soc); | |
//Log.trace("scaled soc is %d", ssoc); | |
// Relay Status | |
if ((ry & 0b00000001) == 0b00000001) { | |
// Discharge state 1 | |
//Log.trace("Discharge State True"); | |
} | |
else { | |
// Discharge state 2 | |
//Log.trace("Discharge State False"); | |
} | |
// Charge Status | |
if ((ry & 0b00000010) == 0b00000010) { | |
// Charge state 1 | |
//Log.trace("Charge State True"); | |
} | |
else { | |
// Charge state 2 | |
//Log.trace("Charge State False"); | |
} | |
// Charge Safety | |
if ((ry & 0b00000100) == 0b00000100) { | |
// Charge Safety 1 | |
//Log.trace("Charge Safety True"); | |
} | |
else { | |
// Charge Safety 2 | |
//Log.trace("Charge Safety False"); | |
} | |
//Log.trace("Runtime Mins %.1f", rt); | |
//Log.trace("Ah is %.2f", ah); | |
//Log.trace("Average amp is %d", avgI); | |
//Log.trace("Fault state %x Fault U %x", fs, fu); | |
//Log.trace("Watts is: %d", p); | |
//Log.trace("Can Int Count is: %d", canIntCount); | |
} | |
void calcFaults(){ | |
// Flag internal communication fault | |
if ((fu & 0x0100) == 0x0100) { | |
//Log.trace("intCom"); | |
} | |
// Flag internal convertions fault | |
if ((fu & 0x0200) == 0x0200) { | |
//Log.trace("intConv"); | |
} | |
// Flag weak cell fault | |
if ((fu & 0x0400) == 0x0400) { | |
//Log.trace("wkCell"); | |
} | |
// Flag low cell fault | |
if ((fu & 0x0800) == 0x0800) { | |
//Log.trace("lowCell"); | |
} | |
// Flag open wire fault | |
if ((fu & 0x1000) == 0x1000) { | |
//Log.trace("opnWire"); | |
} | |
// Flag current sense fault | |
if ((fu & 0x2000) == 0x2000) { | |
//Log.trace("crrSns"); | |
} | |
// Flag volt sense fault | |
if ((fu & 0x4000) == 0x4000) { | |
//Log.trace("vltSns"); | |
} | |
// Flag volt redundancy fault | |
if ((fu & 0x8000) == 0x8000) { | |
//Log.trace("vltRdcy"); | |
} | |
// Flag weak pack fault | |
if ((fu & 0x0001) == 0x0001) { | |
//Log.trace("wkPack"); | |
} | |
// Flag thermistor fault | |
if ((fu & 0x0002) == 0x0002) { | |
//Log.trace("xThrm"); | |
} | |
// Flag charge limit enforcement fault | |
if ((fu & 0x0004) == 0x0004) { | |
//Log.trace("chgRly"); | |
} | |
// Flag discharge limit enforcement fault | |
if ((fu & 0x0008) == 0x0008) { | |
//Log.trace("dchRly"); | |
} | |
// Flag charge safety relay fault | |
if ((fu & 0x0010) == 0x0010) { | |
//Log.trace("sftyRly"); | |
} | |
// Flag internal memory fault | |
if ((fu & 0x0020) == 0x0020) { | |
//Log.trace("intMem"); | |
} | |
// Flag internal thermistor fault | |
if ((fu & 0x0040) == 0x0040) { | |
//Log.trace("intThm"); | |
} | |
// Flag internal logic fault | |
if ((fu & 0x0080) == 0x0080) { | |
//Log.trace("intLog"); | |
} | |
// Flag BMS status | |
if ((st & 0x01) == 0x01){ | |
//Log.trace("VoltFS"); | |
} | |
if ((st & 0x02) == 0x02) { | |
//Log.trace("CurrFS"); | |
} | |
if ((st & 0x04) == 0x04) { | |
//Log.trace("RelyFS"); | |
} | |
if ((st & 0x08) == 0x08) { | |
//Log.trace("CellBlcg"); | |
} | |
} | |
void print_every_second() | |
{ | |
calcStatus(); | |
calcFaults(); | |
} | |
void debounceSetup(){ | |
DebounceSwitch::getInstance()->setup(); | |
DebounceSwitchState *sw; | |
// Push button switch | |
sw = DebounceSwitch::getInstance()->addSwitch(buttonInputPin, DebounceSwitchStyle::PRESS_LOW_PULLUP, powerOnButtonPushed); | |
} | |
void setLed(int whichOne, int value) { | |
int set1Value = (whichOne == 1) ? value : 0; | |
int set2Value = (whichOne == 2) ? value : 0; | |
analogWrite(button1RLedPin, set1Value); | |
analogWrite(button2RLedPin, set1Value); | |
analogWrite(button1BLedPin, set2Value); | |
analogWrite(button2BLedPin, set2Value); | |
} | |
void doTheFade(unsigned long thisMillis) { | |
// is it time to update yet? | |
// if not, nothing happens | |
if (thisMillis - previousFadeMillis >= fadeInterval) { | |
// yup, it's time! | |
if (fadeDirection == UP) { | |
fadeValue = fadeValue + fadeIncrement; | |
if (fadeValue >= maxPWM) { | |
// At max, limit and change direction | |
fadeValue = maxPWM; | |
fadeDirection = DOWN; | |
} | |
} else { | |
//if we aren't going up, we're going down | |
fadeValue = fadeValue - fadeIncrement; | |
if (fadeValue <= minPWM) { | |
// At min, limit and change direction | |
fadeValue = minPWM; | |
fadeDirection = UP; | |
} | |
} | |
// Only need to update when it changes | |
// analogWrite(pwmLED, fadeValue); | |
setLed(1, fadeValue); | |
// reset millis for the next iteration (fade timer only) | |
previousFadeMillis = thisMillis; | |
} | |
} | |
void loop() { | |
// Power down flicker will be true after holding down the power button for several seconds | |
if (powerDownFlicker) { | |
// This bitwise operation will be true in a blinky pattern, thus the LED will flicker | |
// This way the users know they have held down the button long enough | |
// Here is the old flicker code | |
// (millis() >> 3) & 0x88) | |
unsigned long currentMillis = millis(); | |
doTheFade(currentMillis); | |
} else if (powerGlobalState) { | |
// This bitwise operation will be true in a blinky pattern, thus the LED will flicker | |
// This way the users know they have held down the button long enough | |
setLed(1, 255); | |
} else { | |
setLed(1, 0); | |
} | |
// If the system time is 1s later than the last time we printed | |
if (System.millis() > lastPrintMillis + 149) { | |
//print_every_second(); | |
lastPrintMillis = System.millis(); | |
byte sndStat; | |
if (powerGlobalState){ | |
sndStat = CAN0.sendMsgBuf(0x155, CAN_STDID, 1, mpee); | |
} else { | |
sndStat = CAN0.sendMsgBuf(0x155, CAN_STDID, 1, mped); | |
} | |
if(sndStat != CAN_OK){ | |
canErrCountSnd = sndStat; | |
} | |
} else if(!digitalRead(CAN0_INT)) { | |
// when the canbus has data (int pin false) | |
// Read MCP2515 canbus data | |
CAN0.readMsgBuf(&rxId, &len, rxBuf); | |
// canIntReq = false; | |
canIntCount++; | |
} | |
calcCanValues(); | |
if (System.millis() > lastPublishMillis + 900000){ | |
lastPublishMillis = System.millis(); | |
publishStats(); | |
} | |
} | |
// Function called on rising edge of button input (the cart power button) | |
void powerOnButtonPushed(DebounceSwitchState *switchState, void *context){ | |
//Log.trace("Button state=%s", switchState->getPressStateName()); | |
if (switchState->getPressState() == DebouncePressState::TAP) { | |
// Log.trace("%d taps", switchState->getTapCount()); | |
// Check the intended global power state and set to true if it was false | |
if(!powerGlobalState) { | |
discharge(true); | |
} | |
} | |
if (switchState->getPressState() == DebouncePressState::PROGRESS) { | |
//Log.trace("%d taps", switchState->getTapCount()); | |
// Check the intended global power state and set to true if it was false | |
if(powerGlobalState) { | |
powerDownFlicker = true; | |
} | |
} | |
if (switchState->getPressState() == DebouncePressState::LONG) { | |
//Log.trace("%d taps", switchState->getTapCount()); | |
// Check the intended global power state and set to true if it was false | |
if(powerGlobalState) { | |
discharge(false); | |
powerDownFlicker = false; | |
} | |
} | |
} | |
// Function will take the intended state of discharge | |
// Discharge meaning is the battery feeding the primary load. | |
// The particle and BMS should/will be plugged into the battery directly. | |
void discharge(boolean enable){ | |
if(enable) { | |
//digitalWrite(loadEnablePin, HIGH); | |
//digitalWrite(buttonLedPin, HIGH); // this happens in the loop now | |
powerGlobalState = true; | |
// 900,000 is 15 mins ago, then 5 seconds in the future | |
// So the publish will happen 5 seconds after this | |
// to give BMS time to latch relay and report | |
lastPublishMillis = System.millis() - 900000 + 5000; | |
} else { | |
//digitalWrite(loadEnablePin, LOW); | |
//digitalWrite(buttonLedPin, LOW); // this happens in the loop now | |
powerGlobalState = false; | |
lastPublishMillis = System.millis() - 900000 + 5000; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment