Skip to content

Non blocking smart led #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 20, 2025
Merged

Conversation

robamu
Copy link
Collaborator

@robamu robamu commented Oct 12, 2024

No description provided.

@tschundler
Copy link
Contributor

I finally got around to some testing tonight. It only seems to only work with 2 of 6 ws28xx variants.

On my esp32, I measure about 25us between pixels and about 55us total.

From some research like https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/amp/ and the various datasheets, depending on the chip, the last time might be ~6us, ~50us, or ~250us

on the other 4, only one LED lights. So if this is accepted, it needs to be accepted with the caveat that it depends heavily on which LED are used Incidentally among those not working are what I'm pretty sure are genuine WorldSemi LEDs.

But... exciting news, I am able to drive two sets of the type that work in parallel using a join!
PXL_20241017_072807003 NIGHT RAW-01 COVER
(don't mind the bad grounding causing ghost signals)

But... here, the time between pixels is closer to 34us

I should go back to exploring double buffering in RMT async writes. Supposedly there's an ability now to force code into RAM to run faster?

@tschundler
Copy link
Contributor

one more very odd thing

with a buffer size of 65, I can only send 62 pixels of data. Otherwise I get BufferSizeExceeded. To send 64, I need to make a 67 pixel buffer.

#[main]
async fn main(spawner: Spawner) {
    let peripherals = esp_hal::init(esp_hal::Config::default());
    esp_println::logger::init_logger_from_env();

    esp_println::println!("Init!");

    let timg0 = TimerGroup::new(peripherals.TIMG0);
    esp_hal_embassy::init(timg0.timer0);

    let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);

    let led_pin = io.pins.gpio18;
    let l2_pin = io.pins.gpio23;

    let rmt = Rmt::new_async(peripherals.RMT, 80.MHz()).unwrap();

    let rmt_buffer = smartLedBuffer!(67);
    let rmt2_buffer = smartLedBuffer!(67);

    let mut l1 = asynch::SmartLedAdapterAsync::new(rmt.channel0, led_pin, rmt_buffer);
    let mut l2 = asynch::SmartLedAdapterAsync::new(rmt.channel1, l2_pin, rmt2_buffer);

    esp_println::println!("Start!");

    let mut ofs = 0;
    loop {
        let data = {
            let mut i = 0;
            [(); 64].map(|_| {
                i += 1;
                hsv2rgb(Hsv {
                    hue: (((i * 4) + ofs + 60) % 255) as u8,
                    sat: 240,
                    val: 10,
                })
            })
        };

        let f1 = l1.write(data[0..64].iter().cloned());
        let f2 = l2.write(data[0..64].iter().cloned());
        //let (o1, o2) = embassy_futures::join::join(f1, f2).await;
        let o1 = f1.await;
        let o2 = f2.await;
        o1.unwrap_or_else(|e| {
            esp_println::println!("Error1: {:?}", e);
        });
        o2.unwrap_or_else(|e| {
            esp_println::println!("Error2: {:?}", e);
        });

        ofs = (ofs + 1) % 255;
        esp_println::print!(".");
        Timer::after(Duration::from_millis(10)).await;
    }
}

changing the above to SmartLedBuffer!(64); will return errors when running.

@@ -14,18 +14,18 @@ targets = ["riscv32imac-unknown-none-elf"]
[dependencies]
defmt = { version = "0.3.8", optional = true }
document-features = "0.2.10"
esp-hal = "0.20.0"
esp-hal = ">=0.20.0, <=0.21.0"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why to exclude esp-hal 0.21.1?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, no. I just wonder whether <=0.21 or <=0.21.* would be cleaner/better (or even does the job)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh I see! It seems a bit misleading before a good Samaritan points you to the SemVer docs 😉

Copy link

@lulingar lulingar Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh it seems something is missing, this bit me:

