-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathProgram.cs
185 lines (145 loc) · 5.79 KB
/
Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
using SerialPortLib;
using System.Globalization;
var pollFrequencyMillis = 1000;
var cellVoltages = new Dictionary<byte, float>(); //key: cell number //val: cell voltage
var recentAmpReadings = new AmpValQueue(10); //avg value over 10 readings (~10secs)
var bms = new SerialPortInput();
bms.SetPort("/dev/ttyUSB0", 115200);
bms.ConnectionStatusChanged += (object _, ConnectionStatusChangedEventArgs e) =>
{
if (e.Connected)
bms.QueryData();
Console.WriteLine($"CONNECTED: {e.Connected}");
};
bms.MessageReceived += (object _, MessageReceivedEventArgs evnt) =>
{
Console.Clear();
var data = evnt.Data.AsSpan();
if (!data.IsValid())
{
Console.WriteLine("invalid data received!");
Thread.Sleep(pollFrequencyMillis);
bms.QueryData();
return;
}
var res = data[11..]; //skip the first 10 bytes
var cellCount = res[1] / 3; //pos 1 is total cell bytes length. 3 bytes per cell.
Console.WriteLine($"cell count:{cellCount}");
ushort pos = 0;
for (byte i = 1; i <= cellCount; i++)
{
//cell voltage groups (of 3 bytes) start at pos 2
//first cell voltage starts at position 3 (pos 2 is cell number). voltage value is next 2 bytes.
// ex: .....,1,X,X,2,Y,Y,3,Z,Z
//pos is increased by 3 bytes in order to skip the address/code byte
cellVoltages[i] = res.Read2Bytes(pos += 3) / 1000f;
Console.WriteLine($"cell {i}: {cellVoltages[i]:0.000} V");
}
var avgCellVoltage = cellVoltages.Values.Average();
var minCell = cellVoltages.MinBy(x => x.Value);
var maxCell = cellVoltages.MaxBy(x => x.Value);
var cellDiff = maxCell.Value - minCell.Value;
Console.WriteLine($"avg cell voltage: {avgCellVoltage:0.000} V");
Console.WriteLine($"min cell: [{minCell.Key}] {minCell.Value:0.000} V");
Console.WriteLine($"max cell: [{maxCell.Key}] {maxCell.Value:0.000} V");
Console.WriteLine($"cell diff: {cellDiff:0.000} V");
var mosTemp = res.Read2Bytes(pos += 3);
var probe1Temp = res.Read2Bytes(pos += 3);
var probe2Temp = res.Read2Bytes(pos += 3);
Console.WriteLine($"mos temp: {mosTemp} C | t1: {probe1Temp} C | t2: {probe2Temp} C");
var packVoltage = res.Read2Bytes(pos += 3) / 100f;
Console.WriteLine($"pack voltage: {packVoltage:00.00} V");
var rawVal = res.Read2Bytes(pos += 3);
var isCharging = Convert.ToBoolean(int.Parse(Convert.ToString(rawVal, 2).PadLeft(16, '0')[..1])); //pick first bit of padded 16 bit binary representation and turn it in to a bool
Console.WriteLine($"charging: {isCharging}");
rawVal &= (1 << 15) - 1; //unset the MSB with a bitmask
var ampVal = rawVal / 100f;
recentAmpReadings.Enqueue(ampVal);
var avgCurrentAmps = recentAmpReadings.GetAverage();
Console.WriteLine($"current: {avgCurrentAmps:0.0} A");
var capacityPct = Convert.ToUInt16(res[pos += 3]);
Console.WriteLine($"capacity: {capacityPct} %");
var isWarning = res.Read2Bytes(pos += 15) > 0;
Console.WriteLine($"warning: {isWarning}");
var packCapacity = res.Read4Bytes(pos += 88);
Console.WriteLine($"pack capacity: {packCapacity} Ah");
var availableCapacity = packCapacity / 100f * capacityPct;
Console.WriteLine($"available capacity: {availableCapacity:0.0} Ah");
var timeLeft = 0f;
if (avgCurrentAmps > 0)
{
if (isCharging)
timeLeft = (packCapacity - availableCapacity) / avgCurrentAmps;
else
timeLeft = availableCapacity / avgCurrentAmps;
var tSpan = TimeSpan.FromHours(timeLeft);
var totalHrs = (ushort)tSpan.TotalHours;
Console.WriteLine($"time left: {totalHrs} Hrs {tSpan.Minutes} Mins");
}
var cRate = Math.Round(avgCurrentAmps / packCapacity, 2, MidpointRounding.AwayFromZero);
Console.WriteLine($"c-rate: {cRate}");
Thread.Sleep(pollFrequencyMillis);
bms.QueryData();
};
bms.Connect();
public static class Extensions
{
private static readonly byte[] cmd = Convert.FromHexString("4E5700130000000006030000000000006800000129");
public static void QueryData(this SerialPortInput port)
{
port.SendMessage(cmd);
}
public static ushort Read2Bytes(this Span<byte> data, ushort startPos)
{
var hex = Convert.ToHexString(data.Slice(startPos, 2));
return ushort.Parse(hex, NumberStyles.HexNumber);
}
public static uint Read4Bytes(this Span<byte> input, ushort startPos)
{
var hex = Convert.ToHexString(input.Slice(startPos, 4));
return uint.Parse(hex, NumberStyles.HexNumber);
}
public static bool IsValid(this Span<byte> data)
{
if (data.Length < 8)
return false;
var header = Convert.ToHexString(data[..2]); //get hex from first 2 bytes
if (header is not "4E57")
return false;
short crc_calc = 0;
foreach (var b in data[0..^3])//sum up all bytes excluding the last 4 bytes
crc_calc += b;
//convert last 2 bytes to hex and get short from that hex
var crc_lo = data[^2..^0].Read2Bytes(0);
return crc_calc == crc_lo;
}
}
public sealed class AmpValQueue : Queue<float>
{
public int FixedCapacity { get; }
public AmpValQueue(int fixedCapacity)
{
FixedCapacity = fixedCapacity;
}
public new void Enqueue(float val)
{
if (val > 0)
{
base.Enqueue(val);
if (Count > FixedCapacity)
{
Dequeue();
}
}
else
{
Clear();
}
}
public float GetAverage()
{
return Count > 0
? this.Average()
: 0;
}
}