diff --git a/.gitignore b/.gitignore index 5509140..309f296 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,401 @@ *.DS_Store + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +nuget.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml + +# Visual Micro +__vm/ +vs-readme.txt + +# Visual Studio Project Files +*.sln +*.vcxproj +*.vcxproj.filters +*.vcxitems +*.vcxitems.filters diff --git a/Code/Reflow_Master_v2/Reflow_Master_v2.ino b/Code/Reflow_Master_v2/Reflow_Master_v2.ino index e65b268..0bd655c 100644 --- a/Code/Reflow_Master_v2/Reflow_Master_v2.ino +++ b/Code/Reflow_Master_v2/Reflow_Master_v2.ino @@ -50,13 +50,14 @@ #include #include "spline.h" -#include "Adafruit_GFX.h" // Add from Library Manager -#include "Adafruit_ILI9341.h" // Add from Library Manager #include "MAX31855.h" #include "OneButton.h" // Add from Library Manager #include "ReflowMasterProfile.h" #include "FlashStorage.h" +#include "tft_display.h" +#include "menu_settings.h" + // used to obtain the size of an array of any type #define ELEMENTS(x) (sizeof(x) / sizeof(x[0])) @@ -82,49 +83,6 @@ #define RELAY 5 // relay control #define FAN A5 // fan control -// Just a bunch of re-defined colours -#define BLUE 0x001F -#define TEAL 0x0438 -#define GREEN 0x07E0 -#define CYAN 0x07FF -#define RED 0xF800 -#define MAGENTA 0xF81F -#define YELLOW 0xFFE0 -#define ORANGE 0xFC00 -#define PINK 0xF81F -#define PURPLE 0x8010 -#define GREY 0xC618 -#define WHITE 0xFFFF -#define BLACK 0x0000 -#define DKBLUE 0x000D -#define DKTEAL 0x020C -#define DKGREEN 0x03E0 -#define DKCYAN 0x03EF -#define DKRED 0x6000 -#define DKMAGENTA 0x8008 -#define DKYELLOW 0x8400 -#define DKORANGE 0x8200 -#define DKPINK 0x9009 -#define DKPURPLE 0x4010 -#define DKGREY 0x4A49 - -// Save data struct -typedef struct { - boolean valid = false; - boolean useFan = false; - int fanTimeAfterReflow = 60; - byte paste = 0; - float power = 1; - int lookAhead = 6; - int lookAheadWarm = 1; - int tempOffset = 0; - long bakeTime = 1200; // 20 mins - float bakeTemp = 45; // Degrees C - int bakeTempGap = 3; // Aim for the desired temp minus this value to compensate for overrun - bool startFullBlast = false; - bool beep = true; -} Settings; - // UI and runtime states enum states { BOOT = 0, @@ -224,9 +182,12 @@ OneButton button2(BUTTON2, false); OneButton button3(BUTTON3, false); // UI button positions and sizes -int buttonPosY[] = { 19, 74, 129, 184 }; -int buttonHeight = 16; -int buttonWidth = 4; +const unsigned int buttonPosY[] = { 19, 74, 129, 184 }; +const unsigned int buttonHeight = 16; +const unsigned int buttonWidth = 4; +const uint32_t ButtonColor[] = { GREEN, RED, BLUE, YELLOW }; +const unsigned int NumButtons = ELEMENTS(buttonPosY); +String buttonText[NumButtons]; // Initiliase a reference for the settings file that we store in flash storage Settings set; @@ -1062,6 +1023,7 @@ void Buzzer( int hertz, int len ) // Startup Tune void BuzzerStart() { + if (!set.startupTune) return; tone( BUZZER, 262, 200); delay(210); tone( BUZZER, 523, 100); @@ -1147,74 +1109,25 @@ void ShowMenu() tft.setCursor( 20, tft.height() - 20 ); tft.println("Reflow Master - Code v" + String(ver)); - ShowMenuOptions( true ); + ShowButtonOptions( true ); } -void ShowSettings() +void ShowSettings(bool resetSelection = true); + +void ShowSettings(bool resetSelection) { state = SETTINGS; SetRelayFrequency( 0 ); newSettings = false; - int posY = 45; - int incY = 19; - - tft.setTextColor( BLUE, BLACK ); - tft.fillScreen(BLACK); - - tft.setTextColor( BLUE, BLACK ); - tft.setTextSize(2); - tft.setCursor( 20, 20 ); - tft.println( "SETTINGS" ); - - tft.setTextColor( WHITE, BLACK ); - tft.setCursor( 20, posY ); - tft.print( "SWITCH PASTE" ); - - posY += incY; - - // y 64 - UpdateSettingsFan( posY ); - - posY += incY; - - // y 83 - UpdateSettingsFanTime( posY ); - - posY += incY; - - // y 102 - UpdateSettingsLookAhead( posY ); - - posY += incY; - - // y 121 - UpdateSettingsPower( posY ); - - posY += incY; - - // y 140 - UpdateSettingsTempOffset( posY ); - - posY += incY; - - // y 159 - UpdateSettingsStartFullBlast( posY ); - - posY += incY; - - // y 178 - UpdateSettingsBakeTempGap( posY ); - - posY += incY; - tft.setTextColor( WHITE, BLACK ); - tft.setCursor( 20, posY ); - tft.print( "RESET TO DEFAULTS" ); - - posY += incY; + SettingsPage::drawPage(resetSelection); +} - ShowMenuOptions( true ); +void ExitSettings() +{ + flash_store.write(set); + ShowMenu(); } void ShowPaste() @@ -1252,134 +1165,154 @@ void ShowPaste() y += 40; } - ShowMenuOptions( true ); + ShowButtonOptions( true ); +} + +void DrawButton(uint8_t index, const String& str, uint32_t textColor = WHITE, uint32_t boxColor = DEFAULT_COLOR); + +void DrawButton(uint8_t index, const String& str, uint32_t textColor, uint32_t boxColor) +{ + if (index >= NumButtons) return; + + // if default, set colors based on button index + if (boxColor == DEFAULT_COLOR) boxColor = ButtonColor[index]; + + tft.setTextColor(textColor, BLACK); + tft.setTextSize(2); + + const int XPos = tft.width() - 27; + const int YPos = buttonPosY[index] + 9; + + buttonText[index] = str; // save text for reference + tft.fillRect(tft.width() - 5, buttonPosY[index], buttonWidth, buttonHeight, boxColor); // color box + println_Right(tft, str, XPos, YPos); // text } -void ShowMenuOptions( bool clearAll ) +void ClearButton(uint8_t index, uint32_t color = BLACK); + +void ClearButton(uint8_t index, uint32_t color) { - // int buttonPosY[] = { 19, 74, 129, 184 }; - // int buttonHeight = 16; - // int buttonWidth = 4; + tft.setTextSize(2); + tft.setTextColor(color); + + const int XPos = tft.width() - 27; + const int YPos = buttonPosY[index] + 9; + + println_Right(tft, buttonText[index], XPos, YPos); +} +void ShowButtonOptions( bool clearAll ) +{ tft.setTextColor( WHITE, BLACK ); tft.setTextSize(2); if ( clearAll ) { // Clear All - for ( int i = 0; i < 4; i++ ) - tft.fillRect( tft.width() - 95, buttonPosY[i] - 2, 95, buttonHeight + 4, BLACK ); + for ( uint8_t i = 0; i < NumButtons; i++ ) + //tft.fillRect( tft.width() - 95, buttonPosY[i] - 2, 95, buttonHeight + 4, BLACK ); + ClearButton(i); // overwrite text exactly } if ( state == MENU ) { - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "REFLOW", tft.width() - 27, buttonPosY[0] + 9 ); - - // button 1 - tft.fillRect( tft.width() - 5, buttonPosY[1], buttonWidth, buttonHeight, RED ); - println_Right( tft, "BAKE", tft.width() - 27, buttonPosY[1] + 9 ); - - // button 2 - tft.fillRect( tft.width() - 5, buttonPosY[2], buttonWidth, buttonHeight, BLUE ); - println_Right( tft, "SETTINGS", tft.width() - 27, buttonPosY[2] + 9 ); - - // button 3 - tft.fillRect( tft.width() - 5, buttonPosY[3], buttonWidth, buttonHeight, YELLOW ); - println_Right( tft, "OVEN CHECK", tft.width() - 27, buttonPosY[3] + 9 ); + DrawButton(0, "REFLOW"); + DrawButton(1, "BAKE"); + DrawButton(2, "SETTINGS"); + DrawButton(3, "OVEN CHECK"); } else if ( state == SETTINGS ) { - // button 0 - tft.fillRect( tft.width() - 100, buttonPosY[0] - 2, 100, buttonHeight + 4, BLACK ); - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - switch ( settings_pointer ) - { - - case 1: - case 2: - case 3: - case 4: - case 5: - println_Right( tft, "CHANGE", tft.width() - 27, buttonPosY[0] + 9 ); - break; - - default: - println_Right( tft, "SELECT", tft.width() - 27, buttonPosY[0] + 9 ); - break; - } - - // button 1 - tft.fillRect( tft.width() - 5, buttonPosY[1], buttonWidth, buttonHeight, RED ); - println_Right( tft, "BACK", tft.width() - 27, buttonPosY[1] + 9 ); - - // button 2 - tft.fillRect( tft.width() - 5, buttonPosY[2], buttonWidth, buttonHeight, BLUE ); - println_Right( tft, "/\\", tft.width() - 27, buttonPosY[2] + 9 ); - - // button 3 - tft.fillRect( tft.width() - 5, buttonPosY[3], buttonWidth, buttonHeight, YELLOW ); - println_Right( tft, "\\/", tft.width() - 27, buttonPosY[3] + 9 ); - - UpdateSettingsPointer(); + DrawButton(0, SettingsPage::getButtonText()); + DrawButton(1, "BACK"); + DrawButton(2, "/\\"); + DrawButton(3, "\\/"); } else if ( state == SETTINGS_PASTE ) { - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "SELECT", tft.width() - 27, buttonPosY[0] + 9 ); - - // button 1 - tft.fillRect( tft.width() - 5, buttonPosY[1], buttonWidth, buttonHeight, RED ); - println_Right( tft, "BACK", tft.width() - 27, buttonPosY[1] + 9 ); + DrawButton(0, "SELECT"); + DrawButton(1, "BACK"); + DrawButton(2, "/\\"); + DrawButton(3, "\\/"); - // button 2 - tft.fillRect( tft.width() - 5, buttonPosY[2], buttonWidth, buttonHeight, BLUE ); - println_Right( tft, "/\\", tft.width() - 27, buttonPosY[2] + 9 ); - - // button 3 - tft.fillRect( tft.width() - 5, buttonPosY[3], buttonWidth, buttonHeight, YELLOW ); - println_Right( tft, "\\/", tft.width() - 27, buttonPosY[3] + 9 ); - - UpdateSettingsPointer(); + DrawSettingsPointer(); } else if ( state == SETTINGS_RESET ) // restore settings to default { - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "YES", tft.width() - 27, buttonPosY[0] + 9 ); - - // button 1 - tft.fillRect( tft.width() - 5, buttonPosY[1], buttonWidth, buttonHeight, RED ); - println_Right( tft, "NO", tft.width() - 27, buttonPosY[1] + 9 ); + DrawButton(0, "YES"); + DrawButton(1, "NO"); } else if ( state == WARMUP || state == REFLOW || state == OVENCHECK_START ) // warmup, reflow, calibration { - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "ABORT", tft.width() - 27, buttonPosY[0] + 9 ); + DrawButton(0, "ABORT"); } else if ( state == FINISHED ) // Finished { tft.fillRect( tft.width() - 100, buttonPosY[0] - 2, 100, buttonHeight + 4, BLACK ); - - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "MENU", tft.width() - 27, buttonPosY[0] + 9 ); + DrawButton(0, "MENU"); } else if ( state == OVENCHECK ) { - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "START", tft.width() - 27, buttonPosY[0] + 9 ); + DrawButton(0, "START"); + DrawButton(1, "BACK"); + } +} + +void FlashButtonText(unsigned int index, uint32_t colorSteady = WHITE, uint32_t colorBlink = RED, unsigned int hertz = 5, unsigned int count = 2); - // button 1 - tft.fillRect( tft.width() - 5, buttonPosY[1], buttonWidth, buttonHeight, RED ); - println_Right( tft, "BACK", tft.width() - 27, buttonPosY[1] + 9 ); +void FlashButtonText(unsigned int index, uint32_t colorSteady, uint32_t colorBlink, unsigned int hertz, unsigned int count) +{ + if (index >= NumButtons) return; // out of range + const unsigned long period = 1000 / (hertz * 2); // in ms + + for (unsigned int i = 0; i < count; i++) { + DrawButton(index, buttonText[index], colorBlink); + delay(period); + DrawButton(index, buttonText[index], colorSteady); + if(i != count - 1) delay(period); // delay if not on last loop } } +void DrawScrollIndicator(float pct, uint32_t color) +{ + if (pct < 0.0) pct = 0.0; + else if (pct > 1.0) pct = 1.0; + + const unsigned int Width = 15; // all values in pixels + const unsigned int Height = 3; + const unsigned int MarginY = 2; + + const unsigned int XPos = tft.width() - (Width * 2); + const unsigned int YMin = buttonPosY[2] + buttonHeight + MarginY; // inside edge + const unsigned int YMax = buttonPosY[3] - MarginY; // outside edge + + const unsigned int YRange = YMax - YMin; // total range + number of options + + const unsigned int yPos = (pct * (float) YRange) + YMin; // position of indicator + + tft.fillRect(XPos, YMin, Width, YMax - YMin + Height, BLACK); // clear area + tft.fillRect(XPos + Width / 2, YMin, 1, YMax - YMin + Height, WHITE); // draw center line + tft.fillRect(XPos, yPos, Width, Height, color); // draw indicator +} + +void DrawPageIndicator(unsigned int page, unsigned int numPages, uint32_t color) +{ + const unsigned int TextWidth = 31; // total width from right edge, ballpark + const unsigned int TextHeight = 8; // with text size '1' + const unsigned int XPos = tft.width() - TextWidth; // left edge + const unsigned int YPos = (buttonPosY[2] + buttonPosY[3]) / 2 + (TextHeight / 2); + + tft.fillRect(XPos, YPos, TextWidth, TextHeight, BLACK); // clear area + + tft.setTextColor(color, BLACK); + tft.setTextSize(1); + tft.setCursor(XPos, YPos); + + tft.print(page); + tft.print('/'); + tft.print(numPages); +} + void UpdateBakeMenu() { tft.setTextColor( YELLOW, BLACK ); @@ -1412,22 +1345,11 @@ void ShowBakeMenu() tft.fillRect( tft.width() - 100, buttonPosY[0] - 2, 100, buttonHeight + 4, BLACK ); - // button 0 - - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "START", tft.width() - 27, buttonPosY[0] + 9 ); - // button 1 - tft.fillRect( tft.width() - 5, buttonPosY[1], buttonWidth, buttonHeight, RED ); - println_Right( tft, "BACK", tft.width() - 27, buttonPosY[1] + 9 ); - - // button 2 - tft.fillRect( tft.width() - 5, buttonPosY[2], buttonWidth, buttonHeight, BLUE ); - println_Right( tft, "+TEMP", tft.width() - 27, buttonPosY[2] + 9 ); - - // button 3 - tft.fillRect( tft.width() - 5, buttonPosY[3], buttonWidth, buttonHeight, YELLOW ); - println_Right( tft, "+TIME", tft.width() - 27, buttonPosY[3] + 9 ); + DrawButton(0, "START"); + DrawButton(1, "BACK"); + DrawButton(2, "+TEMP"); + DrawButton(3, "+TIME"); UpdateBakeMenu(); } @@ -1495,9 +1417,7 @@ void StartBake() tft.setCursor( 20, 135 ); tft.println( "TIME LEFT"); - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "ABORT", tft.width() - 27, buttonPosY[0] + 9 ); + DrawButton(0, "ABORT"); UpdateBake(); } @@ -1529,75 +1449,16 @@ void BakeDone() tft.setTextColor( WHITE, BLACK ); tft.setTextSize(2); - // button 0 - tft.fillRect( tft.width() - 5, buttonPosY[0], buttonWidth, buttonHeight, GREEN ); - println_Right( tft, "MENU", tft.width() - 27, buttonPosY[0] + 9 ); + DrawButton(0, "MENU"); delay(750); Buzzer( 2000, 500 ); } -void UpdateSettingsPointer() +void DrawSettingsPointer() { - if ( state == SETTINGS ) - { - tft.setTextColor( BLUE, BLACK ); - tft.setTextSize(2); - tft.fillRect( 0, 20, 20, tft.height() - 20, BLACK ); - tft.setCursor( 5, ( 45 + ( 19 * settings_pointer ) ) ); - tft.println(">"); - - tft.setTextSize(1); - tft.setTextColor( GREEN, BLACK ); - tft.fillRect( 0, tft.height() - 20, tft.width(), 20, BLACK ); - - int testPosY = tft.height() - 16; - switch ( settings_pointer ) - { - case 0: - println_Center( tft, "Select which profile to reflow", tft.width() / 2, testPosY ); - break; - - case 1: - println_Center( tft, "Enable fan for end of reflow, requires 5V DC fan", tft.width() / 2, testPosY ); - break; - - case 2: - println_Center( tft, "Keep fan on for XXX sec after reflow", tft.width() / 2, testPosY ); - break; - - case 3: - println_Center( tft, "Soak and Reflow look ahead for rate change speed", tft.width() / 2, testPosY ); - break; - - case 4: - println_Center( tft, "Adjust the power boost", tft.width() / 2, testPosY ); - break; - - case 5: - println_Center( tft, "Adjust temp probe reading offset", tft.width() / 2, testPosY ); - break; - - case 6: - println_Center( tft, "Force full power on initial ramp-up - be careful!", tft.width() / 2, testPosY ); - break; - - case 7: - println_Center( tft, "Bake thermal mass adjustment, higher for more mass", tft.width() / 2, testPosY ); - break; - - case 8: - println_Center( tft, "Reset to default settings", tft.width() / 2, testPosY ); - break; - - default: - //println_Center( tft, "", tft.width() / 2, tft.height() - 20 ); - break; - } - tft.setTextSize(2); - } - else if ( state == SETTINGS_PASTE ) + if ( state == SETTINGS_PASTE ) { tft.setTextColor( BLUE, BLACK ); tft.setTextSize(2); @@ -1613,7 +1474,7 @@ void StartWarmup() state = WARMUP; timeX = 0; - ShowMenuOptions( true ); + ShowButtonOptions( true ); lastWantedTemp = -1; buzzerCount = 5; keepFanOnTime = 0; @@ -1637,7 +1498,7 @@ void StartReflow() tft.fillScreen(BLACK); state = REFLOW; - ShowMenuOptions( true ); + ShowButtonOptions( true ); timeX = 0; SetupGraph(tft, 0, 0, 30, 220, 270, 180, graphRangeMin_X, graphRangeMax_X, graphRangeStep_X, graphRangeMin_Y, graphRangeMax_Y, graphRangeStep_Y, "Reflow Temp", " Time [s]", "deg [C]", DKBLUE, BLUE, WHITE, BLACK ); @@ -1687,7 +1548,7 @@ void EndReflow() DrawHeading( "DONE!", WHITE, BLACK ); - ShowMenuOptions( false ); + ShowButtonOptions( false ); if ( set.useFan && set.fanTimeAfterReflow > 0 ) { @@ -1705,20 +1566,8 @@ void EndReflow() void SetDefaults() { - // Default settings values - set.valid = true; - set.fanTimeAfterReflow = 60; - set.power = 1; - set.paste = 0; - set.useFan = false; - set.lookAhead = 7; - set.lookAheadWarm = 7; - set.startFullBlast = false; - set.tempOffset = 0; - set.beep = true; - set.bakeTime = 1200; - set.bakeTemp = 45; - set.bakeTempGap = 3; + // recreate struct using default constructor + set = Settings(); } void ResetSettingsToDefault() @@ -1731,7 +1580,6 @@ void ResetSettingsToDefault() SetCurrentGraph( set.paste ); // show settings - settings_pointer = 0; ShowSettings(); } @@ -1770,7 +1618,7 @@ void StartOvenCheck() tft.setCursor( 20, 60 ); tft.println( String(CurrentGraph().tempDeg) + "deg"); - ShowMenuOptions( true ); + ShowButtonOptions( true ); } void ShowOvenCheck() @@ -1790,7 +1638,7 @@ void ShowOvenCheck() tft.setTextColor( WHITE, BLACK ); - ShowMenuOptions( true ); + ShowButtonOptions( true ); tft.setTextSize(2); tft.setCursor( 0, 60 ); @@ -1834,7 +1682,7 @@ void ShowResetDefaults() tft.println( "ARE YOU SURE?" ); state = SETTINGS_RESET; - ShowMenuOptions( false ); + ShowButtonOptions( false ); tft.setTextSize(1); tft.setTextColor( GREEN, BLACK ); @@ -1842,113 +1690,33 @@ void ShowResetDefaults() println_Center( tft, "Settings restore cannot be undone!", tft.width() / 2, tft.height() - 20 ); } -void UpdateSettingsFan( int posY ) -{ - tft.fillRect( 15, posY - 5, 200, 20, BLACK ); - tft.setTextColor( WHITE, BLACK ); - tft.setCursor( 20, posY ); - - tft.setTextColor( WHITE, BLACK ); - tft.print( "USE FAN " ); - tft.setTextColor( YELLOW, BLACK ); - - if ( set.useFan ) - { - tft.println( "ON" ); - } - else - { - tft.println( "OFF" ); - } - tft.setTextColor( WHITE, BLACK ); -} - -void UpdateSettingsFanTime( int posY ) -{ - tft.fillRect( 15, posY - 5, 230, 20, BLACK ); - tft.setTextColor( WHITE, BLACK ); - tft.setCursor( 20, posY ); - - tft.setTextColor( WHITE, BLACK ); - tft.print( "FAN COUNTDOWN " ); - tft.setTextColor( YELLOW, BLACK ); - - tft.println( String( set.fanTimeAfterReflow ) + "s"); - - tft.setTextColor( WHITE, BLACK ); -} - -void UpdateSettingsStartFullBlast( int posY ) -{ - tft.fillRect( 15, posY - 5, 240, 20, BLACK ); - tft.setTextColor( WHITE, BLACK ); - tft.setCursor( 20, posY ); - tft.print( "START RAMP 100% " ); - tft.setTextColor( YELLOW, BLACK ); - - if ( set.startFullBlast ) - { - tft.println( "ON" ); - } - else - { - tft.println( "OFF" ); - } - tft.setTextColor( WHITE, BLACK ); -} - -void UpdateSettingsPower( int posY ) -{ - tft.fillRect( 15, posY - 5, 240, 20, BLACK ); - tft.setTextColor( WHITE, BLACK ); - - tft.setCursor( 20, posY ); - tft.print( "POWER "); - tft.setTextColor( YELLOW, BLACK ); - tft.println( String( round((set.power * 100))) + "%"); - tft.setTextColor( WHITE, BLACK ); -} - -void UpdateSettingsLookAhead( int posY ) -{ - tft.fillRect( 15, posY - 5, 260, 20, BLACK ); - tft.setTextColor( WHITE, BLACK ); - - tft.setCursor( 20, posY ); - tft.print( "GRAPH LOOK AHEAD "); - tft.setTextColor( YELLOW, BLACK ); - tft.println( String( set.lookAhead) ); - tft.setTextColor( WHITE, BLACK ); -} +/* + Button press code here +*/ -void UpdateSettingsBakeTempGap( int posY ) +void KeyTone(int hertz, int len) { - tft.fillRect( 15, posY - 5, 260, 20, BLACK ); - tft.setTextColor( WHITE, BLACK ); - - tft.setCursor( 20, posY ); - tft.print( "BAKE TEMP GAP "); - tft.setTextColor( YELLOW, BLACK ); - tft.println( String( set.bakeTempGap ) ); - tft.setTextColor( WHITE, BLACK ); + if (!set.keyTone) return; + Buzzer(hertz, len); } -void UpdateSettingsTempOffset( int posY ) +// returns the a value limited to the min and max ranges, rolling over +// values below and above the provided range as if it was continuous +int constrainLoop(int value, int min, int max) { - tft.fillRect( 15, posY - 5, 220, 20, BLACK ); - tft.setTextColor( WHITE, BLACK ); - - tft.setCursor( 20, posY ); - tft.print( "TEMP OFFSET "); - tft.setTextColor( YELLOW, BLACK ); - tft.println( String( set.tempOffset) ); - tft.setTextColor( WHITE, BLACK ); + // rollover up + if (value > max) { + int diff = (value - max - 1) % (max - min + 1); + return constrain(min + diff, min, max); + } + // rollover down + else if (value < min) { + int diff = (value - min + 1) % (max - min + 1); + return constrain(max + diff, min, max); + } + return value; } -/* - Button press code here -*/ - unsigned long nextButtonPress = 0; void button0Press() @@ -1956,15 +1724,17 @@ void button0Press() if ( nextButtonPress < millis() ) { nextButtonPress = millis() + 20; - Buzzer( 2000, 50 ); + KeyTone( 2000, 50 ); if ( state == MENU ) { // Only allow reflow start if there is no TC error if ( tcError == 0 ) StartWarmup(); - else - Buzzer( 100, 250 ); + else { + Buzzer(100, 250); + FlashButtonText(0); + } } else if ( state == BAKE_MENU ) { @@ -1982,67 +1752,7 @@ void button0Press() } else if ( state == SETTINGS ) { - if ( settings_pointer == 0 ) // change paste - { - settings_pointer = set.paste; - ShowPaste(); - } - else if ( settings_pointer == 1 ) // switch fan use - { - set.useFan = !set.useFan; - - UpdateSettingsFan( 64 ); - } - else if ( settings_pointer == 2 ) // fan countdown after reflow - { - set.fanTimeAfterReflow += 5; - if ( set.fanTimeAfterReflow > 60 ) - set.fanTimeAfterReflow = 0; - - UpdateSettingsFanTime( 83 ); - } - else if ( settings_pointer == 3 ) // change lookahead for reflow - { - set.lookAhead += 1; - if ( set.lookAhead > 15 ) - set.lookAhead = 1; - - UpdateSettingsLookAhead( 102 ); - } - else if ( settings_pointer == 4 ) // change power - { - set.power += 0.1; - if ( set.power > 1.55 ) - set.power = 0.5; - - UpdateSettingsPower( 121 ); - } - else if ( settings_pointer == 5 ) // change temp probe offset - { - set.tempOffset += 1; - if ( set.tempOffset > 15 ) - set.tempOffset = -15; - - UpdateSettingsTempOffset( 140 ); - } - else if ( settings_pointer == 6 ) // change use full power on initial ramp - { - set.startFullBlast = !set.startFullBlast; - - UpdateSettingsStartFullBlast( 159 ); - } - else if ( settings_pointer == 7 ) // bake temp gap - { - set.bakeTempGap += 1; - if ( set.bakeTempGap > 5 ) - set.bakeTempGap = 0; - - UpdateSettingsBakeTempGap( 178 ); - } - else if ( settings_pointer == 8 ) // reset defaults - { - ShowResetDefaults(); - } + SettingsPage::pressButton(0); } else if ( state == SETTINGS_PASTE ) { @@ -2069,27 +1779,31 @@ void button1Press() if ( nextButtonPress < millis() ) { nextButtonPress = millis() + 20; - Buzzer( 2000, 50 ); + KeyTone( 2000, 50 ); if ( state == MENU ) { // Only allow reflow start if there is no TC error if ( tcError == 0 ) ShowBakeMenu(); - else + else { Buzzer( 100, 250 ); + FlashButtonText(1); + } } else if ( state == SETTINGS ) // leaving settings so save { - // save data in flash - flash_store.write(set); - ShowMenu(); + SettingsPage::pressButton(1); } - else if ( state == SETTINGS_PASTE || state == SETTINGS_RESET ) + else if ( state == SETTINGS_PASTE) { - settings_pointer = 0; + settings_pointer = 0; // reset pointer so we stop at the top of the settings list ShowSettings(); } + else if (state == SETTINGS_RESET) // cancel settings reset + { + ShowSettings(false); // do *not* reset pagination + } else if ( state == OVENCHECK ) // cancel oven check { ShowMenu(); @@ -2107,7 +1821,7 @@ void button2Press() if ( nextButtonPress < millis() ) { nextButtonPress = millis() + 20; - Buzzer( 2000, 50 ); + KeyTone( 2000, 50 ); if ( state == MENU ) { @@ -2124,14 +1838,12 @@ void button2Press() } else if ( state == SETTINGS ) { - settings_pointer = constrain( settings_pointer - 1, 0, 8 ); - ShowMenuOptions( false ); - //UpdateSettingsPointer(); + SettingsPage::pressButton(2); } else if ( state == SETTINGS_PASTE ) { settings_pointer = constrain( settings_pointer - 1, 0, (int) ELEMENTS(solderPaste) - 1 ); - UpdateSettingsPointer(); + DrawSettingsPointer(); } } } @@ -2142,15 +1854,17 @@ void button3Press() if ( nextButtonPress < millis() ) { nextButtonPress = millis() + 20; - Buzzer( 2000, 50 ); + KeyTone( 2000, 50 ); if ( state == MENU ) { // Only allow reflow start if there is no TC error if ( tcError == 0 ) ShowOvenCheck(); - else + else { Buzzer( 100, 250 ); + FlashButtonText(3); + } } else if ( state == BAKE_MENU ) { @@ -2162,14 +1876,12 @@ void button3Press() } else if ( state == SETTINGS ) { - settings_pointer = constrain( settings_pointer + 1, 0, 8 ); - ShowMenuOptions( false ); - //UpdateSettingsPointer(); + SettingsPage::pressButton(3); } else if ( state == SETTINGS_PASTE ) { settings_pointer = constrain( settings_pointer + 1, 0, (int) ELEMENTS(solderPaste) - 1 ); - UpdateSettingsPointer(); + DrawSettingsPointer(); } } } @@ -2180,9 +1892,9 @@ void button2LongPressStart() if ( nextButtonPress < millis() ) { nextButtonPress = millis() + 10; - Buzzer( 2000, 10 ); + KeyTone( 2000, 10 ); delay(50); - Buzzer( 2000, 10 ); + KeyTone( 2000, 10 ); } } @@ -2209,9 +1921,9 @@ void button3LongPressStart() if ( nextButtonPress < millis() ) { nextButtonPress = millis() + 20; - Buzzer( 2000, 10 ); + KeyTone( 2000, 10 ); delay(50); - Buzzer( 2000, 10 ); + KeyTone( 2000, 10 ); } } diff --git a/Code/Reflow_Master_v2/linked_list.h b/Code/Reflow_Master_v2/linked_list.h new file mode 100644 index 0000000..ff70827 --- /dev/null +++ b/Code/Reflow_Master_v2/linked_list.h @@ -0,0 +1,75 @@ +template +class LinkedList { +private: + static T* head; + T* next = nullptr; + +public: + static T* getHead() { return head; } + T* getNext() { return next; } + + // indexed at 1 + static size_t getCount() { + size_t count = 0; + T* ptr = head; + while (ptr != nullptr) { + ptr = ptr->next; + count++; + } + return count; + } + + static T* getItemAtIndex(unsigned int i) { + T* ptr = head; + while (ptr != nullptr && i != 0) { + ptr = ptr->getNext(); + i--; + } + if (i != 0) return nullptr; + return ptr; + } + + static int getPositionOf(T* node) { + unsigned int position = 0; + T* ptr = head; + while (ptr != nullptr) { + if (node == ptr) return position; + position++; + ptr = ptr->getNext(); + } + return -1; // not found + } + +protected: + static void add(T* node) { + if (head == nullptr) { + head = node; + } + else { + T* last = head; + while (true) { + if (last->next == nullptr) break; // found last entry + last = last->next; + } + last->next = node; + } + } + + static void remove(T* node) { + if (node == head) { + head = node->next; // Set head to next, and we're done + return; + } + + // Otherwise we're somewhere else in the list. Iterate through to find it. + T* ptr = head; + + while (ptr != nullptr) { + if (ptr->next == node) { // FOUND! + ptr->next = node->next; // Set the previous "next" as this entry's "next" (skip this object) + break; // Stop searching + } + ptr = ptr->next; // Not found. Next entry... + } + } +}; diff --git a/Code/Reflow_Master_v2/menu_settings.cpp b/Code/Reflow_Master_v2/menu_settings.cpp new file mode 100644 index 0000000..664e8ff --- /dev/null +++ b/Code/Reflow_Master_v2/menu_settings.cpp @@ -0,0 +1,294 @@ +#include "menu_settings.h" + + +template<> +SettingsOption* LinkedList::head = nullptr; + +SettingsOption::SettingsOption(const String& name, const String& desc, MenuGetFunc get, MenuSetFunc set) + : + ItemName(name), ItemDescription(desc), + getFunction(get), setFunction(set) +{ + LinkedList::add(this); +} + +SettingsOption::~SettingsOption() +{ + LinkedList::remove(this); +} + +bool SettingsOption::modify() { + if (this->setFunction != nullptr) { + this->setFunction(); + } + return refreshOnModify(); +} + +unsigned long SettingsOption::getYPosition(unsigned int index) { + return (ItemHeight * index) + ItemStartY; // y coordinate on center +} + +void SettingsOption::drawItem(unsigned int position) { + const unsigned int xPos = ItemStartX; + const unsigned int yPos = getYPosition(position); + + int16_t clearX, clearY; + uint16_t clearWidth, clearHeight; + + tft.setTextSize(2); + tft.getTextBounds(ItemName + " ", xPos, yPos, &clearX, &clearY, &clearWidth, &clearHeight); + tft.fillRect(clearX, clearY, clearWidth, clearHeight, BLACK); // clear text area + + tft.setCursor(xPos, yPos); + + tft.setTextColor(WHITE, BLACK); + tft.print(ItemName); + tft.print(' '); + + this->drawValue(position); +} + +void SettingsOption::drawValue(unsigned int position) { + if (getFunction == nullptr) return; // no string, so no need to draw it + + const unsigned int yPos = getYPosition(position); + + int16_t nameX, dummyY; + uint16_t nameWidth, dummyHeight; + + // Calculate size of item name to set cursor at end + tft.setTextSize(2); + tft.getTextBounds(ItemName + " ", ItemStartX, yPos, &nameX, &dummyY, &nameWidth, &dummyHeight); + + const unsigned int xPos = nameX + nameWidth; // start at the end of the item name + + // Note that we're not reusing the variables from above because the program seems to get + // confused and use the values from the first call rather than the second (potentially an optimization issue?) + int16_t clearX, clearY; + uint16_t clearWidth, clearHeight; + + // Calculate size of item description for clearing (using last value) + tft.setTextSize(2); + tft.getTextBounds(lastValue, xPos, yPos, &clearX, &clearY, &clearWidth, &clearHeight); + tft.fillRect(clearX, clearY, clearWidth, clearHeight, BLACK); // clear area + + tft.setCursor(xPos, yPos); + tft.setTextColor(YELLOW, BLACK); + tft.print(getFunction()); + + lastValue = getFunction(); // save value so we know how much to 'clear' for next call +} + +void SettingsOption::drawDescription() { + const unsigned int height_area = 20; + const unsigned int height_text = 16; + + tft.setTextSize(1); + tft.setTextColor(GREEN, BLACK); + tft.fillRect(0, tft.height() - height_area, tft.width(), height_area, BLACK); + + int textPosY = tft.height() - height_text; + println_Center(tft, this->ItemDescription, tft.width() / 2, textPosY); +} + + +SettingsOptionLink::SettingsOptionLink(const String& name, const String& desc, MenuSetFunc set) + // skipping over the 'get' function + : SettingsOption(name, desc, nullptr, set) {} + + +namespace SettingsPage { + enum class ScrollType { + Smooth, // at the end of current page, moves all items up by one + Paged, // at the end of the current page, draws an entire new page of items + }; + + static const unsigned int ItemsPerPage = 9; // should be calculated given font size and screen space, but good enough for now + static unsigned int startingItem = 0; // first item on the page (indexed at 0) + static unsigned int selectedItem = 0; // currently selected item in the list (indexed at 0) + + static const ScrollType Scroll = ScrollType::Paged; + + unsigned int lastItem() { return startingItem + ItemsPerPage - 1; } // index of the last item in the list, indexed at 0 + bool onPage(unsigned int item) { return item >= startingItem && item <= lastItem(); } // returns 'true' if an item (index N) is on the page + + void redraw(); // redraw the current page after a selection update + + void drawCursor(); // draws the '>' cursor, and redraws the page / description if necessary + void drawScrollIndicator(); // draw the scroll indicator between buttons 3/4 + + void changeOption(unsigned int pos); // runs the "change this option" function for the selected option + + void drawItems(); // draw the item names and values across the page + + bool updateScroll(); // recalculate scroll position and items on the screen +} + + +void SettingsPage::pressButton(unsigned int num) { + switch (num) { + + // Select + case(0): + changeOption(selectedItem); + break; + + // Back + case(1): + // save data in flash + ExitSettings(); + break; + + // Up List + case(2): + selectedItem = constrainLoop(selectedItem - 1, 0, (int)SettingsOption::getCount() - 1); + ShowButtonOptions(false); + drawCursor(); + break; + + // Down List + case(3): + selectedItem = constrainLoop(selectedItem + 1, 0, (int)SettingsOption::getCount() - 1); + ShowButtonOptions(false); + drawCursor(); + break; + } +} + + +void SettingsPage::drawPage(bool resetSelection) { + if (resetSelection) { + selectedItem = startingItem = 0; // reset pagination + } + updateScroll(); // recalculate pagination for current position + + tft.fillScreen(BLACK); + + tft.setTextColor(BLUE, BLACK); + tft.setTextSize(2); + tft.setCursor(20, 20); + tft.println("SETTINGS"); + + drawItems(); + + ShowButtonOptions(false); + drawCursor(); + drawScrollIndicator(); // needs to be redrawn on top of buttons +} + +void SettingsPage::redraw() { + // drawing over *only* the items, rather than the entire page (title, cursor, button prompts) + tft.fillRect(15, SettingsOption::getYPosition(0) - 5, 240, SettingsOption::getYPosition(ItemsPerPage), BLACK); + + // clear the description box as well + tft.fillRect(0, tft.height() - 20, tft.width(), 20, BLACK); + + // recalculate pagination for current position (if necessary) + updateScroll(); + + drawItems(); + drawScrollIndicator(); +} + +void SettingsPage::drawItems() { + SettingsOption* ptr = SettingsOption::getHead(); + size_t position = 0; + + while (ptr != nullptr) { + if (position >= startingItem) { + ptr->drawItem(position - startingItem); + } + position++; + ptr = ptr->getNext(); + + if (position > startingItem + SettingsPage::ItemsPerPage - 1) break; // break when we've filled the page (-1 for zero index) + } +} + +void SettingsPage::drawCursor() { + SettingsOption* ptr = SettingsOption::getItemAtIndex(selectedItem); + if (ptr == nullptr) return; // out of range + + // Clear cursor + tft.fillRect(0, 20, 20, tft.height() - 20, BLACK); + + // If the current selection is not on the page, we need to redraw the list accordingly + if (updateScroll()) { + redraw(); // redraws those items on the screen + } + + const unsigned int selectedPos = selectedItem - startingItem; + + // Draw the actual cursor + tft.setTextColor(BLUE, BLACK); + tft.setTextSize(2); + tft.setCursor(5, SettingsOption::getYPosition(selectedPos)); + tft.println(">"); + + ptr->drawDescription(); +} + +void SettingsPage::changeOption(unsigned int pos) { + SettingsOption* ptr = SettingsOption::getItemAtIndex(pos); + if (ptr == nullptr) return; + + if (ptr->modify()) { + ptr->drawValue(SettingsOption::getPositionOf(ptr) - startingItem); + } +} + +bool SettingsPage::updateScroll() { + if (onPage(selectedItem)) return false; // already on page, don't update + + switch (Scroll) { + case(ScrollType::Smooth): + if (selectedItem < startingItem) { + // 'decrement' to current as first + startingItem = selectedItem; + } + else if (selectedItem > lastItem()) { + // 'increment' to position as last + startingItem = selectedItem - ItemsPerPage + 1; // +1 for 0 index + } + break; + case(ScrollType::Paged): + { + const unsigned int Page = (selectedItem / ItemsPerPage); // page for this item + const unsigned int PageStart = Page * ItemsPerPage; // starting item for that page + startingItem = PageStart; + break; + } + } + + return true; +} + +void SettingsPage::drawScrollIndicator() { + if (SettingsOption::getCount() <= ItemsPerPage) return; // no scrolling = no scroll indicator + + switch (Scroll) { + case(ScrollType::Smooth): + { + const unsigned int NumPositions = SettingsOption::getCount() - ItemsPerPage; + const float value = (float) startingItem / (float) NumPositions; + + DrawScrollIndicator(value, BLUE); + break; + } + case(ScrollType::Paged): + { + const unsigned int Page = (startingItem / ItemsPerPage) + 1; + const unsigned int NumItems = SettingsOption::getCount(); + const unsigned int NumPages = (NumItems / ItemsPerPage) + ((NumItems % ItemsPerPage != 0) ? 1 : 0); + DrawPageIndicator(Page, NumPages, WHITE); + break; + } + } +} + +String SettingsPage::getButtonText() { + SettingsOption* ptr = SettingsOption::getItemAtIndex(selectedItem); + if (ptr == nullptr) return ""; + return ptr->getModeString(); +} + diff --git a/Code/Reflow_Master_v2/menu_settings.h b/Code/Reflow_Master_v2/menu_settings.h new file mode 100644 index 0000000..cb364b6 --- /dev/null +++ b/Code/Reflow_Master_v2/menu_settings.h @@ -0,0 +1,109 @@ +#include + +#include "tft_display.h" +#include "linked_list.h" + +// Forward-declare functions used in the menu +int constrainLoop(int value, int min, int max); + +void ExitSettings(); + +void DrawScrollIndicator(float, uint32_t); +void DrawPageIndicator(unsigned int, unsigned int, uint32_t); + +void ShowButtonOptions(bool clearAll); + +void println_Center(Adafruit_ILI9341& d, String heading, int centerX, int centerY); + +// define settings data struct +typedef struct { + boolean valid = true; + boolean useFan = false; + int fanTimeAfterReflow = 60; + byte paste = 0; + float power = 1; + int lookAhead = 7; + int lookAheadWarm = 7; + int tempOffset = 0; + long bakeTime = 1200; // 20 mins + float bakeTemp = 45; // Degrees C + int bakeTempGap = 3; // Aim for the desired temp minus this value to compensate for overrun + bool startFullBlast = false; + bool beep = true; + bool keyTone = true; + bool startupTune = true; +} Settings; + + +class SettingsOption : public LinkedList { +public: + friend LinkedList; + + typedef String(*MenuGetFunc)(); + typedef void (*MenuSetFunc)(); + + SettingsOption(const String& name, const String& desc, MenuGetFunc get, MenuSetFunc set); + virtual ~SettingsOption(); + + bool modify(); + + static unsigned long getYPosition(unsigned int index); + virtual String getModeString() const = 0; + + void drawItem(unsigned int posY); + void drawValue(unsigned int posY); + void drawDescription(); + +private: + virtual bool refreshOnModify() { return true; } + + static const unsigned int ItemHeight = 19; + static const unsigned int ItemStartX = 20; + static const unsigned int ItemStartY = 45; + + const String ItemName; + const String ItemDescription; + + const MenuGetFunc getFunction; + const MenuSetFunc setFunction; + + String lastValue; +}; + + +class SettingsOptionLink : public SettingsOption { +public: + SettingsOptionLink(const String& name, const String& desc, MenuSetFunc set); + + String getModeString() const { return "SELECT"; } + +private: + // don't refresh the menu page on modify because we're + // moving to a different page entirely + bool refreshOnModify() { return false; } +}; + +class SettingsOptionAdjust : public SettingsOption { +public: + using SettingsOption::SettingsOption; + + String getModeString() const { return "CHANGE"; } +}; + +class SettingsOptionToggle : public SettingsOption { +public: + using SettingsOption::SettingsOption; + + String getModeString() const { return "TOGGLE"; } +}; + + +namespace SettingsPage { + void pressButton(unsigned int num); // record a button press on the settings page + + void drawPage(bool resetSelection = true); // draw the Settings page in its entirety + String getButtonText(); // get the string of text to describe button 0 +}; + + +extern Settings set; diff --git a/Code/Reflow_Master_v2/menu_settings_options.cpp b/Code/Reflow_Master_v2/menu_settings_options.cpp new file mode 100644 index 0000000..85a383a --- /dev/null +++ b/Code/Reflow_Master_v2/menu_settings_options.cpp @@ -0,0 +1,143 @@ +#include "menu_settings.h" + +void ShowPaste(); +void ShowResetDefaults(); + +// "Switch Paste" menu option +SettingsOptionLink OptionPasteMenu("SWITCH PASTE", "Select which profile to reflow", ShowPaste); + + +// "Use Fan" menu option +String getUseFan() { + return set.useFan ? "ON" : "OFF"; +} + +void changeUseFan() { + set.useFan = !set.useFan; +} + +SettingsOptionToggle OptionUseFan("USE FAN", "Enable fan for end of reflow, requires 5V DC fan", getUseFan, changeUseFan); + + +// "Fan Countdown" menu option +String getFanCountdown() { + return String(set.fanTimeAfterReflow) + "s"; +} + +void changeFanCountdown() { + set.fanTimeAfterReflow += 5; + if (set.fanTimeAfterReflow > 60) + set.fanTimeAfterReflow = 0; +} + +SettingsOptionAdjust OptionFanCountdown("FAN COUNTDOWN", "Keep fan on for XXX sec after reflow", getFanCountdown, changeFanCountdown); + + +// "Graph Look Ahead" menu option +String getGraphLookAhead() { + return String(set.lookAhead); +} + +void changeGraphLookAhead() { + set.lookAhead += 1; + if (set.lookAhead > 15) + set.lookAhead = 1; +} + +SettingsOptionAdjust OptionGraphLookAhead("GRAPH LOOK AHEAD", "Soak and Reflow look ahead for rate change speed", getGraphLookAhead, changeGraphLookAhead); + + +// "Power" menu option +String getPower() { + return String(round((set.power * 100))) + "%"; +} + +void changePower() { + set.power += 0.1; + if (set.power > 1.55) + set.power = 0.5; +} + +SettingsOptionAdjust OptionPower("POWER", "Adjust the power boost", getPower, changePower); + + +// "Temp Offset" menu option +String getTempOffset() { + return String(set.tempOffset); +} + +void changeTempOffset() { + set.tempOffset += 1; + if (set.tempOffset > 15) + set.tempOffset = -15; +} + +SettingsOptionAdjust OptionTempOffset("TEMP OFFSET", "Adjust temp probe reading offset", getTempOffset, changeTempOffset); + + +// "Start Ramp 100%" menu option +String getStartFullBlast() { + return set.startFullBlast ? "ON" : "OFF"; +} + +void changeStartFullBlast() { + set.startFullBlast = !set.startFullBlast; +} + +SettingsOptionToggle OptionStartFullBlast("START RAMP 100%", "Force full power on initial ramp-up - be careful!", getStartFullBlast, changeStartFullBlast); + + +// "Bake Temp Gap" menu option +String getBakeTempGap() { + return String(set.bakeTempGap); +} + +void changeBakeTempGap() { + set.bakeTempGap += 1; + if (set.bakeTempGap > 5) + set.bakeTempGap = 0; +} + +SettingsOptionAdjust OptionBakeTempGap("BAKE TEMP GAP", "Bake thermal mass adjustment, higher for more mass", getBakeTempGap, changeBakeTempGap); + + +// "Key Tone" menu option +String getKeyToneSetting() { + return set.keyTone ? "ON" : "OFF"; +} + +void setKeyToneSetting() { + set.keyTone = !set.keyTone; +} + +SettingsOptionToggle OptionKeyTone("KEY TONE", "Make a noise whenever a button is pressed", getKeyToneSetting, setKeyToneSetting); + + +// "Disable Buzzer" menu option +String getBuzzerSetting() { + return set.beep ? "OFF" : "ON"; // inverted, as the setting is to 'disable' +} + +void setBuzzerSetting() { + set.beep = !set.beep; +} + +SettingsOptionToggle OptionBuzzer("DISABLE BUZZER", "Disable buzzer noise in ALL modes", getBuzzerSetting, setBuzzerSetting); + + +// "Startup Tone" menu option +String getStartupTuneSetting() { + return set.startupTune ? "ON" : "OFF"; +} + +void setStartupTuneSetting() { + set.startupTune = !set.startupTune; +} + +SettingsOptionToggle OptionStartupTune("STARTUP TUNE", "Play a little song on startup", getStartupTuneSetting, setStartupTuneSetting); + + +// ############################################################################ + +// "Reset to defaults" menu option +SettingsOptionLink OptionResetToDefaults("RESET TO DEFAULTS", "Reset to default settings", ShowResetDefaults); diff --git a/Code/Reflow_Master_v2/tft_display.h b/Code/Reflow_Master_v2/tft_display.h new file mode 100644 index 0000000..16f6272 --- /dev/null +++ b/Code/Reflow_Master_v2/tft_display.h @@ -0,0 +1,36 @@ +#ifndef TFT_DISPLAY_H +#define TFT_DISPLAY_H + +#include // Add from Library Manager +#include // Add from Library Manager + +// Just a bunch of pre-defined colours +#define DEFAULT_COLOR 0xABCD0000 // magic value, not a real color +#define BLUE 0x001F +#define TEAL 0x0438 +#define GREEN 0x07E0 +#define CYAN 0x07FF +#define RED 0xF800 +#define MAGENTA 0xF81F +#define YELLOW 0xFFE0 +#define ORANGE 0xFC00 +#define PINK 0xF81F +#define PURPLE 0x8010 +#define GREY 0xC618 +#define WHITE 0xFFFF +#define BLACK 0x0000 +#define DKBLUE 0x000D +#define DKTEAL 0x020C +#define DKGREEN 0x03E0 +#define DKCYAN 0x03EF +#define DKRED 0x6000 +#define DKMAGENTA 0x8008 +#define DKYELLOW 0x8400 +#define DKORANGE 0x8200 +#define DKPINK 0x9009 +#define DKPURPLE 0x4010 +#define DKGREY 0x4A49 + +extern Adafruit_ILI9341 tft; + +#endif