Components and supplies
CAN-BUS MCP2515 Module TJA1050 Receiver SPI Module
Arduino UNO
NeoPixel Ring: WS2812 5050 RGB LED
16x2 1602 LCD Keypad Shield For Arduino
Bluetooth Low Energy (BLE) Module (Generic)
MAXREFDES99# MAX7219 Display Driver Shield
Apps and platforms
RaceChrono
Arduino IDE
Project description
Code
Prototype
arduino
1#define DEBUG 2#define noDEMO 3 4#include "debug.h" 5#include "vars.h" 6#include "Config.h" 7#include <Adafruit_NeoPixel.h> 8#include <LiquidCrystal.h> 9#include <menu.h> 10#include <menuIO/serialOut.h> 11#include <menuIO/liquidCrystalOut.h> 12#include <SoftwareSerial.h> 13//#include <LedControl.h> 14#include <LEDMatrixDriver.hpp> 15#include <mcp_can.h> 16#include <SPI.h> 17 18/* 19 * GLOBAL VARIABLES 20 */ 21 22#define RPM_MIN RPM_TRIGGER[0] 23#define CONFIG configuration.data 24 25// GEARS 8x8 LED Matrix 26//LedControl gears_lcd (PIN_GEARS_data,PIN_GEARS_clock,PIN_GEARS_select,PIN_GEARS_devices); 27LEDMatrixDriver gears_lcd(1, PIN_GEARS_select, LEDMatrixDriver::INVERT_Y); 28// Multipurpose 16x2 LCD 29LiquidCrystal lcd (PIN_LCD_RS, PIN_LCD_ENABLE, PIN_LCD_D4, PIN_LCD_D5, PIN_LCD_D6, PIN_LCD_D7); 30// Bluetooth Serial console 31SoftwareSerial BTserial (PIN_BT_RX, PIN_BT_TX); 32// Neopixel Ring for RPM 33Adafruit_NeoPixel neoring (NEORING_LEDS, PIN_NEORING, NEO_GRB + NEO_KHZ800); 34// Configuration in EEPROM 35// necessary to pass object inside via pointer to being able to interact and apply() configuration changes 36Configuration configuration(gears_lcd,neoring); 37 38/* 39 * GLOBAL MENU 40 */ 41using namespace Menu; 42bool lcd_menu_active=false; 43#define MENU_MAX_DEPTH 3 44 45// TODO: performance hit when using Configuration class members ? at least in VIRTUAL: 46Menu::result menu_rpm_brightness(eventMask e,navNode& nav,prompt& item) { 47 //neoring.setBrightness(map(set_rpm_brightness,0,100,0,255)); 48 configuration.apply(C_RPM); 49 return proceed; 50} 51 52Menu::result menu_gear_brightness(eventMask e,navNode& nav,prompt& item) { 53 //gears_lcd.setIntensity(0,map(set_gear_brightness,0,100,0,15)); 54 configuration.apply(C_GEAR); 55 return proceed; 56} 57 58Menu::result menu_save_config() { 59 configuration.save(); 60 return quit; 61} 62 63#define MENU_PROCESSING \\ 64 lcd.clear();\\ 65 lcd.setCursor(0,0);\\ 66 lcd.print(F(">> PROCESSING <<")); 67 68Menu::result menu_default_config() { 69 MENU_PROCESSING; 70 configuration.loadDefaults(); 71 configuration.save(); 72 configuration.apply(); 73 return quit; 74} 75 76Menu::result menu_back_action(){ 77 return quit; 78} 79 80Menu::result menu_rpm_change (eventMask e,navNode& nav,prompt& item) { 81 MENU_PROCESSING; 82 configuration.apply(C_COLOR); 83} 84 85Menu::result menu_rpm_color_change (eventMask e,navNode& nav,prompt& item) { 86 // nav.sel has the index of the menu that is currently selected and manipulated 87 neoring.fill(myColorHSV(CONFIG.RPM_COLOR[nav.sel/2+1],CONFIG.RPM_COLOR_LIGHTNESS[nav.sel/2+1])); 88 neoring.show(); 89} 90 91Menu::result menu_rpm_color_display (eventMask e,navNode& nav,prompt& item) { 92 switch (e) 93 { 94 case enterEvent: 95 DBG(F("ENTER CLR MENU")); 96 neoring_active=false; 97 break; 98 case exitEvent: 99 DBG(F("EXIT CLR MENU")); 100 menu_rpm_change(e,nav,item); 101 neoring_active=true; 102 break; 103 } 104 return proceed; 105} 106 107MENU(configMenu_RPM_limits,"Set RPM limits",doNothing,noEvent,wrapStyle 108 ,FIELD(CONFIG.RPM_TRIGGER[0],"RPM min","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 109 ,FIELD(CONFIG.RPM_MAX,"RPM max","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 110 ,FIELD(CONFIG.RPM_TRIGGER[1],"Stage1","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 111 ,FIELD(CONFIG.RPM_NUMPIXELS[1],"Stage1 LEDs","",0,NEORING_LEDS,1,0, menu_rpm_change, exitEvent, noStyle) 112 ,FIELD(CONFIG.RPM_TRIGGER[2],"Stage2","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 113 ,FIELD(CONFIG.RPM_NUMPIXELS[2],"Stage2 LEDs","",0,NEORING_LEDS,1,0, menu_rpm_change, exitEvent, noStyle) 114 ,FIELD(CONFIG.RPM_TRIGGER[3],"Stage3","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 115 ,FIELD(CONFIG.RPM_NUMPIXELS[3],"Stage3 LEDs","",0,NEORING_LEDS,1,0, menu_rpm_change, exitEvent, noStyle) 116 ,FIELD(CONFIG.RPM_TRIGGER[4],"StageFLSH","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 117); 118 119MENU(configMenu_RPM_colors,"Set RPM colors",menu_rpm_color_display, (eventMask)(enterEvent | exitEvent),wrapStyle 120 ,FIELD(CONFIG.RPM_COLOR[1],"Stage1","",0,1529,50,1, menu_rpm_color_change, enterEvent, noStyle) 121 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[1],"Stage1Light","",0,255,20,1, menu_rpm_color_change, enterEvent, noStyle) 122 ,FIELD(CONFIG.RPM_COLOR[2],"Stage2","",0,1529,50,1, menu_rpm_color_change, enterEvent, noStyle) 123 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[2],"Stage2Light","",0,255,20,1, menu_rpm_color_change, enterEvent, noStyle) 124 ,FIELD(CONFIG.RPM_COLOR[3],"Stage3","",0,1529,50,1, menu_rpm_color_change, enterEvent, noStyle) 125 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[3],"Stage3Light","",0,255,20,1, menu_rpm_color_change, enterEvent, noStyle) 126 ,FIELD(CONFIG.RPM_COLOR[4],"StageFLSH","",0,1529,50,1, menu_rpm_color_change, enterEvent, noStyle) 127 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[4],"StageFLLght","",0,255,20,1, menu_rpm_color_change, enterEvent, noStyle) 128); 129 130MENU(configMenu_SAVE,"Save config?",doNothing,noEvent,wrapStyle 131 ,OP("Yes",menu_save_config,enterEvent) 132 ,OP("No",menu_back_action,enterEvent) 133); 134 135MENU(configMenu_DEFAULT,"Reset Defaults?",doNothing,noEvent,wrapStyle 136 ,OP("Yes",menu_default_config,enterEvent) 137 ,OP("No",menu_back_action,enterEvent) 138); 139 140MENU (configMenu,"Configuration",doNothing,noEvent,wrapStyle 141 ,SUBMENU(configMenu_SAVE) 142 ,SUBMENU(configMenu_DEFAULT) 143); 144 145MENU(mainMenu, "Settings", doNothing, noEvent, wrapStyle 146 ,FIELD(CONFIG.rpm_brightness,"RPM LED","%",0,100,5,1, menu_rpm_brightness, enterEvent, noStyle) 147 ,FIELD(CONFIG.gear_brightness,"Gear LED","%",0,100,5,1, menu_gear_brightness, enterEvent, noStyle) 148 ,SUBMENU(configMenu_RPM_limits) 149 ,SUBMENU(configMenu_RPM_colors) 150 ,SUBMENU(configMenu) 151 ,EXIT("<Exit menu") 152); 153 154Menu::noInput noinput; 155//stringIn<0> menu_strIn; 156//serialIn serial(Serial); 157//MENU_INPUTS(in,&serial); 158 159MENU_OUTPUTS(out,MENU_MAX_DEPTH 160 ,LIQUIDCRYSTAL_OUT(lcd,{0,0,16,2}) 161 ,NONE//must have 2 items at least 162); 163 164NAVROOT(nav,mainMenu,MENU_MAX_DEPTH,noinput,out); 165 166/* 167 * SETUP() 168 */ 169 170void setup() { 171 // Serial comms init 172 Serial.begin(9600); 173 BTserial.begin(9600); 174 175 // Neoring init 176 neoring.begin(); 177 // VIRTUAL: do not use low brightness in SIM as it's not visible. Keep brightness low for real NeoRing HW. 178 //neoring.setBrightness(8); 179 180 // 8x8 LED Matrix init 181 gears_lcd.setEnabled(true); 182 183 // Apply EEPROM or Default config for Neoring, Gears matrix and RPM limits 184 configuration.load(); 185 configuration.apply(); 186 187 // 16x2 LCD init 188 lcd.begin(16,2); 189 lcd.clear(); 190 191 // set menu visibility on startup as "idle" 192 // instead use our own Monitor screen and handle Menu callback in lcd_monitor_screen 193 nav.idleTask=lcd_monitor_screen; 194 nav.idleOn(); 195 lcd_monitor_screen(out[0],Menu::idling); 196 197 // play BMW logo rotation animation on startup 198/* 199 gears_display(&GEARS_GLYPH[10]);delay(1500); 200 for (byte anim=0;anim<4;anim++) 201 { 202 gears_display(&GEARS_GLYPH[10]);delay(100); 203 for (byte i=11;i<=13;i++) 204 { 205 gears_display(&GEARS_GLYPH[i]);delay(100); 206 } 207 } 208 gears_display(&GEARS_GLYPH[10]); 209 */ 210 // fill the rpm_scale_val and rpm_scale_col arrays with boundaries for each neopixel 211 //rpm_scale_compute(); 212 213 noInterrupts(); 214 // 10Hz interrupt on TIMER1 for Racechrono BT LE output 215 TCCR1A = 0;// set entire TCCR1A register to 0 216 TCCR1B = 0;// same for TCCR1B 217 TCNT1 = 0;//initialize counter value to 0 218 // set compare match register for 1hz increments 219 OCR1A = 1562; // = (16*10^6) / (10*1024) - 1 (must be <65536) 220 // turn on CTC mode 221 TCCR1B |= (1 << WGM12); 222 // Set CS10 and CS12 bits for 1024 prescaler 223 TCCR1B |= (1 << CS12) | (1 << CS10); 224 // enable timer compare interrupt 225 TIMSK1 |= (1 << OCIE1A); 226 interrupts(); 227} 228 229void loop() { 230 231#ifdef DEMO 232unsigned short r; 233for (byte g=1;g<7;g++) 234{ 235 gears_display(&GEARS_GLYPH[g]); 236 for (r=CONFIG.RPM_MIN;r<5700;r+=10) 237 { 238 rpm_fill(r); 239 lcd.setCursor(0,1); 240 lcd.print(" "); 241 lcd.setCursor(0,1); 242 lcd.print(r); 243 } 244} 245#endif 246 247 int lcd_button=analogRead(PIN_LCD_INPUT); 248 249 if ((millis()-last_debounce_time) > debounce_delay) 250 { 251 for (byte i=1;i<=4;i++) 252 { 253 if (lcd_button>=lcd_button_range[i][1] && lcd_button<=lcd_button_range[i][2]) nav.doNav((Menu::navCmds) lcd_button_range[i][0]); 254 } 255 256 if (lcd_button<lcd_button_range[0][1]) 257 { 258 DBG(F("Refresh menu")); 259 DBG(lcd_button); 260 lcd_menu_active=true; 261 last_debounce_time=millis(); 262 nav.doOutput(); 263 } 264 } 265 266 // TODO: Integrate CANbus readings - currently only temporary PIN_RPM analog value used instead of CANBus 267 // map analog PIN_RPM to values 0-xxxx(RPM_MAX) 268 rpm = map(analogRead(PIN_RPM), 0,1023, 0,CONFIG.RPM_MAX); 269 //rpm = int(RPM_MAX/float(1023)*analogRead(PIN_RPM)); // read the input pin 270 // display the Neoring RPM with that value 271 rpm_fill(rpm); 272 273 // read the DAC convertor value 274 gear_dac=analogRead(PIN_GEARS_INPUT); 275 // and select gear based on DAC convertor lookup table. The lookup KEY is dynamically calculated so it is a direct access to the final gear to be displayed. No min/max Analogread comparisons. 276 // 1024/16= 64 = full scale analogRead divided by 16 possible bits, and shifted by 32 (half of the "ranges") to both sides to make the AnalogRead boundaries. 277 //gear=pgm_read_byte(&(gears_dac_lookup[(gear_dac+32)/(1024/16)][1])); 278 gear=pgm_read_byte(&gears_dac_lookup[(gear_dac+32)/(1024/16)][1]); 279 280 // read GEARs from the serial console if available 281 /* 282 if (Serial.available()) 283 { 284 String console=Serial.readStringUntil('\n'); 285 gear=(byte) console.toInt(); 286 } 287 */ 288 289 // TODO: performance - move to Interrupt section ? Make a millis() for refresh? 290 if( millis()-last_gear_refreshtime>1000) 291 { 292 gears_display(&GEARS_GLYPH[gear]); 293 last_gear_refreshtime=millis(); 294 } 295 296 if (last_rpm!=rpm && !lcd_menu_active) 297 { 298 lcd.setCursor(0,1); 299 lcd.print(" "); 300 lcd.setCursor(0,1); 301 lcd.print(rpm); // Write a character to display 302 last_rpm=rpm; 303 } 304} 305 306// Racechrono BT output interrupt each 100ms aka 10Hz 307ISR(TIMER1_COMPA_vect) 308{ 309 char output[33]; 310 sprintf_P(output,PSTR("$RC2,,%u,,,,%d,%d,,,,,,,,*"),RC_counter,rpm,gear); 311 byte checksum = 0; 312 char checksum_format[]="00"; 313 // to verify, check https://nmeachecksum.eqth.net/ for simple NMEA-CRC online calculator 314 // calulate CRC only for the message "body" between $ and *. These are excluded from the CRC. 315 for (int i = 1; i < strlen(output)-1; i++) 316 { 317 checksum = checksum ^ (unsigned byte)output[i]; 318 } 319 sprintf_P(checksum_format,PSTR("%02X"),checksum); 320 strcat(output,checksum_format); 321 BTserial.println(output); 322 RC_counter++; 323 // as RC_counter is unsigned it roll over automatically 65535+1= back to 0 324 // if (RC_counter==65535) RC_counter=0; 325} 326 327 328void gears_display(const void *image_pointer) 329{ 330 uint64_t image; 331 memcpy_P(&image,image_pointer,sizeof(uint64_t)); 332 for (int i = 0; i < 8; i++) 333 { 334 byte row = (image >> i * 8); 335 for (int j = 0; j < 8; j++) 336 { 337 gears_lcd.setPixel(i, j, bitRead(row, j)); 338 } 339 } 340 gears_lcd.display(); 341} 342 343 344// Used to render Neoring with RPM value 345void rpm_fill(int rpm) 346{ 347 if (!neoring_active) return; 348 349 neoring.clear(); 350 351 // if out of range, just clear the neoring and exit 352 if (rpm <= CONFIG.RPM_MIN || rpm > CONFIG.RPM_MAX) 353 { 354 if (neoring.canShow()) neoring.show(); 355 return; 356 } 357 358 // Flashing all 359 if (rpm >= CONFIG.RPM_TRIGGER[RPM_FLASH]) 360 { 361 neoring.fill(RPM_COLOR[RPM_FLASH]);neoring.show(); 362 delay(50); 363 neoring.fill(0);neoring.show(); 364 delay(50); 365 neoring.fill(RPM_COLOR[RPM_FLASH]);neoring.show(); 366 return; 367 } 368 369 // Normal operation, fill the LEDs according to RPMs 370 for (byte position=0;position < NEORING_LEDS;position++) 371 { 372 if ( rpm > rpm_scale_val[position]) neoring.setPixelColor(NEORING_LEDS-1-position,*rpm_scale_col[position]); 373 else neoring.setPixelColor(NEORING_LEDS-1-position,0); 374 } 375 376 if (neoring.canShow()) neoring.show(); 377} 378 379void rpm_scale_compute() 380{ 381 byte position=0; 382 //for all G,Y,R before FLASH calculate and fill the internal array of RPM values 383 RPM_COLOR[RPM_FLASH]=myColorHSV(CONFIG.RPM_COLOR[RPM_FLASH],CONFIG.RPM_COLOR_LIGHTNESS[RPM_FLASH]); 384 for (byte stage=1;stage < RPM_FLASH;stage++) 385 { 386 RPM_COLOR[stage]=myColorHSV(CONFIG.RPM_COLOR[stage],CONFIG.RPM_COLOR_LIGHTNESS[stage]); 387 position=position+CONFIG.RPM_NUMPIXELS[stage-1]; 388 if (position+CONFIG.RPM_NUMPIXELS[stage] <= NEORING_LEDS) 389 { 390 for (byte i=0;i<CONFIG.RPM_NUMPIXELS[stage];i++) 391 { 392 rpm_scale_val[position+i]=((CONFIG.RPM_TRIGGER[stage]-CONFIG.RPM_TRIGGER[stage-1])/CONFIG.RPM_NUMPIXELS[stage]*(i+0))+CONFIG.RPM_TRIGGER[stage-1]; 393 rpm_scale_col[position+i]=&RPM_COLOR[stage]; 394 DBG(position+i); 395 DBG(rpm_scale_val[position+i]); 396 } 397 } 398 } 399} 400 401Menu::result lcd_monitor_screen(menuOut& out,idleEvent e) 402{ 403 // idleStart - fired when entering idle state, but last menurefresh is still executed 404 // idling - fired once when enering menu idle mode, and after all menu refresh/clear is done 405 // idleEnd - fired when leaving idle state, but before any menu init is done 406 407 // so rely on idling state and prepare the lcd_monitor_screen to take over 408 if (e==Menu::idling) 409 { 410 out.clear(); 411 out.setCursor(0,0); 412 out.print("RPM WATER OIL"); 413 // used for decision if menu must be polled/refreshed to save resources in loop() 414 lcd_menu_active=false; 415 } 416} 417 418uint32_t myColorHSV(uint16_t hue, uint8_t val) { 419 // Remap 0-65535 to 0-1529. Pure red is CENTERED on the 64K rollover; 420 // 0 is not the start of pure red, but the midpoint...a few values above 421 // zero and a few below 65536 all yield pure red (similarly, 32768 is the 422 // midpoint, not start, of pure cyan). The 8-bit RGB hexcone (256 values 423 // each for red, green, blue) really only allows for 1530 distinct hues 424 // (not 1536, more on that below), but the full unsigned 16-bit type was 425 // chosen for hue so that one's code can easily handle a contiguous color 426 // wheel by allowing hue to roll over in either direction. 427///////// hue = (hue * 1530L + 32768) / 65536; 428 // Because red is centered on the rollover point (the +32768 above, 429 // essentially a fixed-point +0.5), the above actually yields 0 to 1530, 430 // where 0 and 1530 would yield the same thing. Rather than apply a 431 // costly modulo operator, 1530 is handled as a special case below. 432 433 uint8_t r, g, b, sat; 434 435 if (val<128) {val=map(val,0,127,0,255);sat=255;} 436 else if (val>=128) {sat=map(val,128,255,255,0);val=255;} 437 438 // Convert hue to R,G,B (nested ifs faster than divide+mod+switch): 439 if(hue < 510) { // Red to Green-1 440 b = 0; 441 if(hue < 255) { // Red to Yellow-1 442 r = 255; 443 g = hue; // g = 0 to 254 444 } else { // Yellow to Green-1 445 r = 510 - hue; // r = 255 to 1 446 g = 255; 447 } 448 } else if(hue < 1020) { // Green to Blue-1 449 r = 0; 450 if(hue < 765) { // Green to Cyan-1 451 g = 255; 452 b = hue - 510; // b = 0 to 254 453 } else { // Cyan to Blue-1 454 g = 1020 - hue; // g = 255 to 1 455 b = 255; 456 } 457 } else if(hue < 1530) { // Blue to Red-1 458 g = 0; 459 if(hue < 1275) { // Blue to Magenta-1 460 r = hue - 1020; // r = 0 to 254 461 b = 255; 462 } else { // Magenta to Red-1 463 r = 255; 464 b = 1530 - hue; // b = 255 to 1 465 } 466 } else { // Last 0.5 Red (quicker than % operator) 467 r = 255; 468 g = b = 0; 469 } 470 471 // Apply saturation and value to R,G,B, pack into 32-bit result: 472 uint32_t v1 = 1 + val; // 1 to 256; allows >>8 instead of /255 473 uint16_t s1 = 1 + sat; // 1 to 256; same reason 474 uint8_t s2 = 255 - sat; // 255 to 0 475 return ((((((r * s1) >> 8) + s2) * v1) & 0xff00) << 8) | 476 (((((g * s1) >> 8) + s2) * v1) & 0xff00) | 477 ( ((((b * s1) >> 8) + s2) * v1) >> 8); 478}
Prototype
arduino
1#define DEBUG 2#define noDEMO 3 4#include "debug.h" 5#include 6 "vars.h" 7#include "Config.h" 8#include <Adafruit_NeoPixel.h> 9#include 10 <LiquidCrystal.h> 11#include <menu.h> 12#include <menuIO/serialOut.h> 13#include 14 <menuIO/liquidCrystalOut.h> 15#include <SoftwareSerial.h> 16//#include <LedControl.h> 17#include 18 <LEDMatrixDriver.hpp> 19#include <mcp_can.h> 20#include <SPI.h> 21 22/* 23 24 * GLOBAL VARIABLES 25 */ 26 27#define RPM_MIN RPM_TRIGGER[0] 28#define CONFIG 29 configuration.data 30 31// GEARS 8x8 LED Matrix 32//LedControl gears_lcd 33 (PIN_GEARS_data,PIN_GEARS_clock,PIN_GEARS_select,PIN_GEARS_devices); 34LEDMatrixDriver 35 gears_lcd(1, PIN_GEARS_select, LEDMatrixDriver::INVERT_Y); 36// Multipurpose 37 16x2 LCD 38LiquidCrystal lcd (PIN_LCD_RS, PIN_LCD_ENABLE, PIN_LCD_D4, PIN_LCD_D5, 39 PIN_LCD_D6, PIN_LCD_D7); 40// Bluetooth Serial console 41SoftwareSerial BTserial 42 (PIN_BT_RX, PIN_BT_TX); 43// Neopixel Ring for RPM 44Adafruit_NeoPixel neoring 45 (NEORING_LEDS, PIN_NEORING, NEO_GRB + NEO_KHZ800); 46// Configuration in EEPROM 47// 48 necessary to pass object inside via pointer to being able to interact and apply() 49 configuration changes 50Configuration configuration(gears_lcd,neoring); 51 52/* 53 54 * GLOBAL MENU 55 */ 56using namespace Menu; 57bool lcd_menu_active=false; 58#define 59 MENU_MAX_DEPTH 3 60 61// TODO: performance hit when using Configuration class 62 members ? at least in VIRTUAL: 63Menu::result menu_rpm_brightness(eventMask e,navNode& 64 nav,prompt& item) { 65 //neoring.setBrightness(map(set_rpm_brightness,0,100,0,255)); 66 67 configuration.apply(C_RPM); 68 return proceed; 69} 70 71Menu::result menu_gear_brightness(eventMask 72 e,navNode& nav,prompt& item) { 73 //gears_lcd.setIntensity(0,map(set_gear_brightness,0,100,0,15)); 74 75 configuration.apply(C_GEAR); 76 return proceed; 77} 78 79Menu::result menu_save_config() 80 { 81 configuration.save(); 82 return quit; 83} 84 85#define MENU_PROCESSING 86 \\ 87 lcd.clear();\\ 88 lcd.setCursor(0,0);\\ 89 lcd.print(F(">> PROCESSING 90 <<")); 91 92Menu::result menu_default_config() { 93 MENU_PROCESSING; 94 configuration.loadDefaults(); 95 96 configuration.save(); 97 configuration.apply(); 98 return quit; 99} 100 101Menu::result 102 menu_back_action(){ 103 return quit; 104} 105 106Menu::result menu_rpm_change (eventMask 107 e,navNode& nav,prompt& item) { 108 MENU_PROCESSING; 109 configuration.apply(C_COLOR); 110} 111 112Menu::result 113 menu_rpm_color_change (eventMask e,navNode& nav,prompt& item) { 114 // nav.sel 115 has the index of the menu that is currently selected and manipulated 116 neoring.fill(myColorHSV(CONFIG.RPM_COLOR[nav.sel/2+1],CONFIG.RPM_COLOR_LIGHTNESS[nav.sel/2+1])); 117 118 neoring.show(); 119} 120 121Menu::result menu_rpm_color_display (eventMask e,navNode& 122 nav,prompt& item) { 123 switch (e) 124 { 125 case enterEvent: 126 DBG(F("ENTER 127 CLR MENU")); 128 neoring_active=false; 129 break; 130 case exitEvent: 131 132 DBG(F("EXIT CLR MENU")); 133 menu_rpm_change(e,nav,item); 134 neoring_active=true; 135 136 break; 137 } 138 return proceed; 139} 140 141MENU(configMenu_RPM_limits,"Set 142 RPM limits",doNothing,noEvent,wrapStyle 143 ,FIELD(CONFIG.RPM_TRIGGER[0],"RPM 144 min","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 145 ,FIELD(CONFIG.RPM_MAX,"RPM 146 max","",0,11000,100,50, menu_rpm_change, exitEvent, noStyle) 147 ,FIELD(CONFIG.RPM_TRIGGER[1],"Stage1","",0,11000,100,50, 148 menu_rpm_change, exitEvent, noStyle) 149 ,FIELD(CONFIG.RPM_NUMPIXELS[1],"Stage1 150 LEDs","",0,NEORING_LEDS,1,0, menu_rpm_change, exitEvent, noStyle) 151 ,FIELD(CONFIG.RPM_TRIGGER[2],"Stage2","",0,11000,100,50, 152 menu_rpm_change, exitEvent, noStyle) 153 ,FIELD(CONFIG.RPM_NUMPIXELS[2],"Stage2 154 LEDs","",0,NEORING_LEDS,1,0, menu_rpm_change, exitEvent, noStyle) 155 ,FIELD(CONFIG.RPM_TRIGGER[3],"Stage3","",0,11000,100,50, 156 menu_rpm_change, exitEvent, noStyle) 157 ,FIELD(CONFIG.RPM_NUMPIXELS[3],"Stage3 158 LEDs","",0,NEORING_LEDS,1,0, menu_rpm_change, exitEvent, noStyle) 159 ,FIELD(CONFIG.RPM_TRIGGER[4],"StageFLSH","",0,11000,100,50, 160 menu_rpm_change, exitEvent, noStyle) 161); 162 163MENU(configMenu_RPM_colors,"Set 164 RPM colors",menu_rpm_color_display, (eventMask)(enterEvent | exitEvent),wrapStyle 165 166 ,FIELD(CONFIG.RPM_COLOR[1],"Stage1","",0,1529,50,1, menu_rpm_color_change, 167 enterEvent, noStyle) 168 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[1],"Stage1Light","",0,255,20,1, 169 menu_rpm_color_change, enterEvent, noStyle) 170 ,FIELD(CONFIG.RPM_COLOR[2],"Stage2","",0,1529,50,1, 171 menu_rpm_color_change, enterEvent, noStyle) 172 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[2],"Stage2Light","",0,255,20,1, 173 menu_rpm_color_change, enterEvent, noStyle) 174 ,FIELD(CONFIG.RPM_COLOR[3],"Stage3","",0,1529,50,1, 175 menu_rpm_color_change, enterEvent, noStyle) 176 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[3],"Stage3Light","",0,255,20,1, 177 menu_rpm_color_change, enterEvent, noStyle) 178 ,FIELD(CONFIG.RPM_COLOR[4],"StageFLSH","",0,1529,50,1, 179 menu_rpm_color_change, enterEvent, noStyle) 180 ,FIELD(CONFIG.RPM_COLOR_LIGHTNESS[4],"StageFLLght","",0,255,20,1, 181 menu_rpm_color_change, enterEvent, noStyle) 182); 183 184MENU(configMenu_SAVE,"Save 185 config?",doNothing,noEvent,wrapStyle 186 ,OP("Yes",menu_save_config,enterEvent) 187 188 ,OP("No",menu_back_action,enterEvent) 189); 190 191MENU(configMenu_DEFAULT,"Reset 192 Defaults?",doNothing,noEvent,wrapStyle 193 ,OP("Yes",menu_default_config,enterEvent) 194 195 ,OP("No",menu_back_action,enterEvent) 196); 197 198MENU (configMenu,"Configuration",doNothing,noEvent,wrapStyle 199 200 ,SUBMENU(configMenu_SAVE) 201 ,SUBMENU(configMenu_DEFAULT) 202); 203 204MENU(mainMenu, 205 "Settings", doNothing, noEvent, wrapStyle 206 ,FIELD(CONFIG.rpm_brightness,"RPM 207 LED","%",0,100,5,1, menu_rpm_brightness, enterEvent, noStyle) 208 ,FIELD(CONFIG.gear_brightness,"Gear 209 LED","%",0,100,5,1, menu_gear_brightness, enterEvent, noStyle) 210 ,SUBMENU(configMenu_RPM_limits) 211 212 ,SUBMENU(configMenu_RPM_colors) 213 ,SUBMENU(configMenu) 214 ,EXIT("<Exit menu") 215); 216 217Menu::noInput 218 noinput; 219//stringIn<0> menu_strIn; 220//serialIn serial(Serial); 221//MENU_INPUTS(in,&serial); 222 223MENU_OUTPUTS(out,MENU_MAX_DEPTH 224 225 ,LIQUIDCRYSTAL_OUT(lcd,{0,0,16,2}) 226 ,NONE//must have 2 items at least 227); 228 229NAVROOT(nav,mainMenu,MENU_MAX_DEPTH,noinput,out); 230 231/* 232 233 * SETUP() 234 */ 235 236void setup() { 237 // Serial comms init 238 Serial.begin(9600); 239 240 BTserial.begin(9600); 241 242 // Neoring init 243 neoring.begin(); 244 // VIRTUAL: 245 do not use low brightness in SIM as it's not visible. Keep brightness low for real 246 NeoRing HW. 247 //neoring.setBrightness(8); 248 249 // 8x8 LED Matrix init 250 251 gears_lcd.setEnabled(true); 252 253 // Apply EEPROM or Default config for Neoring, 254 Gears matrix and RPM limits 255 configuration.load(); 256 configuration.apply(); 257 258 259 // 16x2 LCD init 260 lcd.begin(16,2); 261 lcd.clear(); 262 263 // set menu 264 visibility on startup as "idle" 265 // instead use our own Monitor screen and 266 handle Menu callback in lcd_monitor_screen 267 nav.idleTask=lcd_monitor_screen; 268 269 nav.idleOn(); 270 lcd_monitor_screen(out[0],Menu::idling); 271 272 // play BMW 273 logo rotation animation on startup 274/* 275 gears_display(&GEARS_GLYPH[10]);delay(1500); 276 277 for (byte anim=0;anim<4;anim++) 278 { 279 gears_display(&GEARS_GLYPH[10]);delay(100); 280 281 for (byte i=11;i<=13;i++) 282 { 283 gears_display(&GEARS_GLYPH[i]);delay(100); 284 285 } 286 } 287 gears_display(&GEARS_GLYPH[10]); 288 */ 289 // fill the rpm_scale_val 290 and rpm_scale_col arrays with boundaries for each neopixel 291 //rpm_scale_compute(); 292 293 294 noInterrupts(); 295 // 10Hz interrupt on TIMER1 for Racechrono BT LE output 296 297 TCCR1A = 0;// set entire TCCR1A register to 0 298 TCCR1B = 0;// same for TCCR1B 299 300 TCNT1 = 0;//initialize counter value to 0 301 // set compare match register 302 for 1hz increments 303 OCR1A = 1562; // = (16*10^6) / (10*1024) - 1 (must be <65536) 304 305 // turn on CTC mode 306 TCCR1B |= (1 << WGM12); 307 // Set CS10 and CS12 bits 308 for 1024 prescaler 309 TCCR1B |= (1 << CS12) | (1 << CS10); 310 // enable timer 311 compare interrupt 312 TIMSK1 |= (1 << OCIE1A); 313 interrupts(); 314} 315 316void 317 loop() { 318 319#ifdef DEMO 320unsigned short r; 321for (byte g=1;g<7;g++) 322{ 323 324 gears_display(&GEARS_GLYPH[g]); 325 for (r=CONFIG.RPM_MIN;r<5700;r+=10) 326 { 327 328 rpm_fill(r); 329 lcd.setCursor(0,1); 330 lcd.print(" "); 331 lcd.setCursor(0,1); 332 333 lcd.print(r); 334 } 335} 336#endif 337 338 int lcd_button=analogRead(PIN_LCD_INPUT); 339 340 341 if ((millis()-last_debounce_time) > debounce_delay) 342 { 343 for (byte i=1;i<=4;i++) 344 345 { 346 if (lcd_button>=lcd_button_range[i][1] && lcd_button<=lcd_button_range[i][2]) 347 nav.doNav((Menu::navCmds) lcd_button_range[i][0]); 348 } 349 350 if (lcd_button<lcd_button_range[0][1]) 351 352 { 353 DBG(F("Refresh menu")); 354 DBG(lcd_button); 355 lcd_menu_active=true; 356 357 last_debounce_time=millis(); 358 nav.doOutput(); 359 } 360 } 361 362 363 // TODO: Integrate CANbus readings - currently only temporary PIN_RPM analog value 364 used instead of CANBus 365 // map analog PIN_RPM to values 0-xxxx(RPM_MAX) 366 367 rpm = map(analogRead(PIN_RPM), 0,1023, 0,CONFIG.RPM_MAX); 368 //rpm = int(RPM_MAX/float(1023)*analogRead(PIN_RPM)); 369 // read the input pin 370 // display the Neoring RPM with that value 371 rpm_fill(rpm); 372 373 374 // read the DAC convertor value 375 gear_dac=analogRead(PIN_GEARS_INPUT); 376 377 // and select gear based on DAC convertor lookup table. The lookup KEY is dynamically 378 calculated so it is a direct access to the final gear to be displayed. No min/max 379 Analogread comparisons. 380 // 1024/16= 64 = full scale analogRead divided by 16 381 possible bits, and shifted by 32 (half of the "ranges") to both sides to make 382 the AnalogRead boundaries. 383 //gear=pgm_read_byte(&(gears_dac_lookup[(gear_dac+32)/(1024/16)][1])); 384 385 gear=pgm_read_byte(&gears_dac_lookup[(gear_dac+32)/(1024/16)][1]); 386 387 // 388 read GEARs from the serial console if available 389 /* 390 if (Serial.available()) 391 392 { 393 String console=Serial.readStringUntil('\ 394'); 395 gear=(byte) console.toInt(); 396 397 } 398 */ 399 400 // TODO: performance - move to Interrupt section ? Make a millis() 401 for refresh? 402 if( millis()-last_gear_refreshtime>1000) 403 { 404 gears_display(&GEARS_GLYPH[gear]); 405 406 last_gear_refreshtime=millis(); 407 } 408 409 if (last_rpm!=rpm && !lcd_menu_active) 410 411 { 412 lcd.setCursor(0,1); 413 lcd.print(" "); 414 lcd.setCursor(0,1); 415 416 lcd.print(rpm); // Write a character to display 417 last_rpm=rpm; 418 } 419} 420 421// 422 Racechrono BT output interrupt each 100ms aka 10Hz 423ISR(TIMER1_COMPA_vect) 424{ 425 426 char output[33]; 427 sprintf_P(output,PSTR("$RC2,,%u,,,,%d,%d,,,,,,,,*"),RC_counter,rpm,gear); 428 429 byte checksum = 0; 430 char checksum_format[]="00"; 431 // to verify, check 432 https://nmeachecksum.eqth.net/ for simple NMEA-CRC online calculator 433 // calulate 434 CRC only for the message "body" between $ and *. These are excluded from the CRC. 435 436 for (int i = 1; i < strlen(output)-1; i++) 437 { 438 checksum = checksum 439 ^ (unsigned byte)output[i]; 440 } 441 sprintf_P(checksum_format,PSTR("%02X"),checksum); 442 443 strcat(output,checksum_format); 444 BTserial.println(output); 445 RC_counter++; 446 447 // as RC_counter is unsigned it roll over automatically 65535+1= back to 0 448 449 // if (RC_counter==65535) RC_counter=0; 450} 451 452 453void gears_display(const 454 void *image_pointer) 455{ 456 uint64_t image; 457 memcpy_P(&image,image_pointer,sizeof(uint64_t)); 458 459 for (int i = 0; i < 8; i++) 460 { 461 byte row = (image >> i * 8); 462 for 463 (int j = 0; j < 8; j++) 464 { 465 gears_lcd.setPixel(i, j, bitRead(row, 466 j)); 467 } 468 } 469 gears_lcd.display(); 470} 471 472 473// Used to render 474 Neoring with RPM value 475void rpm_fill(int rpm) 476{ 477 if (!neoring_active) 478 return; 479 480 neoring.clear(); 481 482 // if out of range, just clear the neoring 483 and exit 484 if (rpm <= CONFIG.RPM_MIN || rpm > CONFIG.RPM_MAX) 485 { 486 if 487 (neoring.canShow()) neoring.show(); 488 return; 489 } 490 491 // Flashing 492 all 493 if (rpm >= CONFIG.RPM_TRIGGER[RPM_FLASH]) 494 { 495 neoring.fill(RPM_COLOR[RPM_FLASH]);neoring.show(); 496 497 delay(50); 498 neoring.fill(0);neoring.show(); 499 delay(50); 500 neoring.fill(RPM_COLOR[RPM_FLASH]);neoring.show(); 501 502 return; 503 } 504 505 // Normal operation, fill the LEDs according to RPMs 506 507 for (byte position=0;position < NEORING_LEDS;position++) 508 { 509 if ( rpm 510 > rpm_scale_val[position]) neoring.setPixelColor(NEORING_LEDS-1-position,*rpm_scale_col[position]); 511 512 else neoring.setPixelColor(NEORING_LEDS-1-position,0); 513 } 514 515 if (neoring.canShow()) 516 neoring.show(); 517} 518 519void rpm_scale_compute() 520{ 521 byte position=0; 522 523 //for all G,Y,R before FLASH calculate and fill the internal array of RPM values 524 525 RPM_COLOR[RPM_FLASH]=myColorHSV(CONFIG.RPM_COLOR[RPM_FLASH],CONFIG.RPM_COLOR_LIGHTNESS[RPM_FLASH]); 526 527 for (byte stage=1;stage < RPM_FLASH;stage++) 528 { 529 RPM_COLOR[stage]=myColorHSV(CONFIG.RPM_COLOR[stage],CONFIG.RPM_COLOR_LIGHTNESS[stage]); 530 531 position=position+CONFIG.RPM_NUMPIXELS[stage-1]; 532 if (position+CONFIG.RPM_NUMPIXELS[stage] 533 <= NEORING_LEDS) 534 { 535 for (byte i=0;i<CONFIG.RPM_NUMPIXELS[stage];i++) 536 537 { 538 rpm_scale_val[position+i]=((CONFIG.RPM_TRIGGER[stage]-CONFIG.RPM_TRIGGER[stage-1])/CONFIG.RPM_NUMPIXELS[stage]*(i+0))+CONFIG.RPM_TRIGGER[stage-1]; 539 540 rpm_scale_col[position+i]=&RPM_COLOR[stage]; 541 DBG(position+i); 542 543 DBG(rpm_scale_val[position+i]); 544 } 545 } 546 } 547} 548 549Menu::result 550 lcd_monitor_screen(menuOut& out,idleEvent e) 551{ 552 // idleStart - fired when 553 entering idle state, but last menurefresh is still executed 554 // idling - fired 555 once when enering menu idle mode, and after all menu refresh/clear is done 556 // 557 idleEnd - fired when leaving idle state, but before any menu init is done 558 559 560 // so rely on idling state and prepare the lcd_monitor_screen to take over 561 562 if (e==Menu::idling) 563 { 564 out.clear(); 565 out.setCursor(0,0); 566 567 out.print("RPM WATER OIL"); 568 // used for decision if menu must be 569 polled/refreshed to save resources in loop() 570 lcd_menu_active=false; 571 } 572} 573 574uint32_t 575 myColorHSV(uint16_t hue, uint8_t val) { 576 // Remap 0-65535 to 0-1529. Pure red 577 is CENTERED on the 64K rollover; 578 // 0 is not the start of pure red, but the 579 midpoint...a few values above 580 // zero and a few below 65536 all yield pure 581 red (similarly, 32768 is the 582 // midpoint, not start, of pure cyan). The 8-bit 583 RGB hexcone (256 values 584 // each for red, green, blue) really only allows for 585 1530 distinct hues 586 // (not 1536, more on that below), but the full unsigned 587 16-bit type was 588 // chosen for hue so that one's code can easily handle a contiguous 589 color 590 // wheel by allowing hue to roll over in either direction. 591///////// 592 hue = (hue * 1530L + 32768) / 65536; 593 // Because red is centered on the rollover 594 point (the +32768 above, 595 // essentially a fixed-point +0.5), the above actually 596 yields 0 to 1530, 597 // where 0 and 1530 would yield the same thing. Rather than 598 apply a 599 // costly modulo operator, 1530 is handled as a special case below. 600 601 602 uint8_t r, g, b, sat; 603 604 if (val<128) {val=map(val,0,127,0,255);sat=255;} 605 606 else if (val>=128) {sat=map(val,128,255,255,0);val=255;} 607 608 // Convert 609 hue to R,G,B (nested ifs faster than divide+mod+switch): 610 if(hue < 510) { // 611 Red to Green-1 612 b = 0; 613 if(hue < 255) { // Red to Yellow-1 614 615 r = 255; 616 g = hue; // g = 0 to 254 617 } else { 618 // Yellow to Green-1 619 r = 510 - hue; // r = 255 620 to 1 621 g = 255; 622 } 623 } else if(hue < 1020) { // Green to Blue-1 624 625 r = 0; 626 if(hue < 765) { // Green to Cyan-1 627 g = 255; 628 629 b = hue - 510; // b = 0 to 254 630 } else { // Cyan 631 to Blue-1 632 g = 1020 - hue; // g = 255 to 1 633 b = 255; 634 635 } 636 } else if(hue < 1530) { // Blue to Red-1 637 g = 0; 638 if(hue 639 < 1275) { // Blue to Magenta-1 640 r = hue - 1020; // r = 0 641 to 254 642 b = 255; 643 } else { // Magenta to Red-1 644 645 r = 255; 646 b = 1530 - hue; // b = 255 to 1 647 } 648 } 649 else { // Last 0.5 Red (quicker than % operator) 650 r = 255; 651 652 g = b = 0; 653 } 654 655 // Apply saturation and value to R,G,B, pack into 656 32-bit result: 657 uint32_t v1 = 1 + val; // 1 to 256; allows >>8 instead of 658 /255 659 uint16_t s1 = 1 + sat; // 1 to 256; same reason 660 uint8_t s2 = 255 661 - sat; // 255 to 0 662 return ((((((r * s1) >> 8) + s2) * v1) & 0xff00) << 8) | 663 664 (((((g * s1) >> 8) + s2) * v1) & 0xff00) | 665 ( ((((b * 666 s1) >> 8) + s2) * v1) >> 8); 667}
Downloadable files
Schematics
Proteus simulation
Schematics
Schematics
Proteus simulation
Schematics
Documentation
RPM+Gear enclosure with components
RPM+Gear enclosure with components
Enclosure 3D model
Enclosure 3D model
RPM+Gear enclosure
RPM+Gear enclosure
RPM+Gear enclosure with components
RPM+Gear enclosure with components
Enclosure 3D model
Enclosure 3D model
RPM+Gear enclosure
RPM+Gear enclosure
RPM+Gear enclosure with components
RPM+Gear enclosure with components
Comments
Only logged in users can leave comments
Simple Rally/Racing Dashboard | Arduino Project Hub