Ambilight w oparciu o Javę i Arduino

Podświetlenie z tyłu ekranu, które można znaleźć głównie w telewizorach produkowanych w ostatnich latach sprawiają, że oglądanie filmów dostarcza jeszcze lepszych doznań. Technologią, która najczęściej kojarzona jest z tym rozwiązaniem jest Ambilight od firmy Philips, ale jak się okazuje podobny efekt można osiągnąć wykorzystując połączenie Arduino, Javy i Processing. Najważniejszym elementem, który potrzebujemy w naszym układzie jest adresowalny pasek LED, który można znaleźć na stronie Adafruitlub poszukać podobnego u naszych przyjaciół z dalekiego wschodu w serwisach takich jak AliExpress.

Układ

Ponieważ diody led na pasku mają bardzo niewielki pobór mocy, to w sytuacji, gdy znajduje się na nim nie więcej niż 30 diod, możemy je zasilać bezpośrednio z Arduino bez konieczności podłączania zewnętrznego zasilacza. Dzięki temu faktowi do złożenia całości potrzebujemy jedynie 3 kabli podłączonych do VCC, GND oraz jednego pinu PWM. Problemy z zasilaniem mogłyby się pojawić przy zasilaniu w chwilach świecenia z pełną jasnością, jednak ograniczymy to w kodzie programu.

arduino ambilight

Tak wygląda działanie po wgraniu testowego programu:

Obliczanie kolorów i wysyłanie danych - Java

Program do wysyłania danych napiszemy w Javie. Jego zadaniem będzie obliczenie kolorów poszczególnych diod na podstawie tego co znajduje się na ekranie, a następnie przesłanie danych do Arduino poprzez serial port. Tak jak w poprzednim wpisie o Arduino tworzymy nowy projekt i podpinamy bibliotekę RxTx niezbędną do komunikacji z płytką. Dla prostoty kod całej aplikacji umieścimy w jednej klasie:

import gnu.io.CommPortIdentifier;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Enumeration;

import javax.imageio.ImageIO;

public class Ambilight {

	// prędkość transmisji, timeout, liczba ledów
	public static final int DATA_RATE = 9600;
	public static final int TIMEOUT = 2000;
	//opóźnienie pomiędzy przesyłaniem danych ms
	private static final long DELAY = 10;
	//liczba ledów
	public static final int LEDS_NUM = 30;
	//ilość ledów na sekcję
	public static final int LEDS_PER_SECTION = 3;
	//podział na sekcje dla lepszej wydajności
	public static final int SECTIONS = LEDS_NUM / LEDS_PER_SECTION;

	// rozdzielczość
	public static final int X_RES = 1920;
	public static final int Y_RES = 1080;
	//rozmiary sekcji
	public static final int SECT_WIDTH = X_RES / SECTIONS;
	public static final int SECT_HEIGHT = Y_RES;
	// dla poprawy wydajności nie liczymy średniej ze wszystkich pixeli
	// przeskakujemy co 4 lub więcej
	public static final int SECT_SKIP = 10;

	// odczyt danych ekranu
	private Robot robot;

	// komunikacja z arduino
	private SerialPort serial;
	private OutputStream output;

