From b7bc96c3b298a1efd36a38e20f49ce61c90f3d97 Mon Sep 17 00:00:00 2001
From: Kevin O'Connor <kevin@koconnor.net>
Date: Fri, 12 Jul 2019 19:37:50 -0400
Subject: [PATCH] neopixel: Add initial support for "neopixel" leds

Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
---
 config/example-extras.cfg |  10 ++++
 docs/G-Codes.md           |   8 +++
 klippy/extras/neopixel.py |  45 +++++++++++++++
 src/Makefile              |   2 +-
 src/neopixel.c            | 114 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 178 insertions(+), 1 deletion(-)
 create mode 100644 klippy/extras/neopixel.py
 create mode 100644 src/neopixel.c

diff --git a/config/example-extras.cfg b/config/example-extras.cfg
index 46c0d6cae..27ad4ec53 100644
--- a/config/example-extras.cfg
+++ b/config/example-extras.cfg
@@ -1648,6 +1648,16 @@
 #measurement_delay: 100
 
 
+# Neopixel (aka WS2812) LED support (one may define any number of
+# sections with a "neopixel" prefix). One may set the LED color via a
+# "SET_NEOPIXEL NEOPIXEL=my_neopixel RED=0.1 GREEN=0.1 BLUE=0.1" type
+# extended g-code commands.
+#[neopixel my_neopixel]
+#pin:
+#   The pin connected to the neopixel. This parameter must be
+#   provided.
+
+
 # Firmware filament retraction. This enables G10 (retract) and G11
 # (unretract) GCODE commands issued by many slicers. The parameters
 # below provide startup defaults, although the values can be adjusted
diff --git a/docs/G-Codes.md b/docs/G-Codes.md
index de8c005d2..78be3e723 100644
--- a/docs/G-Codes.md
+++ b/docs/G-Codes.md
@@ -191,6 +191,14 @@ The following command is available when an "output_pin" config section
 is enabled:
 - `SET_PIN PIN=config_name VALUE=<value>`
 
+## Neopixel Commands
+
+The following command is available when a "neopixel" config section
+is enabled:
+- `SET_NEOPIXEL NEOPIXEL=<config_name> RED=<value> GREEN=<value>
+  BLUE=<value>`: This sets the neopixel LED output. Each <value> must
+  be between 0.0 and 1.0.
+
 ## Servo Commands
 
 The following commands are available when a "servo" config section is
diff --git a/klippy/extras/neopixel.py b/klippy/extras/neopixel.py
new file mode 100644
index 000000000..6988b645c
--- /dev/null
+++ b/klippy/extras/neopixel.py
@@ -0,0 +1,45 @@
+# Support for "neopixel" leds
+#
+# Copyright (C) 2019  Kevin O'Connor <kevin@koconnor.net>
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+
+class PrinterNeoPixel:
+    def __init__(self, config):
+        self.printer = config.get_printer()
+        name = config.get_name().split()[1]
+        # Configure neopixel
+        ppins = self.printer.lookup_object('pins')
+        pin_params = ppins.lookup_pin(config.get('pin'))
+        self.mcu = pin_params['chip']
+        self.oid = self.mcu.create_oid()
+        self.mcu.add_config_cmd("config_neopixel oid=%d pin=%s"
+                                % (self.oid, pin_params['pin']))
+        self.mcu.register_config_callback(self.build_config)
+        self.neopixel_send_cmd = None
+        # Register commands
+        self.gcode = self.printer.lookup_object('gcode')
+        self.gcode.register_mux_command("SET_NEOPIXEL", "NEOPIXEL", name,
+                                        self.cmd_SET_NEOPIXEL,
+                                        desc=self.cmd_SET_NEOPIXEL_help)
+    def build_config(self):
+        cmd_queue = self.mcu.alloc_command_queue()
+        self.neopixel_send_cmd = self.mcu.lookup_command(
+            "neopixel_send oid=%c data=%*s", cq=cmd_queue)
+    cmd_SET_NEOPIXEL_help = "Set the color of a neopixel led"
+    def cmd_SET_NEOPIXEL(self, params):
+        # Parse parameters
+        red = self.gcode.get_float('RED', params, 0., minval=0., maxval=1.)
+        green = self.gcode.get_float('GREEN', params, 0., minval=0., maxval=1.)
+        blue = self.gcode.get_float('BLUE', params, 0., minval=0., maxval=1.)
+        red = int(red * 255. + .5)
+        blue = int(blue * 255. + .5)
+        green = int(green * 255. + .5)
+        # Send command
+        print_time = self.printer.lookup_object('toolhead').get_last_move_time()
+        minclock = self.mcu.print_time_to_clock(print_time)
+        self.neopixel_send_cmd.send([self.oid, [green, red, blue]],
+                                    minclock=minclock)
+
+def load_config_prefix(config):
+    return PrinterNeoPixel(config)
diff --git a/src/Makefile b/src/Makefile
index c838e9115..2eff8a930 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -7,4 +7,4 @@ src-$(CONFIG_HAVE_GPIO_SPI) += spicmds.c thermocouple.c
 src-$(CONFIG_HAVE_GPIO_I2C) += i2ccmds.c
 src-$(CONFIG_HAVE_GPIO_HARD_PWM) += pwmcmds.c
 src-$(CONFIG_HAVE_GPIO_BITBANGING) += lcd_st7920.c lcd_hd44780.c buttons.c \
