Skip to content

Commit

Permalink
Merge pull request #7 from MaWalla/threadloop
Browse files Browse the repository at this point in the history
overhauled entire codebase to use threads properly
  • Loading branch information
MaWalla committed Aug 12, 2021
2 parents aa97823 + 6496e72 commit 3b99f95
Show file tree
Hide file tree
Showing 28 changed files with 321 additions and 1,426 deletions.
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "fxmodes/ScreenFX"]
path = fxmodes/ScreenFX
url = git@github.com:MaWalla/ScreenFX.git
[submodule "fxmodes/PulseViz"]
path = fxmodes/PulseViz
url = git@github.com:MaWalla/PulseViz.git
33 changes: 15 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

# ImmersiveFX
ImmersiveFX is a framework which interfaces WLED over UDP, Razer Keyboards (specifically the BlackWidow Chroma 2014) and the Lightbar of DualShock 4 controllers.
ImmersiveFX is a framework which interfaces WLED over UDP, Serial devices, and the Lightbar of DualShock 4 controllers.

It allows you to build Applications that process data to display them onto above devices. It includes 2 sample fxmodes:
It allows you to build Applications that process data to display them onto above devices. It includes 2 reference fxmodes as submodules:

**ScreenFX**, which can display the content of screen borders (similar to Phillips Ambilight)

Expand All @@ -15,18 +15,18 @@ Besides that, you're hopefully only limited by your creativity. If you're limite
#### shipped fxmodes
|Mode |Linux|Windows |Mac OS |
|---------|-----|-----------|-----------|
|ScreenFX:|yes |untested* |untested* |
|ScreenFX:|yes |untested* |untested** |
|PulseViz:|yes |no |no |

#### device compatibility
|Device |Linux|Windows |Mac OS |
|--------------|-----|-----------|-----------|
|WLED: |yes |untested* |untested* |
|Arduino: |yes |untested* |untested* |
|Razer: |yes |no |no |
|WLED: |yes |untested* |yes |
|Serial: |yes |untested* |untested* |
|DualShock 4: |yes |no |no |

*I only use Linux, so Windows and Mac OS are entirely untested. The things should work though, according to their used library documententation for example.
** Tested on a M1 MacBook Air, but prior to the 1.0.0 release. Performance was bad and captured data corrupted

Both pulseaudio and the way I speak to Dualshock 4 controllers are limited to Linux so support for other platforms is unlikely, unless there are different implementations for those.

Expand All @@ -40,6 +40,8 @@ Both pulseaudio and the way I speak to Dualshock 4 controllers are limited to Li
- clone the repo somewhere.
- open a terminal in the cloned folder.

Start off by doing `git submodule init` to pull in the reference fxmodes ScreenFX and PulseViz

I boldly assume that `python` links to `python3` and `python-pip` and `python-venv` are installed.
Distributions such as Ubuntu or Debian might still link `python` to `python2` and offer `python3` as packages etc. so in that case use those.

Expand All @@ -56,12 +58,17 @@ For WLED devices, the following keys are needed:
- `ip`: IP address of the WLED device
- `leds`: The amount of LEDs attached to the device, if you're not sure, use the value specified within WLEDs Config > LED Preferences

Arduino devices need these keys:
- `type`: its "arduino"
Serial devices need these keys:
- `type`: its "serial"
- `path`: path to the device, like '/dev/ttyACM0' on Linux and mac OS, or 'COM3' on Windows
- `baud`: baudrate for communication, defaults to 115200, must match with the client
- `leds`: The amount of LEDs attached to the device, must match with the client

For DualShock 4 controllers, fhe `type` needs to be `dualshock`.
Additionally you'll also have to set the key `device_num` counting up, starting at 1.
With that, you can assign different (custom) cutouts for example to different controllers.
The first parsed usually is the first connected controller, the second parsed the second and so on.

If you wanna use ScreenFX, you'll additionally need to set the key `cutout` with the value being either: `top`, `bottom`, `left`, `right`, `center`, or a custom value as specified in `custom_cutouts.py`. This will reflect the screen area projected onto the LEDs.

Optionally you can also set these keys:
Expand All @@ -70,17 +77,7 @@ Optionally you can also set these keys:
- `brightness`: float value between 0 and 1, sets the brightness of the LEDs where 0 is off and 1 is full brightness. Defaults to 1, but lower values like 0.75 may offer a more accurate color representation on LEDs like the ws2812 strips.
- `port`: number, sets a custom port for the IP. defaults to `21324`, which is the default for wled.

Razer keyboards take the keys `enabled`, `flip`, `brightness` and `cutout`, with exactly the same specification as for WLED above. the type has to be `razer` however.

