diff --git a/KiwiBoard.emf b/KiwiBoard.emf index 223a3ae..0156785 100644 --- a/KiwiBoard.emf +++ b/KiwiBoard.emf @@ -240,7 +240,7 @@ "type": "analogItem", "defaultValue": "50", "item": { - "maxValue": 550, + "maxValue": 950, "offset": 50, "divisor": 1, "unitName": "rpm", @@ -483,6 +483,44 @@ "staticDataInRAM": false } }, + { + "parentId": 30, + "type": "boolItem", + "defaultValue": "false", + "item": { + "naming": "ON_OFF", + "name": "Sound", + "variableName": "sounder", + "id": 49, + "eepromAddress": 97, + "functionName": "@soundChanged", + "readOnly": false, + "localOnly": false, + "visible": true, + "staticDataInRAM": false + } + }, + { + "parentId": 30, + "type": "analogItem", + "defaultValue": "0", + "item": { + "maxValue": 100, + "offset": 0, + "divisor": 1, + "unitName": "%", + "step": 1, + "name": "Volume", + "variableName": "SoundLevel", + "id": 50, + "eepromAddress": 98, + "functionName": "@soundLevel", + "readOnly": false, + "localOnly": false, + "visible": true, + "staticDataInRAM": false + } + }, { "parentId": 30, "type": "boolItem", diff --git a/src/KiwiBoardFirmware_main.cpp b/src/KiwiBoardFirmware_main.cpp index 0da0e92..59f5fa5 100644 --- a/src/KiwiBoardFirmware_main.cpp +++ b/src/KiwiBoardFirmware_main.cpp @@ -10,6 +10,7 @@ #include "KiwiBoardFirmware_main.h" #include "EncoderShim.h" #include "heat.h" +#include "Sounder.h" #ifdef SCREENCAP #include "screenServer.h" @@ -26,6 +27,7 @@ PicoPlatform *platform; MotorControl *motorControl = nullptr; MenuChangeObserver *observer; EncoderShim *encoderShim; +BeepHandler *sounderOps; // Declare sounderOps (based on class BeepHandler) // Error occurred, in HALT state. bool HALT = false; @@ -55,7 +57,6 @@ void setup() { platform = new PicoPlatform(); platform->initializePlatform(); - // Init the graphics subsystem and trigger the splash. gfx.begin(); gfx.setRotation(3); @@ -66,6 +67,9 @@ void setup() { delay(2000); gfx.fillScreen(TFT_BLACK); + // Setup Sounder + sounderOps = new BeepHandler(platform); // Instantiate object sounderOps based on BeepHandler + // Setup switches and encoder? encoderShim = new EncoderShim(); encoderShim->initForEncoder(); @@ -95,6 +99,10 @@ void setup() { setupMenu(); } + // Get saved values for sounder and sound level.. + sounderOps->set_menuSound(menusounder.getBoolean()); + sounderOps->set_sndLevel(menuSoundLevel.getIntValueIncludingOffset()); + observer = new MenuChangeObserver(&menuMgr, &menuRunTime, &menuWash); menuMgr.addChangeNotification(observer); @@ -102,6 +110,7 @@ void setup() { setMenuOptions(); scheduleTasks(); + } /** @@ -117,6 +126,7 @@ void loop() { void stoppedCallback(int pgm) { // Stopped happened. + sounderOps->beep_activate(0, false); // 0 = End of cycle tone resetIcons(); observer->resetConstraint(); } @@ -270,12 +280,22 @@ void run(int program, MenuItem *icon) { motorControl->stopMotion(); } else { motorControl->startProgram(program, getSettings()); - setIconStopped(icon); observer->constrainToStopButton(icon); } } +void CALLBACK_FUNCTION soundLevel(int id) { + + // Get changes to sound level and then set + // beep for every turn of the encoder when setting sound level + + sounderOps->set_sndLevel(menuSoundLevel.getIntValueIncludingOffset()); + sounderOps->beep_activate(1, true); // Short beep, override soundset var + settingsChanged = true; // Save settings + +} + void CALLBACK_FUNCTION settings_changed(int id) { // TODO Look for actual changes in the setting values rather than just saving settings any time // someone went to the settings menu.... Or add a save button? @@ -350,6 +370,19 @@ void CALLBACK_FUNCTION stealthChopChange(int id) { settingsChanged = true; } +/** + * Callback when the user changes the sound setting. + * + * @param id + */ +void CALLBACK_FUNCTION soundChanged(int id) { + Serial.println("Sound changed... "); + + sounderOps->set_menuSound(menusounder.getBoolean()); + + settingsChanged = true; +} + /** * Commit settings to eeprom if they have changed. Try not to thrash the EEPROM with too many * calls. @@ -391,6 +424,10 @@ void scheduleTasks() { // To prevent thrashing of the EEPROM, only save settings periodically if things were touched. taskManager.scheduleFixedRate(60, commit_if_needed, TIME_SECONDS); + // Sounder operation needs to be non-blocking so we update the status regularly + // Schedule sounder updates for every 20ms to ensure granularity for short beeps + taskManager.scheduleFixedRate(20, sounderOps, TIME_MILLIS); + // Only schedule the screen capture if SCREENCAP is defined from platformio.ini #ifdef SCREENCAP taskManager.scheduleFixedRate(2, screenCaptureTask, TIME_SECONDS); @@ -513,7 +550,10 @@ void setIconStopped(MenuItem *icon) { } void checkLongPress(bool direction, bool held) { - + + // Button pressed, so we beep + sounderOps->beep_activate(1, false); // Short beep, no soundset override + // Check for a long press... no idea what menu ... but whatever? if (held) { // what are we long pressing on? diff --git a/src/KiwiBoardFirmware_menu.cpp b/src/KiwiBoardFirmware_menu.cpp index f9c0246..433741a 100644 --- a/src/KiwiBoardFirmware_menu.cpp +++ b/src/KiwiBoardFirmware_menu.cpp @@ -24,8 +24,12 @@ RENDERING_CALLBACK_NAME_INVOKE(fnVersionRtCall, textItemRenderFn, "Version", -1, TextMenuItem menuVersion(fnVersionRtCall, "1.00", 43, 10, NULL); const BooleanMenuInfo minfomotorTest = { "Motor Test", 44, 0xffff, 1, motortest, NAMING_ON_OFF }; BooleanMenuItem menumotorTest(&minfomotorTest, false, &menuVersion, INFO_LOCATION_PGM); +const AnalogMenuInfo minfoSoundLevel = { "Volume", 50, 98, 100, soundLevel, 0, 1, "%" }; +AnalogMenuItem menuSoundLevel(&minfoSoundLevel, 0, &menumotorTest, INFO_LOCATION_PGM); +const BooleanMenuInfo minfosounder = { "Sound", 49, 97, 1, soundChanged, NAMING_ON_OFF }; +BooleanMenuItem menusounder(&minfosounder, false, &menuSoundLevel, INFO_LOCATION_PGM); const BooleanMenuInfo minfoStealthChop = { "StealthChop", 45, 96, 1, stealthChopChange, NAMING_ON_OFF }; -BooleanMenuItem menuStealthChop(&minfoStealthChop, true, &menumotorTest, INFO_LOCATION_PGM); +BooleanMenuItem menuStealthChop(&minfoStealthChop, true, &menusounder, INFO_LOCATION_PGM); const AnalogMenuInfo minfoIRun = { "IRun", 33, 77, 31, iRunChanged, 0, 1, "" }; AnalogMenuItem menuIRun(&minfoIRun, 17, &menuStealthChop, INFO_LOCATION_PGM); const AnalogMenuInfo minfoGlobalScaler = { "Global Scaler", 32, 75, 255, GlobalScalerChanged, 0, 1, "" }; @@ -50,7 +54,7 @@ BackMenuItem menuBackDrySettings(&minfoDrySettings, &menudry_duration, INFO_LOCA SubMenuItem menuDrySettings(&minfoDrySettings, &menuBackDrySettings, &menuAdvanced, INFO_LOCATION_PGM); const AnalogMenuInfo minfospinAMAX = { "Accel", 40, 88, 2000, settings_changed, 500, 1, "" }; AnalogMenuItem menuspinAMAX(&minfospinAMAX, 375, NULL, INFO_LOCATION_PGM); -const AnalogMenuInfo minfospin_speed = { "Speed", 14, 14, 550, settings_changed, 50, 1, "rpm" }; +const AnalogMenuInfo minfospin_speed = { "Speed", 14, 14, 950, settings_changed, 50, 1, "rpm" }; AnalogMenuItem menuspin_speed(&minfospin_speed, 50, &menuspinAMAX, INFO_LOCATION_PGM); const AnalogMenuInfo minfospin_duration = { "Time", 13, 12, 119, settings_changed, 1, 1, "sec" }; AnalogMenuItem menuspin_duration(&minfospin_duration, 44, &menuspin_speed, INFO_LOCATION_PGM); @@ -89,10 +93,10 @@ void setupMenu() { // Now add any readonly, non-remote and visible flags. menuVersion.setReadOnly(true); menuspinAMAX.setStep(25); + menuspin_speed.setStep(5); menuwashAMAX.setStep(25); menuwash_speed.setStep(5); menudry_speed.setStep(5); - menuspin_speed.setStep(5); // Code generated by plugins. gfx.begin(); diff --git a/src/KiwiBoardFirmware_menu.h b/src/KiwiBoardFirmware_menu.h index 9ef9c43..8898ce6 100644 --- a/src/KiwiBoardFirmware_menu.h +++ b/src/KiwiBoardFirmware_menu.h @@ -31,6 +31,8 @@ extern GraphicsDeviceRenderer renderer; // Global Menu Item exports extern TextMenuItem menuVersion; extern BooleanMenuItem menumotorTest; +extern AnalogMenuItem menuSoundLevel; +extern BooleanMenuItem menusounder; extern BooleanMenuItem menuStealthChop; extern AnalogMenuItem menuIRun; extern AnalogMenuItem menuGlobalScaler; @@ -77,6 +79,8 @@ void CALLBACK_FUNCTION dry(int id); void CALLBACK_FUNCTION iRunChanged(int id); void CALLBACK_FUNCTION motortest(int id); void CALLBACK_FUNCTION settings_changed(int id); +void CALLBACK_FUNCTION soundChanged(int id); +void CALLBACK_FUNCTION soundLevel(int id); void CALLBACK_FUNCTION spin(int id); void CALLBACK_FUNCTION stealthChopChange(int id); void CALLBACK_FUNCTION wash(int id); diff --git a/src/Sounder.cpp b/src/Sounder.cpp new file mode 100644 index 0000000..3061206 --- /dev/null +++ b/src/Sounder.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023 Matthew Taylor +*/ +// + +#include "Sounder.h" +#include "picoPlatform.h" +#include "KiwiBoardFirmware_menu.h" + +BeepHandler::tone beepobj[2]; + +void BeepHandler::beep_activate(int tone, bool o_ride) { + + int beep = tone; + + if(menuSound || o_ride) { + + // Stop level menu clicks retriggering sounder before existing beep ended + if(!beepobj[beep].sounderactive) { + beepobj[beep].beep_activate = true; + Serial.println("Beep activated"); + } + } + +} + + +// Called by taskManager() +// regularly checks on any beep tasks + +void BeepHandler::exec() { + + status_update(0); // Update tone 0 (end of cycle) + status_update(1); // Update tone 1 (button press) + +}; + +void BeepHandler::status_update(int tone) { + + int current_tone = beepobj[tone].tones[beepobj[tone].curr_tone]; + + // Turn on sounder for first time + if(beepobj[tone].beep_activate && !beepobj[tone].sounderactive) { + + priv_platform->enableSounder(true); + beepobj[tone].sounderactive = true; + beepobj[tone].beep_activate = false; // We've started so we don't want to start again + beepobj[tone].start_millis = time_us_64() / 1000; + + } + + // Now to update status of sounder + + if(beepobj[tone].sounderactive && !beepobj[tone].beep_activate) { + + uint64_t time = 0; + + // is current beepobj[tone] a 'dot'? + if(current_tone == 1) + time = beepobj[tone].dot_length; + + // is current tone a 'dash'? + else if(current_tone == 2) + time = beepobj[tone].dash_length; + + // is current tone a 'space'? + else if(current_tone == 3) + time = beepobj[tone].space_length; + + // is current tone a 'longer space'? + else if(current_tone == 4) + time = beepobj[tone].space_length2; + + // has tone finished? + if(((time_us_64() / 1000) - beepobj[tone].start_millis) >= time) { + + priv_platform->enableSounder(false); + beepobj[tone].start_millis = time_us_64() / 1000; + Serial.println("SOUNDER OFF"); + + // get next tone + beepobj[tone].curr_tone++; + + // stop if this was the last one + if(current_tone == 0) { + + beepobj[tone].curr_tone = 0; + beepobj[tone].start_millis = 0; + beepobj[tone].sounderactive = false; + return; + + } + + if(beepobj[tone].tones[beepobj[tone].curr_tone] < 3 ) //space + priv_platform->enableSounder(true); + else + priv_platform->enableSounder(false); + + } + } +}; + +void BeepHandler::set_menuSound(bool x) { + + menuSound = x; + +} + +bool BeepHandler::get_menuSound() { + + return menuSound; + +} + +// BeepHandler constructor + +BeepHandler::BeepHandler(PicoPlatform *platform) { + + this->priv_platform = platform; + menuSound = false; + + // Tone for end of cycle + + beepobj[0].dot_length=110; + beepobj[0].dash_length=1000; + beepobj[0].space_length=130; + beepobj[0].space_length2=200; + beepobj[0].tones[0] = {1}; // dot + beepobj[0].tones[1] = {3}; // space + beepobj[0].tones[2] = {1}; // dot + beepobj[0].tones[3] = {4}; // space_longer + beepobj[0].tones[4] = {2}; // dash + beepobj[0].tones[5] = {0}; // END + beepobj[0].curr_tone = 0; + beepobj[0].start_millis = 0; + Serial.println("Initialized Sounder tone 0"); + beepobj[0].beep_activate = false; + beepobj[0].sounderactive = false; + + // Tone for button press + + beepobj[1].dot_length = 50; + beepobj[1].dash_length = 0; + beepobj[1].space_length = 0; + beepobj[1].space_length2 = 0; + beepobj[1].tones[0] = {1}; // dot + beepobj[1].tones[1] = {0}; // End + beepobj[1].tones[2] = {0}; // + beepobj[1].tones[3] = {0}; // + beepobj[1].tones[4] = {0}; // + beepobj[1].tones[5] = {0}; // + beepobj[1].curr_tone = 0; + beepobj[1].start_millis = 0; + Serial.println("Initialized Sounder tone 1"); + beepobj[1].beep_activate = false; + beepobj[1].sounderactive = false; + +} + +void BeepHandler::set_sndLevel(int lev) { + + // Level 100 is full duty cycle and the most quiet output (inverted sounder I/P) + // We focus the percentage range on last 30% of the duty cycle variable(betweem 70 and 100) + // as anything below 70 is at full volume anyway + sndLevel = (0.3 * lev); + sndLevel = 100 - sndLevel; + priv_platform->set_audioLevel(sndLevel); + Serial.print("Lev set to: "); + Serial.println(lev); + Serial.print("Soundlevel set to: "); + Serial.println(sndLevel); + +} \ No newline at end of file diff --git a/src/Sounder.h b/src/Sounder.h new file mode 100644 index 0000000..1701f50 --- /dev/null +++ b/src/Sounder.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Matthew Taylor +*/ +// + +#pragma once +#include +#include "picoPlatform.h" + +#define SIZE 10 + + +class BeepHandler : public Executable + +{ + + public: + + void beep_activate(int, bool); + void status_update(int); + void exec() override; + void set_menuSound(bool); + bool get_menuSound(); + void set_sndLevel(int); + + BeepHandler(PicoPlatform *platform); // constructor declaration (receives pointer to platform object) + + struct tone { + // Initialize the tone array (max 10 notes) + + int tones[SIZE] = {0}; + + // Tones are either 'dots' or 'dashes' with specific length of time in mS + + uint64_t dot_length, dash_length, space_length, space_length2; + int curr_tone; + uint64_t start_millis; + bool sounderactive; + bool finished; + bool beep_activate; + }; + + private: + + bool menuSound; + int sndLevel; + PicoPlatform *priv_platform; + +}; diff --git a/src/picoPlatform.cpp b/src/picoPlatform.cpp index 0590677..7d6ddcc 100644 --- a/src/picoPlatform.cpp +++ b/src/picoPlatform.cpp @@ -6,7 +6,11 @@ #include #include "picoPlatform.h" #include "settings.h" +#include "hardware/pwm.h" +#define FREQ 3000 + +uint slice = 0; /** * Initialize all IO on the Pico to the correct pins */ @@ -27,6 +31,25 @@ void PicoPlatform::initializePlatform() { pinMode(FAN_CTL, OUTPUT_12MA); digitalWrite(FAN_CTL, LOW); + // Sounder output + + // Initially set boolean output to stop sounder during boot + pinMode(EXPANSION1, OUTPUT_12MA); + digitalWrite(EXPANSION1, HIGH); // active LOW + + // Setup PWM funcionality + gpio_set_function(EXPANSION1, GPIO_FUNC_PWM); + uint slice=pwm_gpio_to_slice_num (EXPANSION1); + uint channel=pwm_gpio_to_channel (EXPANSION1); + + // Set parameters for frequency and duty cycle + pwm_set_freq_duty(slice, channel, FREQ, sndLevel); + pwm_set_enabled (slice, true); + + // Activate output after PWM init to stop sounder output on boot + digitalWrite(EXPANSION1, HIGH); // active LOW + + // Turn on the LED pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); @@ -52,7 +75,7 @@ void PicoPlatform::initializePlatform() { // screenshot button, active high - pinMode(EXPANSION1, INPUT_PULLDOWN); + pinMode(EXPANSION4, INPUT_PULLDOWN); EEPROM.begin(768); } @@ -109,6 +132,29 @@ bool PicoPlatform::isMotorEnabled() { return motor_enabled; } +/** + * Enable or disable the sounder output based on provided value. + * Invert the status because the sounder is enabled on logic LOW. +*/ +void PicoPlatform::enableSounder(bool activate) { + + if(!activate) { + + pinMode(EXPANSION1, OUTPUT_12MA); + digitalWrite(EXPANSION1, true); + + } + + else { + + gpio_set_function(EXPANSION1, GPIO_FUNC_PWM); + pwm_set_enabled (slice, activate); + + } + +} + + /** * This is called by TaskManager periodically. It's purpose is to perform any actions * required by the hardware platform. @@ -120,6 +166,9 @@ bool PicoPlatform::isMotorEnabled() { */ void PicoPlatform::exec() { + // update sound level if changed in menu + pwm_set_freq_duty(slice, channel, FREQ, sndLevel);// pass any saved changes to sound level + // Check heater and fan correlation.. this could probably just be a PIO if (heater_enabled && !fan_enabled) { Serial.println("Force fan on, heater on..."); @@ -191,3 +240,30 @@ void PicoPlatform::startPreheat() { preheat_end = preheat_start + (settings.preheatTime * 60000); in_preheat = true; } + +/* +* Initialize PWM for the 'slice' and 'channel' associated with the GPIO we ar using +* Set the duty cycle and frequency required +*/ + +uint32_t PicoPlatform::pwm_set_freq_duty(uint slice_num, + uint chan,uint32_t f, int d) +{ + uint32_t clock = 125000000; + uint32_t divider16 = clock / f / 4096 + + (clock % (f * 4096) != 0); + if (divider16 / 16 == 0) + divider16 = 16; + uint32_t wrap = clock * 16 / divider16 / f - 1; + pwm_set_clkdiv_int_frac(slice_num, divider16/16, + divider16 & 0xF); + pwm_set_wrap(slice_num, wrap); + pwm_set_chan_level(slice_num, chan, wrap * d / 100); + return wrap; +} + +void PicoPlatform::set_audioLevel(int lev) { + + sndLevel = lev; + +} \ No newline at end of file diff --git a/src/picoPlatform.h b/src/picoPlatform.h index 653c3f3..baf0037 100644 --- a/src/picoPlatform.h +++ b/src/picoPlatform.h @@ -65,6 +65,24 @@ class PicoPlatform : public Executable { */ void enableMotor(bool activate); + /** + * Enable or disable the sounder. This is attached to + * to the expansion header. This is active HIGH. + */ + + void enableSounder(bool activate); + + /* + * Setup PWM for Pico GPIO pins + */ + uint32_t pwm_set_freq_duty(uint slice_num, + uint chan,uint32_t f, int d); + + /* + * Set new sound level + */ + void set_audioLevel(int); + /** * Start an optional cooldown. Should be triggered by motion control when the dry cycle ends * @@ -100,6 +118,7 @@ class PicoPlatform : public Executable { bool isMotorEnabled(); bool isHeaterEnabled(); bool isFanEnabled(); + private: bool in_preheat = false; @@ -109,9 +128,14 @@ class PicoPlatform : public Executable { bool fan_enabled = false; bool led = false; // used for heartbeat. + uint slice; + uint channel; + unsigned long cooldown_start; // when did a cooldown start. unsigned long cooldown_end; // when should a cooldown stop. unsigned long preheat_start; unsigned long preheat_end; + int sndLevel; + }; \ No newline at end of file diff --git a/src/settings.h b/src/settings.h index 39fb343..8b4a3e3 100644 --- a/src/settings.h +++ b/src/settings.h @@ -25,7 +25,6 @@ struct SETTINGS int cooldownTime; int preheatTime; bool fanCooldown; - };