-    tmcuart.c spi_software.c
+    tmcuart.c spi_software.c neopixel.c
diff --git a/src/neopixel.c b/src/neopixel.c
new file mode 100644
index 000000000..41ca98dc7
--- /dev/null
+++ b/src/neopixel.c
@@ -0,0 +1,114 @@
+// Support for bit-banging commands to WS2812 type "neopixel" LEDs
+//
+// Copyright (C) 2019  Kevin O'Connor <kevin@koconnor.net>
+//
+// This file may be distributed under the terms of the GNU GPLv3 license.
+
+#include "autoconf.h" // CONFIG_MACH_AVR
+#include "board/gpio.h" // gpio_out_write
+#include "board/irq.h" // irq_poll
+#include "board/misc.h" // timer_read_time
+#include "basecmd.h" // oid_alloc
+#include "command.h" // DECL_COMMAND
+
+struct neopixel_s {
+    struct gpio_out pin;
+    uint32_t last_req_time;
+};
+
+void
+command_config_neopixel(uint32_t *args)
+{
+    struct gpio_out pin = gpio_out_setup(args[1], 0);
+    struct neopixel_s *n = oid_alloc(args[0], command_config_neopixel
+                                     , sizeof(*n));
+    n->pin = pin;
+}
+#if !CONFIG_MACH_AVR
+DECL_COMMAND(command_config_neopixel, "config_neopixel oid=%c pin=%u");
+#endif
+
+uint32_t
+timer_from_ns(uint32_t ns)
+{
+    return timer_from_us(ns * 1000) / 1000000;
+}
+
+static int
+send_data(struct neopixel_s *n, uint8_t *data, uint_fast8_t data_len)
+{
+    // Make sure at least 50us has passed since last request
+    uint32_t last_req_time = n->last_req_time, cur = timer_read_time();
+    while (cur - last_req_time < timer_from_us(50)) {
+        irq_poll();
+        cur = timer_read_time();
+    }
+
+    struct gpio_out pin = n->pin;
+    uint32_t expire_time = cur + 0x40000000;
+    while (data_len--) {
+        uint_fast8_t byte = *data++;
+        uint_fast8_t bits = 8;
+        while (bits--) {
+            // Calculate pulse duration
+            uint32_t on, off;
+            if (byte & 0x80) {
+                on = timer_from_ns(700 - 150);
+                off = timer_from_ns(600 - 150);
+            } else {
+                on = timer_from_ns(350 - 150);
+                off = timer_from_ns(800 - 150);
+            }
+            byte <<= 1;
+
+            // Set output high
+            gpio_out_toggle(pin);
+            uint32_t on_start_time = timer_read_time();
+            if (timer_is_before(expire_time, on_start_time))
+                goto fail;
+            cur = on_start_time;
+            while (timer_is_before(cur, on_start_time + on)) {
+                irq_poll();
+                cur = timer_read_time();
+            }
+
+            // Set output low
+            gpio_out_toggle(pin);
+            uint32_t off_start_time = timer_read_time();
+            expire_time = on_start_time + on + timer_from_ns(300);
+            if (timer_is_before(expire_time, off_start_time))
+                goto fail;
+            cur = off_start_time;
+            while (timer_is_before(cur, off_start_time + off)) {
+                irq_poll();
+                cur = timer_read_time();
+            }
+            expire_time = off_start_time + off + timer_from_ns(300);
+        }
+    }
+    n->last_req_time = cur;
+    return 0;
+fail:
+    // A hardware irq messed up the transmission - report a failure
+    n->last_req_time = cur;
+    return -1;
+}
+
+void
+command_neopixel_send(uint32_t *args)
+{
+    uint8_t oid = args[0];
+    struct neopixel_s *n = oid_lookup(oid, command_config_neopixel);
+    uint_fast8_t data_len = args[1];
+    uint8_t *data = (void*)(size_t)args[2];
+
+    uint_fast8_t retry = 8;
+    while (retry--) {
+        int ret = send_data(n, data, data_len);
+        if (!ret)
+            break;
+    }
+}
+#if !CONFIG_MACH_AVR
+DECL_COMMAND(command_neopixel_send, "neopixel_send oid=%c data=%*s");
+#endif