error: failed to select a version for `esp-hal`.
    ... required by package `esp-hal-smartled v0.13.0`
    ... which satisfies dependency `esp-hal-smartled = "^0.13.0"` of package `test-xtensa-nostd v0.1.0 (/home/being/code/practice/rust/test-xtensa-nostd)`
versions that meet the requirements `^0.20.0` are: 0.20.1

the package `esp-hal` links to the native library `esp-hal`, but it conflicts with a previous package which links to `esp-hal` as well:
package `esp-hal v0.21.1`
    ... which satisfies dependency `esp-hal = "^0.21.1"` of package 

My relevant lines on Cargo.toml are:

esp-hal = { version = "0.21.1", features = ["esp32s3"] }
esp-hal-smartled = { version = "0.13.0", features = ["esp32s3"] }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just fixed the dependency specifier to also allow higher versions.. Can you try 0.14? I also bumped the minor version of the crate itself.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worked fine! I had to set in Cargo.toml:

esp-hal-smartled = { git = "https://github.com/robamu/esp-hal-community.git", rev = "3562306", features = ["esp32s3"] }

@robamu
Copy link
Collaborator Author

robamu commented Oct 17, 2024

On the second problem: the async API requires a different and slightly larger buffer size because each transfer must be 0 terminated. I have not created an async macro to declare that buffer yet, but i provided a const function which calculates the required buffer size.

It might be a reasonable idea to also add an async example?

@horho77
Copy link

horho77 commented Oct 23, 2024

It might be a reasonable idea to also add an async example?

async example sounds like a great idea 👍

@robamu robamu force-pushed the non-blocking-smart-led branch 2 times, most recently from 355dd33 to 3562306 Compare October 29, 2024 12:43
@robamu
Copy link
Collaborator Author

robamu commented Oct 29, 2024

I can try to add an example which is similar to the existing one, except it uses the async API.

@lulingar
Copy link

lulingar commented Oct 30, 2024

I can try to add an example which is similar to the existing one, except it uses the async API.

Here's mine:

#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]

use core::{ptr::addr_of_mut, str::{self, FromStr}};

use alloc::string::String;

extern crate alloc;
use esp_alloc as _;

use esp_backtrace as _;
use esp_hal::{
    cpu_control::{CpuControl, Stack as CPUStack},
    gpio::{AnyPin, Input, Io, Level, Output, Pull},
    prelude::*,
    rmt::Rmt,
    timer::{
        timg::TimerGroup, AnyTimer
    },
    Async,
};

use esp_hal_embassy::Executor;
use embassy_executor::Spawner;
use embassy_time::{Delay, Duration, Instant, Timer};

use esp_println::println;

use esp_hal_smartled::{
    smart_led_buffer,
    asynch::SmartLedAdapterAsync,
};
use smart_leds::{
    brightness, gamma,
    hsv::{hsv2rgb, Hsv},
};


#[main]
async fn main(spawner: Spawner) -> ! {
    esp_println::logger::init_logger_from_env();

    let peripherals = esp_hal::init({
        let mut config = esp_hal::Config::default();
        config.cpu_clock = CpuClock::Clock240MHz;
        config
    });

    let timg0 = TimerGroup::new(peripherals.TIMG0);
    let timer1: AnyTimer = timg0.timer1.into();
    esp_hal_embassy::init(timer1);

    let mut cpu_control = CpuControl::new(peripherals.CPU_CTRL);
    let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);

    let led_pin = io.pins.gpio48;
    let freq = 80.MHz();
    let rmt = Rmt::new_async(peripherals.RMT, freq).unwrap();
    let rmt_buffer = smart_led_buffer!(1);
    let led = SmartLedAdapterAsync::new(rmt.channel0, led_pin, rmt_buffer);

    let mut color = Hsv {
        hue: 0,
        sat: 255,
        val: 255,
    };
    let mut data;

    loop {
        for hue in 0..=255 {
            color.hue = hue;
            // Convert from the HSV color space (where we can easily transition from one
            // color to the other) to the RGB color space that we can then send to the LED
            data = [hsv2rgb(color)];
            // When sending to the LED, we do a gamma correction first (see smart_leds
            // documentation for details) and then limit the brightness to 10 out of 255 so
            // that the output it's not too bright.
            if let Err(e) = led.write(brightness(gamma(data.iter().cloned()), 20)).await {
                log::error!("Driving LED: {:?}", e);
            }
            Timer::after(Duration::from_millis(10)).await;
        }
    }
}

