diff --git a/README.md b/README.md index 71ac73b7..bc6139d9 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ We also maintain a C++/CMake boilerplate with GitHub workflows configured for te * MICS6814 - Gas Sensor - https://shop.pimoroni.com/products/mics6814-gas-sensor-breakout * RGB Potentiometer - https://shop.pimoroni.com/products/rgb-potentiometer-breakout * RGB Encoder - https://shop.pimoroni.com/products/rgb-encoder-breakout +* RGB Encoder Wheel - https://shop.pimoroni.com/products/rgb-encoder-wheel-breakout * IO Expander - https://shop.pimoroni.com/products/io-expander * RV3028 - Real-Time Clock (RTC) - https://shop.pimoroni.com/products/rv3028-real-time-clock-rtc-breakout * ST7735 - 0.96" LCD - https://shop.pimoroni.com/products/0-96-spi-colour-lcd-160x80-breakout diff --git a/drivers/ioexpander/ioexpander.cpp b/drivers/ioexpander/ioexpander.cpp index 7a7450c3..4dfec84c 100644 --- a/drivers/ioexpander/ioexpander.cpp +++ b/drivers/ioexpander/ioexpander.cpp @@ -320,8 +320,21 @@ namespace pimoroni { Pin::adc(1, 7, 0)} {} - bool IOExpander::init(bool skipChipIdCheck) { - bool succeeded = true; + bool IOExpander::init(bool skipChipIdCheck, bool perform_reset) { + if(!skipChipIdCheck) { + uint16_t chip_id = get_chip_id(); + if(chip_id != CHIP_ID) { + if(debug) { + printf("Chip ID invalid: %04x expected: %04x\n", chip_id, CHIP_ID); + } + return false; + } + } + + // Reset the chip if requested, to put it into a known state + if(perform_reset && !reset()) { + return false; + } if(interrupt != PIN_UNUSED) { gpio_set_function(interrupt, GPIO_FUNC_SIO); @@ -331,17 +344,36 @@ namespace pimoroni { enable_interrupt_out(true); } - if(!skipChipIdCheck) { - uint16_t chip_id = get_chip_id(); - if(chip_id != CHIP_ID) { - if(debug) { - printf("Chip ID invalid: %04x expected: %04x\n", chip_id, CHIP_ID); - } - succeeded = false; + return true; + } + + uint8_t IOExpander::check_reset() { + uint8_t user_flash_reg = reg::USER_FLASH; + uint8_t value; + if(i2c_write_blocking(i2c->get_i2c(), address, &user_flash_reg, 1, false) == PICO_ERROR_GENERIC) { + return 0x00; + } + if(i2c_read_blocking(i2c->get_i2c(), address, (uint8_t *)&value, sizeof(uint8_t), false) == PICO_ERROR_GENERIC) { + return 0x00; + } + + return value; + } + + bool IOExpander::reset() { + uint32_t start_time = millis(); + set_bits(reg::CTRL, ctrl_mask::RESET); + // Wait for a register to read its initialised value + while(check_reset() != 0x78) { + sleep_ms(1); + if(millis() - start_time >= RESET_TIMEOUT_MS) { + if(debug) + printf("Timed out waiting for Reset!"); + return false; } } - return succeeded; + return true; } i2c_inst_t* IOExpander::get_i2c() const { @@ -370,7 +402,7 @@ namespace pimoroni { void IOExpander::set_address(uint8_t address) { set_bit(reg::CTRL, 4); - i2c->reg_write_uint8(address, reg::ADDR, address); + i2c->reg_write_uint8(this->address, reg::ADDR, address); this->address = address; sleep_ms(250); //TODO Handle addr change IOError better //wait_for_flash() @@ -491,13 +523,35 @@ namespace pimoroni { return divider_good; } - void IOExpander::set_pwm_period(uint16_t value, bool load) { + void IOExpander::set_pwm_period(uint16_t value, bool load, bool wait_for_load) { value &= 0xffff; i2c->reg_write_uint8(address, reg::PWMPL, (uint8_t)(value & 0xff)); i2c->reg_write_uint8(address, reg::PWMPH, (uint8_t)(value >> 8)); if(load) - pwm_load(); + pwm_load(wait_for_load); + } + + uint16_t IOExpander::set_pwm_frequency(float frequency, bool load, bool wait_for_load) { + uint32_t period = (uint32_t)(CLOCK_FREQ / frequency); + if (period / 128 > MAX_PERIOD) { + return MAX_PERIOD; + } + if (period < 2) { + return 2; + } + + uint8_t divider = 1; + while ((period > MAX_PERIOD) && (divider < MAX_DIVIDER)) { + period >>= 1; + divider <<= 1; + } + + period = MIN(period, MAX_PERIOD); // Should be unnecessary because of earlier raised errors, but kept in case + set_pwm_control(divider); + set_pwm_period((uint16_t)(period - 1), load, wait_for_load); + + return (uint16_t)period; } uint8_t IOExpander::get_mode(uint8_t pin) { @@ -669,7 +723,7 @@ namespace pimoroni { } } - void IOExpander::output(uint8_t pin, uint16_t value, bool load) { + void IOExpander::output(uint8_t pin, uint16_t value, bool load, bool wait_for_load) { if(pin < 1 || pin > NUM_PINS) { printf("Pin should be in range 1-14."); return; @@ -685,7 +739,7 @@ namespace pimoroni { i2c->reg_write_uint8(address, io_pin.reg_pwml, (uint8_t)(value & 0xff)); i2c->reg_write_uint8(address, io_pin.reg_pwmh, (uint8_t)(value >> 8)); if(load) - pwm_load(); + pwm_load(wait_for_load); } else { if(value == LOW) { diff --git a/drivers/ioexpander/ioexpander.hpp b/drivers/ioexpander/ioexpander.hpp index eb4809d1..c06b5adf 100644 --- a/drivers/ioexpander/ioexpander.hpp +++ b/drivers/ioexpander/ioexpander.hpp @@ -26,6 +26,8 @@ namespace pimoroni { static const uint8_t PIN_MODE_PWM = 0b00101; // PWM, Output, Push-Pull mode static const uint8_t PIN_MODE_ADC = 0b01010; // ADC, Input-only (high-impedance) + static const uint32_t RESET_TIMEOUT_MS = 1000; + public: static const uint8_t DEFAULT_I2C_ADDRESS = 0x18; @@ -45,6 +47,10 @@ namespace pimoroni { static const uint16_t LOW = 0; static const uint16_t HIGH = 1; + static const uint32_t CLOCK_FREQ = 24000000; + static const uint32_t MAX_PERIOD = (1 << 16) - 1; + static const uint32_t MAX_DIVIDER = (1 << 7); + //-------------------------------------------------- // Subclasses @@ -171,7 +177,12 @@ namespace pimoroni { // Methods //-------------------------------------------------- public: - bool init(bool skip_chip_id_check = false); + bool init(bool skip_chip_id_check = false, bool perform_reset = false); + + private: + uint8_t check_reset(); + public: + bool reset(); // For print access in micropython i2c_inst_t* get_i2c() const; @@ -198,7 +209,8 @@ namespace pimoroni { void pwm_clear(bool wait_for_clear = true); bool pwm_clearing(); bool set_pwm_control(uint8_t divider); - void set_pwm_period(uint16_t value, bool load = true); + void set_pwm_period(uint16_t value, bool load = true, bool wait_for_load = true); + uint16_t set_pwm_frequency(float frequency, bool load = true, bool wait_for_load = true); uint8_t get_mode(uint8_t pin); void set_mode(uint8_t pin, uint8_t mode, bool schmitt_trigger = false, bool invert = false); @@ -206,7 +218,7 @@ namespace pimoroni { int16_t input(uint8_t pin, uint32_t adc_timeout = 1); float input_as_voltage(uint8_t pin, uint32_t adc_timeout = 1); - void output(uint8_t pin, uint16_t value, bool load = true); + void output(uint8_t pin, uint16_t value, bool load = true, bool wait_for_load = true); void setup_rotary_encoder(uint8_t channel, uint8_t pin_a, uint8_t pin_b, uint8_t pin_c = 0, bool count_microsteps = false); int16_t read_rotary_encoder(uint8_t channel); diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 1ba0f213..557ae24b 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(breakout_dotmatrix) add_subdirectory(breakout_encoder) +add_subdirectory(breakout_encoder_wheel) add_subdirectory(breakout_ioexpander) add_subdirectory(breakout_ltr559) add_subdirectory(breakout_colourlcd160x80) diff --git a/examples/breakout_encoder_wheel/CMakeLists.txt b/examples/breakout_encoder_wheel/CMakeLists.txt new file mode 100644 index 00000000..d258674e --- /dev/null +++ b/examples/breakout_encoder_wheel/CMakeLists.txt @@ -0,0 +1,9 @@ +add_subdirectory(buttons) +add_subdirectory(chase_game) +add_subdirectory(clock) +add_subdirectory(colour_picker) +add_subdirectory(encoder) +add_subdirectory(gpio_pwm) +add_subdirectory(interrupt) +add_subdirectory(led_rainbow) +add_subdirectory(stop_watch) \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/README.md b/examples/breakout_encoder_wheel/README.md new file mode 100644 index 00000000..d8f30a85 --- /dev/null +++ b/examples/breakout_encoder_wheel/README.md @@ -0,0 +1,77 @@ +# RGB Encoder Wheel Breakout Examples (C++) + +- [Function Examples](#function-examples) + - [Buttons](#buttons) + - [Encoder](#encoder) + - [Interrupt](#interrupt) +- [LED Examples](#led-examples) + - [LED Rainbow](#led-rainbow) + - [Clock](#clock) +- [Interactive Examples](#interactive-examples) + - [Colour Picker](#colour-picker) + - [Stop Watch](#stop-watch) + - [Chase Game](#chase-game) +- [GPIO Examples](#gpio-examples) + - [GPIO PWM](#gpio-pwm) + + +## Function Examples + +### Buttons +[buttons/buttons.cpp](buttons/buttons.cpp) + +A demonstration of reading the 5 buttons on Encoder Wheel. + + +### Encoder +[encoder/encoder.cpp](encoder/encoder.cpp) + +A demonstration of reading the rotary dial of the Encoder Wheel breakout. + + +### Interrupt +[interrupt/interrupt.cpp](interrupt/interrupt.cpp) + +How to read the buttons and rotary dial of the Encoder Wheel breakout, only when an interrupt occurs. + + +## LED Examples + +### LED Rainbow +[led_rainbow/led_rainbow.cpp](led_rainbow/led_rainbow.cpp) + +Displays a rotating rainbow pattern on Encoder Wheel's LED ring. + + +### Clock +[clock/clock.cpp](clock/clock.cpp) + +Displays a 12 hour clock on Encoder Wheel's LED ring, getting time from the system. + + +## Interactive Examples + +### Colour Picker +[colour_picker/colour_picker.cpp](colour_picker/colour_picker.cpp) + +Create a colour wheel on the Encoder Wheel's LED ring, and use all functions of the wheel to interact with it. + + +### Stop Watch +[stop_watch/stop_watch.cpp](stop_watch/stop_watch.cpp) + +Display a circular stop-watch on the Encoder Wheel's LED ring. + + +### Chase Game +[chase_game/chase_game.cpp](chase_game/chase_game.cpp) + +A simple alignment game. Use Encoder Wheel's rotary dial to align the coloured band to the white goal. The closer to the goal, the greener your coloured band will be. When you reach the goal, the goal will move to a new random position. + + +## GPIO Examples + +### GPIO PWM +[gpio_pwm/gpio_pwm.cpp](gpio_pwm/gpio_pwm.cpp) + +Output a sine wave PWM sequence on the Encoder Wheel's side GPIO pins. diff --git a/examples/breakout_encoder_wheel/buttons/CMakeLists.txt b/examples/breakout_encoder_wheel/buttons/CMakeLists.txt new file mode 100644 index 00000000..b3a477e8 --- /dev/null +++ b/examples/breakout_encoder_wheel/buttons/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_buttons) +add_executable(${OUTPUT_NAME} buttons.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/buttons/buttons.cpp b/examples/breakout_encoder_wheel/buttons/buttons.cpp new file mode 100644 index 00000000..8f90edff --- /dev/null +++ b/examples/breakout_encoder_wheel/buttons/buttons.cpp @@ -0,0 +1,96 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +A demonstration of reading the 5 buttons on Encoder Wheel. +*/ + +// Constants +const std::string BUTTON_NAMES[] = {"Up", "Down", "Left", "Right", "Centre"}; + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Variables +bool last_pressed[NUM_BUTTONS] = {false, false, false, false, false}; +bool pressed[NUM_BUTTONS] = {false, false, false, false, false}; + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + // Loop forever + while(true) { + // Read all of the encoder wheel's buttons + for(int b = 0 ; b < NUM_BUTTONS; b++) { + pressed[b] = wheel.pressed(b); + if(pressed[b] != last_pressed[b]) { + printf("%s %s\n", BUTTON_NAMES[b].c_str(), pressed[b] ? "Pressed" : "Released"); + } + last_pressed[b] = pressed[b]; + } + + // Clear the LED ring + wheel.clear(); + + for(int i = 0; i < NUM_LEDS; i++) { + if(i % 6 == 3) { + wheel.set_rgb(i, 64, 64, 64); + } + } + + // If up is pressed, set the top LEDs to yellow + if(pressed[UP]) { + int mid = NUM_LEDS; + for(int i = mid - 2; i < mid + 3; i++) { + wheel.set_rgb(i % NUM_LEDS, 255, 255, 0); + } + } + + // If right is pressed, set the right LEDs to red + if(pressed[RIGHT]) { + int mid = NUM_LEDS / 4; + for(int i = mid - 2; i < mid + 3; i++) { + wheel.set_rgb(i % NUM_LEDS, 255, 0, 0); + } + } + + // If down is pressed, set the bottom LEDs to green + if(pressed[DOWN]) { + int mid = NUM_LEDS / 2; + for(int i = mid - 2; i < mid + 3; i++) { + wheel.set_rgb(i % NUM_LEDS, 0, 255, 0); + } + } + + // If left is pressed, set the left LEDs to blue + if(pressed[LEFT]) { + int mid = (NUM_LEDS * 3) / 4; + for(int i = mid - 2; i < mid + 3; i++) { + wheel.set_rgb(i % NUM_LEDS, 0, 0, 255); + } + } + + // If centre is pressed, set the diagonal LEDs to half white + if(pressed[CENTRE]) { + for(int i = 0; i < NUM_LEDS; i++) { + if(i % 6 >= 2 && i % 6 <= 4) { + wheel.set_rgb(i, 128, 128, 128); + } + } + } + wheel.show(); + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/chase_game/CMakeLists.txt b/examples/breakout_encoder_wheel/chase_game/CMakeLists.txt new file mode 100644 index 00000000..367d26ec --- /dev/null +++ b/examples/breakout_encoder_wheel/chase_game/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_chase_game) +add_executable(${OUTPUT_NAME} chase_game.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/chase_game/chase_game.cpp b/examples/breakout_encoder_wheel/chase_game/chase_game.cpp new file mode 100644 index 00000000..191ed0a1 --- /dev/null +++ b/examples/breakout_encoder_wheel/chase_game/chase_game.cpp @@ -0,0 +1,148 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +A simple alignment game. Use Encoder Wheel's rotary dial to align the coloured band +to the white goal. The closer to the goal, the greener your coloured band will be. +When you reach the goal, the goal will move to a new random position. +*/ + +// The band colour hues to show in Angle mode +constexpr float GOAL_HUE = 0.333f; +constexpr float FAR_HUE = 0.0f; + +// The width and colour settings for the band +constexpr float BAND_WIDTH = 5.0f; +constexpr float BAND_SATURATION = 1.0f; +constexpr float BAND_IN_GOAL_SATURATION = 0.5f; +constexpr float BAND_BRIGHTNESS = 1.0f; + +// The width and colour settings for the goal +// Goal should be wider than the band by a small amount +constexpr float GOAL_MARGIN = 1.0f; +constexpr float GOAL_WIDTH = BAND_WIDTH + (2.0f * GOAL_MARGIN); +constexpr float GOAL_BRIGHTNESS = 0.4f; + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Variables +float goal_position = 0.0f; +int16_t band_position = 0; + + +// Maps a value from one range to another +float map(float x, float in_min, float in_max, float out_min, float out_max) { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +// Shows a band and goal with the given widths at the positions on the strip +void colour_band(float centre_position, float width, float goal_position, float goal_width, float hue) { + if(centre_position >= 0.0f && width > 0.0f && goal_width > 0.0) { + float band_start = centre_position - (width / 2); + float band_end = centre_position + (width / 2); + float band_centre = centre_position; + + float goal_start = goal_position - (goal_width / 2); + float goal_end = goal_position + (goal_width / 2); + + // Go through each led in the strip + for(int i = 0; i < NUM_LEDS; i++) { + // Set saturation and brightness values for if the led is inside or outside of the goal + float saturation = BAND_SATURATION; + float brightness = 0.0f; + + if(i >= goal_start && i < goal_end) { + saturation = BAND_IN_GOAL_SATURATION; + brightness = GOAL_BRIGHTNESS; + } + if(goal_end >= NUM_LEDS && i + NUM_LEDS < goal_end) { + saturation = BAND_IN_GOAL_SATURATION; + brightness = GOAL_BRIGHTNESS; + } + if(goal_start < 0 && i - NUM_LEDS >= goal_start) { + saturation = BAND_IN_GOAL_SATURATION; + brightness = GOAL_BRIGHTNESS; + } + + float val = brightness; + float sat = 0.0f; + if(i >= band_start && i < band_end) { + // Inside the band + if(i < band_centre) { + // Transition into the band + val = map(i, band_centre, band_start, BAND_BRIGHTNESS, brightness); + sat = map(i, band_centre, band_start, BAND_SATURATION, saturation); + } + else { + val = map(i, band_centre, band_end, BAND_BRIGHTNESS, brightness); + sat = map(i, band_centre, band_end, BAND_SATURATION, saturation); + } + } + else if(band_end >= NUM_LEDS && i + NUM_LEDS < band_end && i < band_centre) { + val = map(i + NUM_LEDS, band_centre, band_end, BAND_BRIGHTNESS, brightness); + sat = map(i + NUM_LEDS, band_centre, band_end, BAND_SATURATION, saturation); + } + else if(band_start < 0 && i - NUM_LEDS >= band_start && i >= band_centre) { + val = map(i - NUM_LEDS, band_centre, band_start, BAND_BRIGHTNESS, brightness); + sat = map(i - NUM_LEDS, band_centre, band_start, BAND_SATURATION, saturation); + } + //else { + // Outside of the band + //} + wheel.set_hsv(i, hue, sat, val); + } + wheel.show(); + } +} + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + // Loop forever + while(true) { + band_position = wheel.step(); + + // Convert the difference between the band and goal positions into a colour hue + float diff1, diff2; + if(band_position > goal_position) { + diff1 = band_position - goal_position; + diff2 = (goal_position + NUM_LEDS) - band_position; + } + else { + diff1 = goal_position - band_position; + diff2 = (band_position + NUM_LEDS) - goal_position; + } + + float position_diff = MIN(diff1, diff2); + float hue = map(position_diff, 0, NUM_LEDS / 2.0f, GOAL_HUE, FAR_HUE); + + // Convert the band and goal positions to positions on the LED strip + float strip_band_position = map(band_position, 0, NUM_LEDS, 0.0f, (float)NUM_LEDS); + float strip_goal_position = map(goal_position, 0, NUM_LEDS, 0.0f, (float)NUM_LEDS); + + // Draw the band and goal + colour_band(strip_band_position, BAND_WIDTH, strip_goal_position, GOAL_WIDTH, hue); + + // Check if the band is within the goal, and if so, set a new goal + if(band_position >= goal_position - GOAL_MARGIN && band_position <= goal_position + GOAL_MARGIN) + goal_position = rand() % NUM_LEDS; + if(band_position >= NUM_LEDS && band_position + NUM_LEDS < goal_position + GOAL_MARGIN) + goal_position = rand() % NUM_LEDS; + if(goal_position - GOAL_MARGIN < 0 && band_position - NUM_LEDS >= goal_position + GOAL_MARGIN) + goal_position = rand() % NUM_LEDS; + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/clock/CMakeLists.txt b/examples/breakout_encoder_wheel/clock/CMakeLists.txt new file mode 100644 index 00000000..09e3d221 --- /dev/null +++ b/examples/breakout_encoder_wheel/clock/CMakeLists.txt @@ -0,0 +1,14 @@ +set(OUTPUT_NAME encoderwheel_clock) +add_executable(${OUTPUT_NAME} clock.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + hardware_rtc + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/clock/clock.cpp b/examples/breakout_encoder_wheel/clock/clock.cpp new file mode 100644 index 00000000..0b666e23 --- /dev/null +++ b/examples/breakout_encoder_wheel/clock/clock.cpp @@ -0,0 +1,114 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" +#include "hardware/rtc.h" +#include "pico/util/datetime.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +Displays a 12 hour clock on Encoder Wheel's LED ring, getting time from the system. +*/ + +// Datetime Indices +const uint HOUR = 4; +const uint MINUTE = 5; +const uint SECOND = 6; + +// Constants +constexpr float BRIGHTNESS = 1.0f; // The brightness of the LEDs +const uint UPDATES = 50; // How many times the LEDs will be updated per second +const uint UPDATE_RATE_US = 1000000 / UPDATES; + +// Handy values for the number of milliseconds +constexpr float MILLIS_PER_SECOND = 1000; +constexpr float MILLIS_PER_MINUTE = MILLIS_PER_SECOND * 60; +constexpr float MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60; +constexpr float MILLIS_PER_HALF_DAY = MILLIS_PER_HOUR * 12; + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + + +// Calculates the brightness of an LED based on its index and a position along the LED ring +int led_brightness_at(int led, float position, float half_width = 1.0f, float span = 1.0f) { + float brightness = 0.0f; + float upper = position + half_width; + float lower = position - half_width; + if(led > position) + brightness = CLAMP((upper - led) / span, 0.0f, 1.0f); + else + brightness = CLAMP((led - lower) / span, 0.0f, 1.0f); + + // Handle the LEDs being in a circle + if(upper >= NUM_LEDS) + brightness = CLAMP(((upper - NUM_LEDS) - led) / span, brightness, 1.0f); + else if(lower < 0.0f) + brightness = CLAMP((led - (lower + NUM_LEDS)) / span, brightness, 1.0f); + + return (int)(brightness * BRIGHTNESS * 255); +} + + +int main() { + stdio_init_all(); + + // Start on Thursday 4th of May 2023 14:20:00 + datetime_t now = { + .year = 2023, + .month = 05, + .day = 04, + .dotw = 4, // 0 is Sunday, so 4 is Thursday + .hour = 14, + .min = 20, + .sec = 00 + }; + + // Start the RTC + rtc_init(); + rtc_set_datetime(&now); + + // clk_sys is >2000x faster than clk_rtc, so datetime is not updated immediately when rtc_get_datetime() is called. + // tbe delay is up to 3 RTC clock cycles (which is 64us with the default clock settings) + sleep_us(64); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + // Loop forever + while(true) { + // Record the start time of this loop + absolute_time_t start_time = get_absolute_time(); + + // Get the current system time + rtc_get_datetime(&now); + + // Convert the seconds, minutes, and hours into milliseconds (this is done to give a smoother animation, particularly for the seconds hand) + uint sec_as_millis = (now.sec * MILLIS_PER_SECOND); + uint min_as_millis = (now.min * MILLIS_PER_MINUTE) + sec_as_millis; + uint hour_as_millis = ((now.hour % 12) * MILLIS_PER_HOUR) + min_as_millis; + + // Calculate the position on the LED ring that the, second, minute, and hour hands should be + float sec_pos = MIN(sec_as_millis / MILLIS_PER_MINUTE, 1.0f) * NUM_LEDS; + float min_pos = MIN(min_as_millis / MILLIS_PER_HOUR, 1.0f) * NUM_LEDS; + float hour_pos = MIN(hour_as_millis / MILLIS_PER_HALF_DAY, 1.0f) * NUM_LEDS; + + for(int i = 0; i < NUM_LEDS; i++) { + // Turn on the LEDs close to the position of the current second, minute, and hour + int r = led_brightness_at(i, sec_pos); + int g = led_brightness_at(i, min_pos); + int b = led_brightness_at(i, hour_pos); + wheel.set_rgb(i, r, g, b); + } + wheel.show(); + + // Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(delayed_by_us(start_time, UPDATE_RATE_US)); + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/colour_picker/CMakeLists.txt b/examples/breakout_encoder_wheel/colour_picker/CMakeLists.txt new file mode 100644 index 00000000..05266590 --- /dev/null +++ b/examples/breakout_encoder_wheel/colour_picker/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_colour_picker) +add_executable(${OUTPUT_NAME} colour_picker.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/colour_picker/colour_picker.cpp b/examples/breakout_encoder_wheel/colour_picker/colour_picker.cpp new file mode 100644 index 00000000..39f20028 --- /dev/null +++ b/examples/breakout_encoder_wheel/colour_picker/colour_picker.cpp @@ -0,0 +1,168 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +Create a colour wheel on the Encoder Wheel's LED ring, and use all functions of the wheel to interact with it. + +Rotate the wheel to select a Hue +Press the up direction to increase Brightness +Press the down direction to decrease Brightness +Press the left direction to decrease Saturation +Press the right direction to increase Saturation +Press the centre to hide the selection marker +*/ + +// Constants +constexpr float BRIGHTNESS_STEP = 0.02f; // How much to increase or decrease the brightness each update +constexpr float SATURATION_STEP = 0.02f; // How much to increase or decrease the saturation each update +const uint UPDATES = 50; // How many times to update the LEDs per second +const uint UPDATE_RATE_US = 1000000 / UPDATES; + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Variables +float brightness = 1.0f; +float saturation = 1.0f; +int position = 0; +bool changed = true; +bool last_centre_pressed = false; + +// Struct for storing RGB values +struct Pixel { + uint8_t r; + uint8_t g; + uint8_t b; + Pixel() : r(0), g(0), b(0) {}; + Pixel(uint8_t r, uint8_t g, uint8_t b) : r(r), g(g), b(b) {}; +}; + +// Basic function to convert Hue, Saturation and Value to an RGB colour +Pixel hsv_to_rgb(float h, float s, float v) { + if(h < 0.0f) { + h = 1.0f + fmodf(h, 1.0f); + } + + int i = int(h * 6); + float f = h * 6 - i; + + v = v * 255.0f; + + float sv = s * v; + float fsv = f * sv; + + auto p = uint8_t(-sv + v); + auto q = uint8_t(-fsv + v); + auto t = uint8_t(fsv - sv + v); + + uint8_t bv = uint8_t(v); + + switch (i % 6) { + default: + case 0: return Pixel(bv, t, p); + case 1: return Pixel(q, bv, p); + case 2: return Pixel(p, bv, t); + case 3: return Pixel(p, q, bv); + case 4: return Pixel(t, p, bv); + case 5: return Pixel(bv, p, q); + } +} + +// Simple function to clamp a value between 0.0 and 1.0 +float clamp01(float value) { + return MAX(MIN(value, 1.0f), 0.0f); +} + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + // Loop forever + while(true) { + // Record the start time of this loop + absolute_time_t start_time = get_absolute_time(); + + // If up is pressed, increase the brightness + if(wheel.pressed(UP)) { + brightness += BRIGHTNESS_STEP; + changed = true; // Trigger a change + } + + // If down is pressed, decrease the brightness + if(wheel.pressed(DOWN)) { + brightness -= BRIGHTNESS_STEP; + changed = true; // Trigger a change + } + + // If right is pressed, increase the saturation + if(wheel.pressed(RIGHT)) { + saturation += SATURATION_STEP; + changed = true; // Trigger a change + } + + // If left is pressed, decrease the saturation + if(wheel.pressed(LEFT)) { + saturation -= SATURATION_STEP; + changed = true; // Trigger a change + } + + // Limit the brightness and saturation between 0.0 and 1.0 + brightness = clamp01(brightness); + saturation = clamp01(saturation); + + // Check if the encoder has been turned + if(wheel.delta() != 0) { + // Update the position based on the count change + position = wheel.step(); + changed = true; // Trigger a change + } + + // If centre is pressed, trigger a change + bool centre_pressed = wheel.pressed(CENTRE); + if(centre_pressed != last_centre_pressed) { + changed = true; + } + last_centre_pressed = centre_pressed; + + // Was a change triggered? + if(changed) { + // Print the colour at the current hue, saturation, and brightness + Pixel pixel = hsv_to_rgb((float)position / NUM_LEDS, saturation, brightness); + printf("Colour Code = #%02x%02x%02x\n", pixel.r, pixel.g, pixel.b); + + // Set the LED at the current position to either the actual colour, + // or an inverted version to show a "selection marker" + if(centre_pressed) + wheel.set_rgb(position, pixel.r, pixel.g, pixel.b); + else + wheel.set_rgb(position, 255 - pixel.r, 255 - pixel.g, 255 - pixel.b); + + // Set the LEDs below the current position + for(int i = 0; i < position; i++) { + wheel.set_hsv(i, (float)i / NUM_LEDS, saturation, brightness); + } + + // Set the LEDs after the current position + for(int i = position + 1; i < NUM_LEDS; i++) { + wheel.set_hsv(i, (float)i / NUM_LEDS, saturation, brightness); + } + wheel.show(); + changed = false; + } + + // Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(delayed_by_us(start_time, UPDATE_RATE_US)); + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/encoder/CMakeLists.txt b/examples/breakout_encoder_wheel/encoder/CMakeLists.txt new file mode 100644 index 00000000..4da72ee5 --- /dev/null +++ b/examples/breakout_encoder_wheel/encoder/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_encoder) +add_executable(${OUTPUT_NAME} encoder.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/encoder/encoder.cpp b/examples/breakout_encoder_wheel/encoder/encoder.cpp new file mode 100644 index 00000000..a1065faf --- /dev/null +++ b/examples/breakout_encoder_wheel/encoder/encoder.cpp @@ -0,0 +1,60 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +A demonstration of reading the rotary dial of the Encoder Wheel breakout. +*/ + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Variables +int position = 0; +float hue = 0.0f; + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + + // Set the first LED + wheel.clear(); + wheel.set_hsv(position, hue, 1.0f, 1.0f); + wheel.show(); + + // Loop forever + while(true) { + // Has the dial been turned since the last time we checked? + int16_t change = wheel.delta(); + if(change != 0) { + // Print out the direction the dial was turned, and the count + if(change > 0) + printf("Clockwise, Count = %d\n", wheel.count()); + else + printf("Counter Clockwise, Count = %d\n", wheel.count()); + + // Record the new position (from 0 to 23) + position = wheel.step(); + + // Record a colour hue from 0.0 to 1.0 + hue = fmodf(wheel.revolutions(), 1.0f); + + // Set the LED at the new position to the new hue + wheel.clear(); + wheel.set_hsv(position, hue, 1.0f, 1.0f); + wheel.show(); + } + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/gpio_pwm/CMakeLists.txt b/examples/breakout_encoder_wheel/gpio_pwm/CMakeLists.txt new file mode 100644 index 00000000..2f4a07d1 --- /dev/null +++ b/examples/breakout_encoder_wheel/gpio_pwm/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_gpio_pwm) +add_executable(${OUTPUT_NAME} gpio_pwm.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/gpio_pwm/gpio_pwm.cpp b/examples/breakout_encoder_wheel/gpio_pwm/gpio_pwm.cpp new file mode 100644 index 00000000..29f77cca --- /dev/null +++ b/examples/breakout_encoder_wheel/gpio_pwm/gpio_pwm.cpp @@ -0,0 +1,74 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +Output a sine wave PWM sequence on the Encoder Wheel's side GPIO pins. + +Press the centre button to stop the program. +*/ + +// Constants +constexpr float SPEED = 5.0f; // The speed that the LEDs will cycle at +const uint UPDATES = 50; // How many times the LEDs will be updated per second +const uint UPDATE_RATE_US = 1000000 / UPDATES; +constexpr float FREQUENCY = 1000.0f; // The frequency to run the PWM at + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Variables +float offset = 0.0f; + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + + // Set the PWM frequency for the GPIOs + uint16_t period = wheel.gpio_pwm_frequency(FREQUENCY); + + // Set the GPIO pins to PWM outputs + for(int i = 0; i < NUM_GPIOS; i++) { + wheel.gpio_pin_mode(GPIOS[i], IOExpander::PIN_PWM); + } + + // Loop forever + while(!wheel.pressed(CENTRE)) { + // Record the start time of this loop + absolute_time_t start_time = get_absolute_time(); + + offset += SPEED / 1000.0f; + + // Update all the PWMs + for(int i = 0; i < NUM_GPIOS; i++) { + float angle = (((float)i / NUM_GPIOS) + offset) * M_PI; + uint16_t duty = (uint16_t)(((sinf(angle) / 2.0f) + 0.5f) * period); + + // Set the GPIO pin to the new duty cycle, but do not load it yet + wheel.gpio_pin_value(GPIOS[i], duty, false); + } + + // Have all the PWMs load at once + wheel.gpio_pwm_load(); + + // Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(delayed_by_us(start_time, UPDATE_RATE_US)); + } + + // Turn off the PWM outputs + for(int i = 0; i < NUM_GPIOS; i++) { + wheel.gpio_pin_value(GPIOS[i], 0); + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/interrupt/CMakeLists.txt b/examples/breakout_encoder_wheel/interrupt/CMakeLists.txt new file mode 100644 index 00000000..a10f054c --- /dev/null +++ b/examples/breakout_encoder_wheel/interrupt/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_interrupt) +add_executable(${OUTPUT_NAME} interrupt.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/interrupt/interrupt.cpp b/examples/breakout_encoder_wheel/interrupt/interrupt.cpp new file mode 100644 index 00000000..6d822ffd --- /dev/null +++ b/examples/breakout_encoder_wheel/interrupt/interrupt.cpp @@ -0,0 +1,84 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +How to read the buttons and rotary dial of the Encoder Wheel breakout, only when an interrupt occurs. +*/ + +// Constants +const std::string BUTTON_NAMES[] = {"Up", "Down", "Left", "Right", "Centre"}; + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c, + BreakoutEncoderWheel::DEFAULT_IOE_I2C_ADDRESS, + BreakoutEncoderWheel::DEFAULT_LED_I2C_ADDRESS, + 3); // 3 for BG_BASE, 22 for EXPLORER_BASE, or 19 for some RP2040 boards +// If wiring the breakout via the qw/st connector, use the below line instead +// BreakoutEncoderWheel wheel(&i2c); + +// Variables +bool last_pressed[NUM_BUTTONS] = {false, false, false, false, false}; +bool pressed[NUM_BUTTONS] = {false, false, false, false, false}; +int position = 0; +float hue = 0.0f; + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + + // Set the first LED + wheel.clear(); + wheel.set_hsv(position, hue, 1.0f, 1.0f); + wheel.show(); + + // Clear any left over interrupt from previous code + wheel.clear_interrupt_flag(); + + // Loop forever + while(true) { + + // Check if the interrupt has fired + if(wheel.get_interrupt_flag()) { + wheel.clear_interrupt_flag(); + + // Read all of the encoder wheel's buttons + for(int b = 0 ; b < NUM_BUTTONS; b++) { + pressed[b] = wheel.pressed(b); + if(pressed[b] != last_pressed[b]) { + printf("%s %s\n", BUTTON_NAMES[b].c_str(), pressed[b] ? "Pressed" : "Released"); + } + last_pressed[b] = pressed[b]; + } + + // The interrupt may have come from several sources, + // so check if it was a position change + int new_position = wheel.step(); + if(new_position != position) { + // Record the new position (from 0 to 23) + position = new_position; + printf("Position = %d\n", position); + + // Record a colour hue from 0.0 to 1.0 + hue = fmodf(wheel.revolutions(), 1.0f); + + // Set the LED at the new position to the new hue + wheel.clear(); + wheel.set_hsv(position, hue, 1.0f, 1.0f); + wheel.show(); + } + } + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/led_rainbow/CMakeLists.txt b/examples/breakout_encoder_wheel/led_rainbow/CMakeLists.txt new file mode 100644 index 00000000..87151a4d --- /dev/null +++ b/examples/breakout_encoder_wheel/led_rainbow/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_led_rainbow) +add_executable(${OUTPUT_NAME} led_rainbow.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/led_rainbow/led_rainbow.cpp b/examples/breakout_encoder_wheel/led_rainbow/led_rainbow.cpp new file mode 100644 index 00000000..c269c9fc --- /dev/null +++ b/examples/breakout_encoder_wheel/led_rainbow/led_rainbow.cpp @@ -0,0 +1,54 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +Displays a rotating rainbow pattern on Encoder Wheel's LED ring. +*/ + +// Constants +constexpr float SPEED = 5.0f; // The speed that the LEDs will cycle at +constexpr float BRIGHTNESS = 1.0f; // The brightness of the LEDs +const uint UPDATES = 50; // How many times the LEDs will be updated per second +const uint UPDATE_RATE_US = 1000000 / UPDATES; + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Variables +float offset = 0.0; + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + // Loop forever + while(true) { + + // Record the start time of this loop + absolute_time_t start_time = get_absolute_time(); + + offset += SPEED / 1000.0f; + + // Update all the LEDs + for(int i = 0; i < NUM_LEDS; i++) { + float hue = (float)i / NUM_LEDS; + wheel.set_hsv(i, hue + offset, 1.0, BRIGHTNESS); + } + wheel.show(); + + // Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(delayed_by_us(start_time, UPDATE_RATE_US)); + } + } + + return 0; +} \ No newline at end of file diff --git a/examples/breakout_encoder_wheel/stop_watch/CMakeLists.txt b/examples/breakout_encoder_wheel/stop_watch/CMakeLists.txt new file mode 100644 index 00000000..54eb41d1 --- /dev/null +++ b/examples/breakout_encoder_wheel/stop_watch/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OUTPUT_NAME encoderwheel_stop_watch) +add_executable(${OUTPUT_NAME} stop_watch.cpp) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} + pico_stdlib + breakout_encoder_wheel +) + +# enable usb output +pico_enable_stdio_usb(${OUTPUT_NAME} 1) + +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_encoder_wheel/stop_watch/stop_watch.cpp b/examples/breakout_encoder_wheel/stop_watch/stop_watch.cpp new file mode 100644 index 00000000..f5100898 --- /dev/null +++ b/examples/breakout_encoder_wheel/stop_watch/stop_watch.cpp @@ -0,0 +1,151 @@ +#include +#include +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" +#include "time.h" + +using namespace pimoroni; +using namespace encoderwheel; + +/* +Display a circular stop-watch on the Encoder Wheel's LED ring. + +Press the centre button to start the stopwatch, then again to pause and resume. +*/ + +// Constants +constexpr float BRIGHTNESS = 1.0f; // The brightness of the LEDs +const uint UPDATES = 50; // How many times the LEDs will be updated per second +const uint MINUTE_UPDATES = UPDATES * 60; // How many times the LEDs will be updated per minute +const uint HOUR_UPDATES = MINUTE_UPDATES * 60; // How many times the LEDs will be updated per hour +const uint UPDATE_RATE_US = 1000000 / UPDATES; + +constexpr float IDLE_PULSE_MIN = 0.2f; // The brightness (between 0.0 and 1.0) that the idle pulse will go down to +constexpr float IDLE_PULSE_MAX = 0.5f; // The brightness (between 0.0 and 1.0) that the idle pulse will go up to +constexpr float IDLE_PULSE_TIME = 2.0f; // The time (in seconds) to perform a complete idle pulse +constexpr uint UPDATES_PER_PULSE = IDLE_PULSE_TIME * UPDATES; + +// The state constants used for program flow +enum State { + IDLE = 0, + COUNTING, + PAUSED +}; + +// Create a new BreakoutEncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Variables +State state = IDLE; +uint idle_update = 0; +uint second_update = 0; +uint minute_update = 0; +uint hour_update = 0; +bool last_centre_pressed = false; + + +int main() { + stdio_init_all(); + + // Attempt to initialise the encoder wheel + if(wheel.init()) { + // Record the current time + absolute_time_t current_time = get_absolute_time(); + + // Run the update loop forever + while(true) { + + // Record the start time of this loop + absolute_time_t start_time = get_absolute_time(); + + // Read whether or not the wheen centre has been pressed + bool centre_pressed = wheel.pressed(CENTRE); + if(centre_pressed && centre_pressed != last_centre_pressed) { + switch(state) { + case IDLE: // If we're currently idle, switch to counting + second_update = 0; + minute_update = 0; + hour_update = 0; + state = COUNTING; + break; + case COUNTING: // If we're counting, switch to paused + state = PAUSED; + break; + case PAUSED: // If we're paused, switch back to counting + state = COUNTING; + } + } + last_centre_pressed = centre_pressed; + + switch(state) { + // If we're idle, perform a pulsing animation to show the stopwatch is ready to go + case IDLE: + { + float percent_along = MIN((float)idle_update / (float)UPDATES_PER_PULSE, 1.0f); + float brightness = ((cosf(percent_along * M_PI * 2.0f) + 1.0f) / 2.0f) * ((IDLE_PULSE_MAX - IDLE_PULSE_MIN)) + IDLE_PULSE_MIN; + // Update all the LEDs + for(int i = 0; i < NUM_LEDS; i++) { + wheel.set_hsv(i, 0.0, 0.0, brightness); + } + wheel.show(); + + // Advance to the next update, wrapping around to zero if at the end + idle_update += 1; + if(idle_update >= UPDATES_PER_PULSE) { + idle_update = 0; + } + } + break; + + // If we're counting, perform the stopwatch animation + case COUNTING: + { + // Calculate how many LED channels should light, as a proportion of a second, minute, and hour + float r_to_light = MIN((float)second_update / UPDATES, 1.0f) * 24.0f; + float g_to_light = MIN((float)minute_update / MINUTE_UPDATES, 1.0f) * 24.0f; + float b_to_light = MIN((float)hour_update / HOUR_UPDATES, 1.0f) * 24.0f; + + // Set each LED, such that ones below the current time are fully lit, ones after + // are off, and the one at the transition is at a percentage of the brightness + for(int i = 0; i < NUM_LEDS; i++) { + int r = (int)(CLAMP(r_to_light - i, 0.0f, 1.0f) * BRIGHTNESS * 255.0f); + int g = (int)(CLAMP(g_to_light - i, 0.0f, 1.0f) * BRIGHTNESS * 255.0f); + int b = (int)(CLAMP(b_to_light - i, 0.0f, 1.0f) * BRIGHTNESS * 255.0f); + wheel.set_rgb(i, r, g, b); + } + wheel.show(); + + // Advance the second updates count, wrapping around to zero if at the end + second_update += 1; + if(second_update >= UPDATES) { + second_update = 0; + } + + // Advance the minute updates count, wrapping around to zero if at the end + minute_update += 1; + if(minute_update >= MINUTE_UPDATES) { + minute_update = 0; + } + + // Advance the hour updates count, wrapping around to zero if at the end + hour_update += 1; + if(hour_update >= HOUR_UPDATES) { + hour_update = 0; + } + } + break; + + case PAUSED: + // Do nothing + break; + } + + // Sleep until the next update, accounting for how long the above operations took to perform + current_time = delayed_by_us(start_time, UPDATE_RATE_US); + sleep_until(current_time); + } + } + + return 0; +} \ No newline at end of file diff --git a/libraries/CMakeLists.txt b/libraries/CMakeLists.txt index 317ba021..e5bca9d3 100644 --- a/libraries/CMakeLists.txt +++ b/libraries/CMakeLists.txt @@ -2,6 +2,7 @@ add_subdirectory(hershey_fonts) add_subdirectory(bitmap_fonts) add_subdirectory(breakout_dotmatrix) add_subdirectory(breakout_encoder) +add_subdirectory(breakout_encoder_wheel) add_subdirectory(breakout_ioexpander) add_subdirectory(breakout_ltr559) add_subdirectory(breakout_rgbmatrix5x5) diff --git a/libraries/breakout_encoder_wheel/CMakeLists.txt b/libraries/breakout_encoder_wheel/CMakeLists.txt new file mode 100644 index 00000000..8373bfa4 --- /dev/null +++ b/libraries/breakout_encoder_wheel/CMakeLists.txt @@ -0,0 +1 @@ +include(breakout_encoder_wheel.cmake) diff --git a/libraries/breakout_encoder_wheel/README.md b/libraries/breakout_encoder_wheel/README.md new file mode 100644 index 00000000..f592e5e2 --- /dev/null +++ b/libraries/breakout_encoder_wheel/README.md @@ -0,0 +1,334 @@ +# RGB Encoder Wheel Breakout (C++) + +This is the C++ library reference for the [Pimoroni RGB Encoder Wheel Breakout](https://shop.pimoroni.com/products/rgb-encoder-wheel-breakout). + + +## Table of Content +- [Getting Started](#getting-started) +- [Reading the Buttons](#reading-the-buttons) +- [Reading the Encoder](#reading-the-encoder) + - [Count and Angle](#count-and-angle) + - [Count Delta](#count-delta) + - [Step and Turn](#step-and-turn) + - [Changing the Direction](#changing-the-direction) + - [Resetting to Zero](#resetting-to-zero) +- [LEDs](#leds) + - [Setting an LED](#setting-an-led) + - [RGB](#rgb) + - [HSV](#hsv) + - [Clear all LEDs](#clear-all-leds) + - [Showing](#showing) +- [GPIOs](#gpios) + - [Setup](#setup) + - [Mode](#mode) + - [As Input or ADC](#as-input-or-adc) + - [As Output](#as-output) + - [As PWM](#as-pwm) + - [Delayed Loading](#delayed-loading) + - [Limitations](#limitations) +- [Function Reference](#function-reference) +- [Constants Reference](#constants-reference) + - [Address Constants](#address-constants) + - [Value Constants](#value-constants) + - [Button Constants](#button-constants) + - [GPIO Constants](#gpio-constants) + - [Count Constants](#count-constants) + + +## Getting Started + +To start coding for your Encoder Wheel breakout, you will first need to create an object for accessing the I2C bus that the breakout is connected to. The easiest way to do this is via the `PimoroniI2C` class, with one of the handy pin constants from `pimoroni`, like so: + +```c++ +#include "pimoroni_i2c.hpp" +using namespace pimoroni; + +I2C i2c(BOARD::BREAKOUT_GARDEN); +``` + +This creates a `i2c` object that can be passed into the Encoder Wheel's class as part of its creation: +```c++ +#include "breakout_encoder_wheel.hpp" +using namespace encoderwheel; + +BreakoutEncoderWheel wheel(&i2c); +``` + +The above lines of code import the `BreakoutEncoderWheel` class and create an instance of it, called `wheel`. This will be used in the rest of the examples going forward. + + +## Reading the Buttons + +EncoderWheel has five buttons, covering up, down, left, right, and centre. These can be read using the `.pressed(button)` function, which accepts a button number between `0` and `4`. For convenience, each button can be referred to using these constants: + +* `UP` = `0` +* `DOWN` = `1` +* `LEFT` = `2` +* `RIGHT` = `3` +* `CENTRE` = `4` + +For example, to read the centre button you would write: + +```c++ +bool centre_state = wheel.pressed(CENTRE); +``` + +You can also get the number of buttons using the `NUM_BUTTONS` constant. + + +## Reading the Encoder + +Within the directional buttons of the Encoder Wheel breakout is a rotary encoder with 24 counts per revolution. + +### Count and Angle + +The current count can be read by calling `.count()`. It can also be read back as either the number of `.revolutions()` of the encoder, or the angle in `.degrees()` or `.radians()`. + +Be aware that the count is stored as an integer, if it is continually increased or decreased it will eventually wrap at `+32767` and `-32768`. This will cause a jump in the returned by `.revolutions()`, `degrees()` and `.radians()`, that will need handling by your code. + + +### Count Delta + +Often you are not interested in the exact count that the encoder is at, but rather if the count has changed since the last time you checked. This change can be read by calling `.delta()` at regular intervals. The returned value can then be used with a check in code, for the value being non-zero. + + +### Step and Turn + +Sometimes it can be useful to know the encoder's position in the form of which step it is at and how many turns have occurred. The current step can be read by calling `.step()`, which returns a value from `0` to `23`, and the number of turns can be read by calling `.turn()`. + +These functions differ from reading the `.count()` or `.revolutions()` by using separate counters, and so avoid the wrapping issue that these functions experience. + + +### Changing the Direction + +The counting direction of an encoder can be changed by calling `.direction(REVERSED_DIR)` at any time in code. The `REVERSED_DIR` constant comes from `pimoroni_common.hpp`. There is also a `NORMAL_DIR` constant, though this is the default. + + +### Resetting to Zero + +There are times where an encoder's count (and related values) need to be reset back to zero. This can be done by calling `.zero()`. + + +## LEDs + +The Encoder Wheel breakout features 24 RGB LEDs arranged in a ring around the wheel. This is the same number as there are steps on the wheel, letting you use the LEDs to show the current step of the wheel. + + +### Setting an LED + +You can set the colour of a LED on the ring in either the RGB colourspace, or HSV (Hue, Saturation, Value). HSV is useful for creating rainbow patterns. + +#### RGB + +Set the first LED - `0` - to Purple `255, 0, 255`: + +```c++ +wheel.set_rgb(0, 255, 0, 255); +``` + +#### HSV + +Set the first LED - `0` - to Red `0.0`: + +```c++ +wheel.set_hsv(0, 0.0f, 1.0f, 1.0f); +``` + + +### Clear all LEDs + +To turn off all the LEDs, the function `.clear()` can be called. This simply goes through each LED and sets its RGB colour to black, making them emit no light. + +This function is useful to have at the end of your code to turn the lights off, otherwise they will continue to show the last colours they were given. + + +### Showing + +Changes to the LEDs do not get applied immediately, due to the amount of I2C communication involved. As such, to have the LEDs show what they have been set to after calling the `.set_rgb()`, `.set_hsv()`, and `.clear()` functions, a specific call to `.show()` needs to be made. + + +## GPIOs + +There are three spare GPIO pins on the edge of Encoder Wheel. These can be used as digital outputs, pwm outputs, digital inputs, and analog inputs. + + +### Setup + +To start using a GPIO pin, one of the handy constants from the `encoderwheel` namespace can be used to reference them (see [GPIO Constants](#gpio-constants)). + +Then you need to import the constants for the pin mode to use. These are on the `IOExpander` class that Encoder Wheel is based on. + +```c++ +#import "breakout_ioexpander.hpp" +using namespace pimoroni; + +// For input +IOExpander::PIN_IN; // or PIN_IN_PU of a pull-up is wanted + +// For output +IOExpander::PIN_OUT; + +// For PWM +IOExpander::PIN_PWM; + +// For ADC +IOExpander::PIN_ADC; +``` + + +### Mode + +With the intended constants imported, the mode of a GPIO pin can be set by calling `.gpio_pin_mode(gpio, mode)`: + +```c++ +wheel.gpio_pin_mode(GP7, IOExpander::PIN_); +``` + +It is also possible to read the current mode of a GPIO pin by calling `.gpio_pin_mode(gpio)`: + +```c++ +mode = wheel.gpio_pin_mode(GP7); +``` + + +### As Input or ADC + +The current value of an GPIO pin in input or ADC mode can be read by calling `.gpio_pin_value(gpio)`: + +```c++ +value = wheel.gpio_pin_value(GP7); +``` + +If the mode is digital, the value will either be `0` or `1`. +If the mode is analog, the value will be a voltage from `0.0` to `3.3`. + + +### As Output + +The current value of a GPIO pin in output mode can be set by calling `.gpio_pin_value(gpio, value)`: + +```c++ +wheel.gpio_pin_value(GP7, value); +``` + +The expected value is either `0` or `1`, or `True` or `False`. + + +### As PWM + +The GPIO pins can also be set as PWM outputs. The `PIN_PWM` constant can be accessed from the `IOExpander` class, and passed into the `.gpio_pin_mode()` function. + +The frequency of the PWM signal can then be configured by calling `.gpio_pwm_frequency()`, which accepts a frequency (in Hz). It returns the cycle period, which should be used to set duty cycles. + +Finally, the duty cycle of the PWM signal can be set by calling `.gpio_pin_value()` and providing it with a value between `0` and the cycle period. + +Below is an example of setting a gpio pin to output a 25KHz signal with a 50% duty cycle: + +```c++ +#include "pimoroni_i2c.hpp" +#include "breakout_encoder_wheel.hpp" + +using namespace pimoroni; +using namespace encoderwheel; + +// Initialise EncoderWheel +I2C i2c(BOARD::BREAKOUT_GARDEN); +BreakoutEncoderWheel wheel(&i2c); + +// Setup the gpio pin as a PWM output +wheel.gpio_pin_mode(GP7, IOExpander::PIN_PWM); + +// Set the gpio pin's frequency to 25KHz, and record the cycle period +uint16_t period = wheel.gpio_pwm_frequency(25000.0f); + +// Output a 50% duty cycle square wave +wheel.gpio_pin_value(GP7, (int)(period * 0.5f)); +``` + + +#### Delayed Loading + +By default, changes to a gpio pin's frequency or value are applied immediately. However, sometimes this may not be wanted, and instead you want all pins to receive updated parameters at the same time, regardless of how long the code ran that calculated the update. + +For this purpose, `.gpio_pwm_frequency()` and `.gpio_pin_value()` include an optional parameter `load`, which by default is `true`. To avoid this "loading" set the parameter to `false`. Then either the last call can include a `true`, or a specific call to `.gpio_pwm_load()` can be made. + +In addition, any function that performs a load, including the `.gpio_pwm_load()` function, can be made to wait until the new PWM value has been sent out of the pins. By default this is disabled, but can be enabled by including setting the `wait_for_load` parameter to `true` in the relevant function calls. + + +#### Limitations + +All of Encoder Wheel's PWM outputs share the same timing parameters. This means that GP7, GP8, and GP9 share the same frequency. Keep this in mind if changing the frequency of one, as the others will not automatically know about the change, resulting in unexpected duty cycle outputs. + + +## Function Reference + +Here is the complete list of functions available on the `BreakoutEncoderWheel` class: +```c++ +BreakoutEncoderWheel(uint8_t ioe_address = DEFAULT_IOE_I2C_ADDRESS, uint8_t led_address = DEFAULT_LED_I2C_ADDRESS); +BreakoutEncoderWheel(I2C *i2c, uint8_t ioe_address = 0x13, uint8_t led_address = 0x77, uint interrupt = PIN_UNUSED, uint32_t timeout = 1, bool debug = false); +bool init(bool skip_chip_id_check = false); +void set_ioe_address(uint8_t address); +bool get_interrupt_flag(); +void clear_interrupt_flag(); +bool pressed(uint button); +int16_t count(); +int16_t delta(); +void zero(); +int16_t step(); +int16_t turn(); +float revolutions(); +float degrees(); +float radians(); +Direction direction(); +void direction(Direction direction); +void set_rgb(int index, int r, int g, int b); +void set_hsv(int index, float h, float s = 1.0f, float v = 1.0f); +void clear(); +void show(); +uint8_t gpio_pin_mode(uint8_t gpio); +void gpio_pin_mode(uint8_t gpio, uint8_t mode); +int16_t gpio_pin_value(uint8_t gpio); +float gpio_pin_value_as_voltage(uint8_t gpio); +void gpio_pin_value(uint8_t gpio, uint16_t value, bool load = true, bool wait_for_load = false); +void gpio_pwm_load(bool wait_for_load = true); +uint16_t gpio_pwm_frequency(float frequency, bool load = true, bool wait_for_load = false); +``` + +## Constants Reference + +Here is the complete list of public constants on the `BreakoutEncoderWheel` class: + +### Address Constants + +* `DEFAULT_IOE_I2C_ADDR` = `0x13` +* `DEFAULT_LED_I2C_ADDR` = `0x77` +* `ALTERNATE_LED_I2C_ADDR` = `0x74` + +### Value Constants + +* `DEFAULT_DIRECTION` = `NORMAL_DIR` +* `DEFAULT_TIMEOUT` = `1` + + +Here is the complete list of public constants in the `encoderwheel` namespace: + +### Button Constants + +* `UP` = `0` +* `DOWN` = `1` +* `LEFT` = `2` +* `RIGHT` = `3` +* `CENTRE` = `4` + +### GPIO Constants + +* `GP7` = `7` +* `GP8` = `8` +* `GP9` = `9` +* `GPIOS` = (`7`, `8`, `9`) + +### Count Constants + +* `NUM_LEDS` = `24` +* `NUM_BUTTONS` = `5` +* `NUM_GPIOS` = `5` diff --git a/libraries/breakout_encoder_wheel/breakout_encoder_wheel.cmake b/libraries/breakout_encoder_wheel/breakout_encoder_wheel.cmake new file mode 100644 index 00000000..73d9e8ae --- /dev/null +++ b/libraries/breakout_encoder_wheel/breakout_encoder_wheel.cmake @@ -0,0 +1,11 @@ +set(LIB_NAME breakout_encoder_wheel) +add_library(${LIB_NAME} INTERFACE) + +target_sources(${LIB_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/${LIB_NAME}.cpp +) + +target_include_directories(${LIB_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR}) + +# Pull in pico libraries that we need +target_link_libraries(${LIB_NAME} INTERFACE pico_stdlib ioexpander is31fl3731) diff --git a/libraries/breakout_encoder_wheel/breakout_encoder_wheel.cpp b/libraries/breakout_encoder_wheel/breakout_encoder_wheel.cpp new file mode 100644 index 00000000..61d6431d --- /dev/null +++ b/libraries/breakout_encoder_wheel/breakout_encoder_wheel.cpp @@ -0,0 +1,260 @@ +#include "breakout_encoder_wheel.hpp" +#include +#include + +namespace pimoroni { +namespace encoderwheel { + + bool BreakoutEncoderWheel::init(bool skip_chip_id_check) { + bool success = false; + if(ioe.init(skip_chip_id_check, true) && led_ring.init()) { + + ioe.setup_rotary_encoder(ENC_CHANNEL, ENC_TERM_A, ENC_TERM_B, 0, true); // count microsteps + + ioe.set_mode(SW_UP, IOExpander::PIN_IN_PU); + ioe.set_mode(SW_DOWN, IOExpander::PIN_IN_PU); + ioe.set_mode(SW_LEFT, IOExpander::PIN_IN_PU); + ioe.set_mode(SW_RIGHT, IOExpander::PIN_IN_PU); + ioe.set_mode(SW_CENTRE, IOExpander::PIN_IN_PU); + + ioe.set_pin_interrupt(SW_UP, true); + ioe.set_pin_interrupt(SW_DOWN, true); + ioe.set_pin_interrupt(SW_LEFT, true); + ioe.set_pin_interrupt(SW_RIGHT, true); + ioe.set_pin_interrupt(SW_CENTRE, true); + + led_ring.enable({ + 0b00000000, 0b10111111, + 0b00111110, 0b00111110, + 0b00111111, 0b10111110, + 0b00000111, 0b10000110, + 0b00110000, 0b00110000, + 0b00111111, 0b10111110, + 0b00111111, 0b10111110, + 0b01111111, 0b11111110, + 0b01111111, 0b00000000 + }, 0); + + success = true; + } + + return success; + } + + i2c_inst_t* BreakoutEncoderWheel::get_i2c() const { + return ioe.get_i2c(); + } + + int BreakoutEncoderWheel::get_ioe_address() const { + return ioe.get_address(); + } + + int BreakoutEncoderWheel::get_led_address() const { + return led_ring.get_address(); + } + + int BreakoutEncoderWheel::get_sda() const { + return ioe.get_sda(); + } + + int BreakoutEncoderWheel::get_scl() const { + return ioe.get_scl(); + } + + int BreakoutEncoderWheel::get_int() const { + return ioe.get_int(); + } + + void BreakoutEncoderWheel::set_ioe_address(uint8_t address) { + ioe.set_address(address); + } + + bool BreakoutEncoderWheel::get_interrupt_flag() { + return ioe.get_interrupt_flag(); + } + + void BreakoutEncoderWheel::clear_interrupt_flag() { + ioe.clear_interrupt_flag(); + } + + bool BreakoutEncoderWheel::pressed(uint button) { + switch(button) { + case 0: + return ioe.input(SW_UP) == 0; + case 1: + return ioe.input(SW_DOWN) == 0; + case 2: + return ioe.input(SW_LEFT) == 0; + case 3: + return ioe.input(SW_RIGHT) == 0; + case 4: + return ioe.input(SW_CENTRE) == 0; + default: + return false; + } + } + + int16_t BreakoutEncoderWheel::count() { + take_encoder_reading(); + return enc_count; + } + + int16_t BreakoutEncoderWheel::delta() { + take_encoder_reading(); + + // Determine the change in counts since the last time this function was performed + int16_t change = enc_count - last_delta_count; + last_delta_count = enc_count; + + return change; + } + + void BreakoutEncoderWheel::zero() { + ioe.clear_rotary_encoder(ENC_CHANNEL); + enc_count = 0; + enc_step = 0; + enc_turn = 0; + last_raw_count = 0; + last_delta_count = 0; + } + + int16_t BreakoutEncoderWheel::step() { + take_encoder_reading(); + return enc_step; + } + + int16_t BreakoutEncoderWheel::turn() { + take_encoder_reading(); + return enc_turn; + } + + float BreakoutEncoderWheel::revolutions() { + return (float)count() / (float)ENC_COUNTS_PER_REV; + } + + float BreakoutEncoderWheel::degrees() { + return revolutions() * 360.0f; + } + + float BreakoutEncoderWheel::radians() { + return revolutions() * M_PI * 2.0f; + } + + Direction BreakoutEncoderWheel::direction() { + return enc_direction; + } + + void BreakoutEncoderWheel::direction(Direction direction) { + enc_direction = direction; + } + + void BreakoutEncoderWheel::set_rgb(int index, int r, int g, int b) { + RGBLookup rgb = lookup_table[index]; + led_ring.set(rgb.r, r); + led_ring.set(rgb.g, g); + led_ring.set(rgb.b, b); + } + + void BreakoutEncoderWheel::set_hsv(int index, float h, float s, float v) { + int r, g, b; + if(h < 0.0f) { + h = 1.0f + fmodf(h, 1.0f); + } + + int i = int(h * 6); + float f = h * 6 - i; + + v = v * 255.0f; + + float sv = s * v; + float fsv = f * sv; + + auto p = uint8_t(-sv + v); + auto q = uint8_t(-fsv + v); + auto t = uint8_t(fsv - sv + v); + + uint8_t bv = uint8_t(v); + + switch (i % 6) { + default: + case 0: r = bv; g = t; b = p; break; + case 1: r = q; g = bv; b = p; break; + case 2: r = p; g = bv; b = t; break; + case 3: r = p; g = q; b = bv; break; + case 4: r = t; g = p; b = bv; break; + case 5: r = bv; g = p; b = q; break; + } + + set_rgb(index, r, g, b); + } + + void BreakoutEncoderWheel::clear() { + led_ring.clear(); + } + + void BreakoutEncoderWheel::show() { + led_ring.update(); + } + + uint8_t BreakoutEncoderWheel::gpio_pin_mode(uint8_t gpio) { + assert(gpio < GP7 || gpio > GP9); + return ioe.get_mode(gpio); + } + + void BreakoutEncoderWheel::gpio_pin_mode(uint8_t gpio, uint8_t mode) { + assert(gpio < GP7 || gpio > GP9); + ioe.set_mode(gpio, mode); + } + + int16_t BreakoutEncoderWheel::gpio_pin_value(uint8_t gpio) { + assert(gpio < GP7 || gpio > GP9); + return ioe.input(gpio); + } + + float BreakoutEncoderWheel::gpio_pin_value_as_voltage(uint8_t gpio) { + assert(gpio < GP7 || gpio > GP9); + return ioe.input_as_voltage(gpio); + } + + void BreakoutEncoderWheel::gpio_pin_value(uint8_t gpio, uint16_t value, bool load, bool wait_for_load) { + assert(gpio < GP7 || gpio > GP9); + ioe.output(gpio, value, load, wait_for_load); + } + + void BreakoutEncoderWheel::gpio_pwm_load(bool wait_for_load) { + ioe.pwm_load(wait_for_load); + } + + uint16_t BreakoutEncoderWheel::gpio_pwm_frequency(float frequency, bool load, bool wait_for_load) { + return ioe.set_pwm_frequency(frequency, load, wait_for_load); + } + + void BreakoutEncoderWheel::take_encoder_reading() { + // Read the current count + int16_t raw_count = ioe.read_rotary_encoder(ENC_CHANNEL) / ENC_COUNT_DIVIDER; + int16_t raw_change = raw_count - last_raw_count; + last_raw_count = raw_count; + + // Invert the change + if(enc_direction == REVERSED_DIR) { + raw_change = 0 - raw_change; + } + + enc_count += raw_change; + + enc_step += raw_change; + if(raw_change > 0) { + while(enc_step >= ENC_COUNTS_PER_REV) { + enc_step -= ENC_COUNTS_PER_REV; + enc_turn += 1; + } + } + else if(raw_change < 0) { + while(enc_step < 0) { + enc_step += ENC_COUNTS_PER_REV; + enc_turn -= 1; + } + } + } +} +} \ No newline at end of file diff --git a/libraries/breakout_encoder_wheel/breakout_encoder_wheel.hpp b/libraries/breakout_encoder_wheel/breakout_encoder_wheel.hpp new file mode 100644 index 00000000..076b6003 --- /dev/null +++ b/libraries/breakout_encoder_wheel/breakout_encoder_wheel.hpp @@ -0,0 +1,161 @@ +#pragma once + +#include "drivers/ioexpander/ioexpander.hpp" +#include "drivers/is31fl3731/is31fl3731.hpp" +#include "common/pimoroni_common.hpp" + +namespace pimoroni { +namespace encoderwheel { + static const uint8_t NUM_LEDS = 24; + static const uint8_t NUM_BUTTONS = 5; + static const uint8_t NUM_GPIOS = 3; + + static const uint8_t UP = 0; + static const uint8_t DOWN = 1; + static const uint8_t LEFT = 2; + static const uint8_t RIGHT = 3; + static const uint8_t CENTRE = 4; + + static const uint8_t GP7 = 7; + static const uint8_t GP8 = 8; + static const uint8_t GP9 = 9; + static const uint8_t GPIOS[] = {GP7, GP8, GP9}; + + class BreakoutEncoderWheel { + struct RGBLookup { + uint8_t r; + uint8_t g; + uint8_t b; + }; + + + //-------------------------------------------------- + // Constants + //-------------------------------------------------- + public: + static const uint8_t DEFAULT_IOE_I2C_ADDRESS = 0x13; + static const uint8_t DEFAULT_LED_I2C_ADDRESS = 0x77; + static const uint8_t ALTERNATE_LED_I2C_ADDRESS = 0x74; + + static const Direction DEFAULT_DIRECTION = NORMAL_DIR; + static const uint32_t DEFAULT_TIMEOUT = 1; + + private: + static const uint8_t ENC_CHANNEL = 1; + static const uint8_t ENC_TERM_A = 3; + static const uint8_t ENC_TERM_B = 12; + static const uint8_t ENC_COUNTS_PER_REV = 24; + static const uint8_t ENC_COUNT_DIVIDER = 2; + + static const uint8_t SW_UP = 13; + static const uint8_t SW_DOWN = 4; + static const uint8_t SW_LEFT = 11; + static const uint8_t SW_RIGHT = 2; + static const uint8_t SW_CENTRE = 1; + + // This wonderful lookup table maps the LEDs on the encoder wheel + // from their 3x24 (remember, they're RGB) configuration to + // their specific location in the 144 pixel buffer. + static constexpr RGBLookup lookup_table[24] = { + {128, 32, 48}, + {129, 33, 49}, + {130, 17, 50}, + {131, 18, 34}, + {132, 19, 35}, + {133, 20, 36}, + {134, 21, 37}, + {112, 80, 96}, + {113, 81, 97}, + {114, 82, 98}, + {115, 83, 99}, + {116, 84, 100}, + {117, 68, 101}, + {118, 69, 85}, + {127, 47, 63}, + {121, 41, 57}, + {122, 25, 58}, + {123, 26, 42}, + {124, 27, 43}, + {125, 28, 44}, + {126, 29, 45}, + {15, 95, 111}, + {8, 89, 105}, + {9, 90, 106}, + }; + + + //-------------------------------------------------- + // Variables + //-------------------------------------------------- + private: + IOExpander ioe; + IS31FL3731 led_ring; + + Direction enc_direction = DEFAULT_DIRECTION; + int16_t enc_count = 0; + int16_t enc_step = 0; + int16_t enc_turn = 0; + int16_t last_raw_count = 0; + int16_t last_delta_count = 0; + + + //-------------------------------------------------- + // Constructors/Destructor + //-------------------------------------------------- + public: + BreakoutEncoderWheel(uint8_t ioe_address = DEFAULT_IOE_I2C_ADDRESS, uint8_t led_address = DEFAULT_LED_I2C_ADDRESS) + : BreakoutEncoderWheel(new I2C(), ioe_address, led_address) {} + + BreakoutEncoderWheel(I2C *i2c, uint8_t ioe_address = DEFAULT_IOE_I2C_ADDRESS, uint8_t led_address = DEFAULT_LED_I2C_ADDRESS, uint interrupt = PIN_UNUSED, uint32_t timeout = DEFAULT_TIMEOUT, bool debug = false) + : ioe(i2c, ioe_address, interrupt, timeout, debug), led_ring(i2c, led_address) {} + + + //-------------------------------------------------- + // Methods + //-------------------------------------------------- + public: + bool init(bool skip_chip_id_check = false); + + // For print access in micropython + i2c_inst_t* get_i2c() const; + int get_ioe_address() const; + int get_led_address() const; + int get_sda() const; + int get_scl() const; + int get_int() const; + + // Calls through to IOExpander class + void set_ioe_address(uint8_t address); + bool get_interrupt_flag(); + void clear_interrupt_flag(); + + // Encoder breakout specific + bool pressed(uint button); + int16_t count(); + int16_t delta(); + void zero(); + int16_t step(); + int16_t turn(); + float revolutions(); + float degrees(); + float radians(); + Direction direction(); + void direction(Direction direction); + void set_rgb(int index, int r, int g, int b); + void set_hsv(int index, float h, float s = 1.0f, float v = 1.0f); + void clear(); + void show(); + + uint8_t gpio_pin_mode(uint8_t gpio); + void gpio_pin_mode(uint8_t gpio, uint8_t mode); + int16_t gpio_pin_value(uint8_t gpio); + float gpio_pin_value_as_voltage(uint8_t gpio); + void gpio_pin_value(uint8_t gpio, uint16_t value, bool load = true, bool wait_for_load = false); + void gpio_pwm_load(bool wait_for_load = true); + uint16_t gpio_pwm_frequency(float frequency, bool load = true, bool wait_for_load = false); + + private: + void take_encoder_reading(); + }; +} +} \ No newline at end of file diff --git a/micropython/examples/breakout_encoder_wheel/README.md b/micropython/examples/breakout_encoder_wheel/README.md new file mode 100644 index 00000000..7adda3f7 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/README.md @@ -0,0 +1,77 @@ +# RGB Encoder Wheel Breakout Examples (Micropython) + +- [Function Examples](#function-examples) + - [Buttons](#buttons) + - [Encoder](#encoder) + - [Interrupt](#interrupt) +- [LED Examples](#led-examples) + - [LED Rainbow](#led-rainbow) + - [Clock](#clock) +- [Interactive Examples](#interactive-examples) + - [Colour Picker](#colour-picker) + - [Stop Watch](#stop-watch) + - [Chase Game](#chase-game) +- [GPIO Examples](#gpio-examples) + - [GPIO PWM](#gpio-pwm) + + +## Function Examples + +### Buttons +[buttons.py](buttons.py) + +A demonstration of reading the 5 buttons on Encoder Wheel. + + +### Encoder +[encoder.py](encoder.py) + +A demonstration of reading the rotary dial of the Encoder Wheel breakout. + + +### Interrupt +[interrupt.py](interrupt.py) + +How to read the buttons and rotary dial of the Encoder Wheel breakout, only when an interrupt occurs. + + +## LED Examples + +### LED Rainbow +[led_rainbow.py](led_rainbow.py) + +Displays a rotating rainbow pattern on Encoder Wheel's LED ring. + + +### Clock +[clock.py](clock.py) + +Displays a 12 hour clock on Encoder Wheel's LED ring, getting time from the system. + + +## Interactive Examples + +### Colour Picker +[colour_picker.py](colour_picker.py) + +Create a colour wheel on the Encoder Wheel's LED ring, and use all functions of the wheel to interact with it. + + +### Stop Watch +[stop_watch.py](stop_watch.py) + +Display a circular stop-watch on the Encoder Wheel's LED ring. + + +### Chase Game +[chase_game.py](chase_game.py) + +A simple alignment game. Use Encoder Wheel's rotary dial to align the coloured band to the white goal. The closer to the goal, the greener your coloured band will be. When you reach the goal, the goal will move to a new random position. + + +## GPIO Examples + +### GPIO PWM +[gpio_pwm.py](gpio_pwm.py) + +Output a sine wave PWM sequence on the Encoder Wheel's side GPIO pins. diff --git a/micropython/examples/breakout_encoder_wheel/buttons.py b/micropython/examples/breakout_encoder_wheel/buttons.py new file mode 100644 index 00000000..48a63c5f --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/buttons.py @@ -0,0 +1,68 @@ +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, UP, DOWN, LEFT, RIGHT, CENTRE, NUM_BUTTONS, NUM_LEDS + +""" +A demonstration of reading the 5 buttons on Encoder Wheel. + +Press Ctrl+C to stop the program. +""" + +# Constants +BUTTON_NAMES = ["Up", "Down", "Left", "Right", "Centre"] + +# Create a new BreakoutEncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Variables +last_pressed = [False] * NUM_BUTTONS +pressed = [False] * NUM_BUTTONS + +# Loop forever +while True: + + # Read all of the encoder wheel's buttons + for b in range(NUM_BUTTONS): + pressed[b] = wheel.pressed(b) + if pressed[b] != last_pressed[b]: + print(BUTTON_NAMES[b], "Pressed" if pressed[b] else "Released") + last_pressed[b] = pressed[b] + + # Clear the LED ring + wheel.clear() + + for i in range(NUM_LEDS): + if i % 6 == 3: + wheel.set_rgb(i, 64, 64, 64) + + # If up is pressed, set the top LEDs to yellow + if pressed[UP]: + mid = NUM_LEDS + for i in range(mid - 2, mid + 3): + wheel.set_rgb(i % NUM_LEDS, 255, 255, 0) + + # If right is pressed, set the right LEDs to red + if pressed[RIGHT]: + mid = NUM_LEDS // 4 + for i in range(mid - 2, mid + 3): + wheel.set_rgb(i % NUM_LEDS, 255, 0, 0) + + # If down is pressed, set the bottom LEDs to green + if pressed[DOWN]: + mid = NUM_LEDS // 2 + for i in range(mid - 2, mid + 3): + wheel.set_rgb(i % NUM_LEDS, 0, 255, 0) + + # If left is pressed, set the left LEDs to blue + if pressed[LEFT]: + mid = (NUM_LEDS * 3) // 4 + for i in range(mid - 2, mid + 3): + wheel.set_rgb(i % NUM_LEDS, 0, 0, 255) + + # If centre is pressed, set the diagonal LEDs to half white + if pressed[CENTRE]: + for i in range(NUM_LEDS): + if i % 6 >= 2 and i % 6 <= 4: + wheel.set_rgb(i, 128, 128, 128) + wheel.show() diff --git a/micropython/examples/breakout_encoder_wheel/chase_game.py b/micropython/examples/breakout_encoder_wheel/chase_game.py new file mode 100644 index 00000000..9c0bf839 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/chase_game.py @@ -0,0 +1,122 @@ +import random +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, NUM_LEDS + +""" +A simple alignment game. Use Encoder Wheel's rotary dial to align the coloured band +to the white goal. The closer to the goal, the greener your coloured band will be. +When you reach the goal, the goal will move to a new random position. + +Press Ctrl+C to stop the program. +""" + +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# The band colour hues to show in Angle mode +GOAL_HUE = 0.333 +FAR_HUE = 0.0 + +# The width and colour settings for the band +BAND_WIDTH = 5.0 +BAND_SATURATION = 1.0 +BAND_IN_GOAL_SATURATION = 0.5 +BAND_BRIGHTNESS = 1.0 + +# The width and colour settings for the goal +# Goal should be wider than the band by a small amount +GOAL_MARGIN = 1 +GOAL_WIDTH = BAND_WIDTH + (2 * GOAL_MARGIN) +GOAL_BRIGHTNESS = 0.4 + + +# Maps a value from one range to another +def map(x, in_min, in_max, out_min, out_max): + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min + + +# Shows a band and goal with the given widths at the positions on the strip +def colour_band(centre_position, width, goal_position, goal_width, hue): + if centre_position >= 0.0 and width > 0.0 and goal_width > 0.0: + band_start = centre_position - (width / 2) + band_end = centre_position + (width / 2) + band_centre = centre_position + + goal_start = goal_position - (goal_width / 2) + goal_end = goal_position + (goal_width / 2) + + # Go through each led in the strip + for i in range(NUM_LEDS): + # Set saturation and brightness values for if the led is inside or outside of the goal + saturation = BAND_SATURATION + brightness = 0.0 + + if i >= goal_start and i < goal_end: + saturation = BAND_IN_GOAL_SATURATION + brightness = GOAL_BRIGHTNESS + if goal_end >= NUM_LEDS and i + NUM_LEDS < goal_end: + saturation = BAND_IN_GOAL_SATURATION + brightness = GOAL_BRIGHTNESS + if goal_start < 0 and i - NUM_LEDS >= goal_start: + saturation = BAND_IN_GOAL_SATURATION + brightness = GOAL_BRIGHTNESS + + if i >= band_start and i < band_end: + # Inside the band + if i < band_centre: + # Transition into the band + val = map(i, band_centre, band_start, BAND_BRIGHTNESS, brightness) + sat = map(i, band_centre, band_start, BAND_SATURATION, saturation) + else: + val = map(i, band_centre, band_end, BAND_BRIGHTNESS, brightness) + sat = map(i, band_centre, band_end, BAND_SATURATION, saturation) + wheel.set_hsv(i, hue, sat, val) + + elif band_end >= NUM_LEDS and i + NUM_LEDS < band_end and i < band_centre: + val = map(i + NUM_LEDS, band_centre, band_end, BAND_BRIGHTNESS, brightness) + sat = map(i + NUM_LEDS, band_centre, band_end, BAND_SATURATION, saturation) + wheel.set_hsv(i, hue, sat, val) + + elif band_start < 0 and i - NUM_LEDS >= band_start and i >= band_centre: + val = map(i - NUM_LEDS, band_centre, band_start, BAND_BRIGHTNESS, brightness) + sat = map(i - NUM_LEDS, band_centre, band_start, BAND_SATURATION, saturation) + wheel.set_hsv(i, hue, sat, val) + else: + # Outside of the band + wheel.set_hsv(i, hue, 0.0, brightness) + wheel.show() + + +goal_position = 0.0 +band_position = 0 + +while True: + + band_position = wheel.step() + + # Convert the difference between the band and goal positions into a colour hue + if band_position > goal_position: + diff1 = band_position - goal_position + diff2 = (goal_position + NUM_LEDS) - band_position + else: + diff1 = goal_position - band_position + diff2 = (band_position + NUM_LEDS) - goal_position + + position_diff = min(diff1, diff2) + hue = map(position_diff, 0, NUM_LEDS // 2, GOAL_HUE, FAR_HUE) + + # Convert the band and goal positions to positions on the LED strip + strip_band_position = map(band_position, 0, NUM_LEDS, 0.0, float(NUM_LEDS)) + strip_goal_position = map(goal_position, 0, NUM_LEDS, 0.0, float(NUM_LEDS)) + + # Draw the band and goal + colour_band(strip_band_position, BAND_WIDTH, strip_goal_position, GOAL_WIDTH, hue) + + # Check if the band is within the goal, and if so, set a new goal + if band_position >= goal_position - GOAL_MARGIN and band_position <= goal_position + GOAL_MARGIN: + goal_position = random.randint(0, NUM_LEDS - 1) + if band_position >= NUM_LEDS and band_position + NUM_LEDS < goal_position + GOAL_MARGIN: + goal_position = random.randint(0, NUM_LEDS - 1) + if goal_position - GOAL_MARGIN < 0 and band_position - NUM_LEDS >= goal_position + GOAL_MARGIN: + goal_position = random.randint(0, NUM_LEDS - 1) diff --git a/micropython/examples/breakout_encoder_wheel/clock.py b/micropython/examples/breakout_encoder_wheel/clock.py new file mode 100644 index 00000000..547fbac2 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/clock.py @@ -0,0 +1,97 @@ +import time +from machine import RTC +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, NUM_LEDS + +""" +Displays a 12 hour clock on Encoder Wheel's LED ring, getting time from the system. + +Press Ctrl+C to stop the program. +""" + +# Datetime Indices +HOUR = 4 +MINUTE = 5 +SECOND = 6 + +# Constants +BRIGHTNESS = 1.0 # The brightness of the LEDs +UPDATES = 50 # How many times the LEDs will be updated per second +UPDATE_RATE_US = 1000000 // UPDATES + +# Handy values for the number of milliseconds +MILLIS_PER_SECOND = 1000 +MILLIS_PER_MINUTE = MILLIS_PER_SECOND * 60 +MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60 +MILLIS_PER_HALF_DAY = MILLIS_PER_HOUR * 12 + +# Create a new BreakoutEncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Access the built-in RTC +rtc = RTC() + + +# Sleep until a specific time in the future. Use this instead of time.sleep() to correct for +# inconsistent timings when dealing with complex operations or external communication +def sleep_until(end_time): + time_to_sleep = time.ticks_diff(end_time, time.ticks_us()) + if time_to_sleep > 0: + time.sleep_us(time_to_sleep) + + +# Simple function to clamp a value between a minimum and maximum +def clamp(n, smallest, largest): + return max(smallest, min(n, largest)) + + +# Calculates the brightness of an LED based on its index and a position along the LED ring +def led_brightness_at(led, position, half_width=1, span=1): + brightness = 0.0 + upper = position + half_width + lower = position - half_width + if led > position: + brightness = clamp((upper - led) / span, 0.0, 1.0) + else: + brightness = clamp((led - lower) / span, 0.0, 1.0) + + # Handle the LEDs being in a circle + if upper >= NUM_LEDS: + brightness = clamp(((upper - NUM_LEDS) - led) / span, brightness, 1.0) + elif lower < 0.0: + brightness = clamp((led - (lower + NUM_LEDS)) / span, brightness, 1.0) + + return int(brightness * BRIGHTNESS * 255) + + +# Make rainbows +while True: + + # Record the start time of this loop + start_time = time.ticks_us() + + # Get the current system time + now = rtc.datetime() + + # Convert the seconds, minutes, and hours into milliseconds (this is done to give a smoother animation, particularly for the seconds hand) + sec_as_millis = (now[SECOND] * MILLIS_PER_SECOND) + min_as_millis = (now[MINUTE] * MILLIS_PER_MINUTE) + sec_as_millis + hour_as_millis = ((now[HOUR] % 12) * MILLIS_PER_HOUR) + min_as_millis + + # Calculate the position on the LED ring that the, second, minute, and hour hands should be + sec_pos = min(sec_as_millis / MILLIS_PER_MINUTE, 1.0) * NUM_LEDS + min_pos = min(min_as_millis / MILLIS_PER_HOUR, 1.0) * NUM_LEDS + hour_pos = min(hour_as_millis / MILLIS_PER_HALF_DAY, 1.0) * NUM_LEDS + + for i in range(NUM_LEDS): + # Turn on the LEDs close to the position of the current second, minute, and hour + r = led_brightness_at(i, sec_pos) + g = led_brightness_at(i, min_pos) + b = led_brightness_at(i, hour_pos) + wheel.set_rgb(i, r, g, b) + wheel.show() + + # Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(time.ticks_add(start_time, UPDATE_RATE_US)) diff --git a/micropython/examples/breakout_encoder_wheel/colour_picker.py b/micropython/examples/breakout_encoder_wheel/colour_picker.py new file mode 100644 index 00000000..c6e11cf3 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/colour_picker.py @@ -0,0 +1,138 @@ +import time +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, UP, DOWN, LEFT, RIGHT, CENTRE, NUM_LEDS + +""" +Create a colour wheel on the Encoder Wheel's LED ring, and use all functions of the wheel to interact with it. + +Rotate the wheel to select a Hue +Press the up direction to increase Brightness +Press the down direction to decrease Brightness +Press the left direction to decrease Saturation +Press the right direction to increase Saturation +Press the centre to hide the selection marker + +Press Ctrl+C to stop the program. +""" + +# Constants +BRIGHTNESS_STEP = 0.02 # How much to increase or decrease the brightness each update +SATURATION_STEP = 0.02 # How much to increase or decrease the saturation each update +UPDATES = 50 # How many times to update the LEDs per second +UPDATE_RATE_US = 1000000 // UPDATES + +# Create a new BreakoutEncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Variables +brightness = 1.0 +saturation = 1.0 +position = 0 +changed = True +last_centre_pressed = False + + +# From CPython Lib/colorsys.py +def hsv_to_rgb(h, s, v): + if s == 0.0: + return v, v, v + i = int(h * 6.0) + f = (h * 6.0) - i + p = v * (1.0 - s) + q = v * (1.0 - s * f) + t = v * (1.0 - s * (1.0 - f)) + i = i % 6 + if i == 0: + return v, t, p + if i == 1: + return q, v, p + if i == 2: + return p, v, t + if i == 3: + return p, q, v + if i == 4: + return t, p, v + if i == 5: + return v, p, q + + +# Simple function to clamp a value between 0.0 and 1.0 +def clamp01(value): + return max(min(value, 1.0), 0.0) + + +# Sleep until a specific time in the future. Use this instead of time.sleep() to correct for +# inconsistent timings when dealing with complex operations or external communication +def sleep_until(end_time): + time_to_sleep = time.ticks_diff(end_time, time.ticks_us()) + if time_to_sleep > 0: + time.sleep_us(time_to_sleep) + + +while True: + # Record the start time of this loop + start_time = time.ticks_us() + + # If up is pressed, increase the brightness + if wheel.pressed(UP): + brightness += BRIGHTNESS_STEP + changed = True # Trigger a change + + # If down is pressed, decrease the brightness + if wheel.pressed(DOWN): + brightness -= BRIGHTNESS_STEP + changed = True # Trigger a change + + # If right is pressed, increase the saturation + if wheel.pressed(RIGHT): + saturation += SATURATION_STEP + changed = True # Trigger a change + + # If left is pressed, decrease the saturation + if wheel.pressed(LEFT): + saturation -= SATURATION_STEP + changed = True # Trigger a change + + # Limit the brightness and saturation between 0.0 and 1.0 + brightness = clamp01(brightness) + saturation = clamp01(saturation) + + # Check if the encoder has been turned + if wheel.delta() != 0: + # Update the position based on the count change + position = wheel.step() + changed = True # Trigger a change + + # If centre is pressed, trigger a change + centre_pressed = wheel.pressed(CENTRE) + if centre_pressed != last_centre_pressed: + changed = True + last_centre_pressed = centre_pressed + + # Was a change triggered? + if changed: + # Print the colour at the current hue, saturation, and brightness + r, g, b = [int(c * 255) for c in hsv_to_rgb(position / NUM_LEDS, saturation, brightness)] + print("Colour Code = #", '{:02x}'.format(r), '{:02x}'.format(g), '{:02x}'.format(b), sep="") + + # Set the LED at the current position to either the actual colour, + # or an inverted version to show a "selection marker" + if centre_pressed: + wheel.set_rgb(position, r, g, b) + else: + wheel.set_rgb(position, 255 - r, 255 - g, 255 - b) + + # Set the LEDs below the current position + for i in range(0, position): + wheel.set_hsv(i, i / NUM_LEDS, saturation, brightness) + + # Set the LEDs after the current position + for i in range(position + 1, NUM_LEDS): + wheel.set_hsv(i, i / NUM_LEDS, saturation, brightness) + wheel.show() + changed = False + + # Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(time.ticks_add(start_time, UPDATE_RATE_US)) diff --git a/micropython/examples/breakout_encoder_wheel/encoder.py b/micropython/examples/breakout_encoder_wheel/encoder.py new file mode 100644 index 00000000..8d3783cc --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/encoder.py @@ -0,0 +1,45 @@ +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel + +""" +A demonstration of reading the rotary dial of the Encoder Wheel breakout. + +Press Ctrl+C to stop the program. +""" + +# Create a new BreakoutEncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Variables +position = 0 +hue = 0.0 + +# Set the first LED +wheel.clear() +wheel.set_hsv(position, hue, 1.0, 1.0) +wheel.show() + +# Loop forever +while True: + + # Has the dial been turned since the last time we checked? + change = wheel.delta() + if change != 0: + # Print out the direction the dial was turned, and the count + if change > 0: + print("Clockwise, Count =", wheel.count()) + else: + print("Counter Clockwise, Count =", wheel.count()) + + # Record the new position (from 0 to 23) + position = wheel.step() + + # Record a colour hue from 0.0 to 1.0 + hue = wheel.revolutions() % 1.0 + + # Set the LED at the new position to the new hue + wheel.clear() + wheel.set_hsv(position, hue, 1.0, 1.0) + wheel.show() diff --git a/micropython/examples/breakout_encoder_wheel/gpio_pwm.py b/micropython/examples/breakout_encoder_wheel/gpio_pwm.py new file mode 100644 index 00000000..832fd1c3 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/gpio_pwm.py @@ -0,0 +1,67 @@ +import math +import time +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, CENTRE, GPIOS, NUM_GPIOS +from breakout_ioexpander import PWM + +""" +Output a sine wave PWM sequence on the Encoder Wheel's side GPIO pins. + +Press the centre button or Ctrl+C to stop the program. +""" + +# Constants +SPEED = 5 # The speed that the PWM will cycle at +UPDATES = 50 # How many times to update LEDs and Servos per second +UPDATE_RATE_US = 1000000 // UPDATES +FREQUENCY = 1000 # The frequency to run the PWM at + +# Create a new BreakoutEncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Set the PWM frequency for the GPIOs +period = wheel.gpio_pwm_frequency(FREQUENCY) + +# Set the GPIO pins to PWM outputs +for g in GPIOS: + wheel.gpio_pin_mode(g, PWM) + +# Variables +offset = 0.0 + + +# Sleep until a specific time in the future. Use this instead of time.sleep() to correct for +# inconsistent timings when dealing with complex operations or external communication +def sleep_until(end_time): + time_to_sleep = time.ticks_diff(end_time, time.ticks_us()) + if time_to_sleep > 0: + time.sleep_us(time_to_sleep) + + +# Make PWM waves until the centre button is pressed +while not wheel.pressed(CENTRE): + + # Record the start time of this loop + start_time = time.ticks_us() + + offset += SPEED / 1000.0 + + # Update all the PWMs + for i in range(NUM_GPIOS): + angle = ((i / NUM_GPIOS) + offset) * math.pi + duty = int(((math.sin(angle) / 2) + 0.5) * period) + + # Set the GPIO pin to the new duty cycle, but do not load it yet + wheel.gpio_pin_value(GPIOS[i], duty, load=False) + + # Have all the PWMs load at once + wheel.gpio_pwm_load() + + # Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(time.ticks_add(start_time, UPDATE_RATE_US)) + +# Turn off the PWM outputs +for g in GPIOS: + wheel.gpio_pin_value(g, 0) diff --git a/micropython/examples/breakout_encoder_wheel/interrupt.py b/micropython/examples/breakout_encoder_wheel/interrupt.py new file mode 100644 index 00000000..f0b839a0 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/interrupt.py @@ -0,0 +1,62 @@ +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, NUM_BUTTONS + +""" +How to read the buttons and rotary dial of the Encoder Wheel breakout, only when an interrupt occurs. + +Press Ctrl+C to stop the program. +""" + +# Constants +BUTTON_NAMES = ["Up", "Down", "Left", "Right", "Centre"] + +# Create a new BreakoutEncoderWheel with a pin on the Pico specified as an interrupt +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c, interrupt=3) # 3 for BG_BASE, 22 for EXPLORER_BASE, or 19 for some RP2040 boards +# If wiring the breakout via the qw/st connector, use the below line instead +# wheel = BreakoutEncoderWheel(i2c) + +# Variables +last_pressed = [False] * NUM_BUTTONS +pressed = [False] * NUM_BUTTONS +position = 0 +hue = 0.0 + +# Set the first LED +wheel.clear() +wheel.set_hsv(position, hue, 1.0, 1.0) +wheel.show() + +# Clear any left over interrupt from previous code +wheel.clear_interrupt_flag() + +# Loop forever +while True: + + # Check if the interrupt has fired + if wheel.get_interrupt_flag(): + wheel.clear_interrupt_flag() + + # Read all of the encoder wheel's buttons + for b in range(NUM_BUTTONS): + pressed[b] = wheel.pressed(b) + if pressed[b] != last_pressed[b]: + print(BUTTON_NAMES[b], "Pressed" if pressed[b] else "Released") + last_pressed[b] = pressed[b] + + # The interrupt may have come from several sources, + # so check if it was a position change + new_position = wheel.step() + if new_position != position: + # Record the new position (from 0 to 23) + position = new_position + print("Position = ", position) + + # Record a colour hue from 0.0 to 1.0 + hue = wheel.revolutions() % 1.0 + + # Set the LED at the new position to the new hue + wheel.clear() + wheel.set_hsv(position, hue, 1.0, 1.0) + wheel.show() diff --git a/micropython/examples/breakout_encoder_wheel/led_rainbow.py b/micropython/examples/breakout_encoder_wheel/led_rainbow.py new file mode 100644 index 00000000..d6ceea69 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/led_rainbow.py @@ -0,0 +1,49 @@ +import time +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, NUM_LEDS + +""" +Displays a rotating rainbow pattern on Encoder Wheel's LED ring. + +Press Ctrl+C to stop the program. +""" + +# Constants +SPEED = 5 # The speed that the LEDs will cycle at +BRIGHTNESS = 1.0 # The brightness of the LEDs +UPDATES = 50 # How many times the LEDs will be updated per second +UPDATE_RATE_US = 1000000 // UPDATES + +# Create a new BreakoutEncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Variables +offset = 0.0 + + +# Sleep until a specific time in the future. Use this instead of time.sleep() to correct for +# inconsistent timings when dealing with complex operations or external communication +def sleep_until(end_time): + time_to_sleep = time.ticks_diff(end_time, time.ticks_us()) + if time_to_sleep > 0: + time.sleep_us(time_to_sleep) + + +# Make rainbows +while True: + + # Record the start time of this loop + start_time = time.ticks_us() + + offset += SPEED / 1000.0 + + # Update all the LEDs + for i in range(NUM_LEDS): + hue = float(i) / NUM_LEDS + wheel.set_hsv(i, hue + offset, 1.0, BRIGHTNESS) + wheel.show() + + # Sleep until the next update, accounting for how long the above operations took to perform + sleep_until(time.ticks_add(start_time, UPDATE_RATE_US)) diff --git a/micropython/examples/breakout_encoder_wheel/stop_watch.py b/micropython/examples/breakout_encoder_wheel/stop_watch.py new file mode 100644 index 00000000..19e47356 --- /dev/null +++ b/micropython/examples/breakout_encoder_wheel/stop_watch.py @@ -0,0 +1,124 @@ +import math +import time +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_encoder_wheel import BreakoutEncoderWheel, CENTRE, NUM_LEDS + +""" +Display a circular stop-watch on the Encoder Wheel's LED ring. + +Press the centre button to start the stopwatch, then again to pause and resume. + +Press Ctrl+C to stop the program. +""" + +# Constants +BRIGHTNESS = 1.0 # The brightness of the LEDs when the stopwatch is running +UPDATES = 50 # How many times the LEDs will be updated per second +MINUTE_UPDATES = UPDATES * 60 # How many times the LEDs will be updated per minute +HOUR_UPDATES = MINUTE_UPDATES * 60 # How many times the LEDs will be updated per hour +UPDATE_RATE_US = 1000000 // UPDATES + +IDLE_PULSE_MIN = 0.2 # The brightness (between 0.0 and 1.0) that the idle pulse will go down to +IDLE_PULSE_MAX = 0.5 # The brightness (between 0.0 and 1.0) that the idle pulse will go up to +IDLE_PULSE_TIME = 2 # The time (in seconds) to perform a complete idle pulse +UPDATES_PER_PULSE = IDLE_PULSE_TIME * UPDATES + +IDLE, COUNTING, PAUSED = range(3) # The state constants used for program flow + +# Create a new BreakoutEncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Variables +state = IDLE +idle_update = 0 +second_update = 0 +minute_update = 0 +hour_update = 0 +last_centre_pressed = False + + +# Simple function to clamp a value between a minimum and maximum +def clamp(n, smallest, largest): + return max(smallest, min(n, largest)) + + +# Sleep until a specific time in the future. Use this instead of time.sleep() to correct for +# inconsistent timings when dealing with complex operations or external communication +def sleep_until(end_time): + time_to_sleep = time.ticks_diff(end_time, time.ticks_us()) + if time_to_sleep > 0: + time.sleep_us(time_to_sleep) + + +# Record the current time +current_time = time.ticks_us() + +# Run the update loop forever +while True: + + # Read whether or not the wheen centre has been pressed + centre_pressed = wheel.pressed(CENTRE) + if centre_pressed and centre_pressed != last_centre_pressed: + if state == IDLE: # If we're currently idle, switch to counting + second_update = 0 + minute_update = 0 + hour_update = 0 + state = COUNTING + + elif state == COUNTING: # If we're counting, switch to paused + state = PAUSED + + elif state == PAUSED: # If we're paused, switch back to counting + state = COUNTING + last_centre_pressed = centre_pressed + + # If we're idle, perform a pulsing animation to show the stopwatch is ready to go + if state == IDLE: + percent_along = min(idle_update / UPDATES_PER_PULSE, 1.0) + brightness = ((math.cos(percent_along * math.pi * 2) + 1.0) / 2.0) * ((IDLE_PULSE_MAX - IDLE_PULSE_MIN)) + IDLE_PULSE_MIN + # Update all the LEDs + for i in range(NUM_LEDS): + wheel.set_hsv(i, 0.0, 0.0, brightness) + wheel.show() + + # Advance to the next update, wrapping around to zero if at the end + idle_update += 1 + if idle_update >= UPDATES_PER_PULSE: + idle_update = 0 + + # If we're counting, perform the stopwatch animation + elif state == COUNTING: + # Calculate how many LED channels should light, as a proportion of a second, minute, and hour + r_to_light = min(second_update / UPDATES, 1.0) * 24 + g_to_light = min(minute_update / MINUTE_UPDATES, 1.0) * 24 + b_to_light = min(hour_update / HOUR_UPDATES, 1.0) * 24 + + # Set each LED, such that ones below the current time are fully lit, ones after + # are off, and the one at the transition is at a percentage of the brightness + for i in range(NUM_LEDS): + r = int(clamp(r_to_light - i, 0.0, 1.0) * BRIGHTNESS * 255) + g = int(clamp(g_to_light - i, 0.0, 1.0) * BRIGHTNESS * 255) + b = int(clamp(b_to_light - i, 0.0, 1.0) * BRIGHTNESS * 255) + wheel.set_rgb(i, r, g, b) + wheel.show() + + # Advance the second updates count, wrapping around to zero if at the end + second_update += 1 + if second_update >= UPDATES: + second_update = 0 + + # Advance the minute updates count, wrapping around to zero if at the end + minute_update += 1 + if minute_update >= MINUTE_UPDATES: + minute_update = 0 + + # Advance the hour updates count, wrapping around to zero if at the end + hour_update += 1 + if hour_update >= HOUR_UPDATES: + hour_update = 0 + + # Sleep until the next update, accounting for how long the above operations took to perform + current_time = time.ticks_add(current_time, UPDATE_RATE_US) + sleep_until(current_time) diff --git a/micropython/modules/breakout_encoder_wheel/README.md b/micropython/modules/breakout_encoder_wheel/README.md new file mode 100644 index 00000000..365fa566 --- /dev/null +++ b/micropython/modules/breakout_encoder_wheel/README.md @@ -0,0 +1,322 @@ +# RGB Encoder Wheel Breakout (Micropython) + +This is the Micropython library reference for the [Pimoroni RGB Encoder Wheel Breakout](https://shop.pimoroni.com/products/rgb-encoder-wheel-breakout). + + +## Table of Content +- [Getting Started](#getting-started) +- [Reading the Buttons](#reading-the-buttons) +- [Reading the Encoder](#reading-the-encoder) + - [Count and Angle](#count-and-angle) + - [Count Delta](#count-delta) + - [Step and Turn](#step-and-turn) + - [Changing the Direction](#changing-the-direction) + - [Resetting to Zero](#resetting-to-zero) +- [LEDs](#leds) + - [Setting an LED](#setting-an-led) + - [RGB](#rgb) + - [HSV](#hsv) + - [Clear all LEDs](#clear-all-leds) + - [Showing](#showing) +- [GPIOs](#gpios) + - [Setup](#setup) + - [Mode](#mode) + - [As Input or ADC](#as-input-or-adc) + - [As Output](#as-output) + - [As PWM](#as-pwm) + - [Delayed Loading](#delayed-loading) + - [Limitations](#limitations) +- [Function Reference](#function-reference) +- [Constants Reference](#constants-reference) + - [Address Constants](#address-constants) + - [Button Constants](#button-constants) + - [GPIO Constants](#gpio-constants) + - [Count Constants](#count-constants) + + +## Getting Started + +To start coding for your Encoder Wheel breakout, you will first need to create an object for accessing the I2C bus that the breakout is connected to. The easiest way to do this is via the `PimoroniI2C` class, with one of the handy pin constants from `pimoroni`, like so: + +```python +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +``` + +This creates a `i2c` variable that can be passed into the Encoder Wheel's class as part of its creation: +```python +from breakout_encoder_wheel import BreakoutEncoderWheel +wheel = BreakoutEncoderWheel(i2c) +``` + +The above lines of code import the `BreakoutEncoderWheel` class and create an instance of it, called `wheel`. This will be used in the rest of the examples going forward. + + +## Reading the Buttons + +EncoderWheel has five buttons, covering up, down, left, right, and centre. These can be read using the `.pressed(button)` function, which accepts a button number between `0` and `4`. For convenience, each button can be referred to using these constants: + +* `UP` = `0` +* `DOWN` = `1` +* `LEFT` = `2` +* `RIGHT` = `3` +* `CENTRE` = `4` + +For example, to read the centre button you would write: + +```python +centre_state = wheel.pressed(CENTRE) +``` + +You can also get the number of buttons using the `NUM_BUTTONS` constant. + + +## Reading the Encoder + +Within the directional buttons of the Encoder Wheel breakout is a rotary encoder with 24 counts per revolution. + +### Count and Angle + +The current count can be read by calling `.count()`. It can also be read back as either the number of `.revolutions()` of the encoder, or the angle in `.degrees()` or `.radians()`. + +Be aware that the count is stored as an integer, if it is continually increased or decreased it will eventually wrap at `+32767` and `-32768`. This will cause a jump in the returned by `.revolutions()`, `degrees()` and `.radians()`, that will need handling by your code. + + +### Count Delta + +Often you are not interested in the exact count that the encoder is at, but rather if the count has changed since the last time you checked. This change can be read by calling `.delta()` at regular intervals. The returned value can then be used with a check in code, for the value being non-zero. + + +### Step and Turn + +Sometimes it can be useful to know the encoder's position in the form of which step it is at and how many turns have occurred. The current step can be read by calling `.step()`, which returns a value from `0` to `23`, and the number of turns can be read by calling `.turn()`. + +These functions differ from reading the `.count()` or `.revolutions()` by using separate counters, and so avoid the wrapping issue that these functions experience. + + +### Changing the Direction + +The counting direction of an encoder can be changed by calling `.direction(REVERSED_DIR)` at any time in code. The `REVERSED_DIR` constant comes from the `pimoroni` module. There is also a `NORMAL_DIR` constant, though this is the default. + + +### Resetting to Zero + +There are times where an encoder's count (and related values) need to be reset back to zero. This can be done by calling `.zero()`. + + +## LEDs + +The Encoder Wheel breakout features 24 RGB LEDs arranged in a ring around the wheel. This is the same number as there are steps on the wheel, letting you use the LEDs to show the current step of the wheel. + + +### Setting an LED + +You can set the colour of a LED on the ring in either the RGB colourspace, or HSV (Hue, Saturation, Value). HSV is useful for creating rainbow patterns. + +#### RGB + +Set the first LED - `0` - to Purple `255, 0, 255`: + +```python +wheel.set_rgb(0, 255, 0, 255) +``` + +#### HSV + +Set the first LED - `0` - to Red `0.0`: + +```python +wheel.set_hsv(0, 0.0, 1.0, 1.0) +``` + + +### Clear all LEDs + +To turn off all the LEDs, the function `.clear()` can be called. This simply goes through each LED and sets its RGB colour to black, making them emit no light. + +This function is useful to have at the end of your code to turn the lights off, otherwise they will continue to show the last colours they were given. + + +### Showing + +Changes to the LEDs do not get applied immediately, due to the amount of I2C communication involved. As such, to have the LEDs show what they have been set to after calling the `.set_rgb()`, `.set_hsv()`, and `.clear()` functions, a specific call to `.show()` needs to be made. + + +## GPIOs + +There are three spare GPIO pins on the edge of Encoder Wheel. These can be used as digital outputs, pwm outputs, digital inputs, and analog inputs. + + +### Setup + +To start using a GPIO pin, first import one of the handy constants used to reference them (see [GPIO Constants](#gpio-constants)). For example, to use the first GPIO pin: + +```python +from breakout_encoder_wheel import GP7 +``` + +Then you need to import the constants for the pin mode to use. These are on the `breakout_ioexpander` module that Encoder Wheel is based on. + +```python +# For input +from breakout_ioexpander import IN # or IN_PU of a pull-up is wanted + +# For output +from breakout_ioexpander import OUT + +# For PWM +from breakout_ioexpander import PWM + +# For ADC +from breakout_ioexpander import ADC +``` + + +### Mode + +With the intended constants imported, the mode of a GPIO pin can be set by calling `.gpio_pin_mode(gpio, mode)`: + +```python +wheel.gpio_pin_mode(GP7, ) +``` + +It is also possible to read the current mode of a GPIO pin by calling `.gpio_pin_mode(gpio)`: + +```python +mode = wheel.gpio_pin_mode(GP7) +``` + + +### As Input or ADC + +The current value of an GPIO pin in input or ADC mode can be read by calling `.gpio_pin_value(gpio)`: + +```python +value = wheel.gpio_pin_value(GP7) +``` + +If the mode is digital, the value will either be `0` or `1`. +If the mode is analog, the value will be a voltage from `0.0` to `3.3`. + + +### As Output + +The current value of a GPIO pin in output mode can be set by calling `.gpio_pin_value(gpio, value)`: + +```python +wheel.gpio_pin_value(GP7, value) +``` + +The expected value is either `0` or `1`, or `True` or `False`. + + +### As PWM + +The GPIO pins can also be set as PWM outputs. The `PWM` constant can be imported from the `breakout_ioexpander` module, and passed into the `.gpio_pin_mode()` function. + +The frequency of the PWM signal can then be configured by calling `.gpio_pwm_frequency()`, which accepts a frequency (in Hz). It returns the cycle period, which should be used to set duty cycles. + +Finally, the duty cycle of the PWM signal can be set by calling `.gpio_pin_value()` and providing it with a value between `0` and the cycle period. + +Below is an example of setting a gpio pin to output a 25KHz signal with a 50% duty cycle: + +```python +from pimoroni_i2c import PimoroniI2C +from pimoroni import BREAKOUT_GARDEN_I2C_PINS # or PICO_EXPLORER_I2C_PINS or HEADER_I2C_PINS +from breakout_ioexpander import PWM +from breakout_encoder_wheel import BreakoutEncoderWheel, GP7 + +# Initialise EncoderWheel +i2c = PimoroniI2C(**BREAKOUT_GARDEN_I2C_PINS) +wheel = BreakoutEncoderWheel(i2c) + +# Setup the gpio pin as a PWM output +wheel.gpio_pin_mode(GP7, PWM) + +# Set the gpio pin's frequency to 25KHz, and record the cycle period +period = wheel.gpio_pwm_frequency(25000) + +# Output a 50% duty cycle square wave +wheel.gpio_pin_value(GP7, int(period * 0.5)) +``` + + +#### Delayed Loading + +By default, changes to a gpio pin's frequency or value are applied immediately. However, sometimes this may not be wanted, and instead you want all pins to receive updated parameters at the same time, regardless of how long the code ran that calculated the update. + +For this purpose, `.gpio_pwm_frequency()` and `.gpio_pin_value()` include an optional parameter `load`, which by default is `True`. To avoid this "loading" include `load=False` in the relevant function calls. Then either the last call can include `load=True`, or a specific call to `.gpio_pwm_load()` can be made. + +In addition, any function that performs a load, including the `.gpio_pwm_load()` function, can be made to wait until the new PWM value has been sent out of the pins. By default this is disabled, but can be enabled by including `wait_for_load=True` in the relevant function calls. + + +#### Limitations + +All of Encoder Wheel's PWM outputs share the same timing parameters. This means that GP7, GP8, and GP9 share the same frequency. Keep this in mind if changing the frequency of one, as the others will not automatically know about the change, resulting in unexpected duty cycle outputs. + + +## Function Reference + +Here is the complete list of functions available on the `BreakoutEncoderWheel` class: +```python +BreakoutEncoderWheel(ioe_address=0x13, led_address=0x77, interrupt=PIN_UNUSED) +set_ioe_address(address) +get_interrupt_flag() +clear_interrupt_flag() +pressed(button) +count() +delta() +step() +turn() +zero() +revolutions() +degrees() +radians() +direction() +direction(direction) +set_rgb(index, r, g, b) +set_hsv(index, h, s=1.0, v=1.0) +clear() +show() +gpio_pin_mode(gpio) +gpio_pin_mode(gpio, mode) +gpio_pin_value(gpio) +gpio_pin_value(gpio, value, load=True, wait_for_load=False) +gpio_pwm_load(wait_for_load=True) +gpio_pwm_frequency(frequency, load=True, wait_for_load=True) +``` + +## Constants Reference + +Here is the complete list of constants on the `breakoutencoderwheel` module: + +### Address Constants + +* `DEFAULT_IOE_I2C_ADDR` = `0x13` +* `DEFAULT_LED_I2C_ADDR` = `0x77` +* `ALTERNATE_LED_I2C_ADDR` = `0x74` + + +### Button Constants + +* `UP` = `0` +* `DOWN` = `1` +* `LEFT` = `2` +* `RIGHT` = `3` +* `CENTRE` = `4` + + +### GPIO Constants + +* `GP7` = `7` +* `GP8` = `8` +* `GP9` = `9` +* `GPIOS` = (`7`, `8`, `9`) + + +### Count Constants + +* `NUM_LEDS` = `24` +* `NUM_BUTTONS` = `5` +* `NUM_GPIOS` = `5` diff --git a/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.c b/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.c new file mode 100644 index 00000000..06e1de3c --- /dev/null +++ b/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.c @@ -0,0 +1,129 @@ +#include "breakout_encoder_wheel.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// BreakoutEncoderWheel Class +//////////////////////////////////////////////////////////////////////////////////////////////////// + +/***** Methods *****/ +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_set_ioe_address_obj, 2, BreakoutEncoderWheel_set_ioe_address); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_get_interrupt_flag_obj, BreakoutEncoderWheel_get_interrupt_flag); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_clear_interrupt_flag_obj, BreakoutEncoderWheel_clear_interrupt_flag); + +MP_DEFINE_CONST_FUN_OBJ_2(BreakoutEncoderWheel_pressed_obj, BreakoutEncoderWheel_pressed); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_count_obj, BreakoutEncoderWheel_count); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_delta_obj, BreakoutEncoderWheel_delta); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_step_obj, BreakoutEncoderWheel_step); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_turn_obj, BreakoutEncoderWheel_turn); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_zero_obj, BreakoutEncoderWheel_zero); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_revolutions_obj, BreakoutEncoderWheel_revolutions); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_degrees_obj, BreakoutEncoderWheel_degrees); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_radians_obj, BreakoutEncoderWheel_radians); +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_direction_obj, 1, BreakoutEncoderWheel_direction); + +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_set_rgb_obj, 2, BreakoutEncoderWheel_set_rgb); +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_set_hsv_obj, 2, BreakoutEncoderWheel_set_hsv); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_clear_obj, BreakoutEncoderWheel_clear); +MP_DEFINE_CONST_FUN_OBJ_1(BreakoutEncoderWheel_show_obj, BreakoutEncoderWheel_show); + +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_gpio_pin_mode_obj, 2, BreakoutEncoderWheel_gpio_pin_mode); +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_gpio_pin_value_obj, 2, BreakoutEncoderWheel_gpio_pin_value); +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_gpio_pwm_load_obj, 1, BreakoutEncoderWheel_gpio_pwm_load); +MP_DEFINE_CONST_FUN_OBJ_KW(BreakoutEncoderWheel_gpio_pwm_frequency_obj, 2, BreakoutEncoderWheel_gpio_pwm_frequency); + +/***** Binding of Methods *****/ +STATIC const mp_rom_map_elem_t BreakoutEncoderWheel_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_set_ioe_address), MP_ROM_PTR(&BreakoutEncoderWheel_set_ioe_address_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_interrupt_flag), MP_ROM_PTR(&BreakoutEncoderWheel_get_interrupt_flag_obj) }, + { MP_ROM_QSTR(MP_QSTR_clear_interrupt_flag), MP_ROM_PTR(&BreakoutEncoderWheel_clear_interrupt_flag_obj) }, + + { MP_ROM_QSTR(MP_QSTR_pressed), MP_ROM_PTR(&BreakoutEncoderWheel_pressed_obj) }, + { MP_ROM_QSTR(MP_QSTR_count), MP_ROM_PTR(&BreakoutEncoderWheel_count_obj) }, + { MP_ROM_QSTR(MP_QSTR_delta), MP_ROM_PTR(&BreakoutEncoderWheel_delta_obj) }, + { MP_ROM_QSTR(MP_QSTR_step), MP_ROM_PTR(&BreakoutEncoderWheel_step_obj) }, + { MP_ROM_QSTR(MP_QSTR_turn), MP_ROM_PTR(&BreakoutEncoderWheel_turn_obj) }, + { MP_ROM_QSTR(MP_QSTR_zero), MP_ROM_PTR(&BreakoutEncoderWheel_zero_obj) }, + { MP_ROM_QSTR(MP_QSTR_revolutions), MP_ROM_PTR(&BreakoutEncoderWheel_revolutions_obj) }, + { MP_ROM_QSTR(MP_QSTR_degrees), MP_ROM_PTR(&BreakoutEncoderWheel_degrees_obj) }, + { MP_ROM_QSTR(MP_QSTR_radians), MP_ROM_PTR(&BreakoutEncoderWheel_radians_obj) }, + { MP_ROM_QSTR(MP_QSTR_direction), MP_ROM_PTR(&BreakoutEncoderWheel_direction_obj) }, + + { MP_ROM_QSTR(MP_QSTR_set_rgb), MP_ROM_PTR(&BreakoutEncoderWheel_set_rgb_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_hsv), MP_ROM_PTR(&BreakoutEncoderWheel_set_hsv_obj) }, + { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&BreakoutEncoderWheel_clear_obj) }, + { MP_ROM_QSTR(MP_QSTR_show), MP_ROM_PTR(&BreakoutEncoderWheel_show_obj) }, + + { MP_ROM_QSTR(MP_QSTR_gpio_pin_mode), MP_ROM_PTR(&BreakoutEncoderWheel_gpio_pin_mode_obj) }, + { MP_ROM_QSTR(MP_QSTR_gpio_pin_value), MP_ROM_PTR(&BreakoutEncoderWheel_gpio_pin_value_obj) }, + { MP_ROM_QSTR(MP_QSTR_gpio_pwm_load), MP_ROM_PTR(&BreakoutEncoderWheel_gpio_pwm_load_obj) }, + { MP_ROM_QSTR(MP_QSTR_gpio_pwm_frequency), MP_ROM_PTR(&BreakoutEncoderWheel_gpio_pwm_frequency_obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(BreakoutEncoderWheel_locals_dict, BreakoutEncoderWheel_locals_dict_table); + +/***** Class Definition *****/ +#ifdef MP_DEFINE_CONST_OBJ_TYPE +MP_DEFINE_CONST_OBJ_TYPE( + breakout_encoder_wheel_BreakoutEncoderWheel_type, + MP_QSTR_BreakoutEncoderWheel, + MP_TYPE_FLAG_NONE, + make_new, BreakoutEncoderWheel_make_new, + locals_dict, (mp_obj_dict_t*)&BreakoutEncoderWheel_locals_dict +); +#else +const mp_obj_type_t breakout_encoder_wheel_BreakoutEncoderWheel_type = { + { &mp_type_type }, + .name = MP_QSTR_BreakoutEncoderWheel, + .make_new = BreakoutEncoderWheel_make_new, + .locals_dict = (mp_obj_dict_t*)&BreakoutEncoderWheel_locals_dict, +}; +#endif + + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// breakout_encoder_wheel Module +//////////////////////////////////////////////////////////////////////////////////////////////////// + +/***** Module Constants *****/ +const mp_rom_obj_tuple_t breakout_encoder_wheel_gpio_pins = { + {&mp_type_tuple}, 3, { MP_ROM_INT(7), MP_ROM_INT(8), MP_ROM_INT(9) }, +}; + +/***** Globals Table *****/ +STATIC const mp_rom_map_elem_t breakout_encoder_wheel_globals_table[] = { + { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_breakout_encoder_wheel) }, + { MP_OBJ_NEW_QSTR(MP_QSTR_BreakoutEncoderWheel), (mp_obj_t)&breakout_encoder_wheel_BreakoutEncoderWheel_type }, + + { MP_ROM_QSTR(MP_QSTR_DEFAULT_IOE_I2C_ADDR), MP_ROM_INT(0x13) }, + { MP_ROM_QSTR(MP_QSTR_DEFAULT_LED_I2C_ADDR), MP_ROM_INT(0x77) }, + { MP_ROM_QSTR(MP_QSTR_ALTERNATE_LED_I2C_ADDR), MP_ROM_INT(0x74) }, + + { MP_ROM_QSTR(MP_QSTR_NUM_LEDS), MP_ROM_INT(24) }, + { MP_ROM_QSTR(MP_QSTR_NUM_BUTTONS), MP_ROM_INT(5) }, + { MP_ROM_QSTR(MP_QSTR_NUM_GPIOS), MP_ROM_INT(3) }, + + { MP_ROM_QSTR(MP_QSTR_UP), MP_ROM_INT(0) }, + { MP_ROM_QSTR(MP_QSTR_DOWN), MP_ROM_INT(1) }, + { MP_ROM_QSTR(MP_QSTR_LEFT), MP_ROM_INT(2) }, + { MP_ROM_QSTR(MP_QSTR_RIGHT), MP_ROM_INT(3) }, + { MP_ROM_QSTR(MP_QSTR_CENTRE), MP_ROM_INT(4) }, + + { MP_ROM_QSTR(MP_QSTR_GP7), MP_ROM_INT(7) }, + { MP_ROM_QSTR(MP_QSTR_GP8), MP_ROM_INT(8) }, + { MP_ROM_QSTR(MP_QSTR_GP9), MP_ROM_INT(9) }, + { MP_ROM_QSTR(MP_QSTR_GPIOS), MP_ROM_PTR(&breakout_encoder_wheel_gpio_pins) }, +}; +STATIC MP_DEFINE_CONST_DICT(mp_module_breakout_encoder_wheel_globals, breakout_encoder_wheel_globals_table); + +/***** Module Definition *****/ +const mp_obj_module_t breakout_encoder_wheel_user_cmodule = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t*)&mp_module_breakout_encoder_wheel_globals, +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +#if MICROPY_VERSION <= 70144 +MP_REGISTER_MODULE(MP_QSTR_breakout_encoder_wheel, breakout_encoder_wheel_user_cmodule, MODULE_BREAKOUT_ENCODER_WHEEL_ENABLED); +#else +MP_REGISTER_MODULE(MP_QSTR_breakout_encoder_wheel, breakout_encoder_wheel_user_cmodule); +#endif +//////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.cpp b/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.cpp new file mode 100644 index 00000000..2765a06e --- /dev/null +++ b/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.cpp @@ -0,0 +1,361 @@ +#include "libraries/breakout_encoder_wheel/breakout_encoder_wheel.hpp" +#include "micropython/modules/util.hpp" +#include + + +using namespace pimoroni; +using namespace encoderwheel; + +extern "C" { +#include "breakout_encoder_wheel.h" +#include "pimoroni_i2c.h" + +/***** Variables Struct *****/ +typedef struct _breakout_encoder_wheel_BreakoutEncoderWheel_obj_t { + mp_obj_base_t base; + BreakoutEncoderWheel *breakout; + _PimoroniI2C_obj_t *i2c; +} breakout_encoder_wheel_BreakoutEncoderWheel_obj_t; + + +/***** Constructor *****/ +mp_obj_t BreakoutEncoderWheel_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = nullptr; + + enum { ARG_i2c, ARG_ioe_address, ARG_led_address, ARG_interrupt }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_i2c, MP_ARG_OBJ, {.u_obj = nullptr} }, + { MP_QSTR_ioe_address, MP_ARG_INT, {.u_int = BreakoutEncoderWheel::DEFAULT_IOE_I2C_ADDRESS} }, + { MP_QSTR_led_address, MP_ARG_INT, {.u_int = BreakoutEncoderWheel::DEFAULT_LED_I2C_ADDRESS} }, + { MP_QSTR_interrupt, MP_ARG_INT, {.u_int = PIN_UNUSED} }, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + self = m_new_obj(breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + self->base.type = &breakout_encoder_wheel_BreakoutEncoderWheel_type; + + self->i2c = PimoroniI2C_from_machine_i2c_or_native(args[ARG_i2c].u_obj); + + self->breakout = m_new_class(BreakoutEncoderWheel, (pimoroni::I2C *)(self->i2c->i2c), args[ARG_ioe_address].u_int, args[ARG_led_address].u_int, args[ARG_interrupt].u_int); + + if(!self->breakout->init()) { + mp_raise_msg(&mp_type_RuntimeError, "BreakoutEncoderWheel: breakout not found when initialising"); + } + + return MP_OBJ_FROM_PTR(self); +} + +/***** Methods *****/ +mp_obj_t BreakoutEncoderWheel_set_ioe_address(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_address }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_address, MP_ARG_REQUIRED | MP_ARG_INT }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + + self->breakout->set_ioe_address(args[ARG_address].u_int); + + return mp_const_none; +} + +mp_obj_t BreakoutEncoderWheel_get_interrupt_flag(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_bool(self->breakout->get_interrupt_flag()); +} + +mp_obj_t BreakoutEncoderWheel_clear_interrupt_flag(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + self->breakout->clear_interrupt_flag(); + + return mp_const_none; +} + +extern mp_obj_t BreakoutEncoderWheel_pressed(mp_obj_t self_in, mp_obj_t button_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + int button = mp_obj_get_int(button_in); + + if(button < 0 || button >= 5) { + mp_raise_ValueError("button out of range. Expected 0 to 4"); + } + + return mp_obj_new_bool(self->breakout->pressed(button)); +} + +extern mp_obj_t BreakoutEncoderWheel_count(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_int(self->breakout->count()); +} + +extern mp_obj_t BreakoutEncoderWheel_delta(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_int(self->breakout->delta()); +} + +extern mp_obj_t BreakoutEncoderWheel_step(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_int(self->breakout->step()); +} + +extern mp_obj_t BreakoutEncoderWheel_turn(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_int(self->breakout->turn()); +} + +extern mp_obj_t BreakoutEncoderWheel_zero(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + self->breakout->zero(); + + return mp_const_none; +} + +extern mp_obj_t BreakoutEncoderWheel_revolutions(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_float(self->breakout->revolutions()); +} + +extern mp_obj_t BreakoutEncoderWheel_degrees(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_float(self->breakout->degrees()); +} + +extern mp_obj_t BreakoutEncoderWheel_radians(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + return mp_obj_new_float(self->breakout->radians()); +} + +extern mp_obj_t BreakoutEncoderWheel_direction(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_direction }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_direction, MP_ARG_OBJ, { .u_obj = mp_const_none }}, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + + if(n_args <= 1) { + return mp_obj_new_int(self->breakout->direction()); + } + else { + int direction = mp_obj_get_int(args[ARG_direction].u_obj); + if(direction < 0 || direction > 1) { + mp_raise_ValueError("direction out of range. Expected NORMAL_DIR (0) or REVERSED_DIR (1)"); + } + self->breakout->direction((Direction)direction); + return mp_const_none; + } +} + +extern mp_obj_t BreakoutEncoderWheel_set_rgb(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_index, ARG_r, ARG_g, ARG_b }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_index, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_r, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_g, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_b, MP_ARG_REQUIRED | MP_ARG_INT }, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + + int index = args[ARG_index].u_int; + int r = args[ARG_r].u_int; + int g = args[ARG_g].u_int; + int b = args[ARG_b].u_int; + + if(index < 0 || index >= 24) + mp_raise_ValueError("index out of range. Expected 0 to 23"); + else if(r < 0 || r > 255) + mp_raise_ValueError("r out of range. Expected 0 to 255"); + else if(g < 0 || g > 255) + mp_raise_ValueError("g out of range. Expected 0 to 255"); + else if(b < 0 || b > 255) + mp_raise_ValueError("b out of range. Expected 0 to 255"); + else + self->breakout->set_rgb(index, r, g, b); + + return mp_const_none; +} + +extern mp_obj_t BreakoutEncoderWheel_set_hsv(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_index, ARG_h, ARG_s, ARG_v }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_index, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_h, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_s, MP_ARG_OBJ, { .u_obj = mp_const_none }}, + { MP_QSTR_v, MP_ARG_OBJ, { .u_obj = mp_const_none }}, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + + int index = args[ARG_index].u_int; + float h = mp_obj_get_float(args[ARG_h].u_obj); + + float s = 1.0f; + if (args[ARG_s].u_obj != mp_const_none) { + s = mp_obj_get_float(args[ARG_s].u_obj); + } + + float v = 1.0f; + if (args[ARG_v].u_obj != mp_const_none) { + v = mp_obj_get_float(args[ARG_v].u_obj); + } + + if(index < 0 || index >= 24) + mp_raise_ValueError("index out of range. Expected 0 to 23"); + else + self->breakout->set_hsv(index, h, s, v); + + return mp_const_none; +} + +extern mp_obj_t BreakoutEncoderWheel_clear(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + self->breakout->clear(); + + return mp_const_none; +} + +extern mp_obj_t BreakoutEncoderWheel_show(mp_obj_t self_in) { + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(self_in, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + self->breakout->show(); + + return mp_const_none; +} + +extern mp_obj_t BreakoutEncoderWheel_gpio_pin_mode(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { +enum { ARG_self, ARG_gpio, ARG_mode }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_gpio, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_mode, MP_ARG_OBJ, { .u_obj = mp_const_none }}, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + int gpio = args[ARG_gpio].u_int; + if(gpio < 7 || gpio > 9) { + mp_raise_ValueError("gpio out of range. Expected GP7 (7), GP8 (8), or GP9 (9)"); + } + + if(args[ARG_mode].u_obj == mp_const_none) { + return mp_obj_new_int(self->breakout->gpio_pin_mode(gpio)); + } + else { + int mode = mp_obj_get_int(args[ARG_mode].u_obj); + self->breakout->gpio_pin_mode(gpio, mode); + + return mp_const_none; + } +} + +extern mp_obj_t BreakoutEncoderWheel_gpio_pin_value(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_gpio, ARG_value, ARG_load, ARG_wait_for_load }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_gpio, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_value, MP_ARG_OBJ, { .u_obj = mp_const_none }}, + { MP_QSTR_load, MP_ARG_BOOL, { .u_bool = true }}, + { MP_QSTR_wait_for_load, MP_ARG_BOOL, { .u_bool = false }}, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + int gpio = args[ARG_gpio].u_int; + if(gpio < 7 || gpio > 9) { + mp_raise_ValueError("gpio out of range. Expected GP7 (7), GP8 (8), or GP9 (9)"); + } + + if(args[ARG_value].u_obj == mp_const_none) { + if(self->breakout->gpio_pin_mode(gpio) == IOExpander::PIN_ADC) { + return mp_obj_new_float(self->breakout->gpio_pin_value_as_voltage(gpio)); + } + else { + return mp_obj_new_int(self->breakout->gpio_pin_value(gpio)); + } + } + else { + int value = mp_obj_get_int(args[ARG_value].u_obj); + bool load = args[ARG_load].u_bool; + bool wait_for_load = args[ARG_wait_for_load].u_bool; + self->breakout->gpio_pin_value(gpio, value, load, wait_for_load); + + return mp_const_none; + } +} + +extern mp_obj_t BreakoutEncoderWheel_gpio_pwm_load(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_wait_for_load }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_wait_for_load, MP_ARG_BOOL, { .u_bool = true }}, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + + bool wait_for_load = args[ARG_wait_for_load].u_bool; + self->breakout->gpio_pwm_load(wait_for_load); + + return mp_const_none; +} + +extern mp_obj_t BreakoutEncoderWheel_gpio_pwm_frequency(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_self, ARG_frequency, ARG_load, ARG_wait_for_load }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_frequency, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_load, MP_ARG_BOOL, { .u_bool = true }}, + { MP_QSTR_wait_for_load, MP_ARG_BOOL, { .u_bool = true }}, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + breakout_encoder_wheel_BreakoutEncoderWheel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, breakout_encoder_wheel_BreakoutEncoderWheel_obj_t); + float frequency = mp_obj_get_float(args[ARG_frequency].u_obj); + uint32_t period = (uint32_t)(IOExpander::CLOCK_FREQ / frequency); + if (period / 128 > IOExpander::MAX_PERIOD) { + mp_raise_ValueError("The provided frequency is too low"); + } + if (period < 2) { + mp_raise_ValueError("The provided frequency is too high"); + } + + bool load = args[ARG_load].u_bool; + bool wait_for_load = args[ARG_wait_for_load].u_bool; + period = self->breakout->gpio_pwm_frequency(frequency, load, wait_for_load); + + return mp_obj_new_int(period); +} +} \ No newline at end of file diff --git a/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.h b/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.h new file mode 100644 index 00000000..069e5126 --- /dev/null +++ b/micropython/modules/breakout_encoder_wheel/breakout_encoder_wheel.h @@ -0,0 +1,32 @@ +// Include MicroPython API. +#include "py/runtime.h" + +/***** Extern of Class Definition *****/ +extern const mp_obj_type_t breakout_encoder_wheel_BreakoutEncoderWheel_type; + +/***** Extern of Class Methods *****/ +extern mp_obj_t BreakoutEncoderWheel_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args); +extern mp_obj_t BreakoutEncoderWheel_set_ioe_address(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t BreakoutEncoderWheel_get_interrupt_flag(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_clear_interrupt_flag(mp_obj_t self_in); + +extern mp_obj_t BreakoutEncoderWheel_pressed(mp_obj_t self_in, mp_obj_t button_in); +extern mp_obj_t BreakoutEncoderWheel_count(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_delta(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_step(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_turn(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_zero(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_revolutions(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_degrees(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_radians(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_direction(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); + +extern mp_obj_t BreakoutEncoderWheel_set_rgb(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t BreakoutEncoderWheel_set_hsv(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t BreakoutEncoderWheel_clear(mp_obj_t self_in); +extern mp_obj_t BreakoutEncoderWheel_show(mp_obj_t self_in); + +extern mp_obj_t BreakoutEncoderWheel_gpio_pin_mode(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t BreakoutEncoderWheel_gpio_pin_value(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t BreakoutEncoderWheel_gpio_pwm_load(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); +extern mp_obj_t BreakoutEncoderWheel_gpio_pwm_frequency(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); diff --git a/micropython/modules/breakout_encoder_wheel/micropython.cmake b/micropython/modules/breakout_encoder_wheel/micropython.cmake new file mode 100644 index 00000000..de763c17 --- /dev/null +++ b/micropython/modules/breakout_encoder_wheel/micropython.cmake @@ -0,0 +1,21 @@ +set(MOD_NAME breakout_encoder_wheel) +string(TOUPPER ${MOD_NAME} MOD_NAME_UPPER) +add_library(usermod_${MOD_NAME} INTERFACE) + +target_sources(usermod_${MOD_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c + ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../../libraries/${MOD_NAME}/${MOD_NAME}.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../../drivers/ioexpander/ioexpander.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../../drivers/is31fl3731/is31fl3731.cpp +) + +target_include_directories(usermod_${MOD_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR} +) + +target_compile_definitions(usermod_${MOD_NAME} INTERFACE + -DMODULE_${MOD_NAME_UPPER}_ENABLED=1 +) + +target_link_libraries(usermod INTERFACE usermod_${MOD_NAME}) \ No newline at end of file diff --git a/micropython/modules/breakout_ioexpander/breakout_ioexpander.c b/micropython/modules/breakout_ioexpander/breakout_ioexpander.c index eece8446..f43032fe 100644 --- a/micropython/modules/breakout_ioexpander/breakout_ioexpander.c +++ b/micropython/modules/breakout_ioexpander/breakout_ioexpander.c @@ -88,6 +88,11 @@ const mp_obj_type_t breakout_ioexpander_BreakoutIOExpander_type = { STATIC const mp_map_elem_t breakout_ioexpander_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_breakout_ioexpander) }, { MP_OBJ_NEW_QSTR(MP_QSTR_BreakoutIOExpander), (mp_obj_t)&breakout_ioexpander_BreakoutIOExpander_type }, + { MP_ROM_QSTR(MP_QSTR_IN), MP_ROM_INT(IOE_PIN_IN) }, + { MP_ROM_QSTR(MP_QSTR_IN_PU), MP_ROM_INT(IOE_PIN_IN_PU) }, + { MP_ROM_QSTR(MP_QSTR_OUT), MP_ROM_INT(IOE_PIN_OUT) }, + { MP_ROM_QSTR(MP_QSTR_PWM), MP_ROM_INT(IOE_PIN_PWM) }, + { MP_ROM_QSTR(MP_QSTR_ADC), MP_ROM_INT(IOE_PIN_ADC) }, }; STATIC MP_DEFINE_CONST_DICT(mp_module_breakout_ioexpander_globals, breakout_ioexpander_globals_table); diff --git a/micropython/modules/micropython-common-breakouts.cmake b/micropython/modules/micropython-common-breakouts.cmake index 17958fa4..f18c3fb0 100644 --- a/micropython/modules/micropython-common-breakouts.cmake +++ b/micropython/modules/micropython-common-breakouts.cmake @@ -1,5 +1,6 @@ include(breakout_dotmatrix/micropython) include(breakout_encoder/micropython) +include(breakout_encoder_wheel/micropython) include(breakout_ioexpander/micropython) include(breakout_ltr559/micropython) include(breakout_as7262/micropython)