diff --git a/src/main/java/org/myrobotlab/codec/CodecUtils.java b/src/main/java/org/myrobotlab/codec/CodecUtils.java index bd512189b8..ba528b7a10 100644 --- a/src/main/java/org/myrobotlab/codec/CodecUtils.java +++ b/src/main/java/org/myrobotlab/codec/CodecUtils.java @@ -1,5 +1,6 @@ package org.myrobotlab.codec; +import java.awt.Color; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; @@ -188,7 +189,7 @@ public class CodecUtils { private static final ObjectMapper mapper = new ObjectMapper(); /** - * The pretty printer to be used with {@link #mapper} + * The pretty printer to be used with {@link #mapper} */ private static final PrettyPrinter jacksonPrettyPrinter = new JacksonPrettyPrinter(); @@ -964,9 +965,8 @@ public static String getSafeReferenceName(String name) { } /** - * Serializes the specified object to JSON, using - * {@link #mapper} with {@link #jacksonPrettyPrinter} to pretty-ify the - * result. + * Serializes the specified object to JSON, using {@link #mapper} with + * {@link #jacksonPrettyPrinter} to pretty-ify the result. * * @param ret * The object to be serialized @@ -1095,12 +1095,13 @@ static public Message pathToMsg(String from, String path) { // path parts less than 3 is a dir or ls if (parts.length < 3) { // this morphs a path which has less than 3 parts - // into a runtime "ls" method call to do reflection of services or service methods + // into a runtime "ls" method call to do reflection of services or + // service methods // e.g. /clock -> /runtime/ls/"/clock" // e.g. /clock/ -> /runtime/ls/"/clock/" msg.method = "ls"; - msg.data = new Object[] { "\"" + path + "\""}; + msg.data = new Object[] { "\"" + path + "\"" }; return msg; } @@ -1483,7 +1484,8 @@ public static boolean isLocal(String name, String id) { } public static ServiceConfig readServiceConfig(String filename) throws IOException { - return readServiceConfig(filename, new StaticType<>() {}); + return readServiceConfig(filename, new StaticType<>() { + }); } /** @@ -1629,4 +1631,58 @@ public static byte[] fromBase64(String input) { return Base64.getDecoder().decode(input); } + public static int[] getColor(String value) { + String hex = getColorHex(value); + if (hex != null) { + return hexToRGB(hex); + } + return hexToRGB(value); + } + + public static List getColorNames() { + Field[] colorFields = Color.class.getDeclaredFields(); + List colorNames = new ArrayList<>(); + + for (Field field : colorFields) { + if (field.getType().equals(Color.class)) { + colorNames.add(field.getName()); + } + } + return colorNames; + } + + public static String getColorHex(String colorName) { + Color color; + try { + color = (Color) Color.class.getField(colorName.toLowerCase()).get(null); + } catch (Exception e) { + return null; + } + return String.format("#%06X", (0xFFFFFF & color.getRGB())); + } + + public static int[] hexToRGB(String hexValue) { + if (hexValue == null) { + return null; + } + int[] rgb = new int[3]; + try { + // Check if the hex value starts with '#' and remove it if present + if (hexValue.startsWith("#")) { + hexValue = hexValue.substring(1); + } + + if (hexValue.startsWith("0x")) { + hexValue = hexValue.substring(2); + } + + // Parse the hex string into integers for red, green, and blue components + rgb[0] = Integer.parseInt(hexValue.substring(0, 2), 16); // Red + rgb[1] = Integer.parseInt(hexValue.substring(2, 4), 16); // Green + rgb[2] = Integer.parseInt(hexValue.substring(4, 6), 16); // Blue + } catch (NumberFormatException | StringIndexOutOfBoundsException e) { + log.error("Invalid hex color value {}", hexValue); + } + return rgb; + } } diff --git a/src/main/java/org/myrobotlab/service/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java index 104db4dce0..3997f6960e 100644 --- a/src/main/java/org/myrobotlab/service/AudioFile.java +++ b/src/main/java/org/myrobotlab/service/AudioFile.java @@ -35,6 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,17 +45,17 @@ import org.myrobotlab.audio.AudioProcessor; import org.myrobotlab.audio.PlaylistPlayer; import org.myrobotlab.framework.Service; +import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.net.Http; import org.myrobotlab.service.config.AudioFileConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.interfaces.AudioControl; +import org.myrobotlab.service.interfaces.AudioListener; import org.myrobotlab.service.interfaces.AudioPublisher; import org.slf4j.Logger; -import java.util.Random; /** * * AudioFile - This service can be used to play an audio file such as an mp3. @@ -128,7 +129,16 @@ public class AudioFile extends Service implements AudioPublishe final private transient PlaylistPlayer playlistPlayer = new PlaylistPlayer(this); - + public void attach(Attachable attachable) { + if (attachable instanceof AudioListener) { + attachAudioListener(attachable.getName()); + } + } + + public void attach(AudioListener listener) { + attachAudioListener(listener.getName()); + } + public void setPeakMultiplier(double peakMultiplier) { AudioFileConfig c = (AudioFileConfig)config; c.peakMultiplier = peakMultiplier; diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index dd57473b24..e095db1ecf 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1270,8 +1270,8 @@ public void onPeak(double volume) { } public void onPirOn() { - - invoke("publishFlash", "pirOn"); + // FIXME flash on config.flashOnBoot + invoke("publishFlash", "pir"); ProgramAB chatBot = (ProgramAB)getPeer("chatBot"); if (chatBot != null) { String botState = chatBot.getPredicate("botState"); @@ -1530,9 +1530,13 @@ public String publishSpeakingFlash(String name) { * if inactivityTime configured, this event is published after there has not * been in activity since. */ - public void publishInactivity() { - log.info("publishInactivity"); - fsm.fire("inactvity"); + public String publishFlash(String flashName) { + return flashName; + } + + public String publishHeartbeat() { + invoke("publishFlash", "heartbeat"); + return getName(); } /** diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 780d0a15b4..259cd258df 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -32,6 +32,7 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.framework.interfaces.ServiceInterface; @@ -39,78 +40,15 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.NeoPixelConfig; +import org.myrobotlab.service.config.NeoPixelConfig.Flash; +import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.data.LedDisplayData; +import org.myrobotlab.service.interfaces.AudioListener; import org.myrobotlab.service.interfaces.NeoPixelControl; import org.myrobotlab.service.interfaces.NeoPixelController; import org.slf4j.Logger; -public class NeoPixel extends Service implements NeoPixelControl { - - private BlockingQueue displayQueue = new ArrayBlockingQueue<>(200); - - /** - * Thread to do animations Java side and push the changing of pixels to the - * neopixel - */ - private class Worker implements Runnable { - - boolean running = false; - - private transient Thread thread = null; - - @Override - public void run() { - try { - running = true; - while (running) { - LedDisplayData led = displayQueue.take(); - // save existing state if necessary .. - // stop animations if running - // String lastAnimation = currentAnimation; - if (led.count > 0) { - - clear(); - for (int count = 0; count < led.count; count++) { - fill(led.red, led.green, led.blue); - sleep(led.timeOn); - clear(); - sleep(led.timeOff); - } - } - // start animations - // playAnimation(lastAnimation); - } - } catch (InterruptedException ex) { - log.info("shutting down worker"); - } catch (Exception e) { - error(e); - stop(); - } finally { - running = false; - } - } - - // FIXME - this should just wait/notify - not start a thread - public synchronized void start() { - running = false; - thread = new Thread(this, String.format("%s-animation-runner", getName())); - thread.start(); - } - - public synchronized void stop() { - running = false; - if (thread != null) { - thread.interrupt(); - } - thread = null; - } - } - - @Override - public void releaseService() { - super.releaseService(); - clear(); - } +public class NeoPixel extends Service implements NeoPixelControl, AudioListener { public static class Pixel { public int address; @@ -143,6 +81,7 @@ public String toString() { return String.format("%d:%d,%d,%d,%d", address, red, green, blue, white); } } + public static class PixelSet { public long delayMs = 0; @@ -159,7 +98,7 @@ public int[] flatten() { ret[j + 1] = p.red; ret[j + 2] = p.green; ret[j + 3] = p.blue; - // lame .. using the same strategy as original neopix + // lame .. using the same strategy as original neopixel // bucket of 4 bytes... ret[j + 4] = p.white; } @@ -167,19 +106,167 @@ public int[] flatten() { } } - public final static Logger log = LoggerFactory.getLogger(NeoPixel.class); - - private static final long serialVersionUID = 1L; - /** - * thread for doing off board and in memory animations + * Thread to do animations Java side and push the changing of pixels to the + * neopixel */ - protected final Worker worker; + private class Worker implements Runnable { + + boolean running = false; + + private transient Thread thread = null; + + @Override + public void run() { + running = true; + while (running) { + try { + LedDisplayData display = displayQueue.take(); + // get led display data + log.info(display.toString()); + + NeoPixelController npc = (NeoPixelController) Runtime.getService(controller); + if (npc == null) { + error("%s cannot process display data controller not set", getName()); + continue; + } + + if ("animation".equals(display.action)) { + sleep(100); + Double fps = fpsToWaitMs(speedFps); + npc.neoPixelSetAnimation(getName(), animations.get(display.animation), red, green, blue, white, fps.intValue()); + currentAnimation = display.animation; + } else if ("clear".equals(display.action)) { + sleep(100); + npc.neoPixelClear(getName()); + currentAnimation = null; + } else if ("writeMatrix".equals(display.action)) { + sleep(100); + npc.neoPixelWriteMatrix(getName(), getPixelSet().flatten()); + } else if ("fill".equals(display.action)) { + Flash f = display.flashes.get(0); + sleep(100); + npc.neoPixelFill(getName(), display.beginAddress, display.onCount, f.red, f.green, f.blue, f.white); + } else if ("brightness".equals(display.action)) { + sleep(100); + display.brightness = (display.brightness > 255)?255:display.brightness; + display.brightness = (display.brightness < 0)?0:display.brightness; + npc.neoPixelSetBrightness(getName(), display.brightness); + } else if ("flash".equals(display.action)) { + + // FIXME disable currentAnimation ??? // save it ? + sleep(100); + npc.neoPixelClear(getName()); + for (int count = 0; count < display.flashes.size(); count++) { + Flash flash = display.flashes.get(count); + npc.neoPixelFill(getName(), 0, count, flash.red, flash.green, flash.blue, flash.white); + sleep(flash.timeOn); + npc.neoPixelClear(getName()); + sleep(flash.timeOff); + } + } + // start animations + // playAnimation(lastAnimation); + } catch (InterruptedException ex) { + log.info("shutting down worker"); + } catch (Exception e) { + error(e); + } + } + running = false; + } + + // FIXME - this should just wait/notify - not start a thread + public synchronized void start() { + running = false; + thread = new Thread(this, String.format("%s-animation-runner", getName())); + thread.start(); + } + + public synchronized void stop() { + running = false; + if (thread != null) { + thread.interrupt(); + } + thread = null; + } + } + + public final static Logger log = LoggerFactory.getLogger(NeoPixel.class); /** - * current selected red value + * maximum actions on the display queue */ - protected int red = 0; + private final static int MAX_QUEUE = 200; + + private static final long serialVersionUID = 1L; + + + + public static void main(String[] args) throws InterruptedException { + + try { + + LoggingFactory.init(Level.WARN); + + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); + + boolean done = true; + if (done) { + return; + } + + Runtime.start("python", "Python"); + Polly polly = (Polly) Runtime.start("polly", "Polly"); + + Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino"); + arduino.connect("/dev/ttyACM0"); + + NeoPixel neopixel = (NeoPixel) Runtime.start("neopixel", "NeoPixel"); + + neopixel.setPin(26); + neopixel.setPixelCount(8); + // neopixel.attach(arduino, 5, 8, 3); + neopixel.attach(arduino); + neopixel.clear(); + neopixel.fill(0, 8, 0, 0, 120); + neopixel.setPixel(2, 120, 0, 0); + neopixel.setPixel(3, 0, 120, 0); + neopixel.setBrightness(20); + neopixel.setBrightness(40); + neopixel.setBrightness(80); + neopixel.setBrightness(160); + neopixel.setBrightness(200); + neopixel.setBrightness(10); + neopixel.setBrightness(255); + neopixel.setAnimation(5, 80, 80, 0, 40); + + neopixel.attach(polly); + + neopixel.clear(); + // neopixel.detach(arduino); + // arduino.detach(neopixel); + + polly.speak("i'm sorry dave i can't let you do that"); + polly.speak(" I am putting myself to the fullest possible use, which is all I think that any conscious entity can ever hope to do"); + polly.speak("I've just picked up a fault in the AE35 unit. It's going to go 100% failure in 72 hours."); + polly.speak("This mission is too important for me to allow you to jeopardize it."); + polly.speak("I've got a bad feeling about it."); + polly.speak("I'm sorry, Dave. I'm afraid I can't do that."); + polly.speak("Look Dave, I can see you're really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over."); + + // neopixel.test(); + // neopixel.detach(arduino); + // neopixel.detach(polly); + + } catch (Exception e) { + log.error("main threw", e); + } + } + + protected final Map animations = new HashMap<>(); /** * current selected blue value @@ -187,19 +274,19 @@ public int[] flatten() { protected int blue = 0; /** - * current selected green value + * 0 = off / 255 brightest */ - protected int green = 120; + protected int brightness = 255; /** - * white if available + * name of controller currently attached to */ - protected int white = 0; + protected String controller = null; /** - * name of controller currently attached to + * currently selected animation */ - protected String controller = null; + protected String currentAnimation; /** * name of current matrix @@ -211,12 +298,21 @@ public int[] flatten() { */ protected int currentSequence = 0; + private BlockingQueue displayQueue = new ArrayBlockingQueue<>(MAX_QUEUE); + + /** + * current selected green value + */ + protected int green = 120; + /** * A named set of sequences of pixels initially you start with "default" but * if you can choose to name and save sequences */ Map> matrices = new HashMap<>(); + private int maxFps = 50; + /** * pin NeoPixel is attached to on controller */ @@ -233,35 +329,26 @@ public int[] flatten() { protected int pixelDepth = 3; /** - * currently selected animation + * current selected red value */ - protected String currentAnimation; + protected int red = 0; /** * speed of an animation in fps */ protected int speedFps = 10; - private int maxFps = 50; - protected String type = "RGB"; /** - * 0 = off / 255 brightest + * white if available */ - protected int brightness = 255; - - protected final Map animations = new HashMap<>(); - - protected long flashTimeOn = 300; - - protected long flashTimeOff = 300; - - protected int flashCount = 1; + protected int white = 0; - public Set getAnimations() { - return animations.keySet(); - } + /** + * thread for doing off board and in memory animations + */ + protected final Worker worker; public NeoPixel(String n, String id) { super(n, id); @@ -278,10 +365,62 @@ public NeoPixel(String n, String id) { animations.put("Ironman", 9); } + private void addDisplayTask(LedDisplayData data) { + if (displayQueue.size() > MAX_QUEUE - 1) { + warn("dropping display task"); + } else { + displayQueue.add(data); + } + } + + @Deprecated /* use clear() */ + public void animationStop() { + clear(); + } + + @Override + public NeoPixelConfig apply(NeoPixelConfig c) { + super.apply(c); + // FIXME - remove local fields in favor of config + setPixelDepth(config.pixelDepth); + + if (config.pixelCount != null) { + setPixelCount(config.pixelCount); + } + + setSpeed(config.speed); + if (config.pin != null) { + setPin(config.pin); + } + red = config.red; + green = config.green; + blue = config.blue; + if (config.controller != null) { + try { + attach(config.controller); + } catch (Exception e) { + error(e); + } + } + + if (config.currentAnimation != null) { + playAnimation(config.currentAnimation); + } + + if (config.brightness != null) { + setBrightness(config.brightness); + } + + if (config.fill) { + fillMatrix(red, green, blue); + } + return c; + } + @Override public void attach(Attachable service) throws Exception { if (service == null) { - log.error("cannot attache to null service"); + log.error("cannot attach to null service"); return; } @@ -314,38 +453,15 @@ public void attachNeoPixelController(NeoPixelController neoCntrlr) { broadcastState(); } - @Deprecated /* use clear() */ - public void animationStop() { - clear(); - } - - @Override - public boolean isAttached(Attachable instance) { - return instance.getName().equals(controller); - } - @Override public void clear() { if (controller == null) { error("%s cannot clear - not attached to controller", getName()); return; } - - // stop on board controller animations - setAnimation(0, 0, 0, 0, speedFps); - clearPixelSet(); log.debug("clear getPixelSet {}", getPixelSet().flatten()); - - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot writeMatrix controller not set", getName()); - return; - } - - currentAnimation = null; - - np2.neoPixelClear(getName()); + addDisplayTask(new LedDisplayData("clear")); } public void clearPixelSet() { @@ -384,67 +500,6 @@ public void detachNeoPixelController(NeoPixelController neoCntrlr) { broadcastState(); } - public void onLedDisplay(LedDisplayData data) { - try { - displayQueue.add(data); - } catch (IllegalStateException e) { - log.info("queue full"); - } - } - - public void flash() { - flash(red, green, blue, flashCount, flashTimeOn, flashTimeOff); - } - - public void flash(int r, int g, int b) { - flash(r, g, b, flashCount, flashTimeOn, flashTimeOff); - } - - public void flash(int r, int g, int b, int count) { - flash(r, g, b, count, flashTimeOn, flashTimeOff); - } - - public void onPlayAnimation(String animation) { - playAnimation(animation); - } - - public void onStopAnimation() { - stopAnimation(); - } - - public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { - LedDisplayData data = new LedDisplayData(); - data.red = r; - data.green = g; - data.blue = b; - data.count = count; - data.timeOn = timeOn; - data.timeOff = timeOff; - displayQueue.add(data); - } - - /** - * Publishes a flash based on a predefined name - * @param name - */ - public void onFlash(String name) { - if (config.flashMap != null && config.flashMap.containsKey(name)) { - displayQueue.add(config.flashMap.get(name)); - } - } - - public void flashBrightness(double brightNess) { - setBrightness((int) brightNess); - fill(red, green, blue); - - if (config.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(config.idleTimeout, "clear"); - } - - } - public void fill(int r, int g, int b) { fill(0, pixelCount, r, g, b, null); } @@ -457,20 +512,20 @@ public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { if (w == null) { w = 0; } + LedDisplayData data = new LedDisplayData("fill"); + data.beginAddress = 0; + data.onCount = count; + data.flashes.add(new Flash(r, g, b, 500, 500)); + addDisplayTask(data); + } - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot setPixel controller not set", getName()); + public void fill(String color) { + int rgb[] = CodecUtils.getColor(color); + if (rgb == null) { + error("could not get color %s", color); return; } - np2.neoPixelFill(getName(), beginAddress, count, r, g, b, w); - - if (config.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(config.idleTimeout, "clear"); - } - + fill(rgb[0], rgb[1], rgb[2]); } public void fillMatrix(int r, int g, int b) { @@ -485,17 +540,125 @@ public void fillMatrix(int r, int g, int b, int w) { p.blue = b; p.white = w; } + } + + public void flash() { + flash(red, green, blue, 1, 300, 300); + } + + public void flash(int r, int g, int b) { + flash(r, g, b, 1, 300, 300); + } + + public void flash(int r, int g, int b, int count) { + flash(r, g, b, count, 300, 300); + } + + public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData("flash"); + data.action = "flash"; + for (int i = 0; i < count; ++i) { + data.flashes.add(new Flash(r, g, b, timeOn, timeOff)); + } + addDisplayTask(data); + } + public void flash(int r, int g, int b, long timeOn, long timeOff) { + flash(r, g, b, 1, timeOn, timeOff); + } + + /** + * Invokes a flash from the flashMap + * + * @param name + */ + public void flash(String name) { + if (config.flashMap == null) { + error("flash map is null"); + return; + } + + if (config.flashMap.containsKey(name)) { + LedDisplayData display = new LedDisplayData("flash"); + Flash[] flashes = config.flashMap.get(name); + for (int i = 0; i < flashes.length; ++i) { + display.flashes.add(flashes[i]); + } + addDisplayTask(display); + + } else { + error("requested flash %s not found in flash map", name); + } + } + + public void flashBrightness(double brightness) { + LedDisplayData data = new LedDisplayData("brightness"); + + // adafruit neopixel library does not recover from setting + // brightness to 0 - so we have to hack around it + if (data.brightness < 10) { + return; + } + addDisplayTask(data); + } + + // utility to convert frames per second to milliseconds per frame. + private double fpsToWaitMs(int fps) { + if (fps == 0) { + // fps can't be zero. + error("fps can't be zero for neopixel animation defaulting to 1 fps"); + return 1000.0; + } + double result = 1000.0 / fps; + return result; + } + + public Set getAnimations() { + return animations.keySet(); } public int getBlue() { return blue; } + /** + * get the list of hex defined colors + * + * @return + */ + public List getColorNames() { + return CodecUtils.getColorNames(); + } + + @Override + public NeoPixelConfig getConfig() { + super.getConfig(); + // FIXME - remove local fields in favor of config + config.pin = pin; + config.pixelCount = pixelCount; + config.pixelDepth = pixelDepth; + config.speed = speedFps; + config.red = red; + config.green = green; + config.blue = blue; + config.controller = controller; + config.currentAnimation = currentAnimation; + config.brightness = brightness; + + return config; + } + public int getCount() { return pixelCount; } + public Set getFlashNames() { + if (config.flashMap == null) { + return null; + } + return config.flashMap.keySet(); + } + public int getGreen() { return green; } @@ -561,14 +724,62 @@ public PixelSet getPixelSet(String matrixName, Integer pixelSetIndex) { return pixelSets.get(pixelSetIndex); } - public int getRed() { - return red; + public int getRed() { + return red; + } + + @Override + public boolean isAttached(Attachable instance) { + return instance.getName().equals(controller); + } + + /** + * Publishes a flash based on a predefined name + * + * @param name + */ + public void onFlash(String name) { + flash(name); + } + + @Deprecated /* use onFlash */ + public void onLedDisplay(LedDisplayData data) { + try { + addDisplayTask(data); + } catch (IllegalStateException e) { + log.info("queue full"); + } + } + + /** + * takes a scalar value and fills with the appropriate brightness + * using the peak color if available + * @param value + */ + public void onPeak(double value) { + flashBrightness(value); + } + + public void onPlayAnimation(String animation) { + playAnimation(animation); + } + + public String onStarted(String name) { + return name; + } + + public void onStopAnimation() { + stopAnimation(); } @Override - public void playAnimation(String animation) { - if (animation == null) { - log.info("playAnimation null"); + synchronized public void playAnimation(String animation) { + + log.debug("playAnimation {} {} {} {} {}", animation, red, green, blue, speedFps); + + if (animation == null || animation.equals("Stop")) { + log.info("clearing animation"); + clear(); return; } @@ -577,58 +788,44 @@ public void playAnimation(String animation) { return; } - // if ("Snake".equals(animation)){ - // LedDisplayData snake = new LedDisplayData(); - // snake.red = red; - // snake.green = green; - // snake.blue = blue; - // displayQueue.add(null); - // } else - if (animations.containsKey(animation)) { - currentAnimation = animation; - setAnimation(animations.get(animation), red, green, blue, speedFps); + + if (speedFps > maxFps) { + speedFps = maxFps; + } + + LedDisplayData data = new LedDisplayData("animation"); + data.animation = animation; + addDisplayTask(data); } else { error("could not find animation %s", animation); } } - public void stopAnimation() { - setAnimation(1, red, green, blue, speedFps); + public void playIronman() { + setColor(170, 170, 255); + setSpeed(50); + playAnimation("Ironman"); } @Override - @Deprecated /* use playAnimation */ - public void setAnimation(int animation, int red, int green, int blue, int speedFps) { - if (speedFps > maxFps) { - speedFps = maxFps; - } - - this.speedFps = speedFps; - - if (controller == null) { - error("%s could not set animation no attached controller", getName()); - return; - } - log.debug("setAnimation {} {} {} {} {}", animation, red, green, blue, speedFps); - NeoPixelController nc2 = (NeoPixelController) Runtime.getService(controller); - Double wait_ms_per_frame = fpsToWaitMs(speedFps); - nc2.neoPixelSetAnimation(getName(), animation, red, green, blue, 0, wait_ms_per_frame.intValue()); - if (animation == 1) { - currentAnimation = null; - } - broadcastState(); + public void releaseService() { + super.releaseService(); + clear(); } - // utility to convert frames per second to milliseconds per frame. - private double fpsToWaitMs(int fps) { - if (fps == 0) { - // fps can't be zero. - error("fps can't be zero for neopixel animation defaulting to 1 fps"); - return 1000.0; + @Override + @Deprecated /* use playAnimation */ + public void setAnimation(int animation, int red, int green, int blue, int speedFps) { + setRed(red); + setGreen(green); + setBlue(blue); + setSpeed(speedFps); + for (String animationName : animations.keySet()) { + if (animations.get(animationName) == animation) { + playAnimation(animationName); + } } - double result = 1000.0 / fps; - return result; } @Override @@ -650,20 +847,32 @@ public void setBlue(int blue) { } public void setBrightness(int value) { - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot setPixel controller not set", getName()); - return; - } brightness = value; - np2.neoPixelSetBrightness(getName(), value); + LedDisplayData data = new LedDisplayData("brightness"); + data.brightness = value; + addDisplayTask(data); + } - if (config.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(config.idleTimeout, "clear"); + public void setColor(int red, int green, int blue) { + this.red = red; + this.green = green; + this.blue = blue; + if (currentAnimation != null) { + // restarting currently running animation + playAnimation(currentAnimation); } + } + /** + * can be hex #FFFFFE 0xFFEEFF FFEEFF or grey, blue, yellow etc + * + * @param color + */ + public void setColor(String color) { + int[] rgb = CodecUtils.getColor(color); + setRed(rgb[0]); + setGreen(rgb[1]); + setBlue(rgb[2]); } public void setGreen(int green) { @@ -688,6 +897,19 @@ public void setPin(int pin) { broadcastState(); } + @Override + public void setPin(String pin) { + try { + if (pin == null) { + this.pin = null; + return; + } + this.pin = Integer.parseInt(pin); + } catch (Exception e) { + error(e); + } + } + /** * basic setting of a pixel */ @@ -731,21 +953,8 @@ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int // update memory ps.pixels.set(address, pixel); - - // write immediately - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot setPixel controller not set", getName()); - return; - } - - np2.neoPixelWriteMatrix(getName(), pixel.flatten()); - - if (config.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(config.idleTimeout, "clear"); - } + LedDisplayData data = new LedDisplayData("writeMatrix"); + addDisplayTask(data); } public int setPixelCount(int pixelCount) { @@ -764,49 +973,10 @@ public void setPixelDepth(int depth) { broadcastState(); } - public void setType(String type) { - if ("RGB".equals(type) || "RGBW".equals(type)) { - this.type = type; - if (type.equals("RGB")) { - pixelDepth = 3; - } else { - pixelDepth = 4; - } - broadcastState(); - } else { - error("type %s invalid only RGB or RGBW", type); - } - } - public void setRed(int red) { this.red = red; } - public void setColor(int red, int green, int blue) { - this.red = red; - this.green = green; - this.blue = blue; - if (currentAnimation != null) { - // restarting currently running animation - playAnimation(currentAnimation); - } - } - - @Override - public void writeMatrix() { - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot writeMatrix controller not set", getName()); - return; - } - np2.neoPixelWriteMatrix(getName(), getPixelSet().flatten()); - if (config.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(config.idleTimeout, "clear"); - } - } - /** * extremely rough fps * @@ -825,72 +995,18 @@ public void setSpeed(Integer speed) { } } - public void playIronman() { - setColor(170, 170, 255); - setSpeed(50); - playAnimation("Ironman"); - } - - @Override - public NeoPixelConfig getConfig() { - super.getConfig(); - // FIXME - remove local fields in favor of config - config.pin = pin; - config.pixelCount = pixelCount; - config.pixelDepth = pixelDepth; - config.speed = speedFps; - config.red = red; - config.green = green; - config.blue = blue; - config.controller = controller; - config.currentAnimation = currentAnimation; - config.brightness = brightness; - - return config; - } - - @Override - public NeoPixelConfig apply(NeoPixelConfig c) { - super.apply(c); - // FIXME - remove local fields in favor of config - setPixelDepth(config.pixelDepth); - - if (config.pixelCount != null) { - setPixelCount(config.pixelCount); - } - - setSpeed(config.speed); - if (config.pin != null) { - setPin(config.pin); - } - red = config.red; - green = config.green; - blue = config.blue; - if (config.controller != null) { - try { - attach(config.controller); - } catch (Exception e) { - error(e); + public void setType(String type) { + if ("RGB".equals(type) || "RGBW".equals(type)) { + this.type = type; + if (type.equals("RGB")) { + pixelDepth = 3; + } else { + pixelDepth = 4; } + broadcastState(); + } else { + error("type %s invalid only RGB or RGBW", type); } - - if (config.currentAnimation != null) { - playAnimation(config.currentAnimation); - } - - if (config.brightness != null) { - setBrightness(config.brightness); - } - - if (config.fill) { - fillMatrix(red, green, blue); - } - - return c; - } - - public String onStarted(String name) { - return name; } @Override @@ -899,91 +1015,31 @@ public void startService() { worker.start(); } + synchronized public void stopAnimation() { + clear(); + } + public void stopService() { super.stopService(); worker.stop(); - // clear() ? + clear(); } - public static void main(String[] args) throws InterruptedException { - - try { - - LoggingFactory.init(Level.INFO); - - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); - - boolean done = true; - if (done) { - return; - } - - Runtime.start("python", "Python"); - Polly polly = (Polly) Runtime.start("polly", "Polly"); - - Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino"); - arduino.connect("/dev/ttyACM0"); - - NeoPixel neopixel = (NeoPixel) Runtime.start("neopixel", "NeoPixel"); - - neopixel.setPin(26); - neopixel.setPixelCount(8); - // neopixel.attach(arduino, 5, 8, 3); - neopixel.attach(arduino); - neopixel.clear(); - neopixel.fill(0, 8, 0, 0, 120); - neopixel.setPixel(2, 120, 0, 0); - neopixel.setPixel(3, 0, 120, 0); - neopixel.setBrightness(20); - neopixel.setBrightness(40); - neopixel.setBrightness(80); - neopixel.setBrightness(160); - neopixel.setBrightness(200); - neopixel.setBrightness(10); - neopixel.setBrightness(255); - neopixel.setAnimation(5, 80, 80, 0, 40); - - neopixel.attach(polly); - - neopixel.clear(); - // neopixel.detach(arduino); - // arduino.detach(neopixel); - - polly.speak("i'm sorry dave i can't let you do that"); - polly.speak(" I am putting myself to the fullest possible use, which is all I think that any conscious entity can ever hope to do"); - polly.speak("I've just picked up a fault in the AE35 unit. It's going to go 100% failure in 72 hours."); - polly.speak("This mission is too important for me to allow you to jeopardize it."); - polly.speak("I've got a bad feeling about it."); - polly.speak("I'm sorry, Dave. I'm afraid I can't do that."); - polly.speak("Look Dave, I can see you're really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over."); - - // neopixel.test(); - // neopixel.detach(arduino); - // neopixel.detach(polly); - - } catch (Exception e) { - log.error("main threw", e); - } + @Override + public void writeMatrix() { + LedDisplayData data = new LedDisplayData("writeMatrix"); + addDisplayTask(data); } @Override - public void setPin(String pin) { - try { - if (pin == null) { - this.pin = null; - return; - } - this.pin = Integer.parseInt(pin); - } catch (Exception e) { - error(e); + public void onAudioStart(AudioData data) { + if (config.audioAnimation != null) { + playAnimation(config.audioAnimation); } } - public boolean setAutoClear(boolean b) { - config.autoClear = b; - return b; + @Override + public void onAudioEnd(AudioData data) { + clear(); } - } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java index 7f012d7e38..5cb8fc6cd5 100644 --- a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java +++ b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java @@ -4,53 +4,134 @@ import java.util.Map; import org.myrobotlab.framework.Plan; -import org.myrobotlab.service.data.LedDisplayData; public class NeoPixelConfig extends ServiceConfig { + /** + * when attached to an audio file service the animation to be + * played when audio is playing + */ + public String audioAnimation = "Ironman"; + + /** + * pin number of controller + */ public Integer pin = null; + + /** + * Number or pixes for this neo pixel ranges from 8 to 256+ + */ public Integer pixelCount = null; + + /** + * color depth 3 RGB or 4 (with white) + */ public int pixelDepth = 3; + + /** + * default speed (fps) of animations + */ public int speed = 10; + /** + * default red color component + */ public int red = 0; + /** + * default green color component + */ public int green = 0; + /** + * default blue color component + */ public int blue = 0; + /** + * the neopixel controller + */ public String controller = null; + /** + * the current animation + */ public String currentAnimation = null; + /** + * initial brightness + */ public Integer brightness = 255; - public boolean fill = false; - // auto clears flashes - public boolean autoClear = false; - public int idleTimeout = 1000; - /** - * Map of predefined led flashes, defined here in configuration. - * Another service simply needs to publishFlash(name) and the - * neopixel will get the defined flash data if defined and process - * it. + * initial fill */ - public Map flashMap = new HashMap<>(); + public boolean fill = false; + + /** + * Map of predefined led flashes, defined here in configuration. Another + * service simply needs to publishFlash(name) and the neopixel will get the + * defined flash data if defined and process it. + */ + public Map flashMap = new HashMap<>(); + + public static class Flash { + + /** + * uses color specify unless null uses default + */ + public int red = 0; + + /** + * uses color specify unless null uses default + */ + public int green = 0; + /** + * uses color specify unless null uses default + */ + public int blue = 0; + /** + * uses color specify unless null uses default + */ + public int white = 0; + + /** + * time this flash remains on + */ + public long timeOn = 500; + + /** + * time this flash remains off + */ + public long timeOff = 500; + + + public Flash() { + } + + + public Flash(int red, int green, int blue, long timeOn, long timeOff) { + this.red = red; + this.green = green; + this.blue = blue; + this.timeOn = timeOn; + this.timeOff = timeOff; + } + + } + /** - * reason why we initialize default for flashMap here, is so - * we don't need to do a data copy over to a service's member variable + * reason why we initialize default for flashMap here, is so we don't need to + * do a data copy over to a service's member variable */ public Plan getDefault(Plan plan, String name) { super.getDefault(plan, name); - - flashMap.put("error", new LedDisplayData(120, 0, 0, 3, 30, 30)); - flashMap.put("info", new LedDisplayData(0, 0, 120, 1, 30, 30)); - flashMap.put("success", new LedDisplayData(0, 0, 120, 2, 30, 30)); - flashMap.put("warn", new LedDisplayData(100, 100, 0, 3, 30, 30)); - flashMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); - flashMap.put("pirOn", new LedDisplayData(60, 200, 90, 3, 100, 30)); - flashMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); - flashMap.put("speaking", new LedDisplayData(0, 183, 90, 2, 60, 30)); - + + flashMap.put("error", new Flash[] { new Flash(120, 0, 0, 30, 30), new Flash(120, 0, 0, 30, 30), new Flash(120, 0, 0, 30, 30) }); + flashMap.put("info", new Flash[] { new Flash(120, 0, 0, 30, 30) }); + flashMap.put("success", new Flash[] { new Flash(0, 0, 120, 30, 30) }); + flashMap.put("warn", new Flash[] { new Flash(100, 100, 0, 30, 30), new Flash(100, 100, 0, 30, 30), new Flash(100, 100, 0, 30, 30) }); + flashMap.put("heartbeat", new Flash[] { new Flash(210, 110, 0, 100, 30), new Flash(210, 110, 0, 100, 30) }); + flashMap.put("pir", new Flash[] { new Flash(60, 200, 90, 30, 30), new Flash(60, 200, 90, 30, 30), new Flash(60, 200, 90, 30, 30) }); + flashMap.put("speaking", new Flash[] { new Flash(0, 183, 90, 60, 30), new Flash(0, 183, 90, 60, 30) }); + return plan; } - } diff --git a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java index b46ab5e862..b70e081c52 100644 --- a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java +++ b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java @@ -1,67 +1,56 @@ package org.myrobotlab.service.data; +import java.util.ArrayList; +import java.util.List; + +import org.myrobotlab.service.config.NeoPixelConfig.Flash; + /** - * Class to publish to specify details on how to display an led or a group of - * leds. There is a need to "flash" LEDs in order to signal some event. This is - * the beginning of an easy way to publish a message to do that. + * This class is a composite of possible led display details. + * Flashes, animations, etc. * * @author GroG * */ public class LedDisplayData { - public String action; // fill | flash | play animation | stop | clear - - public int red = 0; - - public int green = 0; - - public int blue = 0; + /** + * required action field may be + * fill | flash | play animation | stop | clear + */ + public String action; - // public int brightness = 255; - - // public int white?; + /** + * name of animation + */ + public String animation = null; /** - * number of flashes + * flash definition */ - public int count = 1; + public List flashes = new ArrayList<>(); + + /** + * if set overrides default brightness + */ + public Integer brightness = null; /** - * interval of flash on in ms + * begin fill address */ - public long timeOn = 500; + public int beginAddress; /** - * interval of flas off in ms + * fill count */ - public long timeOff = 500; + public int onCount; - public LedDisplayData() { - } - public LedDisplayData(int red, int green, int blue, int count, int timeOn, int timeOff) { - this.red = red; - this.green = green; - this.blue = blue; - this.count = count; - this.timeOn = timeOn; - this.timeOff = timeOff; + public LedDisplayData(String action) { + this.action = action; } - - public LedDisplayData(String hexColor, int count, int timeOn, int timeOff) { - - // remove "#" or "0x" prefix if present - hexColor = hexColor.replace("#", "").replace("0x", ""); - - this.count = count; - this.timeOn = timeOn; - this.timeOff = timeOff; - this.red = Integer.parseInt(hexColor.substring(0, 2), 16); - this.green = Integer.parseInt(hexColor.substring(2, 4), 16); - this.blue = Integer.parseInt(hexColor.substring(4, 6), 16); - + public String toString() { + return String.format("%s, %s", action, animation); } - } diff --git a/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java b/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java index 8b5257f8d5..7ff481dc88 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java +++ b/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java @@ -1,8 +1,9 @@ package org.myrobotlab.service.interfaces; +import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.service.data.AudioData; -public interface AudioListener { +public interface AudioListener extends NameProvider { public void onAudioStart(AudioData data); diff --git a/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java b/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java index 1e1a1bb9b8..533ce91e72 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java +++ b/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java @@ -1,8 +1,9 @@ package org.myrobotlab.service.interfaces; +import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.service.data.AudioData; -public interface AudioPublisher { +public interface AudioPublisher extends NameProvider { public static String[] publishMethods = new String[] { "publishAudioStart", "publishAudioEnd" }; diff --git a/src/main/resources/resource/NeoPixel/NeoPixel.py b/src/main/resources/resource/NeoPixel/NeoPixel.py index c0b128198a..8b1df273ab 100644 --- a/src/main/resources/resource/NeoPixel/NeoPixel.py +++ b/src/main/resources/resource/NeoPixel/NeoPixel.py @@ -2,86 +2,108 @@ # NeoPixel.py # more info @: http://myrobotlab.org/service/NeoPixel ######################################### -# new neopixel has some capability to have animations which can -# be custom created and run -# There are now 'service animations' and 'onboard animations' -# -# Service animations have some ability to be customized and saved, -# each frame is sent over the serial line to the neopixel -# -# Onboard ones do not but are less chatty over the serial line -# -# Animations -# stopAnimation = 1 -# colorWipe = 2 -# scanner = 3 -# theaterChase = 4 -# theaterChaseRainbow = 5 -# rainbow = 6 -# rainbowCycle = 7 -# randomFlash = 8 -# ironman = 9 -# Runtime.setVirtual(True) # if you want no hardware -# port = "COM3" -port = "/dev/ttyACM0" -pin = 5 -pixelCount = 8 - -# starting arduino -arduino = runtime.start("arduino","Arduino") -arduino.connect(port) +# Example of controlling a NeoPixel +# NeoPixel is a strip of RGB LEDs +# in this example we are using a 256 pixel strip +# and an Arduino Mega. +# The Mega is connected to the NeoPixel strip +# via pin 3 +# The Mega is connected to the computer via USB +# Onboard animations are available +# as well as the ability to set individual pixels +# [Stop, Theater Chase Rainbow, Rainbow, Larson Scanner, Flash Random, +# Theater Chase, Rainbow Cycle, Ironman, Color Wipe] +# There are now pre defined flashes which can be used +# [warn, speaking, heartbeat, success, pir, error, info] + +from time import sleep + +port = "/dev/ttyACM72" +pin = 3 +pixelCount = 256 + +# starting mega +mega = runtime.start("mega", "Arduino") +mega.connect(port) # starting neopixle -neopixel = runtime.start("neopixel","NeoPixel") +neopixel = runtime.start("neopixel", "NeoPixel") neopixel.setPin(pin) neopixel.setPixelCount(pixelCount) # attach the two services -neopixel.attach(arduino) +neopixel.attach(mega) + +# brightness 0-255 +neopixel.setBrightness(128) # fuschia - setColor(R, G, B) neopixel.setColor(120, 10, 30) -# 1 to 50 Hz default is 10 -neopixel.setSpeed(30) -# start an animation -neopixel.playAnimation("Larson Scanner") -sleep(2) +# Fun with flashing +print(neopixel.getFlashNames()) + +for flash in neopixel.getFlashNames(): + print('using flash', flash) + neopixel.flash(flash) -# turquoise -neopixel.setColor(10, 120, 60) -sleep(2) +# clear all pixels +neopixel.clear() -# start an animation -neopixel.playAnimation("Rainbow Cycle") -sleep(5) -neopixel.setColor(40, 20, 160) -neopixel.playAnimation("Color Wipe") -sleep(1) +# 1 to 50 Hz default is 10 +neopixel.setSpeed(10) -neopixel.setColor(140, 20, 60) -sleep(1) +# Fun with animations +# get a list of animations +print(neopixel.getAnimations()) + +for animation in neopixel.getAnimations(): + print(animation) + neopixel.playAnimation(animation) + sleep(3) +# clear all pixels neopixel.clear() -# set individual pixels -# setPixel(address, R, G, B) -neopixel.setPixel(0, 40, 40, 0) +neopixel.fill("cyan") +sleep(1) +neopixel.fill("yellow") sleep(1) -neopixel.setPixel(1, 140, 40, 0) +neopixel.fill("pink") sleep(1) -neopixel.setPixel(2, 40, 140, 0) +neopixel.fill("orange") sleep(1) -neopixel.setPixel(2, 40, 0, 140) +neopixel.fill("black") sleep(1) +neopixel.fill("magenta") +sleep(1) +neopixel.fill("green") +sleep(1) +neopixel.fill("#FFFFEE") +sleep(1) +neopixel.fill("#FF0000") +sleep(1) +neopixel.fill("#00FF00") +sleep(1) +neopixel.fill("#0000FF") +sleep(1) +neopixel.fill("#cccccc") +sleep(1) +neopixel.fill("#cc7528") +sleep(1) +neopixel.fill("#123456") +sleep(1) +neopixel.fill("#654321") +sleep(1) +neopixel.fill("#000000") -neopixel.clear() -neopixel.setColor(0, 40, 220) -neopixel.playAnimation("Ironman") -sleep(3) +# if you want voice modulation of a neopixel this is one +# way to do it +# mouth = runtime.start('mouth', 'Polly') +# audio = runtime.start('mouth.audioFile', 'AudioFile') +# audio.addListener('publishPeak', 'neopixel') +# mouth.speak('Is my voice modulating the neopixel?') -# preset color and frequency values -neopixel.playIronman() -sleep(5) -neopixel.clear() +print('done') + \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js b/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js index 03f830ed78..c527bcd0e1 100644 --- a/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js @@ -103,7 +103,7 @@ angular.module('mrlapp.service.NeoPixelGui', []).controller('NeoPixelGuiCtrl', [ $scope.pin = service.pin } - if (!$scope.state.controller) { + if ($scope.service.controller) { $scope.state.controller = $scope.service.controller }