@Be-ing
Copy link

Be-ing commented Jan 2, 2025

smart-leds-trait just added an async version of the trait, though it hasn't been released yet:

smart-leds-rs/smart-leds-trait#14
smart-leds-rs/smart-leds-trait#15

@robamu
Copy link
Collaborator Author

robamu commented Jan 14, 2025

looks interesting. I'll rebase this PR :)

@robamu robamu force-pushed the non-blocking-smart-led branch from 3562306 to e009f90 Compare January 14, 2025 17:19
@robamu
Copy link
Collaborator Author

robamu commented Jan 14, 2025

What do you think about removing the asynch module and simply keeping the *Async variants in the same module?

@Be-ing
Copy link

Be-ing commented Jan 31, 2025

smart-leds-trait 0.3.1 has been released with the SmartLedsWriteAsync trait.

@robamu
Copy link
Collaborator Author

robamu commented Feb 5, 2025

I need to rebase this anyway.. I can look whether I can already integrate this.

@robamu robamu force-pushed the non-blocking-smart-led branch from 2cf8647 to 222c7e5 Compare February 5, 2025 09:55
@robamu
Copy link
Collaborator Author

robamu commented Feb 5, 2025

I now implemented the new SmartLedsWriteAsync trait for the Async structure. I also removed the asynch submodule, and new new API is now placed in the root. This is more in line how it is commonly done for other asynch APIs.

@robamu robamu force-pushed the non-blocking-smart-led branch 3 times, most recently from cf01f28 to f083668 Compare February 5, 2025 10:52
/// Function to calculate the required RMT buffer size for a given number of LEDs when using
/// the asynchronous API. This buffer size is calculated for the asynchronous API provided by the
/// [SmartLedsAdapterAsync]. [buffer_size] should be used for the synchronous API.
pub const fn buffer_size_async(num_leds: usize) -> usize {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs a _async postfix?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually do we need this it all, it's a duplicate of the other function isn't it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, if there is no submodule, it needs a distinct name.


/// Adapter taking an RMT channel and a specific pin and providing RGB LED
/// interaction functionality.
pub struct SmartLedsAdapterAsync<Tx, const BUFFER_SIZE: usize> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a shame we have to duplicate this, I think if Rmt in esp-hal was updated to use the Async/Blocking mode parameters, we could have one smartled driver which could work in blocking or async mode.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're not going to change the Rmt peripheral, I suggest that we try and share as much code between the two async and blocking adapters as possible

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is a bit of duplication. That's why I tried to extract as much common logic as possible into free functions or re-usable functions. I think the subtle differences of those structs made it difficult to unify them. Having two distincts structs makes it explicit on type level which one is used though. It might also be that the flexibility of having two structs makes it easier to accomodate (external) changes in the future

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may be able to get rid of the code duplication using the bisync crate. I just did this for the embedded-sdmmc crate and I'm quite happy with how the code turned out using bisync.

@robamu robamu force-pushed the non-blocking-smart-led branch from 4706ad7 to 6e145f4 Compare February 5, 2025 11:18
@robamu
Copy link
Collaborator Author

robamu commented Feb 5, 2025

I also added an asynchronous example now. Haven´t tested it on hardware yet though. It is really similar to the hello_rgb example.

@robamu
Copy link
Collaborator Author

robamu commented Apr 3, 2025

I am currently updating my small lightstrip project, so I hope I can test this again soon :)

