#include "U8glib.h"
// pins assignemnt
#define BUTTON_UP_PIN 12 // pin for UP button
#define BUTTON_SELECT_PIN 8 // pin for SELECT button
#define BUTTON_DOWN_PIN 4 // pin for DOWN button
#define BUTTON_BACK_PIN 7 // pin for BACK button
#define OLED_SCL A5 // pin for UP button
#define OLED_SDA A4 // pin for SELECT button
// value types
#define SMALL_NUMBER 0
#define LARGE_NUMBER 1
#define HVAC_MODE 2
#define TEMP 3
#define PERC 4
#define KNX_ADR 5
// value options
#define OFF 0
#define ON 1
#define AUTO 2
#define PARTY 3
// item types
#define TYPE_NONE 0
#define MENU_LIST 1
#define MENU_SCREEN 2
#define MENU_PAGE 3
#define TYPE_EDITOR 4
#define BOOL_VALUE 5
#define HVAC_VALUE 6
#define PERC_VALUE 7
#define SMALL_VALUE 8
#define LARGE_VALUE 9
#define TEMP_VALUE 10
// item data identificators of all items
#define ITEM_TYPE 0
// item data identificators of menu items
#define FIRST_ITEM 1
#define ITEM_COUNT 2
#define ORIGIN 3
// item data identificators of value items
#define KNX_ADR 1
#define ITEM_VALUE 2
#define DEF_VALUE 3
#define MAX_STRING_LENGTH 16 // maximum characters for the item name
#define NUM_ITEMS 63
// display
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_DEV_0 | U8G_I2C_OPT_NO_ACK | U8G_I2C_OPT_FAST);
//U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g(U8G2_R0, OLED_SCL, OLED_SDA, U8X8_PIN_NONE);
enum BUTTON {
none,
back,
up,
down,
select
};
// help variables to handle button press detection
BUTTON button_pressed = none;
BUTTON button_action = none;
static uint32_t timer;
// variables to handle menus
byte firstItemIndex;
byte lastItemIndex;
byte activeScreenType;
byte activeScreenIndex;
byte revertScreenIndex;
bool editMode = false;
byte columnCount = 4;
byte columnItemIndex[8];
byte columnSelected;
byte selectedItemIndex;
// variables for consumption sensors
const char hvacModes[4][16] PROGMEM = {"OFF", "ON", "AUTO", "PARTY"};
const char lightModes[4][16] PROGMEM = {"OFF", "ON", "AUTO", "ECO"};
const byte valueTypeSize[11] PROGMEM = {0, 0, 0, 0, 0, 1, 3, 100, 255, 65535, 255};
bool boolValues[1] = {0};
byte smallValues[1] = {0};
unsigned int largeValues[1] = {0};
char hvacValues[4] = {0, 0, 2, 2};
byte tempValues[1] = {0};
char knxGroupAdr[2][9] = {
"0/0/0",
"0/0/0" // HVAC STATUS
};
// all items names
struct itemStruct
{
const char itemName[MAX_STRING_LENGTH];
const byte itemData [4];
};
const itemStruct items[NUM_ITEMS] PROGMEM =
{
{ "", { TYPE_NONE, 0, 0, 1 }}, // null screen
{ "Main menu", { MENU_LIST, 2, 5, 1 }},
// ------------------- MENUS -----------------------
// {"name", {MENU_LIST, first item index, number of items, return menu index}}
// {"name", {MENU_SCREEN, first page index, number of pages, return menu index}}
// {"name", {MENU_PAGE, first item index, number of items, return menu index}}
// {"name", {BOOL_VALUE (0-1), knx adress index (0=internal value), value index, default value index (0 = info only) }}
// {"name", {SMALL_VALUE(0-255), knx adress index (0=internal value), value index, default value index (0 = info only) }}
// {"name", {PERC_VALUE(0-100), knx adress index (0=internal value), value index, default value index (0 = info only) }}
// {"name", {LARGE_VALUE(0-2 147 483 647), knx adress index (0=internal value), value index, default value index (0 = info only) }}
// {"name", {HVAC_VALUE(0-2 147 483 647), knx adress index (0=internal value), value index, default value index (0 = info only) }}
// Main menu: index 2 - 8
{ "Heating", { MENU_LIST, 9, 3, 1} },
{ "Monitoring", { MENU_LIST, 14, 4, 1} },
{ "Lightning", { MENU_LIST, 19, 3, 1} },
{ "Settings", { MENU_LIST, 24, 3, 1} },
{ "KNX bus", { MENU_LIST, 29, 2, 1} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
// Heating menu: index 9 - 13
{ "uHVAC mode", { HVAC_VALUE, 0, 1, 2} },
{ "Room Controls", { MENU_SCREEN, 36, 8, 2} },
{ "Boiler status", { MENU_LIST, 60, 3, 2} },
{ "HVAC status", { HVAC_VALUE, 1, 3, 0} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
// Monitoring menu: index 14 - 19
{ "Consumption", { MENU_LIST, 32, 4, 3} },
{ "Brightness", { TYPE_NONE, 0, 0, 3} },
{ "Door contacts", { TYPE_NONE, 0, 0, 3} },
{ "Flood detect", { TYPE_NONE, 0, 0, 3} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
// Lightning menu: index 19 - 23
{ "Navi lights", { TYPE_NONE, 0, 0, 4} },
{ "Wardrobe Light", { TYPE_NONE, 0, 0, 4} },
{ "Brightness", { TYPE_NONE, 0, 0, 4} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
// Settings menu: index 24 - 28
{ "Date/Time", { TYPE_NONE, 0, 0, 5} },
{ "Circulation", { TYPE_NONE, 0, 0, 5} },
{ "Smartglass", { TYPE_NONE, 0, 0, 5} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
// KNX bus menu: index 29 - 31
{ "BUSMonitor", { TYPE_NONE, 0, 0, 6} },
{ "Send telegram", { TYPE_NONE, 0, 0, 6} },
{ "RESERVED", { TYPE_NONE, 0, 0, 0} },
// ---------------- SUBMENUS -----------------------
// consumption yeilds 32 - 35
{ "GAS", { LARGE_VALUE, LARGE_NUMBER, 0, 14} },
{ "L1", { LARGE_VALUE, LARGE_NUMBER, 1, 14} },
{ "L2", { LARGE_VALUE, LARGE_NUMBER, 2, 14} },
{ "L3", { LARGE_VALUE, LARGE_NUMBER, 3, 14} },
// temperatures 36 - 43
{ "Central", { MENU_PAGE, 44, 2, 10} },
{ "Entrance", { MENU_PAGE, 46, 2, 10} },
{ "Toilet", { MENU_PAGE, 48, 2, 10} },
{ "LivingRoom", { MENU_PAGE, 50, 2, 10} },
{ "Kitchen", { MENU_PAGE, 52, 2, 10} },
{ "KidsRoom", { MENU_PAGE, 54, 2, 10} },
{ "BedRoom", { MENU_PAGE, 56, 2, 10} },
{ "BathRoom", { MENU_PAGE, 58, 2, 10} },
// values for Central temperatures 44 - 45
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 0} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 0} },
// values for Entrance temperatures 46 - 47
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 1} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 1} },
// values for toilet temperatures 48 - 49
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 2} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 2} },
// values for livingroom temperatures 50 - 51
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 3} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 3} },
// values for kitchen temperatures 52 - 53
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 4} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 4} },
// values for kidsroom temperatures 54 - 55
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 5} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 5} },
// values for bedroom temperatures 56 - 57
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 6} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 6} },
// values for bathroom temperatures 58 - 59
{ "SetPoint", { TEMP_VALUE, TEMP, 20, 7} },
{ "RoomTemp", { TEMP_VALUE, TEMP, 20, 7} },
// boiler status - 60 - 62
{ "HVAC MODE", { HVAC_VALUE, HVAC_MODE, AUTO, 0} },
{ "Water temp", { TEMP_VALUE, TEMP, 55, 0} },
{ "HWater temp", { TEMP_VALUE, TEMP, 60, 0} }
};
// const char items.itemName [NUM_ITEMS][MAX_STRING_LENGTH] PROGMEM = {
//};
//byte itemData [NUM_ITEMS][4] = {
//};
// functions
void readFromKnxBus() {
}
void sendToKnxBus() {
}
void readInputs() {
}
void doAutomation() {
}
void prepareScreenData() {
switch (activeScreenType) {
//prepare data for menu lists
case MENU_LIST:
// fill up columns with data
for (int i = 1; i <= columnCount - 1; i++) {
columnItemIndex[i] = selectedItemIndex + (i - columnSelected);
if (columnItemIndex[i] > lastItemIndex) {
columnItemIndex[i] = 0;
}
}
break;
// prepare data for screen with pages
case MENU_SCREEN:
// fill up remaining columns with data
columnItemIndex[0] = selectedItemIndex;
for (int i = 0; i < columnCount - 1; i++) {
if (i < items[columnItemIndex[0]].itemData[ITEM_COUNT]) {
columnItemIndex[i + 1] = items[columnItemIndex[0]].itemData[FIRST_ITEM] + i;
}
else {
columnItemIndex[i + 1] = 0;
}
}
break;
default:
break;
}
}
void changeScreen(byte destination) {
revertScreenIndex = items[destination].itemData[ORIGIN];
firstItemIndex = items[destination].itemData[FIRST_ITEM];
lastItemIndex = items[destination].itemData[FIRST_ITEM] + items[destination].itemData[ITEM_COUNT] - 1 ;
switch (items[destination].itemData[ITEM_TYPE]) {
case MENU_LIST:
activeScreenType = MENU_LIST;
columnItemIndex[0] = destination;
selectedItemIndex = activeScreenIndex >= firstItemIndex ? activeScreenIndex : firstItemIndex; // select previous item in destination list
// calculate column position of selected item according to count of items to be displayed
if (lastItemIndex - selectedItemIndex + 1 >= columnCount - 1) { // there is enough items to fill all columns
columnSelected = 1;
Serial.println("SELECTED ");
Serial.println(selectedItemIndex);
} else if(items[destination].itemData[ITEM_COUNT] < columnCount - 1) { // there is less items in total then columns
columnSelected = items[destination].itemData[ITEM_COUNT] - (lastItemIndex - selectedItemIndex);
} else { // there is less items remaining then colums
columnSelected = (columnCount - 1) - (lastItemIndex - selectedItemIndex);
}
break;
case MENU_SCREEN:
activeScreenType = MENU_SCREEN;
// columnItemIndex[1] = items.itemData[items[destination].itemData[FIRST_ITEM]][FIRST_ITEM]; // pointing to first item of page of screen
selectedItemIndex = items[destination].itemData[FIRST_ITEM];
columnSelected = 0;
break;
default:
break;
}
activeScreenIndex = destination;
}
void valueToText(byte value, byte itemType, char* OutStr) {
switch (itemType) {
case SMALL_VALUE:
sprintf(OutStr, "%d", value);
break;
case LARGE_VALUE:
sprintf(OutStr, "%d", largeValues[value]);
break;
case TEMP_VALUE:
sprintf(OutStr, "%d\xB0""C", value);
break;
case HVAC_VALUE:
strcpy(OutStr, hvacModes[value]);
break;
default:
break;
}
}
void valueIncrease(byte valueindex, byte valueType) {
switch (valueType) {
case BOOL_VALUE:
boolValues[valueindex] = boolValues[valueindex] < valueTypeSize[valueType] ? boolValues[valueindex] + 1 : 0;
break;
case SMALL_VALUE:
smallValues[valueindex] = smallValues[valueindex] < valueTypeSize[valueType] ? smallValues[valueindex] + 1 : 0;
break;
case LARGE_VALUE:
largeValues[valueindex] = largeValues[valueindex] < valueTypeSize[valueType] ? largeValues[valueindex] + 1 : 0;
break;
case TEMP_VALUE:
tempValues[valueindex] = tempValues[valueindex] < valueTypeSize[valueType] ? tempValues[valueindex] + 1 : 0;
break;
case HVAC_VALUE:
hvacValues[valueindex] = hvacValues[valueindex] < valueTypeSize[valueType] ? hvacValues[valueindex] + 1 : 0;
break;
default:
break;
}
}
void valueDecrease(byte valueindex, byte valueType) {
switch (valueType) {
case BOOL_VALUE:
boolValues[valueindex] = boolValues[valueindex] > 0 ? boolValues[valueindex] - 1 : valueTypeSize[valueType];
break;
case SMALL_VALUE:
smallValues[valueindex] = smallValues[valueindex] > 0 ? smallValues[valueindex] - 1 : valueTypeSize[valueType];
break;
case LARGE_VALUE:
largeValues[valueindex] = largeValues[valueindex] > 0 ? largeValues[valueindex] - 1 : valueTypeSize[valueType];
break;
case TEMP_VALUE:
tempValues[valueindex] = tempValues[valueindex] > 0 ? tempValues[valueindex] - 1 : valueTypeSize[valueType];
break;
case HVAC_VALUE:
hvacValues[valueindex] = hvacValues[valueindex] > 0 ? hvacValues[valueindex] - 1 : valueTypeSize[valueType];
break;
default:
break;
}
}
void drawScreen(byte drawItemIndex[], int columnNum, int selected) {
u8g_fntpgm_uint8_t *fontStd;
u8g_fntpgm_uint8_t *fontBold;
u8g_uint_t infoWidth;
char buffer[MAX_STRING_LENGTH];
byte columnHeight;
byte spaceThickness;
switch(columnNum){
case 3:
fontStd = u8g_font_10x20;
fontBold = u8g_font_10x20;
columnHeight = 21;
spaceThickness = 2;
break;
case 4:
fontStd = u8g_font_7x14;
fontBold = u8g_font_7x14B;
columnHeight = 16;
spaceThickness = 2;
break;
case 5:
fontStd = u8g_font_6x13;
fontBold = u8g_font_6x13B;
columnHeight = 12;
spaceThickness = 1;
break;
case 6:
fontStd = u8g_font_6x13;
fontBold = u8g_font_6x13B;
columnHeight = 10;
spaceThickness = 1;
break;
case 7:
fontStd = u8g_font_6x10;
fontBold = u8g_font_6x10;
columnHeight = 9;
spaceThickness = 1;
break;
case 8:
fontStd = u8g_font_5x8;
fontBold = u8g_font_5x8;
columnHeight = 8;
spaceThickness = 0;
break;
default:
break;
fontStd = u8g_font_7x14;
fontBold = u8g_font_7x14B;
columnHeight = 16;
spaceThickness = 2;
}
u8g.firstPage(); // required for page drawing mode for u8g library
do {
// draw line for label and use bold text for first column if header is desired
if (activeScreenType == MENU_LIST) {
u8g.drawHLine(0, columnHeight, 120);
}
// draw text into columns
for (int i = 0; i < columnNum; i++) {
if (i == selected) { // highlight selected item
u8g.setFont(fontBold);
if (editMode == false) {
u8g.drawBox(0, (columnHeight + spaceThickness) + (columnHeight * (selected - 1)), 120, (columnHeight - spaceThickness));
u8g.setColorIndex(0);
}
}
if ((items[drawItemIndex[i]].itemData[ITEM_TYPE] >= BOOL_VALUE)) { // show values for infos
valueToText(items[drawItemIndex[i]].itemData[ITEM_VALUE], items[drawItemIndex[i]].itemData[ITEM_TYPE], buffer);
infoWidth = u8g.getStrWidth(buffer);
if (editMode == true) {
u8g.drawBox(119 - infoWidth, (columnHeight + spaceThickness) + (columnHeight * (selected - 1)), infoWidth + 1, (columnHeight - spaceThickness));
u8g.setColorIndex(0);
u8g.drawStr(120 - infoWidth, (columnHeight - spaceThickness) + (columnHeight * i), buffer);
u8g.setColorIndex(1);
} else {
u8g.drawStr(120 - infoWidth, (columnHeight - spaceThickness) + (columnHeight * i), buffer);
}
}
strcpy_P(buffer, items[drawItemIndex[i]].itemName);
u8g.drawStr(3, (columnHeight - spaceThickness) + (columnHeight * i), buffer); // draw column text
u8g.setColorIndex(1); // reset color for next item
u8g.setFont(fontStd); // reset color font next item
}
// draw scrollbar handle
//if(lastItemIndex - firstItemIndex + 1 > 3){
//u8g.drawBox(125, 16+(48/(lastItemIndex - firstItemIndex + 1) * (columnItemIndex[1] - firstItemIndex)), 3, (3*48/(lastItemIndex - firstItemIndex + 1)));
//}
} while ( u8g.nextPage() ); // required for page drawing mode with u8g library
}
void handleButtons() {
// checking button pressed
if ((digitalRead(BUTTON_BACK_PIN) == LOW) && (button_pressed == none)) {
button_pressed = back;
button_action = back;
timer = millis();
}
else if ((digitalRead(BUTTON_SELECT_PIN) == LOW) && (button_pressed == none)) {
button_pressed = select;
button_action = select;
timer = millis();
}
else if ((digitalRead(BUTTON_UP_PIN) == LOW) && (button_pressed == none)) {
button_pressed = up;
button_action = up;
timer = millis();
}
else if ((digitalRead(BUTTON_DOWN_PIN) == LOW) && (button_pressed == none)) {
button_pressed = down;
button_action = down;
timer = millis();
}
// unclick buttons
if ((digitalRead(BUTTON_BACK_PIN) == HIGH) && (button_pressed == back)) {
button_pressed = none;
}
if ((digitalRead(BUTTON_SELECT_PIN) == HIGH) && (button_pressed == select)) {
button_pressed = none;
}
if ((digitalRead(BUTTON_UP_PIN) == HIGH) && (button_pressed == up)) {
button_pressed = none;
}
if ((digitalRead(BUTTON_DOWN_PIN) == HIGH) && (button_pressed == down)) {
button_pressed = none;
}
// repeat action every 250 miliseconds for button holded down
if (button_pressed != none) {
if (millis() - timer > 250) {
timer = millis();
button_action = button_pressed;
}
}
// do action according to active screen
switch (activeScreenType) {
case MENU_LIST:
// do action according to active button
switch (button_action) {
case back:
if (editMode == true) { // if edit mode is active, deactivate it
editMode = false;
} else { // otherwise just return to previous screen
changeScreen(revertScreenIndex);
}
break;
case select:
//if selected item is editable try to set edit mode
if (items[selectedItemIndex].itemData[ITEM_TYPE] >= BOOL_VALUE || items[selectedItemIndex].itemData[DEF_VALUE] != 0 ) {
if (editMode == false) {
editMode = true;
} else { // if edit mode is already active, confirm changes
editMode = false;
// =missing_code=
// confirm value change to bus
}
// if selected item is another menu, go to that menu
} else if ((items[selectedItemIndex].itemData[ITEM_TYPE] == MENU_LIST) || (items[selectedItemIndex].itemData[ITEM_TYPE] == MENU_SCREEN)) {
changeScreen(selectedItemIndex);
}
break;
case up:
if (editMode == true) { // for value editing
valueIncrease(items[selectedItemIndex].itemData[ITEM_VALUE], items[selectedItemIndex].itemData[ITEM_TYPE]);
}
else { // for list browsing
if (selectedItemIndex > firstItemIndex) {
selectedItemIndex -= 1;
columnSelected = columnSelected > 1 ? columnSelected - 1 : 1 ; // move selection one column up
} else {
selectedItemIndex = lastItemIndex;
columnSelected = lastItemIndex - firstItemIndex + 1 >= columnCount - 1 ? columnCount-1 : lastItemIndex - firstItemIndex + 1;
}
}
break;
case down:
if (editMode == true) { // for value editing
valueDecrease(items[selectedItemIndex].itemData[ITEM_VALUE], items[selectedItemIndex].itemData[ITEM_TYPE]);
}
else { // for list browsing
if (selectedItemIndex < lastItemIndex) {
selectedItemIndex += 1;
columnSelected = columnSelected < columnCount-1 ? columnSelected + 1 : columnCount-1 ; // move selection one column up
} else {
selectedItemIndex = firstItemIndex;
columnSelected = 1;
}
}
break;
default:
break;
}
break;
case MENU_SCREEN: // need rewrite!!!
// do action according to active button
switch (button_action) {
case back:
changeScreen(revertScreenIndex);
break;
case up: // up button clicked - jump to previous page
selectedItemIndex = selectedItemIndex > firstItemIndex ? selectedItemIndex - 1 : lastItemIndex;// if there are no more previous pages go to last page
break;
case down: // up button clicked - jump to next page
selectedItemIndex = selectedItemIndex < lastItemIndex ? selectedItemIndex + 1 : firstItemIndex; // if there are no more next pages go ti first page
break;
default:
break;
}
break;
default:
break;
}
button_action = none; // reset button_action for next action detection
}
// program functions
void setup() {
Serial.begin(9600); //for debuging
u8g.begin();
u8g.setColorIndex(1); // set the color to white
// define pins for buttons
// INPUT_PULLUP means the button is HIGH when not pressed, and LOW when pressed
// since it´s connected between some pin and GND
pinMode(BUTTON_UP_PIN, INPUT_PULLUP); // up button
pinMode(BUTTON_SELECT_PIN, INPUT_PULLUP); // select button
pinMode(BUTTON_DOWN_PIN, INPUT_PULLUP); // down button
pinMode(BUTTON_BACK_PIN, INPUT_PULLUP); // back button
changeScreen(1); //set screen to main menu
}
void loop() {
readFromKnxBus();
readInputs();
doAutomation();
sendToKnxBus();
handleButtons();
prepareScreenData();
drawScreen(columnItemIndex, columnCount, columnSelected);
}