Skip to content

Commit

Permalink
prepare 2.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jackw01 committed Jul 13, 2023
1 parent 7169ee7 commit 7e0e487
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 48 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ include(pico_sdk_import.cmake)

set(OUTPUT_NAME "ledcontrol")

set(PICO_BOARD "adafruit_feather_rp2040")
#set(PICO_BOARD "adafruit_feather_rp2040")

project(${OUTPUT_NAME} C CXX ASM)
set(CMAKE_C_STANDARD 11)
Expand Down
51 changes: 41 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* In-browser code editor and color palette editor make creating and modifying animations easy
* Large selection of built-in animations and color palettes means you don't have to write any code
* Works with cheap and readily available WS281x and SK6812 LED strips and strings
* Runs on a Raspberry Pi single-board computer directly connected to LEDs, and on any other computer with the help of a low-cost microcontroller board (Raspberry Pi Pico) connected via USB
* Supports pixel mapping for arbitrary 2D and 3D LED arrangements
* Seamlessly supports HSV-to-RGBW and RGB-to-RGBW conversion for RGBW LED strips
* Supports networked E1.31 sACN DMX control for music visualization through [LedFx](https://github.com/LedFx/LedFx)
Expand All @@ -23,16 +24,27 @@ The theoretical maximum framerate for 150 RGBW LEDs is 800000 Hz / (8*4) bits /
All built-in animations run at over 50FPS on a Raspberry Pi Zero, and will run faster on any other Raspberry Pi model. The framerate is limited to 60FPS by default to reduce CPU usage.

## Install
### Hardware Setup
1. Obtain a Raspberry Pi (any model), a WS2812B or SK6812B LED strip (**SK6812 RGB/White LEDs are highly recommended**), and a suitable 5V power supply.

### Hardware Setup (All Platforms)
Obtain a WS2812B or SK6812B LED strip (**SK6812 RGB/White LEDs are highly recommended**) and a suitable 5V power supply. Using USB power may be suitable, and an external power supply may not be needed, when using small numbers of LEDs (less than 80 RGBW or 50 RGB LEDs).

### Hardware Setup (External LED Driver)
1. Obtain a Raspberry Pi Pico (US$4) or any microcontroller board based on the RP2040 chip.
2. Connect the LED strip to your microcontroller:
- MCU GND to LED GND
- MCU GPIO12 to LED Data in
- Power supply ground to LED GND
- Power supply 5V to LED 5V
3. Flash the firmware onto your microcontroller board: hold down the BOOTSEL button when connecting it to your computer with a USB cable and it should enumerate as a USB storage device. Drag and drop or copy and paste the `.uf2` binary file, which can be downloaded on the [Releases](https://github.com/jackw01/led-control/releases) page, into the microcontroller. The provided binaries are compiled for the Raspberry Pi Pico and have been tested to also run on Adafruit Feather RP2040 boards.

### Hardware Setup (Raspberry Pi)
1. Obtain a Raspberry Pi single-board computer (any model). Due to the unavailability of Raspberry Pis, using any other computer with an external LED driver is recommended (see above).
2. Connect the LED strip to your Raspberry Pi:
- Pi GND to LED GND
- Pi GPIO18 to LED Data in
- Power supply ground to LED GND
- Power supply 5V to LED 5V

See [this Adafruit guide](https://learn.adafruit.com/neopixels-on-raspberry-pi/raspberry-pi-wiring#using-external-power-source-without-level-shifting-3005993-11) for other ways to connect the LED strips or for using a level shifter.

#### RGBW LEDs Are Highly Recommended
Know what you're doing with electricity. Addressable LEDs can draw a lot of current, especially in long strips. You should use RGBW LEDs for the reason that **they look better and require much less power** when displaying whiter colors (a good quality 5V 4A power supply can comfortably handle 150 RGBW LEDs at full brightness).

Expand All @@ -42,15 +54,28 @@ Addressable LED strips usually come with seriously undersized power wires and ba

For more information on which GPIO pins LED strips can be connected to, see [here](https://github.com/jgarff/rpi_ws281x).

For more information un using a level shifter, which may be necessary with some WS2812 RGB LED strips, see [this Adafruit guide](https://learn.adafruit.com/neopixels-on-raspberry-pi/raspberry-pi-wiring#using-external-power-source-without-level-shifting-3005993-11).

#### If You Really Want To Use RGB LEDs
You should budget [at least 50mA for each LED at full brightness](https://www.pjrc.com/how-much-current-do-ws2812-neopixel-leds-really-use/), which means 7.5A for 150 LEDs (5 meters of 30 LED/m strip, 2.5m of 60LED/m strip...). In practice, your LED strips won't draw this much current, but your power supply should be capable of handling it.

The flexible PCBs and connectors used in these LED strips are not really designed to handle these currents, and begin to heat up when passing as little as 2-3A. Again, each group of up to ~150 LEDs should be powered through its own adequately sized wires.

### Software Setup
### Software Setup (With Pi Pico LED Driver)
Python 3.7 or newer is required.

1. `sudo apt-get install scons swig libev-dev python3-dev python3-setuptools`
1. Ensure that git, python, and pip are installed.
2. Determine the serial port ID of your microcontroller board. On Windows, this can be done through Device Manager.
3. `git clone https://github.com/jackw01/led-control.git`
4. `cd led-control`
5. `git checkout tags/v2.1.0`
6. `python setup.py develop`
7. `ledcontrol --led_count 150 --serial_port SERIAL_PORT_HERE` (add `--led_pixel_order GRBW` if using RGBW LEDs)

### Software Setup (Raspberry Pi)
Python 3.7 or newer is required.

1. `sudo apt-get install scons swig libev-dev python3-dev python3-setuptools git python3-pip`
2. `git clone --recurse-submodules https://github.com/jackw01/led-control.git`
3. `cd led-control`
4. `git checkout tags/v2.0.0`
Expand All @@ -61,7 +86,7 @@ Python 3.7 or newer is required.
LEDControl and the Raspberry Pi audio subsystem cannot be use together since they both use the PWM hardware. On some Linux distributions, you must disable the audio kernel module by commenting out the line `dtparam=audio=on` in `/boot/config.txt` or by creating a file `/etc/modprobe.d/snd-blacklist.conf` with the contents `blacklist snd_bcm2835`.

### Command Line Configuration Arguments
Web server and LED hardware parameters must be specified as command line arguments when running ledcontrol.
Web server and LED hardware parameters must be specified as command line arguments when running ledcontrol. Note that none of the LED hardware-related arguments will have an effect when using a Pi Pico to drive the LEDs.
```
usage: ledcontrol [-h] [--port PORT] [--host HOST] [--led_count LED_COUNT]
[--config_file CONFIG_FILE]
Expand All @@ -70,8 +95,8 @@ usage: ledcontrol [-h] [--port PORT] [--host HOST] [--led_count LED_COUNT]
[--led_dma_channel LED_DMA_CHANNEL]
[--led_pixel_order LED_PIXEL_ORDER]
[--led_brightness_limit LED_BRIGHTNESS_LIMIT]
[--save_interval SAVE_INTERVAL] [--sacn] [--no_timer_reset]
[--dev]
[--save_interval SAVE_INTERVAL] [--sacn] [--hap] [--no_timer_reset]
[--dev] [--serial_port SERIAL_PORT]
optional arguments:
-h, --help show this help message and exit
Expand Down Expand Up @@ -102,9 +127,12 @@ optional arguments:
Interval for automatically saving settings in seconds.
Default: 60
--sacn Enable sACN / E1.31 support. Default: False
--hap Enable HomeKit Accessory Protocol support. Default: False
--no_timer_reset Do not reset the animation timer when patterns are
changed. Default: False
--dev Development flag. Default: False
--serial_port SERIAL_PORT
Serial port for external LED driver.
```

### Color Correction
Expand All @@ -127,6 +155,9 @@ LEDControl can function as a E1.31 streaming ACN receiver, allowing the connecte

While sACN receiver mode is enabled, the LED refresh rate is determined by your sACN server. There may be noticeable latency when using sACN on congested networks or if other software on the Raspberry Pi is using its network hardware; this is a known limitation of sACN.

### HomeKit Accessory Protocol Support (Experimental)
The brightness and on/off state of LEDControl can be controlled through Apple HomeKit. Run LEDControl with the `--hap` command line flag and a setup code will be printed for manually pairing in the Apple Home app.

### Pixel Mapping
LEDControl supports pixel mapping, which allows 2- and 3-dimensional animation patterns to be mapped to any physical arrangement of LEDs. Currently, pixel mappings can only be specified with a JSON file containing an array of points representing the positions of each LED, using the `--pixel_mapping_json` command line argument. `--led_count` does not need to be specified when pixel mapping is used. The points must be in the same order that the correresponding LEDs are connected, and the units used to define the pixel mapping do not matter (negative and floating-point values are allowed).

Expand Down Expand Up @@ -310,7 +341,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
```

## Development
To build the C extension module:
To build the C extension module (on Raspberry Pi single-board computers only):
```
swig -python ./ledcontrol/driver/ledcontrol_rpi_ws281x_driver.i && sudo python3 setup.py develop
```
Expand Down
10 changes: 3 additions & 7 deletions firmware/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ void write_frame_buffer() {
}
}

// hue = hue % 1.0

uint32_t render_hsv2rgb_rainbow(uint8_t h, uint8_t s, uint8_t v,
uint8_t corr_r, uint8_t corr_g, uint8_t corr_b,
uint8_t saturation, uint8_t brightness) {
Expand Down Expand Up @@ -164,8 +162,6 @@ uint32_t render_hsv2rgb_rainbow(uint8_t h, uint8_t s, uint8_t v,
return pack_rgbw(r, g, b, w);
}

// clamp rgb

uint32_t render_rgb(uint8_t r, uint8_t g, uint8_t b,
uint8_t corr_r, uint8_t corr_g, uint8_t corr_b,
uint8_t saturation, uint8_t brightness) {
Expand All @@ -180,9 +176,9 @@ uint32_t render_rgb(uint8_t r, uint8_t g, uint8_t b,
b = 0;
min = max;
} else {
r = scale_8(r - max, saturation) + max;
g = scale_8(g - max, saturation) + max;
b = scale_8(b - max, saturation) + max;
r = (int)r - max * (int)saturation / 255 + max;
g = (int)g - max * (int)saturation / 255 + max;
b = (int)b - max * (int)saturation / 255 + max;
min = r < g ? (r < b ? r : b) : (g < b ? g : b);
r -= min;
g -= min;
Expand Down
30 changes: 18 additions & 12 deletions ledcontrol/animationcontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,12 @@ def update_leds(self):
msg = traceback.format_exception(type(e), e, e.__traceback__)
print(f'Animation execution: {msg}')
r = 0.1 * driver.wave_pulse(time_fix, 0.5)
self._led_controller.set_all_rgb([(r, 0, 0) for i in range(self._led_count)],
self._correction,
1.0,
1.0)
self._led_controller.set_range_rgb([(r, 0, 0) for i in range(self._led_count)],
0, self._led_count,
self._correction,
1.0,
1.0)
self._led_controller.render()
return

# If displaying a static pattern, brightness is 0, or speed is 0:
Expand All @@ -417,17 +419,21 @@ def _sacn_callback(self, packet):
self._sacn_perf_avg = 0

data = [x / 255.0 for x in packet.dmxData[:self._led_count * 3]]
self._led_controller.set_all_rgb(list(zip_longest(*(iter(data),) * 3)),
self._correction,
1.0,
self._settings['global_brightness'])
self._led_controller.set_range_rgb(list(zip_longest(*(iter(data),) * 3)),
0, self._led_count,
self._correction,
1.0,
self._settings['global_brightness'])
self._led_controller.render()

def clear_leds(self):
'Turn all LEDs off'
self._led_controller.set_all_rgb([(0, 0, 0) for i in range(self._led_count)],
self._correction,
1.0,
1.0)
self._led_controller.set_range_rgb([(0, 0, 0) for i in range(self._led_count)],
0, self._led_count,
self._correction,
1.0,
1.0)
self._led_controller.render()

def end_animation(self):
'Stop rendering in the animation thread and stop sACN receiver'
Expand Down
87 changes: 77 additions & 10 deletions ledcontrol/driver/driver_non_raspberry_pi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# Copyright 2023 jackw01. Released under the MIT License (see LICENSE for details).

import math
import pyfastnoisesimd as fns

noise_coords = fns.empty_coords(3)
noise = fns.Noise()

def float_to_int_1000(t):
return int(t * 999.9) % 1000
Expand All @@ -10,32 +14,95 @@ def float_to_int_1000_mirror(t):
return abs(int(t * 1998.9) % 1999 - 999)

def wave_pulse(t, duty_cycle):
return math.ceil(duty_cycle - (t % 1.0))
return math.ceil(duty_cycle - math.fmod(t, 1.0))

def wave_triangle(t):
ramp = (2.0 * t) % 2.0
return abs((ramp + 2.0 if ramp < 0 else ramp) - 1.0)
ramp = math.fmod((2.0 * t), 2.0)
return math.fabs((ramp + 2.0 if ramp < 0 else ramp) - 1.0)

def wave_sine(t):
return math.cos(6.283 * t) / 2.0 + 0.5

def wave_cubic(t):
return 0
ramp = math.fmod((2.0 * t), 2.0)
tri = math.fabs((ramp + 2.0 if ramp < 0 else ramp) - 1.0)
if tri > 0.5:
t2 = 1.0 - tri
return 1.0 - 4.0 * t2 * t2 * t2
else:
return 4.0 * tri * tri * tri

def plasma_sines(x, y, t, coeff_x, coeff_y, coeff_x_y, coeff_dist_x_y):
return 0
v = 0
v += math.sin((x + t) * coeff_x)
v += math.sin((y + t) * coeff_y)
v += math.sin((x + y + t) * coeff_x_y)
v += math.sin((math.sqrt(x * x + y * y) + t) * coeff_dist_x_y)
return v

def plasma_sines_octave(x, y, t, octaves, lacunarity, persistence):
return 0
vx = x
vy = y
freq = 1.0
amplitude = 1.0
for i in range(octaves):
vx1 = vx
vx += math.cos(vy * freq + t * freq) * amplitude
vy += math.sin(vx1 * freq + t * freq) * amplitude
freq *= lacunarity
amplitude *= persistence
return vx / 2.0

def perlin_noise_3d(x, y, z):
return 0
noise_coords[0,:] = x
noise_coords[1,:] = y
noise_coords[2,:] = z
return noise.genFromCoords(noise_coords)[0]

def fbm_noise_3d(x, y, z, octaves, lacunarity, persistence):
return 0
v = 0
freq = 1.0
amplitude = 1.0
for i in range(octaves):
v += amplitude * perlin_noise_3d(freq * x, freq * y, freq * z)
freq *= lacunarity
amplitude *= persistence
return v / 2.0

def clamp(x, min, max):
if x < min:
return min
elif x > max:
return max
else:
return x

def blackbody_to_rgb(kelvin):
return [1, 1, 1]
tmp_internal = kelvin / 100.0
r_out = 0
g_out = 0
b_out = 0

if tmp_internal <= 66:
xg = tmp_internal - 2.0
r_out = 1.0
g_out = clamp((-155.255 - 0.446 * xg + 104.492 * math.log(xg)) / 255.0, 0, 1)
else:
xr = tmp_internal - 55.0
xg = tmp_internal - 50.0
r_out = clamp((351.977 + 0.114 * xr - 40.254 * math.log(xr)) / 255.0, 0, 1)
g_out = clamp((325.449 + 0.079 * xg - 28.085 * math.log(xg)) / 255.0, 0, 1)

if tmp_internal >= 66:
b_out = 1.0
elif tmp_internal <= 19:
b_out = 0.0
else:
xb = tmp_internal - 10.0
b_out = clamp((-254.769 + 0.827 * xb + 115.680 * math.log(xb)) / 255.0, 0, 1)

return [r_out, g_out, b_out]

def blackbody_correction_rgb(rgb, kelvin):
return 0
bb = blackbody_to_rgb(kelvin)
return [rgb[0] * bb[0], rgb[1] * bb[1], rgb[2] * bb[2]]
25 changes: 17 additions & 8 deletions ledcontrol/ledcontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ def __init__(self,
str_resp = driver.ws2811_get_return_t_str(resp)
raise RuntimeError('ws2811_init failed with code {0} ({1})'.format(resp, str_resp))
else:
self._where_hue = np.zeros((led_count * 3,),dtype=bool)
self._where_hue[0::3] = True
self._ser = serial.Serial(serial_port, 115200, timeout=0.01, write_timeout=0)

def _cleanup(self):
Expand All @@ -97,7 +99,8 @@ def set_range_hsv(self, pixels, start, end, correction, saturation, brightness):
self._has_white)
else:
data = np.fromiter(itertools.chain.from_iterable(pixels), np.float32)
data = 255 * data
np.fmod(data, 1.0, where=self._where_hue[0:(end - start) * 3], out=data)
data = data * 255.0
data = data.astype(np.uint8)
self._ser.write(b'\x00\x02'
+ int((end - start) * 3 + 13).to_bytes(2, 'big')
Expand All @@ -108,18 +111,24 @@ def set_range_hsv(self, pixels, start, end, correction, saturation, brightness):
+ end.to_bytes(2, 'big')
+ data.tobytes())

def set_all_rgb(self, pixels, correction, saturation, brightness):
if driver.is_raspberrypi():
driver.ws2811_rgb_render_all_float(self._leds, self._channel,
pixels, len(pixels),
correction, saturation, brightness, 1.0,
self._has_white)

def set_range_rgb(self, pixels, start, end, correction, saturation, brightness):
if driver.is_raspberrypi():
driver.ws2811_rgb_render_range_float(self._channel, pixels, start, end,
correction, saturation, brightness, 1.0,
self._has_white)
else:
data = np.fromiter(itertools.chain.from_iterable(pixels), np.float32)
data = data * 255.0
data = np.clip(data, 0.0, 255.0)
data = data.astype(np.uint8)
self._ser.write(b'\x00\x01'
+ int((end - start) * 3 + 13).to_bytes(2, 'big')
+ correction.to_bytes(3, 'big')
+ int(saturation * 255).to_bytes(1, 'big')
+ int(brightness * 255).to_bytes(1, 'big')
+ start.to_bytes(2, 'big')
+ end.to_bytes(2, 'big')
+ data.tobytes())

def show_calibration_color(self, count, correction, brightness):
if driver.is_raspberrypi():
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def is_raspberrypi():
'HAP-python==4.4.0',
'pyopenssl==22.1.0',
'numpy>=1.21.0',
'pyfastnoisesimd>=0.4.2',
] + (['bjoern>=3.2.1'] if sys.platform.startswith('linux') else [])

extensions = [
Expand Down

0 comments on commit 7e0e487

Please sign in to comment.