@kaspar030
Copy link

We used this on an Ulanzi Pixel Clock with a lot of (attention gathering) success on Chemitzer Linux Tage. 🙂
It seems rock solid.

@robamu
Copy link
Collaborator Author

robamu commented Apr 3, 2025

that's nice, so this was verified again :) needs to be rebased again though

@robamu robamu force-pushed the non-blocking-smart-led branch 2 times, most recently from fe3815d to 309f19e Compare April 3, 2025 13:20
@BlinkyStitt
Copy link

This looks like exactly what I need for my current project. Anything I can help with to get this merged in?

@robamu robamu force-pushed the non-blocking-smart-led branch from 309f19e to 8f319a9 Compare May 17, 2025 10:49
@robamu robamu requested a review from taorye as a code owner May 17, 2025 10:49
@robamu
Copy link
Collaborator Author

robamu commented May 17, 2025

I updated this to the most recent esp-hal.. maybe we should wait until the v1 release? there were differences between the latest beta version and the upstream again. I don't think it works in its current form due to trait bound issues on the pin.

@jamessizeland jamessizeland added enhancement New feature or request smart led Related to esp-hal-smartled labels May 20, 2025
@robamu
Copy link
Collaborator Author

robamu commented May 20, 2025

I will update this, just waiting for the update for the project files, it is kind of painful to work without them

@robamu robamu force-pushed the non-blocking-smart-led branch from 8f319a9 to 7d44871 Compare May 20, 2025 12:46
@robamu robamu requested a review from jamessizeland as a code owner May 20, 2025 12:46
@robamu
Copy link
Collaborator Author

robamu commented May 20, 2025

I wonder what this is, trying to compile the async example:

