Skip to content

Instantly share code, notes, and snippets.

@markamber
Created April 12, 2023 20:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markamber/3c24759a1b5db58e1ca1836539a409be to your computer and use it in GitHub Desktop.
Save markamber/3c24759a1b5db58e1ca1836539a409be to your computer and use it in GitHub Desktop.
/*
* 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