	/**
	 * Inicjalizujemy połączenie z arduino
	 */
	private void initSerial() {
		// znajdujemy port, do którego podpięte jest Arduino
		CommPortIdentifier serialPortId = null;
		Enumeration enumComm = CommPortIdentifier.getPortIdentifiers();
		while (enumComm.hasMoreElements() && serialPortId == null) {
			serialPortId = (CommPortIdentifier) enumComm.nextElement();
		}

		try {
			serial = (SerialPort) serialPortId.open(this.getClass().getName(),
					TIMEOUT);
			serial.setSerialPortParams(DATA_RATE, SerialPort.DATABITS_8,
					SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
		} catch (PortInUseException | UnsupportedCommOperationException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Inicjalizujemy robota do odczytywania danych z ekranu
	 */
	private void initRobot() {
		try {
			robot = new Robot();
		} catch (AWTException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Inicjalizujemy wyjście do Arduino
	 */
	private void initOutputStream() {
		try {
			output = serial.getOutputStream();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Odczytujemy kolory z ekranu
	 * 
	 * @return tablica kolorów, które mają być wysłane do paska led
	 */
	private Color[] getColors() {
		BufferedImage screen = robot.createScreenCapture(new Rectangle(
				new Dimension(X_RES, Y_RES)));
		Color[] leds = new Color[SECTIONS];

		for (int led = 0; led < SECTIONS; led++) {
			BufferedImage section = screen.getSubimage(led * SECT_WIDTH, 0, SECT_WIDTH, SECT_HEIGHT);
			Color sectionAvgColor = getAvgColor(section);
			leds[led] = sectionAvgColor;
		}
		return leds;
	}

	/**
	 * Obliczanie średniego koloru na podstawie przekazanej sekcji
	 */
	private Color getAvgColor(BufferedImage imgSection) {
		int width = imgSection.getWidth();
		int height = imgSection.getHeight();
		int r = 0, g = 0, b = 0;
		int loops = 0;
		for (int x = 0; x < width; x += SECT_SKIP) {
			for (int y = 0; y < height; y += SECT_SKIP) {
				int rgb = imgSection.getRGB(x, y);
				Color color = new Color(rgb);
				r += color.getRed();
				g += color.getGreen();
				b += color.getBlue();
				loops++;
			}
		}
		r = r / loops;
		g = g / loops;
		b = b / loops;
		return new Color(r, g, b);
	}

	/**
	 * Wysyłamy dane do Arduino i paska led
	 */
	private void sendColors(Color[] leds) {
		try {
			output.write(0xff);
			for (int i = 0; i < SECTIONS; i++) {
				output.write(leds[i].getRed());
				output.write(leds[i].getGreen());
				output.write(leds[i].getBlue());
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	//nieskończona pętla programu
	private void loop() {
		while (true) {
			Color[] leds = getColors();
			sendColors(leds);
			try {
				Thread.sleep(DELAY);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		Ambilight ambi = new Ambilight();
		ambi.initRobot();
		ambi.initSerial();
		ambi.initOutputStream();
		ambi.loop();
		ambi.clean();
	}
	
	private void clean() {
		if(output != null)
			try {
				output.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		if(serial != null)
			serial.close();
	}
}

Najważniejsze w całym kodzie jest wykorzystanie klasy Robot do wykonywania co chwilę zrzutów ekranu i na ich podstawie obliczanie koloru kolejnych sekcji ekranu. Ekran dzielimy na 10 części, więc każde 3 kolejne diody na pasku będą świeciły tym samym kolorem. W przypadku obliczania koloru każdej diody osobno moglibyśmy nie nadążać z wysyłaniem kolejnych danych chyba, że wymyślilibyśmy efektywniejszą metodę uśredniania kolorów. Dane do płytki wysyłamy poprzez serial port wykorzystując obiekt typu OutputStream i jego metodę write(). Jedyne zmiany jakie należy wykonać w kodzie, aby działał na innym komputerze to zmiana stałych odpowiadających za rozdzielczość ekranu oraz ilość diod LED. Jeżeli nie dysponujesz paskiem typu Neopixel, ale masz zwykłe diody RGB, to bez większego problemu powinno Ci się udać złożyć układ, który będzie działał niemal identycznie. Będzie trzeba dokonać jedynie stosownych zmian w kodzie programu wgrywanego na Arduino.

Odbieranie danych i przekazywanie do paska led - Arduino

Ostatni brakujący element to aplikacja, która odbierze dane i zapali odpowiednie diody na pasku. Żeby to zrobić musimy pobrać bibliotekę Neopixel i zaimportować ją do edytora. Można ją znaleźć na GitHubie.

#include <Adafruit_NeoPixel.h>
#define PIN 6 //pin PWM
#define LEDS 30 //liczba ledów
#define SECTIONS 10 //liczba sekcji
#define LEDS_PER_SECTION 3 //ile ledów na sekcję
#define DELAY 10 //przerwa
//konfiguracja paska
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LEDS, PIN, NEO_GRB + NEO_KHZ800);
//tablice kolorów
int r[SECTIONS];
int g[SECTIONS];
int b[SECTIONS];

void setup() {
  Serial.begin(9600);
  strip.begin();
  //ustawiamy niższą jasność(max 255), aby 
  //nie mieć problemów z zasilaniem
  strip.setBrightness(128);
  strip.show();
}

void loop() {
  //odbieramy dane w kolejności w jakiej były wysłane
  if(Serial.available() > 30) {
    if(Serial.read() == 0xff) {
      for(int i = 0; i<SECTIONS; i++) {
        r[i] = Serial.read();
        g[i] = Serial.read();
        b[i] = Serial.read();
      }
    }
  }

  //zapalamy diody na pasku
  for(int i=0; i<SECTIONS; i+=3) {
    strip.setPixelColor(i*LEDS_PER_SECTION, r[i], g[i], b[i]);
    strip.setPixelColor(i*LEDS_PER_SECTION+1, r[i], g[i], b[i]);
    strip.setPixelColor(i*LEDS_PER_SECTION+2, r[i], g[i], b[i]);
  }
  strip.show();
  delay(DELAY);
}

Jak widać, kod jest bardzo prosty i sprowadza się do odebrania kolejnych danych z serial portu i zapalania diod w pętli.

Test

Najpierw postanowiłem sprawdzić, czy pasek faktycznie poprawnie reaguje na zmieniające się kolory, co widać na poniższym filmie.

Efekt końcowy

Efekt końcowy jak na niecałą godzinę zabawy (łącznie z przenoszeniem monitora i kombinowaniem z montażem ledów) jest moim zdaniem znakomity. Jak widać Arduino jak na tak proste urządzenie, daje wielkie możliwości, a Java sprawdza się bardzo dobrze jako wsparcie.

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.