error: linking with `rust-lld` failed: exit status: 1
  |
  = note:  "rust-lld" "-flavor" "gnu" "/tmp/rustcTA5e0N/symbols.o" "<59 object files omitted>" "--as-needed" "-Bstatic" "/home/rmueller/Rust/esp-hal-community/esp-hal-smartled/target/riscv32imac-unknown-none-elf/debug/deps/{libesp_hal_embassy-0b570ad2ccdcbe31.rlib,libstatic_cell-2fe59a31e8f78c59.rlib,libembassy_executor-196bff06069a43a8.rlib,libsmart_leds-3f75fd4ede618e7e.rlib,libesp_hal_smartled-a3ea92e38901e2ad.rlib,libsmart_leds_trait-a604cb46675aa2a9.rlib,librgb-fc2138c8ab3dfa9e.rlib,libesp_hal-333444a37969506e.rlib,libembedded_can-b9fe4f377ed1cd0f.rlib,librand_core-649b74d2bdd1b284.rlib,libufmt_write-4b219053f0868c4d.rlib,libfugit-c57e125a9136b376.rlib,libgcd-75f78a47cc6ded6a.rlib,libembassy_embedded_hal-f8e6cd2fccfd0ed9.rlib,libembassy_time-100f4b9db317a8e2.rlib,libembassy_time_driver-3af4d7a0de091818.rlib,libembassy_futures-f92de4155a8254b9.rlib,libembedded_storage_async-04cf31a3227f3a6c.rlib,libembedded_storage-21c58fc38769de16.rlib,libembedded_hal-424a9e9c45a58ce5.rlib,libvoid-9cad67ac0ed01aaa.rlib,libnb-570cb8537b8c7a7f.rlib,libchrono-d35a563323106706.rlib,libnum_traits-c3c08c7f10bbc1c8.rlib,libembassy_sync-c0ba62720071d255.rlib,libfutures_util-4870ba19dfbbd0e2.rlib,libpin_project_lite-b54fa9bacd7a6895.rlib,libfutures_task-2a3b7b57fe26ef75.rlib,libpin_utils-767783140774eb37.rlib,libfutures_core-f5973be1f36c664f.rlib,libfutures_sink-771e8a53c31f1186.rlib,libembedded_io_async-81f3e71899145521.rlib,libheapless-ce22fe139a4b4f91.rlib,libhash32-21bb8fde374278b8.rlib,libbyteorder-d979f2953ef7d250.rlib,libbitflags-0b9ab7067d6069a0.rlib,libnb-0b491629be2a3667.rlib,libbitfield-b0ef1c64dabde8d7.rlib,libesp_config-d3cc4dc8c8530809.rlib,libcfg_if-3ab91fc94e10801e.rlib,libbytemuck-9eddb76f309b053c.rlib,libesp32c6-5fec569ce6b06d0a.rlib,libvcell-6f683f7881d6c8de.rlib,libembedded_io-5d70309093d948d5.rlib,libenumset-1bf2f2c2cd5aa154.rlib,libembedded_hal_async-edc1bc5fb2e8fa41.rlib,libstrum-0ff07aaea6a69cc5.rlib,libesp_riscv_rt-29ee066acf154428.rlib,libriscv-cb8806e4f964e57c.rlib,libriscv_pac-8aad88270ef81da3.rlib,libembedded_hal-52028cfd21c291a1.rlib,libesp_backtrace-0834b3259d067ed4.rlib,libesp_println-d84f3260855be6f5.rlib,libcritical_section-c0dd728f05931daf.rlib,libportable_atomic-26ccc9f29d4ae4f5.rlib}.rlib" "<sysroot>/lib/rustlib/riscv32imac-unknown-none-elf/lib/{librustc_std_workspace_core-*,libcore-*,libcompiler_builtins-*}.rlib" "-Bdynamic" "-z" "noexecstack" "-L" "/home/rmueller/Rust/esp-hal-community/esp-hal-smartled/target/riscv32imac-unknown-none-elf/debug/build/esp-hal-d06c557878c8f3cf/out" "-L" "/home/rmueller/Rust/esp-hal-community/esp-hal-smartled/target/riscv32imac-unknown-none-elf/debug/build/esp32c6-25d5d06b111af851/out" "-o" "/home/rmueller/Rust/esp-hal-community/esp-hal-smartled/target/riscv32imac-unknown-none-elf/debug/examples/hello_rgb_async-f58f20f05f1ab989" "--gc-sections" "-Tlinkall.x" "-Tlinkall.x"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: rust-lld: error: /home/rmueller/Rust/esp-hal-community/esp-hal-smartled/target/riscv32imac-unknown-none-elf/debug/build/esp-hal-d06c557878c8f3cf/out/memory.x:22: region 'RAM' already defined
          >>>     RAM : ORIGIN = 0x40800000 , LENGTH = 0x6E610
          >>>                                          ^


error: could not compile `esp-hal-smartled` (example "hello_rgb_async") due to 1 previous error

esp-hal-community/esp-hal-smartled on  non-blocking-smart-led [$?] is 📦 v0.15.0 via 🦀 v1.86.0 took 30s

probably just some setup issue

@jamessizeland
Copy link
Collaborator

hmm yeah I'll pull your branch and take a look myself

@robamu robamu force-pushed the non-blocking-smart-led branch from 7d44871 to 72115e1 Compare May 20, 2025 14:22
@robamu
Copy link
Collaborator Author

robamu commented May 20, 2025

i actually also wanted to test this on an esp32c6 devboard I have here, just rebased as well.

@robamu
Copy link
Collaborator Author

robamu commented May 20, 2025

Just reveived the same error on a different machine, building the hello_rgb example.. can you re-produce this?

@MabezDev
Copy link
Member

I'll dismiss my review shortly, as I don't want to block progress here.