The same applies to DualShock 4 controllers minus `flip` (since there's only 1 LED anyway).
The `type` needs to be `ds4` here. Additionally you'll also have to set the key `device_num` counting up, starting at 1.
With that, you can assign different (custom) cutouts for example to different controllers.
The first parsed usually is the first connected controller, the second parsed the second and so on.

## Notes

The razer support relies on openrazer (e.g. using the Arch Linux AUR package `python-openrazer`). My implementation currently is very hacky and slow though.

For ds4 support, you need to first copy the `ds4perm` file from this repo to /opt, then run `sudo chmod +x /opt/ds4perm` to make it executable. Then you'll need to copy the `10-local.rules`, also from this repo, to `/etc/udev/rules.d/`

Just to be safe, run `sudo udevadm control --reload` to reload udev rules.
Expand Down
61 changes: 0 additions & 61 deletions benchmark.py

This file was deleted.

8 changes: 1 addition & 7 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,8 @@
"flip": false,
"cutout": "bottom"
},
"keyboard": {
"enabled": false,
"type": "razer",
"flip": false,
"cutout": "bottom"
},
"ps4": {
"type": "ds4",
"type": "dualshock",
"device_num": 1,
"cutout": "center"
}
Expand Down
3 changes: 3 additions & 0 deletions devices/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .serial import Serial
from .dualshock import DualShock
from .wled import WLED
54 changes: 54 additions & 0 deletions devices/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import numpy as np
from math import sqrt

__all__ = [
'Device',
]


class Device:
"""
Generic class for devices
"""

def __init__(self, device, *args, **kwargs):
"""
common attributes
"""
self.name = device.get('name')
self.enabled = device.get('enabled')

self.brightness = device.get('brightness')
self.flip = device.get('flip')
self.color_correction = device.get('color_correction')
self.leds = device.get('leds', 1)

def process_data(self, data):
"""
converts a list of rgb values into something device-compatible
:param data: a list of rgb values
"""

if self.flip:
data = np.flip(data, axis=0)

flat_data = np.array([
self.color_correction(np.array(value) * self.brightness)
for value in data
]).astype(int).ravel()

if self.color_correction:
flat_data = self.color_correction(flat_data)

return flat_data

def loop(self, data):
"""
Function that will be repeatedly called by a threadloop.
It should do the necessary work to get the device to display the sent data.
:param data: 2d array containing a list of rgb values for the target device
"""

def __str__(self):
return self.name
60 changes: 60 additions & 0 deletions devices/dualshock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import glob

from .device import Device


__all__ = [
'DualShock',
]


class DualShock(Device):
ds4_paths = {
counter + 1: path
for counter, path in enumerate(glob.glob('/sys/class/leds/0005:054C:05C4.*:global'))
}

def __init__(self, device, *args, **kwargs):
super().__init__(device, *args, **kwargs)

device_num = device.get('device_num')
self.path = self.ds4_paths.get(device_num)

if not self.path:
print(f'WARNING: DualShock Controller with device_num {device_num} specified,')
print(f'but there is no device path available for it. Disabling it.')
self.enabled = False

def set_dualshock_color(self, color):
"""
Sends an rgb color to a DualShock 4 controller for its lightbar
:param color: rgb value that is sent to the lightbar
"""
red, green, blue = color

# TODO improve the path handling. [:-6 ain't the best solution]
red_path = f'{self.path[:-6]}red/brightness'
green_path = f'{self.path[:-6]}green/brightness'
blue_path = f'{self.path[:-6]}blue/brightness'

r = open(red_path, 'w')
r.write(str(red))
r.close()

g = open(green_path, 'w')
g.write(str(green))
g.close()

b = open(blue_path, 'w')
b.write(str(blue))
b.close()

def loop(self, data):
"""
Function that will be repeatedly called by a threadloop.
:param data: 2d array containing a list of rgb values for the target device
"""

# we get a list of rgb values which only contains one entry (since there is only one LED), so we grab that
self.set_dualshock_color(data[0])
34 changes: 34 additions & 0 deletions devices/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import serial
import numpy as np

from .device import Device


__all__ = [
'Serial',
]


class Serial(Device):

def __init__(self, device, *args, **kwargs):
super().__init__(device, *args, **kwargs)

path = device.get('path')

try:
self.serial_device = serial.Serial(path, device.get('baud'))
except serial.serialutil.SerialException:
print(f'WARNING: Serial device path "{path}" invalid. Disabling it.')
self.enabled = False

def set_serial_strip(self, data):
"""
Sends an array of colors to a Serial device
:param data: the color value in rgb
"""
byte_data = bytes(np.array(data).ravel().tolist())
self.serial_device.write(byte_data)

def loop(self, data):
self.set_serial_strip(data)
26 changes: 26 additions & 0 deletions devices/wled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import socket

from .device import Device


__all__ = [
'WLED',
]


class WLED(Device):

def __init__(self, device, *args, **kwargs):
super().__init__(device, *args, **kwargs)

self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.ip = device.get('ip')
self.port = device.get('port')

def set_wled_strip(self, data):
byte_data = bytes([2, 5, *data.ravel().astype(int).tolist()])

self.sock.sendto(byte_data, (self.ip, self.port))

def loop(self, data):
self.set_wled_strip(data)
1 change: 1 addition & 0 deletions fxmodes/PulseViz
Submodule PulseViz added at 108d7f
1 change: 1 addition & 0 deletions fxmodes/ScreenFX
Submodule ScreenFX added at 68425b
8 changes: 0 additions & 8 deletions fxmodes/__init__.py

This file was deleted.

5 changes: 0 additions & 5 deletions fxmodes/pulseviz/__init__.py

This file was deleted.

0 comments on commit 3b99f95

Please sign in to comment.