-
Notifications
You must be signed in to change notification settings - Fork 1
/
autooffset.ino
508 lines (435 loc) · 13.1 KB
/
autooffset.ino
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
/* Automate carbon offset using Particle and OBDII
* made by Kane
* lol you're using my code godspeed
*
* Accumulates miles in a counter stored in EEPROM.
* When carbon offset interval is reached, use wren.co to automatically purchase offsets.
*
* Based on https://github.com/carloop/app-reminder
* Hardware required: Carloop Basic and Particle Photon
*/
#include "application.h"
#include "carloop/carloop.h"
#include <math.h>
// TODO: FILL THESE OUT!
const double mpg = 0.25; // mpg of your vehicle; 2004 Jeep Wrangler gets 14 mpg; using 0.25 for testing
const double fractionTon = 10.0; // fraction of ton you want offsets to run, eg. 10 -> every 1/10 ton (i think the wren min)
const double engineOnVoltage = 12.75; // voltage above which indicates engine is running
// Don't block the main program while connecting to WiFi/cellular.
// This way the main program runs on the Carloop even outside of WiFi range.
SYSTEM_THREAD(ENABLED);
// Tell the program which revision of Carloop you are using.
Carloop<CarloopRevision2> carloop;
// Function declarations
void setupCloud();
int resetIntervalCounter(String);
int changeIntervalLimit(String arg);
void updateSpeed();
void requestVehicleSpeed();
void waitForVehicleSpeedResponse();
void updateMileage(uint8_t newVehicleSpeedKmh);
double computeDeltaMileage(uint8_t newVehicleSpeedKmh);
void checkIntervalLimit();
void storeMileage();
void loadFromStorage();
void saveToStorage();
void test();
void checkEngineRunning();
// Helpers for converting doubles to strings
void ftoa(float n, char *res, int afterpoint);
int intToStr(int x, char str[], int d);
void reverse(char *str, int len);
// Structures
/*
* This struct must not be re-ordered since it is the EEPROM layout.
* Elements must not be deleted.
* To remove an element, replace the name with _unused1/2/3.
* - e.g. intervalReached --> _unused1
* TODO-dev: [2022-02-21] Is the above example correct? If so, should we update the code to reflect that?
* Elements must only be added at the end, and when elements are added,
* version should be incremented.
*/
struct Data
{
uint16_t appId; // Used to make sure the EEPROM was properly initialized for this app
uint16_t version; // Increment in case more fields are added in a later version
double intervalCounter; // The count of miles
// TODO-dev: [2022-02-21] Wonder if it'd be more self-descriptive if we called this milesCount?
double intervalLimit; // The upper limit of miles to trigger a reminder
// TODO-dev: [2022-02-21] Wonder if it'd be more self-descriptive if we called this milesThreshold?
uint8_t intervalReached; // (this isn't used anymore) Whether a reminder must be triggered next time the Carloop is online
double tonsOffset; // total tons offset
};
// 100 gallons = 1 ton CO2
// https://www.epa.gov/energy/greenhouse-gases-equivalencies-calculator-calculations-and-references
const double gallonsPerTonCarbon = 100.0; // gallons per ton of carbon
const double milesPerTonCarbon = gallonsPerTonCarbon * mpg; // miles per fractional ton carbon
const double milesPerFractionTonCarbon = milesPerTonCarbon / fractionTon;
// The default values for the EEPROM on first run
const Data DEFAULT_DATA = {
/*appId */
0x4352, // Letters CR = Carloop Reminder
/*version */
1,
/*intervalCounter */
0.0,
/*intervalLimit */
0.0,
/*intervalReached */
0,
/*tonsOffset */
0.0,
};
// The data that is stored and loaded in permanent storage (EEPROM)
Data data;
// Only store to EEPROM every so often
const auto STORAGE_PERIOD_MS = 60 * 1000; /* every minute */
uint32_t lastStorageTime = 0;
// USB power voltage for debug
const double usbPowerVoltage = 5.0;
// OBD constants for CAN
// reference: https://x-engineer.org/on-board-diagnostics-obd-modes-operation-diagnostic-services/
// CAN IDs for OBD messages
const auto OBD_CAN_REQUEST_ID = 0x7E0;
const auto OBD_CAN_REPLY_ID = 0x7E8;
// TODO-dev: [2022-02-21] Are these IDs also from the reference link above? Didn't see them mentioned
// Modes (aka services) for OBD
const auto OBD_MODE_CURRENT_DATA = 0x01;
// OBD signals (aka PID) that can be requested
const auto OBD_PID_VEHICLE_SPEED = 0x0d;
// Time to wait for a reply for an OBD request
const auto OBD_TIMEOUT_MS = 20;
// Track # of responses received
// TODO-dev: [2022-02-21] Why do we need this?
int obdResponseCount = 0;
uint8_t vehicleSpeedKmh = 0;
uint32_t lastVehicleSpeedUpdateTime = 0;
/*
* Called at boot
* Sets up the CAN bus and cloud functions
*/
void setup()
{
setupCloud();
// set up variables; write interval limit to EEPROM
vehicleSpeedKmh = 0;
lastVehicleSpeedUpdateTime = 0;
data.intervalLimit = milesPerFractionTonCarbon;
saveToStorage();
// auto select antenna in case using external
// STARTUP(WiFi.selectAntenna(ANT_AUTO));
// Configure the CAN bus speed for 500 kbps, the standard speed for the OBD-II port.
// Other common speeds are 250 kbps and 1 Mbps.
carloop.setCANSpeed(500000);
// Connect to the CAN bus
carloop.begin();
}
/*
* Allow interacting with the Carloop remotely
*/
void setupCloud()
{
Particle.function("reset", resetIntervalCounter);
Particle.function("limit", changeIntervalLimit);
Particle.variable("count", data.intervalCounter);
Particle.variable("msg", obdResponseCount);
}
/*
* Reset the interval counter and store the zero value in EEPROM
*/
int resetIntervalCounter(String = String())
{
data.intervalCounter = 0;
saveToStorage();
return 0;
}
/*
* Set the interval upper limit and make sure the current value is below
* that. Store the values value in EEPROM
*/
int changeIntervalLimit(String arg)
{
long newLimit = arg.toInt();
if (newLimit <= 0)
{
return -1;
}
data.intervalLimit = newLimit;
checkIntervalLimit();
saveToStorage();
return 0;
}
/*
* Called over and over
*
* Process new CAN messages here to update the vehicle speed, update
* the mileage and update the interval counter
*/
void loop()
{
carloop.update();
updateSpeed();
storeMileage();
// Uncomment below when testing
// test();
checkIntervalLimit();
checkEngineRunning();
delay(100);
}
/*
* Publish offset carbon as private event for testing purposes
*/
void test()
{
// convert to string bc publish() takes string
char str[16];
ftoa(data.intervalLimit / milesPerTonCarbon, str, 2);
Particle.publish("TEST", str, PRIVATE);
delay(2000);
}
/*
* Checks if engine is running, sleeps photon if not
*/
void checkEngineRunning()
{
double batteryVoltage = carloop.battery();
// if battery voltage is below 12.75V car is off, sleep for 30s to save battery
// if it's also above usbPowerVoltage limit, assume it's plugged into USB for diagnostics and don't sleep
if (batteryVoltage < engineOnVoltage && batteryVoltage > usbPowerVoltage)
{
Particle.publish("STATUS", "Sleeping");
System.sleep(D1, RISING, 30); // sleep for 30 seconds
}
}
/*
* Request the vehicle speed through OBD and wait for the response
*/
void updateSpeed()
{
requestVehicleSpeed();
waitForVehicleSpeedResponse();
}
/*
* Send a PID request for the vehicle speed
*/
void requestVehicleSpeed()
{
CANMessage message;
// A CAN message to request the vehicle speed
message.id = OBD_CAN_REQUEST_ID;
message.len = 8;
// Data is an OBD request: get current value of the vehicle speed PID
message.data[0] = 2; // 2 byte request
message.data[1] = OBD_MODE_CURRENT_DATA;
message.data[2] = OBD_PID_VEHICLE_SPEED;
// Send the message on the bus!
carloop.can().transmit(message);
}
/*
* Wait for the PID response with a timeout and update mileage to new
* value if response received — otherwise, update mileage to zero
*/
void waitForVehicleSpeedResponse()
{
uint32_t start = millis();
while ((millis() - start) < OBD_TIMEOUT_MS)
{
CANMessage message;
if (carloop.can().receive(message))
{
if (message.id == OBD_CAN_REPLY_ID &&
message.data[2] == OBD_PID_VEHICLE_SPEED)
{
uint8_t newVehicleSpeedKmh = message.data[3];
updateMileage(newVehicleSpeedKmh);
obdResponseCount++;
return;
}
}
}
// A timeout occurred
updateMileage(0);
}
/*
* Update the interval counter based on the new speed and check if the
* interval limit has been reached
*/
void updateMileage(uint8_t newVehicleSpeedKmh)
{
double deltaMileage = computeDeltaMileage(newVehicleSpeedKmh);
data.intervalCounter += deltaMileage;
// TODO-dev [2022-02-21] Does this need to be saved to storage?
}
/*
* Calculate the increase in mileage given the old and new speed
*/
double computeDeltaMileage(uint8_t newVehicleSpeedKmh)
{
uint32_t now = millis();
double deltaMileage = 0.0;
int PLAUSIBLE_DIFF_MS = 1000;
// TODO-dev: [2022-02-21] How was this determined?
// If the speed was previously 0 or newly 0, or timed out because the
// car was off, just save the new speed value
// TODO-dev: [2022-02-21] Is the comment above still correct?
if (vehicleSpeedKmh > 0 && newVehicleSpeedKmh > 0)
{
// The car was previously driving and is still driving
// Figure out the distance driven using the trapezoidal rule
uint32_t msDiff = now - lastVehicleSpeedUpdateTime;
// Calculate only if the difference is plausible
if (msDiff < PLAUSIBLE_DIFF_MS)
{
// distance in km/h * ms
uint32_t deltaDistance = msDiff * (vehicleSpeedKmh + newVehicleSpeedKmh) / 2;
// Convert to miles
// (1 kilometer per hour * ms) * 1.72603109 × 10^-7 = miles
// (km/h * ms) *(1 h / 3600 s) * (0.621371 mi / 1 km) * (1 s / 1000 ms)
deltaMileage = deltaDistance * 1.72603109e-7;
}
}
// Update logged speed and time with latest values
vehicleSpeedKmh = newVehicleSpeedKmh;
lastVehicleSpeedUpdateTime = now;
return deltaMileage;
}
/*
* If the interval limit is reached, mark it so we can publish an event
* when we come back to network range
* TODO-dev: [2022-02-21] Is this description correct? Seems like data
* will only be updated if the limit is reached AND we're connected,
* and this function won't do anything otherwise? Could probably benefit
* from a rename if so since it's doing more than just checking.
*/
void checkIntervalLimit()
{
// over limit and connected
while ((data.intervalCounter >= data.intervalLimit) && Particle.connected())
{
// calculate amount to offset
// this should equal 1/fractionTon
double tcOffset = data.intervalLimit / milesPerTonCarbon;
// increment total aggregate offset
data.tonsOffset += tcOffset;
// publish amount to offset via wren api
char str[16];
ftoa(tcOffset, str, 2);
Particle.publish("Offset Carbon", str, PRIVATE);
delay(1500); // don't trip the rate limiter
/* publish total amount offset
char str2[16];
ftoa(data.tonsOffset, str2, 2);
Particle.publish("Total Tons Carbon Offset", str2, PRIVATE);
delay(2000); // don't trip the rate limiter
// publish total miles offset
char str3[16];
ftoa(data.intervalCounter, str3, 2);
Particle.publish("Total Miles Driven", str3, PRIVATE);
delay(2000); // don't trip the rate limiter
*/
// decrement the counter by the limit we just offset
data.intervalCounter -= data.intervalLimit;
// update EEPROM intervalCounter
saveToStorage();
}
// otherwise just keep counting until you get to internet
}
/*
* Store data to EEPROM every STORAGE_PERIOD_MS ms and
* update lastStorageTime
*/
void storeMileage()
{
if (millis() - lastStorageTime > STORAGE_PERIOD_MS)
{
saveToStorage();
lastStorageTime = millis();
}
}
/*
* Load the data structure to EEPROM permanent storage
*/
void loadFromStorage()
{
EEPROM.get(0, data);
// On first load, set the EEPROM to default values
if (data.appId != DEFAULT_DATA.appId)
{
data = DEFAULT_DATA;
saveToStorage();
}
}
/*
* Save the data structure to EEPROM permanent storage
*/
void saveToStorage()
{
EEPROM.put(0, data);
}
/*
* Helper functions for converting doubles to string for publish() call
* These are only here bc Wren API needs a "0."-prefixed double,
* but Particle.publish() only gives strings
*/
/*
* Reverses a string 'str' of length 'len'
*/
void reverse(char *str, int len)
{
int i = 0, j = len - 1, temp;
while (i < j)
{
temp = str[i];
str[i] = str[j];
str[j] = temp;
i++;
j--;
}
}
/*
* Converts a given integer x to string str[].
* d is the number of digits required in the output.
* If d is more than the number of digits in x,
* then 0s are added at the beginning.
*/
int intToStr(int x, char str[], int d)
{
int i = 0;
if (!x)
str[i++] = '0'; // If the int part is 0, make it explicit for Wren API
while (x)
{
str[i++] = (x % 10) + '0';
x = x / 10;
}
// If number of digits required is more, then add 0s at the beginning
while (i < d)
{
str[i++] = '0';
}
reverse(str, i);
str[i] = '\0';
return i;
}
/*
* Converts a floating-point/double number to a string.
*/
void ftoa(float n, char *res, int afterpoint)
{
// Extract integer part
int ipart = (int)n;
// Extract floating part
float fpart = n - (float)ipart;
// Convert integer part to string
int i = intToStr(ipart, res, 0);
// Check for display option after point
if (afterpoint != 0)
{
res[i] = '.'; // add dot
// Get the value of fraction part upto given no.
// of points after dot. The third parameter
// is needed to handle cases like 233.007
fpart = fpart * pow(10, afterpoint);
intToStr((int)fpart, res + i + 1, afterpoint);
}
}