Just an FYI, @wisp3rwind is working on a pretty radical RMT refactor. In particular the latest PR has some interesting changes which should make supporting blocking and async smartled stuff easier: https://github.com/esp-rs/esp-hal/pull/3505/files#diff-a18a0af13c09debca19ef19cda6411ea68747fb7fd9ef38a6a3c57e4a7005fb6R702.

Hopefully that link works correctly, but if it doesn't here is the snippet I was referring to:

/// RMT Channel Creator
pub struct ChannelCreator<Dm, const CHANNEL: u8, Cap>
where
    Dm: crate::DriverMode,
{

Channel creator will be generic over the mode (Blocking/Async), which should eventually reduce some boiler plate here.

@MabezDev MabezDev dismissed their stale review May 20, 2025 14:52

Not strictly relevant to this PR anymore

@jamessizeland
Copy link
Collaborator

can you sync this branch again? Its missing the .cargo changes

@jamessizeland
Copy link
Collaborator

I've added the .cargo/config.toml files and run this with cargo esp32c3 -p esp-hal-smartled --example hello_rgb_async and it works nicely for me! I'll try it on my c6 board too now.

@jamessizeland
Copy link
Collaborator

yep working. I think if you just merge in the latest changes it'll work.

Copy link
Collaborator

@jamessizeland jamessizeland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works on my devices. Just needs main merging in first

@wisp3rwind
Copy link

I'll dismiss my review shortly, as I don't want to block progress here.

Just an FYI, @wisp3rwind is working on a pretty radical RMT refactor. In particular the latest PR has some interesting changes which should make supporting blocking and async smartled stuff easier: https://github.com/esp-rs/esp-hal/pull/3505/files#diff-a18a0af13c09debca19ef19cda6411ea68747fb7fd9ef38a6a3c57e4a7005fb6R702.

Hopefully that link works correctly, but if it doesn't here is the snippet I was referring to:

/// RMT Channel Creator
pub struct ChannelCreator<Dm, const CHANNEL: u8, Cap>
where
    Dm: crate::DriverMode,
{

Channel creator will be generic over the mode (Blocking/Async), which should eventually reduce some boiler plate here.

FWIW, completely over-engineering smart LED driving is how I got there.

What I have locally is a completely interrupt-driven async RMT writer, which uses an Encoder type (similar to the IDF driver) to directly generate pulse codes from RGB(WW) arrays in the ISR and write it to the RMT channel RAM, without any intermediate buffers that would contain pulse codes.

I'll attempt to upstream all of this, but it's going to take a while: In particular, the ISR case raises a bunch of issues regarding safety/soundness of the code (because the future needs to take ownership of the buffer/Encoder for this to be sound on drop of leak of the future), and I don't think how I designed it is ideal yet. I also tried an async implementation where the buffer is refilled on poll of the RmtTxFuture, but that didn't seem to be fast enough and led to glitches.

@robamu robamu force-pushed the non-blocking-smart-led branch from 72115e1 to 3257eb0 Compare May 20, 2025 16:34
@robamu
Copy link
Collaborator Author

robamu commented May 20, 2025

Rebased on upstream again.

@wisp3rwind @MabezDev That sounds really cool. I have a question: I actually had to re-configure the RMT output pin as input to avoid phantom current through the data pin when my smart led strip was off. maybe the esp RMT channel structure can keep a type erased flex/any pin instance in the future so we can enable stuff like temporarily re-configuring as input, and later re-configuring a pin as a RMT pin?

@jamessizeland thanks a lot for testing. I am still wondering what kind of issue I am looking at here.. I cleaned the repo multiple times as well.. which rust version are you using?

@robamu
Copy link
Collaborator Author

robamu commented May 20, 2025

and it just disappeared :D

@jamessizeland jamessizeland merged commit c188289 into esp-rs:main May 20, 2025
7 checks passed
@robamu robamu deleted the non-blocking-smart-led branch May 20, 2025 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request smart led Related to esp-hal-smartled
Projects
None yet
Development

Successfully merging this pull request may close these issues.