diff --git a/.vscode/launch.json b/.vscode/launch.json index e2826c6c69..0ad59dd7dd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,21 +4,1628 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "java", + "name": "Current File", + "request": "launch", + "mainClass": "${file}" + }, + { + "type": "java", + "name": "PeerDiscovery", + "request": "launch", + "mainClass": "PeerDiscovery", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ArduinoMsgGenerator", + "request": "launch", + "mainClass": "org.myrobotlab.arduino.ArduinoMsgGenerator", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Msg", + "request": "launch", + "mainClass": "org.myrobotlab.arduino.Msg", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VirtualMsg", + "request": "launch", + "mainClass": "org.myrobotlab.arduino.VirtualMsg", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CaptureCalibrationImagesApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.CaptureCalibrationImagesApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CreateRgbPointCloudFileApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.CreateRgbPointCloudFileApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DisplayKinectPointCloudApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.DisplayKinectPointCloudApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ExampleDepthPointCloud", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.ExampleDepthPointCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ExampleVisualOdometryDepth", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.ExampleVisualOdometryDepth", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IntrinsicToDepthParameters", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.IntrinsicToDepthParameters", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LogKinectDataApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.LogKinectDataApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenKinectOdometry", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OpenKinectOdometry", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenKinectPointCloud", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OpenKinectPointCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenKinectStreamingTest", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OpenKinectStreamingTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OverlayRgbDepthStreamsApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OverlayRgbDepthStreamsApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "PlaybackKinectLogApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.PlaybackKinectLogApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CmdLine", + "request": "launch", + "mainClass": "org.myrobotlab.cmdline.CmdLine", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CodecUtils", + "request": "launch", + "mainClass": "org.myrobotlab.codec.CodecUtils", + "projectName": "mrl" + }, + { + "type": "java", + "name": "PolymorphicSerializer", + "request": "launch", + "mainClass": "org.myrobotlab.codec.PolymorphicSerializer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RecorderPythonFile", + "request": "launch", + "mainClass": "org.myrobotlab.codec.RecorderPythonFile", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CvData", + "request": "launch", + "mainClass": "org.myrobotlab.cv.CvData", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WikipediaIndexer", + "request": "launch", + "mainClass": "org.myrobotlab.document.connector.WikipediaIndexer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Index", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Index", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MRLListener", + "request": "launch", + "mainClass": "org.myrobotlab.framework.MRLListener", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Message", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Message", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MethodCacheTest", + "request": "launch", + "mainClass": "org.myrobotlab.framework.MethodCacheTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "NameGenerator", + "request": "launch", + "mainClass": "org.myrobotlab.framework.NameGenerator", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Platform", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Platform", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Status", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Status", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TypeConverter", + "request": "launch", + "mainClass": "org.myrobotlab.framework.TypeConverter", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IvyWrapper", + "request": "launch", + "mainClass": "org.myrobotlab.framework.repo.IvyWrapper", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MavenWrapper", + "request": "launch", + "mainClass": "org.myrobotlab.framework.repo.MavenWrapper", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ServiceData", + "request": "launch", + "mainClass": "org.myrobotlab.framework.repo.ServiceData", + "projectName": "mrl" + }, + { + "type": "java", + "name": "I2CFactory", + "request": "launch", + "mainClass": "org.myrobotlab.i2c.I2CFactory", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SerializableImage", + "request": "launch", + "mainClass": "org.myrobotlab.image.SerializableImage", + "projectName": "mrl" + }, + { + "type": "java", + "name": "FileIO", + "request": "launch", + "mainClass": "org.myrobotlab.io.FileIO", + "projectName": "mrl" + }, + { + "type": "java", + "name": "FindFile", + "request": "launch", + "mainClass": "org.myrobotlab.io.FindFile", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Zip", + "request": "launch", + "mainClass": "org.myrobotlab.io.Zip", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestPhysicsHingeJoint", + "request": "launch", + "mainClass": "org.myrobotlab.jme3.TestPhysicsHingeJoint", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestSimplePhysics", + "request": "launch", + "mainClass": "org.myrobotlab.jme3.TestSimplePhysics", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestJmeIMModel", + "request": "launch", + "mainClass": "org.myrobotlab.kinematics.TestJmeIMModel", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestJmeIntegratedMovement", + "request": "launch", + "mainClass": "org.myrobotlab.kinematics.TestJmeIntegratedMovement", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MapperLinearTest", + "request": "launch", + "mainClass": "org.myrobotlab.math.MapperLinearTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Node", + "request": "launch", + "mainClass": "org.myrobotlab.memory.Node", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Email", + "request": "launch", + "mainClass": "org.myrobotlab.net.Email", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Http", + "request": "launch", + "mainClass": "org.myrobotlab.net.Http", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HttpRequest", + "request": "launch", + "mainClass": "org.myrobotlab.net.HttpRequest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InstallCert", + "request": "launch", + "mainClass": "org.myrobotlab.net.InstallCert", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MjpegServer", + "request": "launch", + "mainClass": "org.myrobotlab.net.MjpegServer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "NanoHTTPD", + "request": "launch", + "mainClass": "org.myrobotlab.net.NanoHTTPD", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TcpSerialHub", + "request": "launch", + "mainClass": "org.myrobotlab.net.TcpSerialHub", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TcpServer", + "request": "launch", + "mainClass": "org.myrobotlab.net.TcpServer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WsClient", + "request": "launch", + "mainClass": "org.myrobotlab.net.WsClient", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OculusDisplay", + "request": "launch", + "mainClass": "org.myrobotlab.oculus.OculusDisplay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVData", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVData", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFaceRecognizer", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFaceRecognizer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFilterAddMask", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFilterAddMask", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFilterKinectFloorFinder", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFilterKinectFloorFinder", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFilterKinectPointCloud", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFilterKinectPointCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InProcessCli", + "request": "launch", + "mainClass": "org.myrobotlab.process.InProcessCli", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Launcher", + "request": "launch", + "mainClass": "org.myrobotlab.process.Launcher", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RTTTLParser", + "request": "launch", + "mainClass": "org.myrobotlab.roomba.RTTTLParser", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RTTTLPlay", + "request": "launch", + "mainClass": "org.myrobotlab.roomba.RTTTLPlay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ProcParser", + "request": "launch", + "mainClass": "org.myrobotlab.runtime.ProcParser", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Adafruit16CServoDriver", + "request": "launch", + "mainClass": "org.myrobotlab.service.Adafruit16CServoDriver", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AdafruitIna219", + "request": "launch", + "mainClass": "org.myrobotlab.service.AdafruitIna219", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AdafruitMotorHat4Pi", + "request": "launch", + "mainClass": "org.myrobotlab.service.AdafruitMotorHat4Pi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Ads1115", + "request": "launch", + "mainClass": "org.myrobotlab.service.Ads1115", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Amt203Encoder", + "request": "launch", + "mainClass": "org.myrobotlab.service.Amt203Encoder", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Android", + "request": "launch", + "mainClass": "org.myrobotlab.service.Android", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Arduino", + "request": "launch", + "mainClass": "org.myrobotlab.service.Arduino", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Arm", + "request": "launch", + "mainClass": "org.myrobotlab.service.Arm", + "projectName": "mrl" + }, + { + "type": "java", + "name": "As5048AEncoder", + "request": "launch", + "mainClass": "org.myrobotlab.service.As5048AEncoder", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AudioCapture", + "request": "launch", + "mainClass": "org.myrobotlab.service.AudioCapture", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AudioFile", + "request": "launch", + "mainClass": "org.myrobotlab.service.AudioFile", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AzureTranslator", + "request": "launch", + "mainClass": "org.myrobotlab.service.AzureTranslator", + "projectName": "mrl" + }, + { + "type": "java", + "name": "BeagleBoardBlack", + "request": "launch", + "mainClass": "org.myrobotlab.service.BeagleBoardBlack", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Blender", + "request": "launch", + "mainClass": "org.myrobotlab.service.Blender", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Blocks", + "request": "launch", + "mainClass": "org.myrobotlab.service.Blocks", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Bno055", + "request": "launch", + "mainClass": "org.myrobotlab.service.Bno055", + "projectName": "mrl" + }, + { + "type": "java", + "name": "BodyPart", + "request": "launch", + "mainClass": "org.myrobotlab.service.BodyPart", + "projectName": "mrl" + }, + { + "type": "java", + "name": "BoofCv", + "request": "launch", + "mainClass": "org.myrobotlab.service.BoofCv", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Chassis", + "request": "launch", + "mainClass": "org.myrobotlab.service.Chassis", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ChessGame", + "request": "launch", + "mainClass": "org.myrobotlab.service.ChessGame", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Clock", + "request": "launch", + "mainClass": "org.myrobotlab.service.Clock", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Cron", + "request": "launch", + "mainClass": "org.myrobotlab.service.Cron", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Database", + "request": "launch", + "mainClass": "org.myrobotlab.service.Database", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Deeplearning4j", + "request": "launch", + "mainClass": "org.myrobotlab.service.Deeplearning4j", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DiscordBot", + "request": "launch", + "mainClass": "org.myrobotlab.service.DiscordBot", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DiyServo", + "request": "launch", + "mainClass": "org.myrobotlab.service.DiyServo", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Docker", + "request": "launch", + "mainClass": "org.myrobotlab.service.Docker", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DocumentPipeline", + "request": "launch", + "mainClass": "org.myrobotlab.service.DocumentPipeline", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DruppNeck", + "request": "launch", + "mainClass": "org.myrobotlab.service.DruppNeck", + "projectName": "mrl" + }, + { + "type": "java", + "name": "EddieControlBoard", + "request": "launch", + "mainClass": "org.myrobotlab.service.EddieControlBoard", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Elasticsearch", + "request": "launch", + "mainClass": "org.myrobotlab.service.Elasticsearch", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Emoji", + "request": "launch", + "mainClass": "org.myrobotlab.service.Emoji", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Esp8266", + "request": "launch", + "mainClass": "org.myrobotlab.service.Esp8266", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Esp8266_01", + "request": "launch", + "mainClass": "org.myrobotlab.service.Esp8266_01", + "projectName": "mrl" + }, + { + "type": "java", + "name": "FiniteStateMachine", + "request": "launch", + "mainClass": "org.myrobotlab.service.FiniteStateMachine", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Git", + "request": "launch", + "mainClass": "org.myrobotlab.service.Git", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoPro", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoPro", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoogleCloud", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoogleCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoogleSearch", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoogleSearch", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoogleTranslate", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoogleTranslate", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Gps", + "request": "launch", + "mainClass": "org.myrobotlab.service.Gps", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Gpt3", + "request": "launch", + "mainClass": "org.myrobotlab.service.Gpt3", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Hd44780", + "request": "launch", + "mainClass": "org.myrobotlab.service.Hd44780", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HtmlFilter", + "request": "launch", + "mainClass": "org.myrobotlab.service.HtmlFilter", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HtmlParser", + "request": "launch", + "mainClass": "org.myrobotlab.service.HtmlParser", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HttpClient", + "request": "launch", + "mainClass": "org.myrobotlab.service.HttpClient", + "projectName": "mrl" + }, + { + "type": "java", + "name": "I2cMux", + "request": "launch", + "mainClass": "org.myrobotlab.service.I2cMux", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IBus", + "request": "launch", + "mainClass": "org.myrobotlab.service.IBus", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ImageDisplay", + "request": "launch", + "mainClass": "org.myrobotlab.service.ImageDisplay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ImapEmailConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.ImapEmailConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2Hand", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2Hand", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2Head", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2Head", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2Torso", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2Torso", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IndianTts", + "request": "launch", + "mainClass": "org.myrobotlab.service.IndianTts", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IntegratedMovement", + "request": "launch", + "mainClass": "org.myrobotlab.service.IntegratedMovement", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Intro", + "request": "launch", + "mainClass": "org.myrobotlab.service.Intro", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InverseKinematics", + "request": "launch", + "mainClass": "org.myrobotlab.service.InverseKinematics", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InverseKinematics3D", + "request": "launch", + "mainClass": "org.myrobotlab.service.InverseKinematics3D", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IpCamera", + "request": "launch", + "mainClass": "org.myrobotlab.service.IpCamera", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JFugue", + "request": "launch", + "mainClass": "org.myrobotlab.service.JFugue", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JMonkeyEngine", + "request": "launch", + "mainClass": "org.myrobotlab.service.JMonkeyEngine", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JMonkeyEngineTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.JMonkeyEngineTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JavaScript", + "request": "launch", + "mainClass": "org.myrobotlab.service.JavaScript", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Joystick", + "request": "launch", + "mainClass": "org.myrobotlab.service.Joystick", + "projectName": "mrl" + }, + { + "type": "java", + "name": "KafkaConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.KafkaConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Keyboard", + "request": "launch", + "mainClass": "org.myrobotlab.service.Keyboard", + "projectName": "mrl" + }, + { + "type": "java", + "name": "KeyboardSim", + "request": "launch", + "mainClass": "org.myrobotlab.service.KeyboardSim", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LeapMotion", + "request": "launch", + "mainClass": "org.myrobotlab.service.LeapMotion", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LeapMotion2", + "request": "launch", + "mainClass": "org.myrobotlab.service.LeapMotion2", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Lidar", + "request": "launch", + "mainClass": "org.myrobotlab.service.Lidar", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LidarVlp16", + "request": "launch", + "mainClass": "org.myrobotlab.service.LidarVlp16", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Lloyd", + "request": "launch", + "mainClass": "org.myrobotlab.service.Lloyd", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Lm75a", + "request": "launch", + "mainClass": "org.myrobotlab.service.Lm75a", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LocalSpeech", + "request": "launch", + "mainClass": "org.myrobotlab.service.LocalSpeech", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Log", + "request": "launch", + "mainClass": "org.myrobotlab.service.Log", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mail", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mail", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MarySpeech", + "request": "launch", + "mainClass": "org.myrobotlab.service.MarySpeech", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Maven", + "request": "launch", + "mainClass": "org.myrobotlab.service.Maven", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MobilePlatform", + "request": "launch", + "mainClass": "org.myrobotlab.service.MobilePlatform", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Motor", + "request": "launch", + "mainClass": "org.myrobotlab.service.Motor", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MotorDualPwm", + "request": "launch", + "mainClass": "org.myrobotlab.service.MotorDualPwm", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MotorHat4Pi", + "request": "launch", + "mainClass": "org.myrobotlab.service.MotorHat4Pi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MotorPort", + "request": "launch", + "mainClass": "org.myrobotlab.service.MotorPort", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MouseSim", + "request": "launch", + "mainClass": "org.myrobotlab.service.MouseSim", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MouthControl", + "request": "launch", + "mainClass": "org.myrobotlab.service.MouthControl", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mpr121", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mpr121", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mpu6050", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mpu6050", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mqtt", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mqtt", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MqttBroker", + "request": "launch", + "mainClass": "org.myrobotlab.service.MqttBroker", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MultiWii", + "request": "launch", + "mainClass": "org.myrobotlab.service.MultiWii", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MyoThalmic", + "request": "launch", + "mainClass": "org.myrobotlab.service.MyoThalmic", + "projectName": "mrl" + }, + { + "type": "java", + "name": "NeoPixel", + "request": "launch", + "mainClass": "org.myrobotlab.service.NeoPixel", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OakD", + "request": "launch", + "mainClass": "org.myrobotlab.service.OakD", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OculusDiy", + "request": "launch", + "mainClass": "org.myrobotlab.service.OculusDiy", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OculusRift", + "request": "launch", + "mainClass": "org.myrobotlab.service.OculusRift", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OledSsd1306", + "request": "launch", + "mainClass": "org.myrobotlab.service.OledSsd1306", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCV", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenCV", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenCVTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenNi", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenNi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenWeatherMap", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenWeatherMap", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Osc", + "request": "launch", + "mainClass": "org.myrobotlab.service.Osc", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pcf8574", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pcf8574", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pid", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pid", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pingdar", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pingdar", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pir", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pir", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Polly", + "request": "launch", + "mainClass": "org.myrobotlab.service.Polly", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ProgramAB", + "request": "launch", + "mainClass": "org.myrobotlab.service.ProgramAB", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Py4j", + "request": "launch", + "mainClass": "org.myrobotlab.service.Py4j", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Python", + "request": "launch", + "mainClass": "org.myrobotlab.service.Python", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RSSConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.RSSConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Random", + "request": "launch", + "mainClass": "org.myrobotlab.service.Random", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RasPi", + "request": "launch", + "mainClass": "org.myrobotlab.service.RasPi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Rekognition", + "request": "launch", + "mainClass": "org.myrobotlab.service.Rekognition", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Relay", + "request": "launch", + "mainClass": "org.myrobotlab.service.Relay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RoboClaw", + "request": "launch", + "mainClass": "org.myrobotlab.service.RoboClaw", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Roomba", + "request": "launch", + "mainClass": "org.myrobotlab.service.Roomba", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Ros", + "request": "launch", + "mainClass": "org.myrobotlab.service.Ros", + "projectName": "mrl" + }, { "type": "java", "name": "Runtime", "request": "launch", "mainClass": "org.myrobotlab.service.Runtime", - "projectName": "mrl", - "args": [ - "--log-level", - "info", - "-s", - "webgui", "WebGui", "intro", "Intro", "python", "Python", - "-c", - "dev" - ] - + "projectName": "mrl" + }, + { + "type": "java", + "name": "Sabertooth", + "request": "launch", + "mainClass": "org.myrobotlab.service.Sabertooth", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Security", + "request": "launch", + "mainClass": "org.myrobotlab.service.Security", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SegmentDisplay", + "request": "launch", + "mainClass": "org.myrobotlab.service.SegmentDisplay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SensorMonitor", + "request": "launch", + "mainClass": "org.myrobotlab.service.SensorMonitor", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Serial", + "request": "launch", + "mainClass": "org.myrobotlab.service.Serial", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SerialRelay", + "request": "launch", + "mainClass": "org.myrobotlab.service.SerialRelay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Servo", + "request": "launch", + "mainClass": "org.myrobotlab.service.Servo", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ServoMixer", + "request": "launch", + "mainClass": "org.myrobotlab.service.ServoMixer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Shoutbox", + "request": "launch", + "mainClass": "org.myrobotlab.service.Shoutbox", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SlackBot", + "request": "launch", + "mainClass": "org.myrobotlab.service.SlackBot", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Solr", + "request": "launch", + "mainClass": "org.myrobotlab.service.Solr", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Sphinx", + "request": "launch", + "mainClass": "org.myrobotlab.service.Sphinx", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SpotMicro", + "request": "launch", + "mainClass": "org.myrobotlab.service.SpotMicro", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Ssc32UsbServoController", + "request": "launch", + "mainClass": "org.myrobotlab.service.Ssc32UsbServoController", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TarsosDsp", + "request": "launch", + "mainClass": "org.myrobotlab.service.TarsosDsp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Tensorflow", + "request": "launch", + "mainClass": "org.myrobotlab.service.Tensorflow", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TesseractOcr", + "request": "launch", + "mainClass": "org.myrobotlab.service.TesseractOcr", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Test", + "request": "launch", + "mainClass": "org.myrobotlab.service.Test", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestCatcher", + "request": "launch", + "mainClass": "org.myrobotlab.service.TestCatcher", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ThingSpeak", + "request": "launch", + "mainClass": "org.myrobotlab.service.ThingSpeak", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TopCodes", + "request": "launch", + "mainClass": "org.myrobotlab.service.TopCodes", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Tracking", + "request": "launch", + "mainClass": "org.myrobotlab.service.Tracking", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Twitter", + "request": "launch", + "mainClass": "org.myrobotlab.service.Twitter", + "projectName": "mrl" + }, + { + "type": "java", + "name": "UltrasonicSensor", + "request": "launch", + "mainClass": "org.myrobotlab.service.UltrasonicSensor", + "projectName": "mrl" + }, + { + "type": "java", + "name": "UltrasonicSensorTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.UltrasonicSensorTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Updater", + "request": "launch", + "mainClass": "org.myrobotlab.service.Updater", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VideoStreamer", + "request": "launch", + "mainClass": "org.myrobotlab.service.VideoStreamer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VirtualArduino", + "request": "launch", + "mainClass": "org.myrobotlab.service.VirtualArduino", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VoiceRss", + "request": "launch", + "mainClass": "org.myrobotlab.service.VoiceRss", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WatchDogTimer", + "request": "launch", + "mainClass": "org.myrobotlab.service.WatchDogTimer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebGui", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebGui", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebSocketConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebSocketConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Webcam", + "request": "launch", + "mainClass": "org.myrobotlab.service.Webcam", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebkitSpeechRecognition", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebkitSpeechRecognition", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebkitSpeechSynthesis", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebkitSpeechSynthesis", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Wii", + "request": "launch", + "mainClass": "org.myrobotlab.service.Wii", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WiiDar", + "request": "launch", + "mainClass": "org.myrobotlab.service.WiiDar", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WikiDataFetcher", + "request": "launch", + "mainClass": "org.myrobotlab.service.WikiDataFetcher", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Wikipedia", + "request": "launch", + "mainClass": "org.myrobotlab.service.Wikipedia", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WolframAlpha", + "request": "launch", + "mainClass": "org.myrobotlab.service.WolframAlpha", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WorkE", + "request": "launch", + "mainClass": "org.myrobotlab.service.WorkE", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Xmpp", + "request": "launch", + "mainClass": "org.myrobotlab.service.Xmpp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "YahooFinanceStockQuote", + "request": "launch", + "mainClass": "org.myrobotlab.service.YahooFinanceStockQuote", + "projectName": "mrl" + }, + { + "type": "java", + "name": "_TemplateService", + "request": "launch", + "mainClass": "org.myrobotlab.service._TemplateService", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pin", + "request": "launch", + "mainClass": "org.myrobotlab.service.data.Pin", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SpeechSynthesisTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.interfaces.SpeechSynthesisTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "StringUtil", + "request": "launch", + "mainClass": "org.myrobotlab.string.StringUtil", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AbstractTest", + "request": "launch", + "mainClass": "org.myrobotlab.test.AbstractTest", + "projectName": "mrl" } ] } \ No newline at end of file diff --git a/depthai b/depthai new file mode 160000 index 0000000000..65fc1c6ad7 --- /dev/null +++ b/depthai @@ -0,0 +1 @@ +Subproject commit 65fc1c6ad7495929c0f58897067af2d88f62f0f8 diff --git a/src/main/java/VectorAngleCalculator.java b/src/main/java/VectorAngleCalculator.java new file mode 100644 index 0000000000..84501a49c8 --- /dev/null +++ b/src/main/java/VectorAngleCalculator.java @@ -0,0 +1,45 @@ +import java.util.Vector; + +public class VectorAngleCalculator { + public static double calculateAngle(Vector vector1, Vector vector2) { + // Check if the vectors have the same dimension + if (vector1.size() != vector2.size()) { + throw new IllegalArgumentException("Vectors must have the same dimension"); + } + + // Calculate the dot product of the vectors + double dotProduct = 0.0; + double magnitude1 = 0.0; + double magnitude2 = 0.0; + + for (int i = 0; i < vector1.size(); i++) { + dotProduct += vector1.get(i) * vector2.get(i); + magnitude1 += Math.pow(vector1.get(i), 2); + magnitude2 += Math.pow(vector2.get(i), 2); + } + + magnitude1 = Math.sqrt(magnitude1); + magnitude2 = Math.sqrt(magnitude2); + + // Calculate the angle in radians + double radians = Math.acos(dotProduct / (magnitude1 * magnitude2)); + + // Convert radians to degrees + double degrees = Math.toDegrees(radians); + + return degrees; + } + + public static void main(String[] args) { + Vector vector1 = new Vector<>(); + vector1.add(1.0); + vector1.add(0.0); + + Vector vector2 = new Vector<>(); + vector2.add(1.0); + vector2.add(1.0); + + double angleDegrees = calculateAngle(vector1, vector2); + System.out.println("Angle between vectors: " + angleDegrees + " degrees"); + } +} diff --git a/src/main/java/org/myrobotlab/caliko/Application.java b/src/main/java/org/myrobotlab/caliko/Application.java new file mode 100644 index 0000000000..d8474c157e --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/Application.java @@ -0,0 +1,82 @@ +package org.myrobotlab.caliko; + +import static org.lwjgl.glfw.GLFW.glfwPollEvents; +import static org.lwjgl.glfw.GLFW.glfwWindowShouldClose; +import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.glClear; + +import org.myrobotlab.service.Caliko; + +import au.edu.federation.caliko.demo.CalikoDemo; +import au.edu.federation.utils.Vec3f; + +/** + * An example application to demonstrate the Caliko library in both 2D and 3D + * modes. + * + * Use up/down cursors to change between 2D/3D mode and left/right cursors to + * change demos. In 2D mode clicking using the left mouse button (LMB) changes + * the target location, and you can click and drag. In 3D mode, use W/S/A/D to + * move the camera and the mouse with LMB held down to look. + * + * See the README.txt for further documentation and controls. + * + * @author Al Lansley + * @version 1.0 - 31/01/2016 + */ +public class Application { + // Define cardinal axes + final Vec3f X_AXIS = new Vec3f(1.0f, 0.0f, 0.0f); + final Vec3f Y_AXIS = new Vec3f(0.0f, 1.0f, 0.0f); + final Vec3f Z_AXIS = new Vec3f(0.0f, 0.0f, 1.0f); + + // State tracking variables + boolean use3dDemo = true; + int demoNumber = 7; + boolean fixedBaseMode = true; + boolean rotateBasesMode = false; + boolean drawLines = true; + boolean drawAxes = false; + boolean drawModels = true; + boolean drawConstraints = true; + boolean leftMouseButtonDown = false; + boolean paused = true; + + // Create our window and OpenGL context + int windowWidth = 800; + int windowHeight = 600; + public OpenGLWindow window = null; + + // Declare a CalikoDemo object which can run our 3D and 2D demonstration + // scenarios + transient private CalikoDemo demo; + transient private Caliko service; + public boolean running = true; + + public Application(Caliko service) { + this.service = service; + window = new OpenGLWindow(this, service, windowWidth, windowHeight); + demo = new CalikoDemo3D(this); + mainLoop(); + window.cleanup(); + } + + public Caliko getService() { + return service; + } + + private void mainLoop() { + // Run the rendering loop until the user closes the window or presses Escape + while (!glfwWindowShouldClose(window.mWindowId) && running) { + // Clear the screen and depth buffer then draw the demo + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + demo.draw(); + + // Swap the front and back colour buffers and poll for events + window.swapBuffers(); + glfwPollEvents(); + } + } + +} // End of Application class diff --git a/src/main/java/org/myrobotlab/caliko/CalikoDemo3D.java b/src/main/java/org/myrobotlab/caliko/CalikoDemo3D.java new file mode 100644 index 0000000000..8e977fe9d7 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/CalikoDemo3D.java @@ -0,0 +1,148 @@ +package org.myrobotlab.caliko; + +import au.edu.federation.caliko.FabrikBone3D; +import au.edu.federation.caliko.FabrikChain3D; +import au.edu.federation.caliko.FabrikStructure3D; +import au.edu.federation.caliko.demo.CalikoDemo; +import au.edu.federation.caliko.demo3d.CalikoDemoStructure3D; +import au.edu.federation.caliko.visualisation.Axis; +import au.edu.federation.caliko.visualisation.Camera; +import au.edu.federation.caliko.visualisation.FabrikConstraint3D; +import au.edu.federation.caliko.visualisation.FabrikLine3D; +// import au.edu.federation.caliko.visualisation.FabrikModel3D; +import au.edu.federation.caliko.visualisation.Grid; +import au.edu.federation.caliko.visualisation.MovingTarget3D; +import au.edu.federation.utils.Mat4f; +import au.edu.federation.utils.Utils; +import au.edu.federation.utils.Vec3f; + +/** + * Class to demonstrate some of the features of the Caliko library in 3D. + * + * @author Al Lansley + * @version 0.7.1 - 20/07/2016 + */ +public class CalikoDemo3D implements CalikoDemo +{ + static float defaultBoneLength = 10.0f; + static float boneLineWidth = 5.0f; + static float constraintLineWidth = 2.0f; + static float baseRotationAmountDegs = 0.3f; + + // Set yo a camera which we'll use to navigate. Params: location, orientation, width and height of window. + static Camera camera = new Camera(new Vec3f(0.0f, 00.0f, 150.0f), new Vec3f(), 800, 600); + + // Setup some grids to aid orientation + static float extent = 1000.0f; + static float gridLevel = 100.0f; + static int subdivisions = 20; + static Grid lowerGrid = new Grid(extent, extent, -gridLevel, subdivisions); + static Grid upperGrid = new Grid(extent, extent, gridLevel, subdivisions); + + // An axis to show the X/Y/Z orientation of each bone. Params: Axis length, axis line width + static Axis axis = new Axis(3.0f, 1.0f); + + // A constraint we can use to draw any joint angle restrictions of ball and hinge joints + static FabrikConstraint3D constraint = new FabrikConstraint3D(); + + // A simple Wavefront .OBJ format model of a pyramid to display around each bone (set to draw with a 1.0f line width) + static FabrikModel3D model = new FabrikModel3D("/pyramid.obj", 1.0f); + + // Setup moving target. Params: location, extents, interpolation frames, grid height for vertical bar + static MovingTarget3D target = new MovingTarget3D(new Vec3f(0, -30, 0), new Vec3f(60.0f), 200, gridLevel); + + private FabrikStructure3D mStructure; + + private CalikoDemoStructure3D demoStructure3d; + + private transient Application application; + + /** + * Constructor. + * + * @param demoNumber The number of the demo to set up. + */ + public CalikoDemo3D(Application application) + { + this.application = application; + setup(0); + } + + /** + * Set up a demo consisting of an arrangement of 3D IK chains with a given configuration. + * + * @param demoNumber The number of the demo to set up. + */ + public void setup(int demoNumber) + { + this.mStructure = application.getService().getStructure(); + + this.demoStructure3d = new GuiDemoStructure(application.getService(), null); + + // Set the appropriate window title and make an initial solve pass of the structure + application.window.setWindowTitle(this.mStructure.getName()); + //structure.updateTarget( target.getCurrentLocation() ); + } + + /** Set all chains in the structure to be in fixed-base mode whereby the base locations cannot move. */ + public void setFixedBaseMode(boolean value) { mStructure.setFixedBaseMode(value); } + + /** Handle the movement of the camera using the W/S/A/D keys. */ + public void handleCameraMovement(int key, int action) { camera.handleKeypress(key, action); } + + public void draw() + { + // Move the camera based on keypresses and mouse movement + camera.move(1.0f / 60.0f); + + // Get the ModelViewProjection matrix as we use it multiple times + Mat4f mvpMatrix = application.window.getMvpMatrix(); + + // Draw our grids + lowerGrid.draw(mvpMatrix); + upperGrid.draw(mvpMatrix); + + // If we're not paused then step the target and solve the structure for the new target location + if (!application.paused) + { + target.step(); + this.demoStructure3d.drawTarget(mvpMatrix); + + // Solve the structure (chains with embedded targets will use those, otherwise the provided target is used) + mStructure.solveForTarget( target.getCurrentLocation() ); + + FabrikChain3D chain = application.getService().getChain("default"); + + for (FabrikBone3D bone : chain.getChain()) { + bone.getStartLocation().getGlobalPitchDegs(); + bone.getStartLocation().getGlobalYawDegs(); + + System.out.println("Bone X: " + bone.getStartLocation().toString()); + } + + } + + // If we're in rotate base mode then rotate the base location(s) of all chains in the structure + if (application.rotateBasesMode) + { + int numChains = mStructure.getNumChains(); + for (int loop = 0; loop < numChains; ++loop) + { + Vec3f base = mStructure.getChain(loop).getBaseLocation(); + base = Vec3f.rotateAboutAxisDegs(base, baseRotationAmountDegs, CalikoDemoStructure3D.Y_AXIS); + mStructure.getChain(loop).setBaseLocation(base); + } + } + + // Draw the target + target.draw(Utils.YELLOW, 8.0f, mvpMatrix); + + // Draw the structure as required + // Note: bone lines are drawn in the bone colour, models are drawn in white by default but you can specify a colour to the draw method, + // axes are drawn X/Y/Z as Red/Green/Blue and constraints are drawn the colours specified in the FabrikConstraint3D class. + if (application.drawLines) { FabrikLine3D.draw(mStructure, boneLineWidth, mvpMatrix); } + if (application.drawModels) { model.drawStructure(mStructure, camera.getViewMatrix(), application.window.mProjectionMatrix); } + if (application.drawAxes) { axis.draw(mStructure, camera.getViewMatrix(), application.window.mProjectionMatrix); } + if (application.drawConstraints) { constraint.draw(mStructure, constraintLineWidth, mvpMatrix); } + } +} diff --git a/src/main/java/org/myrobotlab/caliko/FabrikModel3D.java b/src/main/java/org/myrobotlab/caliko/FabrikModel3D.java new file mode 100644 index 0000000000..470f98b01f --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/FabrikModel3D.java @@ -0,0 +1,316 @@ +package org.myrobotlab.caliko; + +import java.nio.FloatBuffer; + +import au.edu.federation.caliko.FabrikBone3D; +import au.edu.federation.caliko.FabrikChain3D; +import au.edu.federation.caliko.FabrikStructure3D; +import au.edu.federation.caliko.visualisation.ShaderProgram; +import au.edu.federation.utils.Colour4f; +import au.edu.federation.utils.Mat3f; +import au.edu.federation.utils.Mat4f; +import au.edu.federation.utils.Utils; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL15.*; +import static org.lwjgl.opengl.GL20.*; +import static org.lwjgl.opengl.GL30.*; + +/** + * A class to represent a 3D model that can easily be attached to a FabrikBone3D object. + * + * @author Al Lansley + * @version 0.3.1 - 20/07/2016 + */ +public class FabrikModel3D +{ + // Each vertex has three positional components - the x, y and z values. + private static final int VERTEX_COMPONENTS = 3; + + // A single static ShaderProgram is used to draw all axes + private static ShaderProgram shaderProgram; + + // Vertex shader source + private static final String VERTEX_SHADER_SOURCE = + "#version 330" + Utils.NEW_LINE + + "in vec3 vertexLocation; // Incoming vertex attribute" + Utils.NEW_LINE + + "uniform mat4 mvpMatrix; // Combined Model/View/Projection matrix " + Utils.NEW_LINE + + "void main(void) {" + Utils.NEW_LINE + + " gl_Position = mvpMatrix * vec4(vertexLocation, 1.0); // Project our geometry" + Utils.NEW_LINE + + "}"; + + // Fragment shader source + private static final String FRAGMENT_SHADER_SOURCE = + "#version 330" + Utils.NEW_LINE + + "out vec4 outputColour;" + Utils.NEW_LINE + + "uniform vec4 colour;" + Utils.NEW_LINE + + "void main() {" + Utils.NEW_LINE + + " outputColour = colour;" + Utils.NEW_LINE + + "}"; + + // Hold id values for the Vertex Array Object (VAO) and Vertex Buffer Object (VBO) + private static int vaoId; + private static int vboId; + + // Float buffers for the ModelViewProjection matrix and model colour + private static FloatBuffer mvpMatrixFB; + private static FloatBuffer colourFB; + + // We'll keep track of and restore the current OpenGL line width, which we'll store in this FloatBuffer. + // Note: Although we only need a single float for this, LWJGL insists upon a minimum size of 16 floats. + private static FloatBuffer currentLineWidthFB; + + // ----- Non-Static Properties ----- + + // The FloatBuffer which will contain our vertex data - as we may load multiple different models this cannot be static + private FloatBuffer vertexFB; + + /** The actual Model associated with this FabrikModel3D. */ + private Model model; + + /** The float array storing the axis vertex (including colour) data. */ + private float[] modelData; + + /** + * The line width with which to draw the model in pixels. + * + * @default 1.0f + */ + private float mLineWidth = 1.0f; + + static { + mvpMatrixFB = Utils.createFloatBuffer(16); + colourFB = Utils.createFloatBuffer(16); + currentLineWidthFB = Utils.createFloatBuffer(16); + + // ----- Grid shader program setup ----- + + shaderProgram = new ShaderProgram(); + shaderProgram.initFromStrings(VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE); + + // ----- Grid shader attributes and uniforms ----- + + // Add the shader attributes and uniforms + shaderProgram.addAttribute("vertexLocation"); + shaderProgram.addUniform("mvpMatrix"); + shaderProgram.addUniform("colour"); + + // ----- Set up our Vertex Array Object (VAO) to hold the shader attributes ----- + + // Create a VAO and bind to it + vaoId = glGenVertexArrays(); + glBindVertexArray(vaoId); + + // ----- Vertex Buffer Object (VBO) ----- + + // Create a VBO and bind to it + vboId = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, vboId); + + // Note: We do NOT copy the data into the buffer at this time - we do that on draw! + + // Vertex attribute configuration + glVertexAttribPointer(shaderProgram.attribute("vertexLocation"), // Vertex location attribute index + VERTEX_COMPONENTS, // Number of components per vertex + GL_FLOAT, // Data type + false, // Normalised? + VERTEX_COMPONENTS * Float.BYTES, // Stride + 0); // Offset + + // Unbind VBO + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Enable the vertex attributes + glEnableVertexAttribArray(shaderProgram.attribute("vertexLocation")); + + // Unbind VAO - all the buffer and attribute settings above will now be associated with our VAO + glBindVertexArray(0); + } + + /** + * Default constructor. + * + * @param modelFilename The filename of the model to load. + * @param lineWidth The width of the lines used to draw the model in pixels. + */ + // Note: width is along +/- x-axis, depth is along +/- z-axis, height is the location on + // the y-axis, numDivisions is how many lines to draw across each axis + public FabrikModel3D(String modelFilename, float lineWidth) + { + // Load the model, get the vertex data and put it into our vertex FloatBuffer + model = new Model(modelFilename); + modelData = model.getVertexFloatArray(); + vertexFB = Utils.createFloatBuffer(model.getNumVertices() * VERTEX_COMPONENTS); + + mLineWidth = lineWidth; + + } // End of constructor + + /** Private method to actually draw the model. */ + private void drawModel(float lineWidth, Colour4f colour, Mat4f mvpMatrix) + { + // Enable our shader program and bind to our VAO + shaderProgram.use(); + glBindVertexArray(vaoId); + + // Bind to our VBO so we can update the axis data for this particular axis object + glBindBuffer(GL_ARRAY_BUFFER, vboId); + + // Copy the data for this particular model into the vertex float buffer + // Note: The model is scaled to each individual bone length, hence the GL_DYNAMIC_DRAW performance hint. + vertexFB.put(modelData); + vertexFB.flip(); + glBufferData(GL_ARRAY_BUFFER, vertexFB, GL_DYNAMIC_DRAW); + + // Provide the mvp matrix uniform data + mvpMatrixFB.put( mvpMatrix.toArray() ); + mvpMatrixFB.flip(); + glUniformMatrix4fv(shaderProgram.uniform("mvpMatrix"), false, mvpMatrixFB); + + // Provide the model vertex colour data + colourFB.put( colour.toArray() ); + colourFB.flip(); + glUniform4fv(shaderProgram.uniform("colour"), colourFB); + + // Store the current GL_LINE_WIDTH + // IMPORTANT: We MUST allocate a minimum of 16 floats in our FloatBuffer in LWJGL, we CANNOT just get a FloatBuffer with 1 float! + // ALSO: glPushAttrib(GL_LINE_BIT); /* do stuff */ glPopAttrib(); should work instead of this in theory - but LWJGL fails with 'function not supported'. + glGetFloatv(GL_LINE_WIDTH, currentLineWidthFB); + + /// Set the GL_LINE_WIDTH to be the width requested, as passed to the constructor + glLineWidth(lineWidth); + + // Draw the model as lines + glDrawArrays( GL_LINES, 0, model.getNumVertices() ); + + // Reset the line width to the previous value + glLineWidth( currentLineWidthFB.get(0) ); + + // Unbind from our VBO + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Unbind from our VAO + glBindVertexArray(0); + + // Disable our shader program + shaderProgram.disable(); + } + + /** + * Draw a bone using the model loaded on this FabrikModel3D. + * + * @param bone The bone to draw. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + * @param colour The colour of the lines used to draw the bone. + */ + public void drawBone(FabrikBone3D bone, Mat4f viewMatrix, Mat4f projectionMatrix, Colour4f colour) + { + // Clone the model and scale the clone to be twice as wide and deep, and scaled along the z-axis to match the bone length + Model modelCopy = Model.clone(model); + modelCopy.scale( 2.0f, 2.0f, bone.length() ); + + // Get our scaled model data + modelData = modelCopy.getVertexFloatArray(); + + // Construct a model matrix for this bone + Mat4f modelMatrix = new Mat4f( Mat3f.createRotationMatrix( bone.getDirectionUV().normalised() ), bone.getStartLocation() ); + + // Construct a ModelViewProjection and draw the model for this bone + Mat4f mvpMatrix = projectionMatrix.times(viewMatrix).times(modelMatrix); + this.drawModel(mLineWidth, colour, mvpMatrix); + } + + /** + * Draw a bone using the model loaded on this FabrikModel3D using a default colour of white at full opacity. + * + * @param bone The bone to draw. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + */ + public void drawBone(FabrikBone3D bone, Mat4f viewMatrix, Mat4f projectionMatrix) + { + this.drawBone(bone, viewMatrix, projectionMatrix, Utils.WHITE); + } + + /** + * Draw a chain using the model loaded on this FabrikModel3D. + * + * @param chain The FabrikChain3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + * @param colour The colour of the lines used to draw the model. + */ + public void drawChain(FabrikChain3D chain, Mat4f viewMatrix, Mat4f projectionMatrix, Colour4f colour) + { + int numBones = chain.getNumBones(); + for (int loop = 0; loop < numBones; ++loop) + { + this.drawBone( chain.getBone(loop), viewMatrix, projectionMatrix, colour ); + } + } + + /** + * Draw a chain using the model loaded on this FabrikModel3D using a default colour of white at full opacity. + * + * @param chain The FabrikChain3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + */ + public void drawChain(FabrikChain3D chain, Mat4f viewMatrix, Mat4f projectionMatrix) + { + int numBones = chain.getNumBones(); + for (int loop = 0; loop < numBones; ++loop) + { + this.drawBone( chain.getBone(loop), viewMatrix, projectionMatrix, Utils.WHITE); + } + } + + /** + * Draw a structure using the model loaded on this FabrikModel3D. + * + * @param structure The FabrikStructure3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + * @param colour The colour of the lines used to draw the model. + */ + public void drawStructure(FabrikStructure3D structure, Mat4f viewMatrix, Mat4f projectionMatrix, Colour4f colour) + { + int numChains = structure.getNumChains(); + for (int loop = 0; loop < numChains; ++loop) + { + this.drawChain( structure.getChain(loop), viewMatrix, projectionMatrix, colour ); + } + } + + /** + * Draw a structure using the model loaded on this FabrikModel3D using a default colour of white at full opacity. + * + * @param structure The FabrikStructure3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + */ + public void drawStructure(FabrikStructure3D structure, Mat4f viewMatrix, Mat4f projectionMatrix) + { + int numChains = structure.getNumChains(); + for (int loop = 0; loop < numChains; ++loop) + { + this.drawChain( structure.getChain(loop), viewMatrix, projectionMatrix, Utils.WHITE); + } + } + + /** + * Line width property setter. + *

+ * Valid line widths are between 1.0f an 32.0f - values outside of this range will result + * in an IllegalArgumentException being thrown. + * + * @param lineWidth The width of the line used to draw this FabrikModel3D in pixels. + */ + void setLineWidth(float lineWidth) + { + Utils.validateLineWidth(lineWidth); + mLineWidth = lineWidth; + } + +} // End of FabrikModel3D class diff --git a/src/main/java/org/myrobotlab/caliko/GuiDemoStructure.java b/src/main/java/org/myrobotlab/caliko/GuiDemoStructure.java new file mode 100644 index 0000000000..b1b7c6b824 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/GuiDemoStructure.java @@ -0,0 +1,30 @@ +package org.myrobotlab.caliko; + +import org.myrobotlab.service.Caliko; + +import au.edu.federation.caliko.demo3d.CalikoDemoStructure3D; +import au.edu.federation.utils.Mat4f; + +public class GuiDemoStructure extends CalikoDemoStructure3D { + + private transient Caliko service; + protected String name; + + public GuiDemoStructure(Caliko service, String name) { + this.service = service; + this.name = name; + } + + @Override + public void setup() { + // TODO Auto-generated method stub + + } + + @Override + public void drawTarget(Mat4f mvpMatrix) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/org/myrobotlab/caliko/Model.java b/src/main/java/org/myrobotlab/caliko/Model.java new file mode 100644 index 0000000000..887934bdc6 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/Model.java @@ -0,0 +1,928 @@ +package org.myrobotlab.caliko; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import au.edu.federation.utils.Vec3f; +import au.edu.federation.utils.Vec3i; + +//TODO: This is pretty ineficient - change all the for..each loops to be normals loops to stop Java allocating memory. +//TODO: Also provide a proper copy-constructor rather than a clone method - they should do the same thing. + +/** + * A class to represent and load a 3D model in WaveFront .OBJ format. + *

+ * Vertices, normals, and faces with or without normal indices are supported. + *

+ * Models must be stored as triangles, not quads. + *

+ * There is no support for textures, texture coordinates, or grouped objects at this time. + * + * @author Al Lansley + * @version 0.5.1 - 07/01/2016 + */ +public class Model +{ + private static boolean VERBOSE = false; + private static final String NUMBER_OF_VERTICES_LOG = "Number of vertices in data array: %d (%d bytes)"; + private static final String NUMBER_OF_NORMALS_LOG = "Number of normals in data array: %d (%d bytes)"; + private static final String WRONG_COMPONENT_COUNT_LOG = "Found %s data with wrong component count at line number: %d - Skipping!"; + + // ---------- Private Properties ---------- + + // These are the models values as read from the file - they are not the final, consolidated model data + private List vertices = new ArrayList<>(); + private List normals = new ArrayList<>(); + private List normalIndices = new ArrayList<>(); + private List faces = new ArrayList<>(); + //ArrayList texCoords = new ArrayList(); + + // The vertexData and normalData arrays are the final consolidated data which we can draw with. + // + // Note: If the model has only vertices and/or normals, then the vertexData and normalData will be a direct 'unwrapped' + // version of the vertices and normals arrays. However, if we're using faces then there will likely be a lower number of + // vertices / normals as each vertex / normal may be used more than once in the model - the end result of this is that + // the vertexData and normalData will likely be larger than the 'as-read-from-file' vertices and normals arrays because + // of this duplication of values from the face data. + private List vertexData = new ArrayList<>(); + private List normalData = new ArrayList<>(); + //ArrayList texCoordData = new ArrayList(); + + // Counters to keep track of how many vertices, normals, normal indices, texture coordinates and faces + private int numVertices; + private int numNormals; + private int numNormalIndices; + private int numFaces; + //private int numTexCoords; + + // ---------- Public Methods ---------- + + /** Default constructor. */ + public Model() { } + + /** + * Constructor which creates a model object and loads the model from file. + * + * @param filename The file to load the model data from. + */ + public Model(String filename) { load(filename); } + + /** Enable verbose messages. */ + public static void enableVerbose() { VERBOSE = true; } + + /** Disable verbose messages. */ + public static void disableVerbose() { VERBOSE = false; } + + // Method provide create a deep copy of a Model so we can say copyOfMyModel.equals(myModel); + // Note: We only deep copy the vertexData and normalData arrays, not all the vectors! + public static Model clone(Model sourceModel) + { + // Create a new Model which we will clone across the data to from our source model + Model model = new Model(); + + // Update the counts of vertices and normals for the clone to match the source model + model.numVertices = sourceModel.getNumVertices(); + model.numNormals = sourceModel.getNumNormals(); + + // If the source model has vertices then copy them across to the clone... + if (model.numVertices > 0) + { + // For (foo IN bar) loops leak memory - go old-school + int vertCount = sourceModel.getNumVertices(); + for (int loop = 0; loop < vertCount; loop++) + { + //model.vertexData.add(f) + } + for ( Float f : sourceModel.getVertexData() ) { + model.vertexData.add(f); + } + } + else // ...or abort if we have no vertices to copy! + { + throw new RuntimeException("Model created using clone method has 0 vertices!"); + } + + // If the source model has normals then copy them across to the clone... + if (model.numNormals > 0) + { + for ( Float f : sourceModel.getNormalData() ) { + model.normalData.add(f); + } + } + else // ...or (potentially) inform the user if they are no normals. This is not necessarily a deal breaker though, so we don't abort. + { + if (VERBOSE) { + System.out.println( "Model created using clone method has 0 normals - continuing..."); + } + } + + // Display final status if appropriate + if (VERBOSE) { + System.out.println( "Model successfully cloned."); + } + + // Finally, return our cloned model + return model; + } + + /** + * Load a .OBJ model from file. + *

+ * By default, no feedback is provided on the model loading. If you wish to see what's going on + * internally, call Model.enableVerbose() before loading the model - this will display statistics + * about any vertices/normals/normal indices/faces found in the model, as well as any malformed data. + *

+ * If the model file does not contain any vertices then a RuntimeException is thrown. + * If the file cannot be found then a FileNotFoundException is thrown. + * If there was a file-system-type error when reading the file then an IOException is thrown. + * + * @param filename The name of the Wavefront .OBJ format model to load, include the path if necessary. + * @return Whether the file loaded successfully or not. Loading with warnings still counts as a + * successful load - if necessary enable verbose mode to ensure your model loaded cleanly. + */ + public boolean load(String filename) + { + // Load the model file + boolean modelLoadedCleanly = loadModel(filename); + + // Did we load the file without errors? + if (VERBOSE) + { + if (modelLoadedCleanly) { + System.out.println("Model loaded cleanly."); + } + else { + System.out.println("Model loaded with errors."); + } + } + + // Do we have vertices? If not then this is a non-recoverable error and we abort! + if ( hasVertices() ) + { + if (VERBOSE) + { + System.out.println("Model vertex count: " + getNumVertices() ); + if ( hasFaces() ) { + System.out.println( "Model face count: " + getNumFaces() ); + } + if ( hasNormals() ) { + System.out.println( "Model normal count: " + getNumNormals() ); + } + if ( hasNormalIndices() ) { + System.out.println( "Model normal index count: " + getNumNormalIndices() ); + } + } + } + else { throw new RuntimeException("Model has no vertices."); } + + // Transfer the loaded data in our vectors to the data arrays + setupData(); + + // Delete the vertices, normals, normalIndices and faces Lists as we now have the final + // data stored in the vertexData and normalData Lists. + vertices.clear(); + normals.clear(); + normalIndices.clear(); + faces.clear(); + vertices = null; + normals = null; + normalIndices = null; + faces = null; + + // Indicate that the model loaded successfully + return true; + } + + + // ---------- Getters ---------- + + /** + * Get the vertex data as a list of floats. + * + * @return The vertex data. + */ + public List getVertexData() { return vertexData; } + + /** + * Get the vertex normals as a list of floats. + * + * @return The vertex normal data. + */ + public List getNormalData() { return normalData; } + + /** + * Get the vertex data as a float array suitable for transfer into a FloatBuffer for drawing. + * + * @return The vertex data as a float array. + **/ + public float[] getVertexFloatArray() + { + // How many floats are there in our list of vertex data? + int numVertexFloats = vertexData.size(); + + // Create an array big enough to hold them + float[] vertexFloatArray = new float[numVertexFloats]; + + // Loop over each item in the list, setting it to the appropriate element in the array + for (int loop = 0; loop < numVertexFloats; loop++) { + vertexFloatArray[loop] = vertexData.get(loop); + } + + // Finally, return the float array + return vertexFloatArray; + } + + /** + * Get the vertex normal data as a float array suitable for transfer into a FloatBuffer for drawing. + * + * @return The vertex normal data as a float array. + */ + public float[] getNormalFloatArray() + { + // How many floats are there in our list of normal data? + int numNormalFloats = normalData.size(); + + // Create an array big enough to hold them + float[] normalFloatArray = new float[numNormalFloats]; + + // Loop over each item in the list, setting it to the appropriate element in the array + for (int loop = 0; loop < numNormalFloats; loop++) { + normalFloatArray[loop] = normalData.get(loop); + } + + // Finally, return the float array + return normalFloatArray; + } + + // Methods to get the sizes of various data arrays + // Note: Type.BYTES returns the size of on object of this type in Bytes, and we multiply + // by 3 because there are 3 components to a vertex (x/y/z), normal (s/t/p) and 3 vertexes comprising a face (i.e. triangle) + + /** + * Get the vertex data size in bytes. + * + * @return The vertex data size in bytes. + */ + public int getVertexDataSizeBytes() { return numVertices * 3 * Float.BYTES; } + + /** + * Get the vertex normal data size in bytes. + * + * @return The vertex normal data size in bytes. + */ + public int getNormalDataSizeBytes() { return numNormals * 3 * Float.BYTES; } + + /** + * Get the face data size in bytes. + * + * @return The face data size in bytes. + **/ + public int getFaceDataSizeBytes() { return numFaces * 3 * Integer.BYTES; } + + /** + * Get the number of vertices in this model. + * + * @return The number of vertices in this model. + */ + public int getNumVertices() { return numVertices; } + + /** + * Get the number of vertex normals in this model. + * + * @return The number of normals in this model. + */ + public int getNumNormals() { return numNormals; } + + /** Get the number of normal indices in this model. + * + * + * @return The number of normal indices in this model. + */ + public int getNumNormalIndices() { return numNormalIndices; } + + /** + * Get the number of faces in this model. + * + * @return The number of faces in this model. + */ + public int getNumFaces() { return numFaces; } + + // ---------- Utility Methods ---------- + + /** + * Scale this model uniformly along the x/y/z axes. + * + * @param scale The amount to scale the model. + **/ + public void scale(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 0; loop < numVerts; ++loop) { + vertexData.set(loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model on the X axis. + * + * @param scale The amount to scale the model on the X axis. + */ + public void scaleX(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 0; loop < numVerts; loop += 3) { + vertexData.set( loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model on the Y axis. + * + * @param scale The amount to scale the model on the Y axis. + */ + public void scaleY(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 1; loop < numVerts; loop += 3) { + vertexData.set( loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model on the Z axis. + * + * @param scale The amount to scale the model on the Z axis. + */ + public void scaleZ(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 2; loop < numVerts; loop += 3) { + vertexData.set( loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model by various amounts along separate axes. + * + * @param xScale The amount to scale the model on the X axis. + * @param yScale The amount to scale the model on the Y axis. + * @param zScale The amount to scale the model on the Z axis. + */ + public void scale(float xScale, float yScale, float zScale) + { + int numVerts = vertexData.size(); + for (int loop = 0; loop < numVerts; ++loop) + { + switch (loop % 3) + { + case 0: + vertexData.set(loop, vertexData.get(loop) * xScale); + break; + case 1: + vertexData.set(loop, vertexData.get(loop) * yScale); + break; + case 2: + vertexData.set(loop, vertexData.get(loop) * zScale); + break; + } + } + } + + /** Print out the vertices of this model. */ + public void printVertices() { + for (Vec3f v : vertices) { + System.out.println( "Vertex: " + v.toString() ); + } + } + + /** + * Print out the vertex normal data for this model. + *

+ * Note: This is the contents of the normals list, not the (possibly expanded) normalData array. + */ + public void printNormals() + { + for (Vec3f n : normals) { + System.out.println( "Normal: " + n.toString() ); + } + } + + /** + * Print the face data of this model. + *

+ * Note: Faces are ONE indexed, not zero indexed. + */ + public void printFaces() + { + for (Vec3i face : faces) { + System.out.println( "Face: " + face.toString() ); + } + } + + /** + * Print the vertex data of this model. + *

+ * Note: This is the contents of the vertexlData array which is actually used when drawing - and which may be + * different to the 'vertices' list when using faces (where vertices get re-used). + */ + public void printVertexData() + { + for (int loop = 0; loop < vertexData.size(); loop += 3) + { + System.out.println( "Vertex data element " + (loop / 3) + " is x: " + vertexData.get(loop) + "\ty: " + vertexData.get(loop+1) + "\tz: " + vertexData.get(loop+2) ); + } + } + + /** + * Print the normal data of this model. + *

+ * Note: This is the contents of the normalData array which is actually used when drawing - and which may be + * different to the 'normals' list when using faces (where normals get re-used). + */ + public void printNormalData() + { + for (int loop = 0; loop < normalData.size(); loop += 3) + { + System.out.println( "Normal data element " + (loop / 3) + " is x: " + normalData.get(loop) + "\ty: " + normalData.get(loop+1) + "\tz: " + normalData.get(loop+2)); + } + } + + // ---------- Private Methods ---------- + + // Method to read through the model file adding all vertices, faces and normals to our + // vertices, faces and normals vectors. + + // Note: This does NOT transfer the data into our vertexData, faceData or normalData arrays! + // That must be done as a separate step by calling setupData() after building up the + // arraylists with this method! + + // Also: This method does not decrement the face number of normal index by 1 (because .OBJ + // files start their counts at 1) to put them in a range starting from 0, that job + // is done in the setupData() method performed after calling this method! + private boolean loadModel(String filename) + { + // Initialise lists + vertices = new ArrayList<>(); + normals = new ArrayList<>(); + normalIndices = new ArrayList<>(); + faces = new ArrayList<>(); + //texCoords = new ArrayList(); + normalData = new ArrayList<>(); + //texCoordData = new ArrayList(); + + // Our vectors of attributes are initially empty + numVertices = 0; + numNormals = 0; + numNormalIndices = 0; + numFaces = 0; + //numTexCoords = 0; + + boolean loadedCleanly = true; + + // Counter to keep track of what line we're on + int lineCount = 0; + + // Use this for jar packaged resources + InputStream is = this.getClass().getResourceAsStream(filename); + try (BufferedReader br = new BufferedReader( new InputStreamReader(is) ) ) // This version loads from within jar archive, required for caliko-demo-jar-with-resources.jar + //try (BufferedReader br = new BufferedReader( new FileReader(filename) ) ) // Use this for loading from file in Eclipse or such + { + // We'll read through the file one line at a time - this will hold the current line we're working on + String line; + + // While there are lines left to read in the file... + while ((line = br.readLine()) != null) + { + ++lineCount; + + // If the line isn't empty (the 1 character is the carriage return), process it... + if (line.length() > 1) + { + // Split line on spaces into an array of strings.The + on the end of "\\s+" means 'do not accept + // blank entries' which can occur if you have two consecutive space characters (as happens when + // you export a .obj from 3ds max - you get "v 1.23 4.56 7.89" etc.) + String[] token = line.split("\\s+"); + + // If the first token is "v", then we're dealing with vertex data + if ( "v".equalsIgnoreCase(token[0]) ) + { + // As long as there's 4 tokens on the line... + if (token.length == 4) + { + // ...get the remaining 3 tokens as the x/y/z float values... + float x = Float.parseFloat(token[1]); + float y = Float.parseFloat(token[2]); + float z = Float.parseFloat(token[3]); + + // ... and push them into the vertices vector ... + vertices.add( new Vec3f(x, y, z) ); + + // .. then increase our vertex count. + numVertices++; + } + else // If we got vertex data without 3 components - whine! + { + loadedCleanly = false; + System.out.printf(WRONG_COMPONENT_COUNT_LOG,"vertex",lineCount); + } + + } + else if ( "vn".equalsIgnoreCase(token[0]) ) // If the first token is "vn", then we're dealing with a vertex normal + { + // As long as there's 4 tokens on the line... + if (token.length == 4) + { + // ...get the remaining 3 tokens as the x/y/z normal float values... + float normalX = Float.parseFloat(token[1]); + float normalY = Float.parseFloat(token[2]); + float normalZ = Float.parseFloat(token[3]); + + // ... and push them into the normals vector ... + normals.add( new Vec3f(normalX, normalY, normalZ) ); + + // .. then increase our normal count. + numNormals++; + } + else // If we got normal data without 3 components - whine! + { + loadedCleanly = false; + System.out.printf(WRONG_COMPONENT_COUNT_LOG,"normal",lineCount); + } + + } // End of vertex line parsing + + // If the first token is "f", then we're dealing with faces + // + // Note: Faces can be described in two ways - we can have data like 'f 123 456 789' which means that the face is comprised + // of vertex 123, vertex 456 and vertex 789. Or, we have have data like f 123//111 456//222 789//333 which means that + // the face is comprised of vertex 123 using normal 111, vertex 456 using normal 222 and vertex 789 using normal 333. + else if ( "f".equalsIgnoreCase(token[0]) ) + { + // Check if there's a double-slash in the line + int pos = line.indexOf("//"); + + // As long as there's four tokens on the line and they don't contain a "//"... + if ( (token.length == 4) && (pos == -1) ) + { + // ...get the face vertex numbers as ints ... + int v1 = Integer.parseInt(token[1]); + int v2 = Integer.parseInt(token[2]); + int v3 = Integer.parseInt(token[3]); + + // ... and push them into the faces vector ... + faces.add( new Vec3i(v1, v2, v3) ); + + // .. then increase our face count. + numFaces++; + } + else if ( (token.length == 4) && (pos != -1) ) // 4 tokens and found 'vertex//normal' notation? + { + // ----- Get the 1st of three tokens as a String ----- + + // Find where the double-slash starts in that token + int faceEndPos = token[1].indexOf("//"); + + // Put sub-String from the start to the beginning of the double-slash into our subToken String + String faceToken1 = token[1].substring(0, faceEndPos); + + // Convert face token value to int + int ft1 = Integer.parseInt(faceToken1); + + // Mark the start of our next subtoken + int nextTokenStartPos = faceEndPos + 2; + + // Copy from first character after the "//" to the end of the token + String normalToken1 = token[1].substring(nextTokenStartPos); + + // Convert normal token value to int + int nt1 = Integer.parseInt(normalToken1); + + // ----- Get the 2nd of three tokens as a String ----- + + // Find where the double-slash starts in that token + faceEndPos = token[2].indexOf("//"); + + // Put sub-String from the start to the beginning of the double-slash into our subToken String + String faceToken2 = token[2].substring(0, faceEndPos); + + // Convert face token value to int + int ft2 = Integer.parseInt(faceToken2); + + // Mark the start of our next subtoken + nextTokenStartPos = faceEndPos + 2; + + // Copy from first character after the "//" to the end of the token + String normalToken2 = token[2].substring(nextTokenStartPos); + + // Convert normal token value to int + int nt2 = Integer.parseInt(normalToken2); + + // ----- Get the 3rd of three tokens as a String ----- + + // Find where the double-slash starts in that token + faceEndPos = token[3].indexOf("//"); + + // Put sub-String from the start to the beginning of the double-slash into our subToken String + String faceToken3 = token[3].substring(0, faceEndPos); + + // Convert face token value to int + int ft3 = Integer.parseInt(faceToken3); + + // Mark the start of our next subtoken + nextTokenStartPos = faceEndPos + 2; + + // Copy from first character after the "//" to the end + String normalToken3 = token[3].substring(nextTokenStartPos); + + // Convert normal token value to int + int nt3 = Integer.parseInt(normalToken3); + + + // Finally, add the face to the faces array list and increment the face count... + faces.add( new Vec3i(ft1, ft2, ft3) ); + numFaces++; + + // ...and do the same for the normal indices and the normal index count. + normalIndices.add( new Vec3i(nt1, nt2, nt3) ); + numNormalIndices++; + + } + else // If we got face data without 3 components - whine! + { + loadedCleanly = false; + System.out.printf(WRONG_COMPONENT_COUNT_LOG,"face",lineCount); + } + + } // End of if token is "f" (i.e. face indices) + + // IMPLIED ELSE: If first token is something we don't recognise then we ignore it as a comment. + + } // End of line parsing section + + } // End of if line length > 1 check + + // No need to close the file ( i.e. br.close() ) - try-with-resources does that for us. + } + catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); } + catch (IOException ioe) { ioe.printStackTrace(); } + + // Return our boolean flag to say whether we loaded the model cleanly or not + return loadedCleanly; + } + + // ----- Helper Methods ----- + + /** + * Return whether or not this model contains vertex data. + * + * @return whether or not this model contains vertex data. + */ + public boolean hasVertices() { return (numVertices > 0); } + + /** + * Return whether or not this model contains face data. + * + * @return whether or not this model contains face data. + */ + public boolean hasFaces() { return (numFaces > 0); } + + /** + * Return whether or not this model contains normal data. + * + * @return whether or not this model contains normal data. + */ + public boolean hasNormals() { return (numNormals > 0); } + + /** + * Return whether or not this model contains normal index data. + * + * @return whether or not this model contains normal index data. + */ + public boolean hasNormalIndices() { return (numNormalIndices > 0); } + + /** + * Set up our plain arrays of floats for OpenGL to work with. + *

+ * Note: We CANNOT have size mismatches! The vertex count must match the + * normal count i.e. every vertex must have precisely ONE normal - no more, no less! + */ + private void setupData() + { + if (VERBOSE) { + System.out.println( "Setting up model data to draw as arrays."); + } + + // If we ONLY have vertex data, then transfer just that... + if ( ( hasVertices() ) && ( !hasFaces() ) && ( !hasNormals() ) ) + { + if (VERBOSE) { + System.out.println( "Model has no faces or normals. Transferring vertex data only."); + } + + // Reset the vertex count + numVertices = 0; + + // Transfer all vertices from the vertices vector to the vertexData array + for (Vec3f v : vertices) + { + vertexData.add( v.x ); + vertexData.add( v.y ); + vertexData.add( v.z ); + ++numVertices; + } + + // Print a summary of the vertex data + if (VERBOSE) { + System.out.printf( NUMBER_OF_VERTICES_LOG, numVertices, getVertexDataSizeBytes()); + } + } + // If we have vertices AND faces BUT NOT normals... + else if ( ( hasVertices() ) && ( hasFaces() ) && ( !hasNormals() ) ) + { + if (VERBOSE) { + System.out.println("Model has vertices and faces, but no normals. Per-face normals will be generated.") ; + } + + // Create the vertexData and normalData arrays from the vector of faces + // Note: We generate the face normals ourselves. + int vertexCount = 0; + int normalCount = 0; + + for (Vec3i iv : faces) + { + // Get the numbers of the three vertices that this face is comprised of + int firstVertexNum = iv.x; + int secondVertexNum = iv.y; + int thirdVertexNum = iv.z; + + // Now that we have the vertex numbers, we need to get the actual vertices + // Note: We subtract 1 from the number of the vertex because faces start at + // face number 1 in the .OBJ format, while in our code the first vertex + // will be at location zero. + Vec3f faceVert1 = vertices.get(firstVertexNum - 1); + Vec3f faceVert2 = vertices.get(secondVertexNum - 1); + Vec3f faceVert3 = vertices.get(thirdVertexNum - 1); + + // Now that we have the 3 vertices, we need to calculate the normal of the face + // formed by these vertices... + + // Convert this vertex data into a pure form + Vec3f v1 = faceVert2.minus(faceVert1); + Vec3f v2 = faceVert3.minus(faceVert1); + + // Generate the normal as the cross product and normalise it + Vec3f normal = v1.cross(v2); + Vec3f normalisedNormal = normal.normalise(); + + // Put the vertex data into our vertexData array + vertexData.add( faceVert1.x ); + vertexData.add( faceVert1.y ); + vertexData.add( faceVert1.z ); + vertexCount++; + + vertexData.add( faceVert2.x ); + vertexData.add( faceVert2.y ); + vertexData.add( faceVert2.z ); + vertexCount++; + + vertexData.add( faceVert3.x ); + vertexData.add( faceVert3.y ); + vertexData.add( faceVert3.z ); + vertexCount++; + + // Put the normal data into our normalData array + // + // Note: we put the same normal into the normalData array for each of the 3 vertices comprising the face! + // This gives use a 'faceted' looking model, but is easy! You could try to calculate an interpolated + // normal based on surrounding normals, but that's not a trivial task (although 3ds max will do it for you + // if you load up the model and export it with normals!) + normalData.add( normalisedNormal.x ); + normalData.add( normalisedNormal.y ); + normalData.add( normalisedNormal.z ); + normalCount++; + + normalData.add( normalisedNormal.x ); + normalData.add( normalisedNormal.y ); + normalData.add( normalisedNormal.z ); + normalCount++; + + normalData.add( normalisedNormal.x ); + normalData.add( normalisedNormal.y ); + normalData.add( normalisedNormal.z ); + normalCount++; + + } // End of loop iterating over the model faces + + numVertices = vertexCount; + numNormals = normalCount; + + if (VERBOSE) + { + System.out.printf( NUMBER_OF_VERTICES_LOG, numVertices, getVertexDataSizeBytes()); + System.out.printf( NUMBER_OF_NORMALS_LOG, numNormals, getNormalDataSizeBytes()); + } + } + // If we have vertices AND faces AND normals AND normalIndices... + else if ( ( hasVertices() ) && ( hasFaces() ) && ( hasNormals() ) && ( hasNormalIndices() ) ) + { + if (VERBOSE) + { + System.out.println("Model has vertices, faces, normals & normal indices. Transferring data."); + } + + //FIXME: Change this to use the numVertices and numNormals directly - I don't see a reason to use separate vertexCount and normalCount vars... + + int vertexCount = 0; + int normalCount = 0; + + // Look up each vertex specified by each face and add the vertex data to the vertexData array + for (Vec3i iv : faces) + { + // Get the numbers of the three vertices that this face is comprised of + int firstVertexNum = iv.x; + int secondVertexNum = iv.y; + int thirdVertexNum = iv.z; + + // Now that we have the vertex numbers, we need to get the actual vertices + // Note: We subtract 1 from the number of the vertex because faces start at + // face number 1 in the .oBJ format, while in our code the first vertex + // will be at location zero. + Vec3f faceVert1 = vertices.get(firstVertexNum - 1); + Vec3f faceVert2 = vertices.get(secondVertexNum - 1); + Vec3f faceVert3 = vertices.get(thirdVertexNum - 1); + + // Put the vertex data into our vertexData array + vertexData.add( faceVert1.x ); + vertexData.add( faceVert1.y ); + vertexData.add( faceVert1.z ); + ++vertexCount; + + vertexData.add( faceVert2.x ); + vertexData.add( faceVert2.y ); + vertexData.add( faceVert2.z ); + ++vertexCount; + + vertexData.add( faceVert3.x ); + vertexData.add( faceVert3.y ); + vertexData.add( faceVert3.z ); + ++vertexCount; + } + + // Look up each normal specified by each normal index and add the normal data to the normalData array + for (Vec3i normInd : normalIndices) + { + // Get the numbers of the three normals that this face uses + int firstNormalNum = normInd.x; + int secondNormalNum = normInd.y; + int thirdNormalNum = normInd.z; + + // Now that we have the normal index numbers, we need to get the actual normals + // Note: We subtract 1 from the number of the normal because normals start at + // number 1 in the .obJ format, while in our code the first vertex + // will be at location zero. + Vec3f normal1 = normals.get(firstNormalNum - 1); + Vec3f normal2 = normals.get(secondNormalNum - 1); + Vec3f normal3 = normals.get(thirdNormalNum - 1); + + // Put the normal data into our normalData array + normalData.add( normal1.x ); + normalData.add( normal1.y ); + normalData.add( normal1.z ); + normalCount++; + + normalData.add( normal2.x ); + normalData.add( normal2.y ); + normalData.add( normal2.z ); + normalCount++; + + normalData.add( normal3.x ); + normalData.add( normal3.y ); + normalData.add( normal3.z ); + normalCount++; + + } // End of loop iterating over the model faces + + numVertices = vertexCount; + numNormals = normalCount; + + if (VERBOSE) + { + System.out.printf( NUMBER_OF_VERTICES_LOG, numVertices, getVertexDataSizeBytes()); + System.out.printf( NUMBER_OF_NORMALS_LOG, numNormals, getNormalDataSizeBytes()); + } + } + else + { + System.out.println("Something bad happened in Model.setupData() =("); + } + + } // End of setupData method + +} // End of Model class diff --git a/src/main/java/org/myrobotlab/caliko/OpenGLWindow.java b/src/main/java/org/myrobotlab/caliko/OpenGLWindow.java new file mode 100644 index 0000000000..9582c6adf7 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/OpenGLWindow.java @@ -0,0 +1,505 @@ +package org.myrobotlab.caliko; + +import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MAJOR; +import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MINOR; +import static org.lwjgl.glfw.GLFW.GLFW_CURSOR; +import static org.lwjgl.glfw.GLFW.GLFW_CURSOR_DISABLED; +import static org.lwjgl.glfw.GLFW.GLFW_CURSOR_NORMAL; +import static org.lwjgl.glfw.GLFW.GLFW_FOCUSED; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_A; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_C; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_D; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_DOWN; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_F; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_L; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_LEFT; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_M; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_P; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_R; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_RIGHT; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_S; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_SPACE; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_UP; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_W; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_X; +import static org.lwjgl.glfw.GLFW.GLFW_MOUSE_BUTTON_1; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_CORE_PROFILE; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_PROFILE; +import static org.lwjgl.glfw.GLFW.GLFW_PRESS; +import static org.lwjgl.glfw.GLFW.GLFW_RELEASE; +import static org.lwjgl.glfw.GLFW.GLFW_REPEAT; +import static org.lwjgl.glfw.GLFW.GLFW_RESIZABLE; +import static org.lwjgl.glfw.GLFW.GLFW_SAMPLES; +import static org.lwjgl.glfw.GLFW.GLFW_TRUE; +import static org.lwjgl.glfw.GLFW.GLFW_VISIBLE; +import static org.lwjgl.glfw.GLFW.glfwCreateWindow; +import static org.lwjgl.glfw.GLFW.glfwDestroyWindow; +import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor; +import static org.lwjgl.glfw.GLFW.glfwGetVideoMode; +import static org.lwjgl.glfw.GLFW.glfwInit; +import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; +import static org.lwjgl.glfw.GLFW.glfwSetCursorPos; +import static org.lwjgl.glfw.GLFW.glfwSetCursorPosCallback; +import static org.lwjgl.glfw.GLFW.glfwSetErrorCallback; +import static org.lwjgl.glfw.GLFW.glfwSetInputMode; +import static org.lwjgl.glfw.GLFW.glfwSetKeyCallback; +import static org.lwjgl.glfw.GLFW.glfwSetMouseButtonCallback; +import static org.lwjgl.glfw.GLFW.glfwSetWindowPos; +import static org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose; +import static org.lwjgl.glfw.GLFW.glfwSetWindowSizeCallback; +import static org.lwjgl.glfw.GLFW.glfwSetWindowTitle; +import static org.lwjgl.glfw.GLFW.glfwShowWindow; +import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; +import static org.lwjgl.glfw.GLFW.glfwSwapInterval; +import static org.lwjgl.glfw.GLFW.glfwTerminate; +import static org.lwjgl.glfw.GLFW.glfwWindowHint; +import static org.lwjgl.opengl.GL11.GL_BLEND; +import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST; +import static org.lwjgl.opengl.GL11.GL_LEQUAL; +import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA; +import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA; +import static org.lwjgl.opengl.GL11.glBlendFunc; +import static org.lwjgl.opengl.GL11.glClearColor; +import static org.lwjgl.opengl.GL11.glClearDepth; +import static org.lwjgl.opengl.GL11.glDepthFunc; +import static org.lwjgl.opengl.GL11.glEnable; +import static org.lwjgl.opengl.GL11.glViewport; +import static org.lwjgl.system.MemoryUtil.NULL; + +import org.lwjgl.glfw.GLFWCursorPosCallback; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWKeyCallback; +import org.lwjgl.glfw.GLFWMouseButtonCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.glfw.GLFWWindowSizeCallback; +import org.lwjgl.opengl.GL; +import org.myrobotlab.service.Caliko; +import org.myrobotlab.service.config.CalikoConfig; + +import au.edu.federation.caliko.visualisation.Axis; +import au.edu.federation.caliko.visualisation.Camera; +import au.edu.federation.caliko.visualisation.Grid; +//import au.edu.federation.caliko.demo2d.CalikoDemoStructure2DFactory.CalikoDemoStructure2DEnum; +//import au.edu.federation.caliko.demo3d.CalikoDemoStructure3DFactory.CalikoDemoStructure3DEnum; +import au.edu.federation.utils.Mat4f; +import au.edu.federation.utils.Utils; +import au.edu.federation.utils.Vec2f; +import au.edu.federation.utils.Vec3f; + +/** + * Class to set up an OpenGL window. + * + * @author Al Lansley + * @version 0.3 - 07/12/2015 + */ +public class OpenGLWindow +{ + + // Mouse cursor locations in screen-space and world-space + public Vec2f screenSpaceMousePos = null; + public Vec2f worldSpaceMousePos = new Vec2f(); + + // Window properties + long mWindowId; + int mWindowWidth; + int mWindowHeight; + float mAspectRatio; + + // Matrices + Mat4f mProjectionMatrix; + Mat4f mModelMatrix = new Mat4f(1.0f); + Mat4f mMvpMatrix = new Mat4f(); + + // Matrix properties + boolean mOrthographicProjection; // Use orthographic projection? If false, we use a standard perspective projection + float mVertFoVDegs; + float mZNear; + float mZFar; + float mOrthoExtent; + + // We need to strongly reference callback instances so that they don't get garbage collected. + private GLFWErrorCallback errorCallback; + private GLFWKeyCallback keyCallback; + private GLFWWindowSizeCallback windowSizeCallback; + private GLFWMouseButtonCallback mouseButtonCallback; + private GLFWCursorPosCallback cursorPosCallback; + + CalikoConfig config = null; + + Caliko service = null; + public Application application; + + // Constructor + public OpenGLWindow(Application application, Caliko service, int windowWidth, int windowHeight, float vertFoVDegs, float zNear, float zFar, float orthoExtent) + { + this.service = service; + this.application = application; + + // Set properties and create the projection matrix + mWindowWidth = windowWidth <= 0 ? 1 : windowWidth; + mWindowHeight = windowHeight <= 0 ? 1 : windowHeight; + mAspectRatio = (float)mWindowWidth / (float)mWindowHeight; + + mVertFoVDegs = vertFoVDegs; + mZNear = zNear; + mZFar = zFar; + mOrthoExtent = orthoExtent; + + config = service.getConfig(); + + screenSpaceMousePos = new Vec2f(config.windowWidth / 2.0f, config.windowHeight / 2.0f); + +// if (config.use3dDemo) + + mOrthographicProjection = false; + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + +// else +// { +// mOrthographicProjection = true; +// mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); +// } + + // Setup the error callback to output to System.err + glfwSetErrorCallback(errorCallback = GLFWErrorCallback.createPrint(System.err)); + + // Initialize GLFW. Most GLFW functions will not work before doing this. + if ( !glfwInit() ) { throw new IllegalStateException("Unable to initialize GLFW"); } + + // ----- Specify window hints ----- + // Note: Window hints must be specified after glfwInit() (which resets them) and before glfwCreateWindow where the context is created. + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // Request OpenGL version 3.3 (the minimum we can get away with) + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // We want a core profile without any deprecated functionality... + //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // ...however we do NOT want a forward compatible profile as they've removed line widths! + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // We want the window to be resizable + glfwWindowHint(GLFW_VISIBLE, GLFW_TRUE); // We want the window to be visible (false makes it hidden after creation) + glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE); // We want the window to take focus on creation + glfwWindowHint(GLFW_SAMPLES, 4); // Ask for 4x anti-aliasing (this doesn't mean we'll get it, though) + + // Create the window + mWindowId = glfwCreateWindow(mWindowWidth, mWindowHeight, "LWJGL3 Test", NULL, NULL); + if (mWindowId == NULL) { throw new RuntimeException("Failed to create the GLFW window"); } + + // Get the resolution of the primary monitor + GLFWVidMode vidmode = glfwGetVideoMode( glfwGetPrimaryMonitor() ); + int windowHorizOffset = (vidmode.width() - mWindowWidth) / 2; + int windowVertOffset = (vidmode.height() - mWindowHeight) / 2; + + glfwSetWindowPos(mWindowId, windowHorizOffset, windowVertOffset); // Center our window + glfwMakeContextCurrent(mWindowId); // Make the OpenGL context current + glfwSwapInterval(1); // Swap buffers every frame (i.e. enable vSync) + + // This line is critical for LWJGL's interoperation with GLFW's OpenGL context, or any context that is managed externally. + // LWJGL detects the context that is current in the current thread, creates the ContextCapabilities instance and makes + // the OpenGL bindings available for use. + glfwMakeContextCurrent(mWindowId); + + // Enumerate the capabilities of the current OpenGL context, loading forward compatible capabilities + GL.createCapabilities(true); + + // Setup our keyboard, mouse and window resize callback functions + setupCallbacks(); + + // ---------- OpenGL settings ----------- + + // Set the clear color + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + + // Specify the size of the viewport. Params: xOrigin, yOrigin, width, height + glViewport(0, 0, mWindowWidth, mWindowHeight); + + // Enable depth testing + glDepthFunc(GL_LEQUAL); + glEnable(GL_DEPTH_TEST); + + // When we clear the depth buffer, we'll clear the entire buffer + glClearDepth(1.0f); + + // Enable blending to use alpha channels + // Note: blending must be enabled to use transparency / alpha values in our fragment shaders. + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + + glfwShowWindow(mWindowId); // Make the window visible + } + + // Constructor with some sensible projection matrix values hard-coded + public OpenGLWindow(Application application, Caliko service, int width, int height) { this(application, service, width, height, 35.0f, 1.0f, 5000.0f, 120.0f); } + + /** Return a calculated ModelViewProjection matrix. + *

+ * This MVP matrix is the result of multiplying the projection matrix by the view matrix obtained from the camera, and + * as such is really a ProjectionView matrix or 'identity MVP', however you'd like to term it. + * + * If you want a MVP matrix specific to your model, simply multiply this matrix by your desired model matrix to create + * a MVP matrix specific to your model. + * + * @return A calculate ModelViewProjection matrix. + */ + public Mat4f getMvpMatrix() { return mProjectionMatrix.times( service.getCamera().getViewMatrix() ); } + + /** + * Return the projection matrix. + * + * @return The projection matrix. + */ + public Mat4f getProjectionMatrix() { return mProjectionMatrix; } + + /** Swap the front and back buffers to update the display. */ + public void swapBuffers() { glfwSwapBuffers(mWindowId); } + + /** + * Set the window title to the specified String argument. + * + * @param title The String that will be used as the title of the window. + */ + public void setWindowTitle(String title) { glfwSetWindowTitle(mWindowId, title); } + + /** Destroy the window, finish up glfw and release all callback methods. */ + public void cleanup() + { + // Free the window callbacks and destroy the window + //glfwFreeCallbacks(mWindowId); + cursorPosCallback.close(); + mouseButtonCallback.close(); + windowSizeCallback.close(); + keyCallback.close(); + + glfwDestroyWindow(mWindowId); + + // Terminate GLFW and free the error callback + glfwTerminate(); + glfwSetErrorCallback(null).free(); + } + + // Setup keyboard, mouse cursor, mouse button and window resize callback methods. + private void setupCallbacks() + { + // Key callback + glfwSetKeyCallback(mWindowId, keyCallback = GLFWKeyCallback.create( (long window, int key, int scancode, int action, int mods) -> + { + if (action == GLFW_PRESS) + { + switch (key) + { +// // Setup demos +// case GLFW_KEY_RIGHT: +// if (config.use3dDemo) +// { +// if (config.demoNumber < CalikoDemoStructure3DEnum.values().length) { config.demoNumber++; } +// } +// else // 2D Demo mode +// { +// if (config.demoNumber < CalikoDemoStructure2DEnum.values().length) { config.demoNumber++; } +// } +// config.demo.setup(config.demoNumber); +// break; +// case GLFW_KEY_LEFT: +// if (config.demoNumber > 1) { config.demoNumber--; } +// config.demo.setup(config.demoNumber); +// break; +// +// // Toggle fixed base mode +// case GLFW_KEY_F: +// config.fixedBaseMode = !config.fixedBaseMode; +// config.demo.setFixedBaseMode(config.fixedBaseMode); +// break; +// // Toggle rotating bases +// case GLFW_KEY_R: +// config.rotateBasesMode = !config.rotateBasesMode; +// break; + + // Various drawing options + case GLFW_KEY_C: + config.drawConstraints = !config.drawConstraints; + break; + case GLFW_KEY_L: + config.drawLines = !config.drawLines; + break; + case GLFW_KEY_M: + config.drawModels = !config.drawModels; + break; + case GLFW_KEY_P: + mOrthographicProjection = !mOrthographicProjection; + if (mOrthographicProjection) + { + mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); + } + else + { + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + } + break; + case GLFW_KEY_X: + config.drawAxes = !config.drawAxes; + break; + + // Camera controls + case GLFW_KEY_W: + case GLFW_KEY_S: + case GLFW_KEY_A: + case GLFW_KEY_D: + // if (config.use3dDemo) { config.demo.handleCameraMovement(key, action); } + break; + + // Close the window + case GLFW_KEY_ESCAPE: + glfwSetWindowShouldClose(window, true); + break; + + // Cycle through / switch between 2D and 3D demos with the up and down cursors + case GLFW_KEY_UP: + case GLFW_KEY_DOWN: + config.use3dDemo = !config.use3dDemo; + config.demoNumber = 1; + + // Viewing 2D demos? + if (!config.use3dDemo) + { + mOrthographicProjection = true; + mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); + // config.demo = new CalikoDemo2D(config.demoNumber); + } + else // Viewing 3D demos + { + mOrthographicProjection = false; + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + // config.demo = new CalikoDemo3D(config.demoNumber); + } + break; + + // Dynamic add/remove bones for first demo +// case GLFW_KEY_COMMA: +// if (config.demoNumber == 1 && config.structure.getChain(0).getNumBones() > 1) +// { +// config.structure.getChain(0).removeBone(0); +// } +// break; +// case GLFW_KEY_PERIOD: +// if (config.demoNumber == 1) +// { +// config.structure.getChain(0).addConsecutiveBone(config.X_AXIS, config.defaultBoneLength); +// } +// break; + + case GLFW_KEY_SPACE: + application.paused = !application.paused; + break; + + } // End of switch + + } + else if (action == GLFW_REPEAT || action == GLFW_RELEASE) // Camera must also handle repeat or release actions + { + switch (key) + { + case GLFW_KEY_W: + case GLFW_KEY_S: + case GLFW_KEY_A: + case GLFW_KEY_D: + //if (config.use3dDemo) { config.demo.handleCameraMovement(key, action); } + break; + } + } + })); + + // Mouse cursor position callback + glfwSetCursorPosCallback(mWindowId, cursorPosCallback = GLFWCursorPosCallback.create( (long windowId, double mouseX, double mouseY) -> + { + // Update the screen space mouse location + screenSpaceMousePos.set( (float)mouseX, (float)mouseY ); + + // If we're holding down the LMB, then... + if (config.leftMouseButtonDown) + { + // ...in the 3D demo we update the camera look direction... + if (config.use3dDemo) + { + service.getCamera().handleMouseMove(mouseX, mouseY); + } + else // ...while in the 2D demo we update the 2D target. + { + // Convert the mouse position in screen-space coordinates to our orthographic world-space coordinates +// worldSpaceMousePos.set( Utils.convertRange(screenSpaceMousePos.x, 0.0f, mWindowWidth, -mOrthoExtent, mOrthoExtent), +// -Utils.convertRange(screenSpaceMousePos.y, 0.0f, mWindowHeight, -mOrthoExtent, mOrthoExtent) ); +// +// CalikoDemo2D.mStructure.solveForTarget(worldSpaceMousePos); + } + } + })); + + // Mouse button callback + glfwSetMouseButtonCallback(mWindowId, mouseButtonCallback = GLFWMouseButtonCallback.create( (long windowId, int button, int action, int mods) -> + { + // If the left mouse button was the button that invoked the callback... + if (button == GLFW_MOUSE_BUTTON_1) + { + // ...then set the LMB status flag accordingly + // Note: We cannot simply toggle the flag here as double-clicking the title bar to fullscreen the window confuses it and we + // then end up mouselook-ing without the LMB being held down! + if (action == GLFW_PRESS) { config.leftMouseButtonDown = true; } else { config.leftMouseButtonDown = false; } + + if (config.use3dDemo) + { + // Immediately set the cursor position to the centre of the screen so our view doesn't "jump" on first cursor position change + glfwSetCursorPos(windowId, ((double)mWindowWidth / 2), ((double)mWindowHeight / 2) ); + + switch (action) + { + case GLFW_PRESS: + // Make the mouse cursor hidden and put it into a 'virtual' mode where its values are not limited + glfwSetInputMode(mWindowId, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + break; + + case GLFW_RELEASE: + // Restore the mouse cursor to normal and reset the camera last cursor position to be the middle of the window + glfwSetInputMode(windowId, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + service.getCamera().resetLastCursorPosition(); + break; + } + } + else + { + // Convert the mouse position in screen-space coordinates to our orthographic world-space coordinates + worldSpaceMousePos.set( Utils.convertRange(screenSpaceMousePos.x, 0.0f, mWindowWidth, -mOrthoExtent, mOrthoExtent), + -Utils.convertRange(screenSpaceMousePos.y, 0.0f, mWindowHeight, -mOrthoExtent, mOrthoExtent) ); + + // CalikoDemo2D.mStructure.solveForTarget(worldSpaceMousePos); + } + + // Nothing needs be done in 2D demo mode - the config.leftMouseButtonDown flag plus the mouse cursor handler take care of it. + } + })); + + // Window size callback + glfwSetWindowSizeCallback(mWindowId, windowSizeCallback = GLFWWindowSizeCallback.create( (long windowId, int windowWidth, int windowHeight) -> + { + // Update our window width and height and recalculate the aspect ratio + if (windowWidth <= 0) { windowWidth = 1; } + if (windowHeight <= 0) { windowHeight = 1; } + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + mAspectRatio = (float)mWindowWidth / (float)mWindowHeight; + + // Let our camera know about the new size so it can correctly recentre the mouse cursor + service.getCamera().updateWindowSize(windowWidth, windowHeight); + + // Update our viewport + glViewport(0, 0, mWindowWidth, mWindowHeight); + + // Recalculate our projection matrix + if (mOrthographicProjection) + { + mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); + } + else + { + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + } + })); + + } // End of setupCallbacks method + +} // End of OpenGLWindow class \ No newline at end of file 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/framework/Plan.java b/src/main/java/org/myrobotlab/framework/Plan.java index 92f48b066e..ae26195553 100644 --- a/src/main/java/org/myrobotlab/framework/Plan.java +++ b/src/main/java/org/myrobotlab/framework/Plan.java @@ -142,21 +142,5 @@ public void addRegistry(String service) { runtime.add(service); } - /** - * good to prune trees of peers from starting - expecially if the peers - * require re-configuring - * - * @param startsWith - * - removes RuntimeConfig.registry all services that start with - * input - */ - public void removeStartsWith(String startsWith) { - RuntimeConfig runtime = (RuntimeConfig) config.get("runtime"); - if (runtime == null) { - log.error("removeRegistry - runtime null !"); - return; - } - runtime.removeStartsWith(startsWith); - } } diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java index 8d4dcc7ab8..36d3078f63 100644 --- a/src/main/java/org/myrobotlab/framework/Service.java +++ b/src/main/java/org/myrobotlab/framework/Service.java @@ -721,8 +721,8 @@ public void addListener(MRLListener listener) { } @Override - public void addListener(String topicMethod, String callbackName) { - addListener(topicMethod, callbackName, CodecUtils.getCallbackTopicName(topicMethod)); + public void addListener(String localMethod, String remoteName) { + addListener(localMethod, remoteName, CodecUtils.getCallbackTopicName(localMethod)); } /** @@ -730,18 +730,18 @@ public void addListener(String topicMethod, String callbackName) { * "subscribe" from a different service FIXME !! - implement with HashMap or * HashSet .. WHY ArrayList ??? * - * @param topicMethod + * @param localMethod * - method when called, it's return will be sent to the - * callbackName/calbackMethod - * @param callbackName + * remoteName.remoteMethod + * @param remoteName * - name of the service to send return message to - * @param callbackMethod + * @param remoteMethod * - name of the method to send return data to */ @Override - public void addListener(String topicMethod, String callbackName, String callbackMethod) { - callbackName = CodecUtils.getFullName(callbackName); - MRLListener listener = new MRLListener(topicMethod, callbackName, callbackMethod); + public void addListener(String localMethod, String remoteName, String remoteMethod) { + remoteName = CodecUtils.getFullName(remoteName); + MRLListener listener = new MRLListener(localMethod, remoteName, remoteMethod); if (outbox.notifyList.containsKey(listener.topicMethod)) { // iterate through all looking for duplicate boolean found = false; @@ -889,27 +889,9 @@ public void broadcastState() { } @Override + @Deprecated /* use publishStatus */ public void broadcastStatus(Status status) { - long now = System.currentTimeMillis(); - /* - * if (status.equals(lastStatus) && now - lastStatusTs < - * statusBroadcastLimitMs) { return; } - */ - if (status.name == null) { - status.name = getName(); - } - if (status.level.equals(StatusLevel.ERROR)) { - lastError = status; - lastErrorTs = now; - log.error(status.toString()); - invoke("publishError", status); - } else { - log.info(status.toString()); - } - invoke("publishStatus", status); - lastStatusTs = now; - lastStatus = status; } @Override @@ -1434,7 +1416,8 @@ public

P getPeerConfig(String peerKey, StaticType

t } // Java generics don't let us create a new StaticType using - // P here because the type variable is erased, so we have to cast anyway for now + // P here because the type variable is erased, so we have to cast anyway for + // now ConfigurableService

si = (ConfigurableService

) Runtime.getService(peerName); if (si != null) { // peer is currently running - get its config @@ -1465,7 +1448,8 @@ public void setPeerConfigValue(String peerKey, String fieldname, Object value) t field.set(sc, value); savePeerConfig(peerKey, sc); String peerName = getPeerName(peerKey); - var cs = Runtime.getConfigurableService(peerName, new StaticType>() {}); + var cs = Runtime.getConfigurableService(peerName, new StaticType>() { + }); if (cs != null) { cs.apply(sc); // TODO - look for applies if its read from the file system // it needs to update Runtime.plan @@ -1612,7 +1596,7 @@ public QueueStats publishQueueStats(QueueStats stats) { * @return the service */ @Override - public Service publishState() { + public Service publishState() { return this; } @@ -2175,11 +2159,11 @@ public void unsubscribe(String topicName, String topicMethod, String callbackNam @Override public Status error(Exception e) { log.error("status:", e); - Status ret = Status.error(e); - ret.name = getName(); - log.error(ret.toString()); - invoke("publishStatus", ret); - return ret; + Status status = Status.error(e); + status.name = getName(); + log.error(status.toString()); + invoke("publishStatus", status); + return status; } @Override @@ -2194,22 +2178,29 @@ public Status error(String format, Object... args) { } public Status error(String msg) { - return error(msg, (Object[]) null); + Status status = Status.error(msg); + status.name = getName(); + log.error(status.toString()); + lastError = status; + invoke("publishStatus", status); + return status; } public Status warn(String msg) { - return warn(msg, (Object[]) null); - } - - @Override - public Status warn(String format, Object... args) { - Status status = Status.warn(format, args); + Status status = Status.warn(msg); status.name = getName(); log.warn(status.toString()); invoke("publishStatus", status); return status; } + @Override + public Status warn(String format, Object... args) { + String msg = String.format(Objects.requireNonNullElse(format, ""), args); + + return warn(msg); + } + /** * set status broadcasts an info string to any subscribers * @@ -2245,8 +2236,18 @@ public Status publishError(Status status) { return status; } + public Status publishWarn(Status status) { + return status; + } + @Override public Status publishStatus(Status status) { + // demux over different channels + if (status.isError()) { + invoke("publishError", status); + } else if (status.isWarn()) { + invoke("publishWarn", status); + } return status; } @@ -2856,7 +2857,8 @@ public void apply() { } public void applyPeerConfig(String peerKey, ServiceConfig config) { - applyPeerConfig(peerKey, config, new StaticType<>() {}); + applyPeerConfig(peerKey, config, new StaticType<>() { + }); } /** 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/Caliko.java b/src/main/java/org/myrobotlab/service/Caliko.java new file mode 100644 index 0000000000..01381f1161 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/Caliko.java @@ -0,0 +1,401 @@ +package org.myrobotlab.service; + +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.caliko.Application; +import org.myrobotlab.framework.Service; +import org.myrobotlab.logging.Level; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.CalikoConfig; +import org.slf4j.Logger; + +import au.edu.federation.caliko.FabrikBone3D; +import au.edu.federation.caliko.FabrikChain3D; +import au.edu.federation.caliko.FabrikChain3D.BaseboneConstraintType3D; +import au.edu.federation.caliko.FabrikJoint3D.JointType; +import au.edu.federation.caliko.FabrikStructure3D; +import au.edu.federation.caliko.visualisation.Camera; +import au.edu.federation.utils.Colour4f; +import au.edu.federation.utils.Utils; +import au.edu.federation.utils.Vec3f; + +public class Caliko extends Service { + + private class WindowWorker extends Thread { + + Application application; + String name; + Caliko service; + + public WindowWorker(Caliko service, String name) { + super(String.format("%s.WindowWorker.%s", service.getName(), name)); + this.name = name; + this.service = service; + } + + public void run() { + try { + application = new Application(service); + } catch (Exception e) { + error(e); + shutdown(); + } + } + + public void shutdown() { + application.running = false; + windows.remove(name); + application.window.cleanup(); + } + + } + + public final static Logger log = LoggerFactory.getLogger(Caliko.class); + + private static final long serialVersionUID = 1L; + +// public static final Vec3f X_AXIS = new Vec3f(1.0f, 0.0f, 0.0f); +// +// public static final Vec3f Y_AXIS = new Vec3f(0.0f, 1.0f, 0.0f); +// +// public static final Vec3f Z_AXIS = new Vec3f(0.0f, 0.0f, 1.0f); +// +// public static final Vec3f NEG_X_AXIS = new Vec3f(-1.0f, -0.0f, -0.0f); +// +// public static final Vec3f NEG_Y_AXIS = new Vec3f(-0.0f, -1.0f, -0.0f); +// +// public static final Vec3f NEG_Z_AXIS = new Vec3f(-0.0f, -0.0f, -1.0f); + + + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + // identical to command line start + // Runtime.startConfig("inmoov2"); + Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + + Caliko caliko = (Caliko) Runtime.start("caliko", "Caliko"); + caliko.test(); + + boolean done = true; + if (done) + return; + + Runtime.start("webgui", "WebGui"); + caliko.test(); + // Runtime.start("webgui", "WebGui"); + log.info("here"); + + } catch (Exception e) { + log.error("main threw", e); + } + } + + private transient Map chains = new HashMap<>(); + + private transient FabrikStructure3D structure; + + protected int windowHeight = 600; + + transient private Map windows = new HashMap<>(); + + protected int windowWidth = 800; + + private transient Camera camera = new Camera(new Vec3f(0.0f, 00.0f, 150.0f), new Vec3f(), windowWidth, windowHeight); + + public Caliko(String n, String id) { + super(n, id); + structure = new FabrikStructure3D(n); + } + + public void addBaseBone(float startX, float startY, float startZ, float endX, float endY, float endZ) { + addChain("default"); + addBone("default", startX, startY, startZ, endX, endY, endX, "-x", 0.0f, "red"); + } + + public void addBone(String chainName, float startX, float startY, float startZ, float endX, float endY, float endZ, String axis, float constraints, String color) { + if (!chains.containsKey(chainName)) { + error("%s chain does not exist", chainName); + } + FabrikBone3D basebone = new FabrikBone3D(new Vec3f(startX, startY, startZ), new Vec3f(endX, endY, endZ)); + basebone.setColour(getColor(color)); + FabrikChain3D chain = chains.get(chainName); + chain.addBone(basebone); + // TODO set type + chain.setRotorBaseboneConstraint(BaseboneConstraintType3D.GLOBAL_ROTOR, getAxis(axis), constraints); + } + + public void addChain(String name) { + if (chains.containsKey(name)) { + error("chain %s already exists", name); + return; + } + FabrikChain3D chain = new FabrikChain3D(); + structure.addChain(chain); + chains.put(name, chain); + } + + public void addFreelyRotatingHingedBone(String directionUV, float length, String hingeRotationAxis, String color) { + addFreelyRotatingHingedBone("default", directionUV, length, hingeRotationAxis, color); + } + + /** + * Add a consecutive hinge constrained bone to the end of this chain. The bone + * may rotate freely about the hinge axis. + *

+ * The bone will be drawn with a default colour of white. + *

+ * This method can only be used when the IK chain contains a basebone, as + * without it we do not have a start location for this bone (i.e. the end + * location of the previous bone). + *

+ * If this method is executed on a chain which does not contain a basebone + * then a RuntimeException is thrown. If this method is provided with a + * direction unit vector of zero, then an IllegalArgumentException is thrown. + * If the joint type requested is not JointType.LOCAL_HINGE or + * JointType.GLOBAL_HINGE then an IllegalArgumentException is thrown. If this + * method is provided with a hinge rotation axis unit vector of zero, then an + * IllegalArgumentException is thrown. + * + * @param directionUV + * The initial direction of the new bone. + * @param length + * The length of the new bone. + * @param jointType + * The type of hinge joint to be used - either JointType.LOCAL or + * JointType.GLOBAL. + * @param hingeRotationAxis + * The axis about which the hinge joint freely rotates. + * @param colour + * The colour to draw the bone. + */ + public void addFreelyRotatingHingedBone(String chainName, String directionUV, float length, String hingeRotationAxis, String color) { + if (!chains.containsKey(chainName)) { + error("%s chain does not exist", chainName); + } + FabrikChain3D chain = chains.get(chainName); + chain.addConsecutiveFreelyRotatingHingedBone(getAxis(directionUV), length, JointType.LOCAL_HINGE, getAxis(hingeRotationAxis), getColor(color)); + } + + public void addHingeBone(String directionUV, float length, String hingeRotationAxis, float clockwiseDegs, float anticlockwiseDegs, String hingeReferenceAxis, String color) { + addHingeBone("default", directionUV, length, hingeRotationAxis, clockwiseDegs, anticlockwiseDegs, hingeReferenceAxis, color); + } + + /** + * Add a consecutive hinge constrained bone to the end of this IK chain. + *

+ * The hinge type may be a global hinge where the rotation axis is specified + * in world-space, or a local hinge, where the rotation axis is relative to + * the previous bone in the chain. + *

+ * If this method is executed on a chain which does not contain a basebone + * then a RuntimeException is thrown. If this method is provided with bone + * direction or hinge constraint axis of zero then an IllegalArgumentException + * is thrown. If the joint type requested is not LOCAL_HINGE or GLOBAL_HINGE + * then an IllegalArgumentException is thrown. + * + * @param chainName The name of chain + * @param directionUV + * The initial direction of the new bone. + * @param length + * The length of the new bone. + * @param jointType + * The joint type of the new bone. + * @param hingeRotationAxis + * The axis about which the hinge rotates. + * @param clockwiseDegs + * The clockwise constraint angle in degrees. + * @param anticlockwiseDegs + * The anticlockwise constraint angle in degrees. + * @param hingeReferenceAxis + * The axis about which any clockwise/anticlockwise rotation + * constraints are enforced. + * @param colour + * The colour to draw the bone. + */ + public void addHingeBone(String chainName, String directionUV, float length, String hingeRotationAxis, float clockwiseDegs, float anticlockwiseDegs, String hingeReferenceAxis, + String color) { + if (!chains.containsKey(chainName)) { + error("%s chain does not exist", chainName); + } + FabrikChain3D chain = chains.get(chainName); + chain.addConsecutiveHingedBone(getAxis(directionUV), length, JointType.LOCAL_HINGE, getAxis(hingeRotationAxis), clockwiseDegs, anticlockwiseDegs, getAxis(hingeReferenceAxis), + getColor(color)); + } + + Vec3f getAxis(String axis) { + if (axis == null) { + return null; + } + axis = axis.toLowerCase(); + if ("x".equals(axis)) { + return new Vec3f(1.0f, 0.0f, 0.0f); + } else if ("-x".equals(axis)) { + return new Vec3f(-1.0f, -0.0f, -0.0f); + } else if ("y".equals(axis)) { + return new Vec3f(0.0f, 1.0f, 0.0f); + } else if ("-y".equals(axis)) { + return new Vec3f(-0.0f, -1.0f, -0.0f); + } else if ("z".equals(axis)) { + return new Vec3f(0.0f, 0.0f, 1.0f); + } else if ("-z".equals(axis)) { + return new Vec3f(-0.0f, -0.0f, -1.0f); + } else { + error("axis %s not found", axis); + return null; + } + } + + public Camera getCamera() { + return camera; + } + + Colour4f getColor(String color) { + if (color == null) { + return Utils.GREY; + } + color = color.toLowerCase(); + if ("red".equals(color)) { + return Utils.RED; + } + if ("green".equals(color)) { + return Utils.GREEN; + } + if ("blue".equals(color)) { + return Utils.BLUE; + } + if ("black".equals(color)) { + return Utils.BLACK; + } + if ("grey".equals(color)) { + return Utils.GREY; + } + if ("white".equals(color)) { + return Utils.WHITE; + } + if ("yellow".equals(color)) { + return Utils.YELLOW; + } + if ("cyan".equals(color)) { + return Utils.CYAN; + } + if ("magenta".equals(color)) { + return Utils.MAGENTA; + } + return Utils.GREY; + } + + public FabrikStructure3D getStructure() { + return structure; + } + + public void openWindow(String name) { + if (windows.containsKey(name)) { + info("%s already started", name); + return; + } + WindowWorker worker = new WindowWorker(this, name); + windows.put(name, worker); + worker.start(); + } + + public void stopService() { + super.stopService(); + for (WindowWorker worker : windows.values()) { + worker.shutdown(); + } + } + + public void test() { + + /* @param directionUV The initial direction of the new bone. + * @param hingeRotationAxis The axis about which the hinge rotates. + * @param hingeReferenceAxis The axis about which any clockwise/anticlockwise rotation constraints are enforced. + */ + + addBaseBone(0, 10, 0, -10, 10, 0); + // addHingeBone("x", 5, "x", 70, 10, "z", "grey"); + addHingeBone("-x", 5, "x", 10, 60, "z", "grey"); + addHingeBone("-y", 20, "z", 90, 90, "-y", "red"); + addFreelyRotatingHingedBone("y", 18, "y", "grey"); + + FabrikChain3D chain = chains.get("default"); + chain.solveForTarget(new Vec3f(-30, -300, -30)); + + for (FabrikBone3D bone : chains.get("default").getChain()) { + bone.getStartLocation().getGlobalPitchDegs(); + bone.getStartLocation().getGlobalYawDegs(); + Vec3f.getDirectionUV(bone.getStartLocation(), bone.getEndLocation()); + + System.out.println("Bone X: " + bone.getStartLocation().toString()); + } + + openWindow("default"); + + log.info("here"); + + // FabrikBone3D base =new FabrikBone3D(new Vec3f(),new + // Vec3f(0.0f,50.0f,0.0f)); + // + // chain.addBone(base); + // FabrikBone3D bone1 = new FabrikBone3D(new Vec3f(0.0f,50.0f,0.0f), new + // Vec3f(0f, 0f, 100f)); + // FabrikBone3D bone2 = new FabrikBone3D(new Vec3f(0f, 0f, 100f), new + // Vec3f(0f, 0f, 200f)); + + // chain.setEff + + // for(intboneLoop =0;boneLoop + // <5;++boneLoop){chain.addConsecutiveBone(newVec2f(0.0f,1.0f),50.0f); + + // Create a FabrikStructure3D + // FabrikStructure3D structure = new FabrikStructure3D(); + // + // // Create bones and joints + // FabrikBone3D root = new FabrikBone3D(0, 0, 0); + // FabrikBone3D bone1 = new FabrikBone3D(0, 0, 100); + // FabrikBone3D bone2 = new FabrikBone3D(0, 0, 100); + // + // // Create joints (optional but can be used for constraints) + // FabrikJoint3D joint1 = new FabrikJoint3D(); + // FabrikJoint3D joint2 = new FabrikJoint3D(); + // + // // Add bones and joints to the structure + // structure.addBone(root); + // structure.addBone(bone1); + // structure.addBone(bone2); + // + // // Set up the chain + // FabrikChain3D chain = new FabrikChain3D("ChainName"); + // chain.addBone(root); + // chain.addBone(bone1); + // chain.addBone(bone2); + // + // // Set the target position for the end effector (bone2) + // chain.setEffectorLocation(0, 0, 300); + // + // // Solve the IK problem + // structure.solveForTarget(chain.getEffectorLocation()); + // + // // Print the bone positions after solving + // for (FabrikBone3D bone : structure.getChain("ChainName").getChain()) { + // System.out.println("Bone X: " + bone.getStartLocation().getX() + + // ", Y: " + bone.getStartLocation().getY() + + // ", Z: " + bone.getStartLocation().getZ()); + // } + // + } + + public FabrikChain3D getChain(String name) { + if (!chains.containsKey(name)) { + error("no chain %s", name); + return null; + } + return chains.get(name); + } + +} diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 50ba4d3a8a..e095db1ecf 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -3,11 +3,14 @@ import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import org.apache.commons.io.FilenameUtils; @@ -26,13 +29,13 @@ import org.myrobotlab.opencv.OpenCVData; import org.myrobotlab.programab.PredicateEvent; import org.myrobotlab.programab.Response; +import org.myrobotlab.service.Log.LogEntry; import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; import org.myrobotlab.service.config.OpenCVConfig; import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.JoystickData; -import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; import org.myrobotlab.service.interfaces.IKJointAngleListener; import org.myrobotlab.service.interfaces.JoystickListener; @@ -54,6 +57,8 @@ public class InMoov2 extends Service implements ServiceLifeCycleL private static final long serialVersionUID = 1L; + protected static final Set stateDefaults = new TreeSet<>(); + static String speechRecognizer = "WebkitSpeechRecognition"; /** @@ -95,6 +100,12 @@ public static boolean loadFile(String file) { return true; } + + /** + * the config that was processed before booting, if there was one. + */ + protected String bootedConfig = null; + protected transient ProgramAB chatBot; protected List configList; @@ -102,19 +113,43 @@ public static boolean loadFile(String file) { /** * Configuration from runtime has started. This is when runtime starts * processing a configuration set for the first time since inmoov was started + * + * When configuration is being processed this is true, otherwise false. It's a + * state of the InMoov2 lifecycle, but the FSM isn't guaranteed to be started + * (or configured) at this time, so a member variable was created to guarantee + * this information is available */ protected boolean configStarted = false; - String currentConfigurationName = "default"; + /** + * map of events or states to sounds + */ + protected Map customSoundMap = new TreeMap<>(); protected transient SpeechRecognizer ear; + protected List errors = new ArrayList<>(); + + /** + * The finite state machine is core to managing state of InMoov2. There is + * very little benefit gained in having the interactions pub/sub. Therefore, + * there will be a direct reference to the fsm. If different state graph is + * needed, then the fsm can provide that service. + */ + private transient FiniteStateMachine fsm = null; // waiting controable threaded gestures we warn user protected boolean gestureAlreadyStarted = false; protected Set gestures = new TreeSet(); + /** + * Prevents actions or events from happening when InMoov2 is first booted + */ + private boolean hasBooted = false; + + protected boolean isPirOn = false; + protected transient HtmlFilter htmlFilter; protected transient ImageDisplay imageDisplay; @@ -123,27 +158,54 @@ public static boolean loadFile(String file) { protected Long lastPirActivityTime; - protected LedDisplayData led = new LedDisplayData(); - /** * supported locales */ protected Map locales = null; - protected int maxInactivityTimeSeconds = 120; - protected transient SpeechSynthesis mouth; protected boolean mute = false; protected transient OpenCV opencv; + protected List peersStarted = new ArrayList<>(); + protected transient Python python; + protected long stateLastIdleTime = System.currentTimeMillis(); + + protected long stateLastRandomTime = System.currentTimeMillis(); + protected String voiceSelected; public InMoov2(String n, String id) { super(n, id); + + // add the default InMoov2 state handlers - so the FSM can invoke them + // this is hardcode, because it requires Java methods in InMoov2 + // so it makes sense to hardcode them... + // if a user needs something different, it will happen in pyton-land + // consequence it this will need maintenance if there are new InMoov2 java + // state handlers + stateDefaults.add("wake"); + stateDefaults.add("firstInit"); + stateDefaults.add("idle"); + stateDefaults.add("random"); + stateDefaults.add("sleep"); // listens & dreams, no opencv, waits for + // wakeWord, pir active + stateDefaults.add("powerDown"); // stops heartbeat, listening ? + stateDefaults.add("shutdown");// ends mrl + + customSoundMap.put("boot", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/confirmation.wav")); + customSoundMap.put("wake", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); + customSoundMap.put("firstInit", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/select.wav")); + customSoundMap.put("idle", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/start.wav")); + customSoundMap.put("random", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/reveal.wav")); + customSoundMap.put("sleep", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/back.wav")); + customSoundMap.put("powerDown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); + customSoundMap.put("shutdown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/marimba.wav")); + } // should be removed in favor of general listeners @@ -165,10 +227,11 @@ public InMoov2Config apply(InMoov2Config c) { setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString())); } + if (c.loadInitScripts) { + loadInitScripts(); + } loadAppsScripts(); - loadInitScripts(); - if (c.loadGestures) { loadGestures(); } @@ -185,8 +248,6 @@ public InMoov2Config apply(InMoov2Config c) { return c; } - - @Override public void attachTextListener(String name) { addListener("publishText", name); @@ -207,18 +268,6 @@ public void attachTextPublisher(TextPublisher service) { subscribe(service.getName(), "publishText"); } - public void beginCheckingOnInactivity() { - beginCheckingOnInactivity(maxInactivityTimeSeconds); - } - - public void beginCheckingOnInactivity(int maxInactivityTimeSeconds) { - this.maxInactivityTimeSeconds = maxInactivityTimeSeconds; - // speakBlocking("power down after %s seconds inactivity is on", - // this.maxInactivityTimeSeconds); - log.info("power down after %s seconds inactivity is on", this.maxInactivityTimeSeconds); - addTask("checkInactivity", 5 * 1000, 0, "checkInactivity"); - } - public void cameraOff() { if (opencv != null) { opencv.stopCapture(); @@ -299,23 +348,6 @@ public String captureGesture(String gesture) { return script.toString(); } - public long checkInactivity() { - // speakBlocking("checking"); - long lastActivityTime = getLastActivityTime(); - long now = System.currentTimeMillis(); - long inactivitySeconds = (now - lastActivityTime) / 1000; - if (inactivitySeconds > maxInactivityTimeSeconds) { - // speakBlocking("%d seconds have passed without activity", - // inactivitySeconds); - powerDown(); - } else { - // speakBlocking("%d seconds have passed without activity", - // inactivitySeconds); - info("checking checkInactivity - %d seconds have passed without activity", inactivitySeconds); - } - return lastActivityTime; - } - public void closeAllImages() { // FIXME - follow this pattern ? // CON npe possible although unlikely @@ -455,9 +487,32 @@ public void finishedGesture(String nameOfGesture) { } } - // FIXME - this isn't the callback for fsm - why is it needed here ? - public void fire(String event) { - invoke("publishEvent", event); + public void firstInit() { + log.info("firstInit"); + // cheap way to prevent race condition + // of "wake" firing a state change .. which will spawn + // a system event of FIRST_INIT that will answer this + // question ... + sleep(2000); + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); + if (chatBot != null) { + chatBot.getResponse("FIRST_INIT"); + } + } + + /** + * used to configure a flashing event - could use configuration to signal + * different colors and states + * + * @return + */ + public void flash() { + invoke("publishFlash", "default"); + } + + public String flash(String name) { + invoke("publishFlash", name); + return name; } public void fullSpeed() { @@ -504,30 +559,32 @@ public InMoov2Head getHead() { * @return the timestamp of the last activity time. */ public Long getLastActivityTime() { - try { - - Long lastActivityTime = 0L; - - Long head = (Long) sendToPeerBlocking("head", "getLastActivityTime", getName()); - Long leftArm = (Long) sendToPeerBlocking("leftArm", "getLastActivityTime", getName()); - Long rightArm = (Long) sendToPeerBlocking("rightArm", "getLastActivityTime", getName()); - Long leftHand = (Long) sendToPeerBlocking("leftHand", "getLastActivityTime", getName()); - Long rightHand = (Long) sendToPeerBlocking("rightHand", "getLastActivityTime", getName()); - Long torso = (Long) sendToPeerBlocking("torso", "getLastActivityTime", getName()); - - lastActivityTime = Math.max(head, leftArm); - lastActivityTime = Math.max(lastActivityTime, rightArm); - lastActivityTime = Math.max(lastActivityTime, leftHand); - lastActivityTime = Math.max(lastActivityTime, rightHand); - lastActivityTime = Math.max(lastActivityTime, torso); - - return lastActivityTime; - - } catch (Exception e) { - error(e); - return null; + Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null; + Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null; + Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null; + Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null; + Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null; + Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null; + + Long lastActivityTime = null; + + if (head != null || leftArm != null || rightArm != null || leftHand != null || rightHand != null || torso != null) { + lastActivityTime = 0L; + if (head != null) + lastActivityTime = Math.max(lastActivityTime, head); + if (leftArm != null) + lastActivityTime = Math.max(lastActivityTime, leftArm); + if (rightArm != null) + lastActivityTime = Math.max(lastActivityTime, rightArm); + if (leftHand != null) + lastActivityTime = Math.max(lastActivityTime, leftHand); + if (rightHand != null) + lastActivityTime = Math.max(lastActivityTime, rightHand); + if (torso != null) + lastActivityTime = Math.max(lastActivityTime, torso); } + return lastActivityTime; } public InMoov2Arm getLeftArm() { @@ -576,6 +633,14 @@ public InMoov2Hand getRightHand() { return (InMoov2Hand) getPeer("rightHand"); } + public String getState() { + FiniteStateMachine fsm = (FiniteStateMachine) getPeer("fsm"); + if (fsm == null) { + return null; + } + return fsm.getCurrent(); + } + /** * matches on language only not variant expands language match to full InMoov2 * bot locale @@ -606,10 +671,6 @@ public InMoov2Torso getTorso() { return (InMoov2Torso) getPeer("torso"); } - public InMoov2Config getTypedConfig() { - return (InMoov2Config) config; - } - public void halfSpeed() { sendToPeer("head", "setSpeed", 25.0, 25.0, 25.0, 25.0, 100.0, 25.0); sendToPeer("rightHand", "setSpeed", 30.0, 30.0, 30.0, 30.0, 30.0, 30.0); @@ -620,12 +681,10 @@ public void halfSpeed() { } /** - * execute python scripts in the init directory on startup of the service - * - * @throws IOException + * pir active ear listening for wakeword */ - public void loadInitScripts() throws IOException { - loadScripts(getResourceDir() + fs + "init"); + public void idle() { + log.info("idle"); } public boolean isCameraOn() { @@ -665,7 +724,7 @@ public void loadGestures() { * @return true/false */ public boolean loadGestures(String directory) { - invoke("publishEvent", "LOAD GESTURES"); + systemEvent("LOAD GESTURES"); // iterate over each of the python files in the directory // and load them into the python interpreter. @@ -701,6 +760,15 @@ public boolean loadGestures(String directory) { return true; } + /** + * execute python scripts in the init directory on startup of the service + * + * @throws IOException + */ + public void loadInitScripts() throws IOException { + loadScripts(getResourceDir() + fs + "init"); + } + /** * Generalized directory python script loading method * @@ -711,7 +779,7 @@ public void loadScripts(String directory) throws IOException { File dir = new File(directory); if (!dir.exists() || !dir.isDirectory()) { - invoke("publishEvent", "LOAD SCRIPTS ERROR"); + systemEvent("LOAD SCRIPTS ERROR"); return; } @@ -728,6 +796,7 @@ public boolean accept(File dir, String name) { for (File file : files) { Python p = (Python) Runtime.start("python", "Python"); if (p != null) { + // FIXME error("x") when an error occurs p.execFile(file.getAbsolutePath()); } } @@ -736,7 +805,17 @@ public boolean accept(File dir, String name) { } public void moveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) { - invoke("publishMoveArm", which, bicep, rotate, shoulder, omoplate); + HashMap map = new HashMap<>(); + Optional.ofNullable(bicep).ifPresent(value -> map.put("bicep", value)); + Optional.ofNullable(rotate).ifPresent(value -> map.put("rotate", value)); + Optional.ofNullable(shoulder).ifPresent(value -> map.put("shoulder", value)); + Optional.ofNullable(omoplate).ifPresent(value -> map.put("omoplate", value)); + + if ("left".equals(which)) { + invoke("publishMoveLeftArm", map); + } else { + invoke("publishMoveRightArm", map); + } } public void moveEyelids(Double eyelidleftPos, Double eyelidrightPos) { @@ -752,7 +831,19 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D } public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist); + HashMap map = new HashMap<>(); + Optional.ofNullable(thumb).ifPresent(value -> map.put("thumb", value)); + Optional.ofNullable(index).ifPresent(value -> map.put("index", value)); + Optional.ofNullable(majeure).ifPresent(value -> map.put("majeure", value)); + Optional.ofNullable(ringFinger).ifPresent(value -> map.put("ringFinger", value)); + Optional.ofNullable(pinky).ifPresent(value -> map.put("pinky", value)); + Optional.ofNullable(wrist).ifPresent(value -> map.put("wrist", value)); + + if ("left".equals(which)) { + invoke("publishMoveLeftHand", map); + } else { + invoke("publishMoveRightHand", map); + } } public void moveHead(Double neck, Double rothead) { @@ -768,7 +859,14 @@ public void moveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Doub } public void moveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { - invoke("publishMoveHead", neck, rothead, eyeX, eyeY, jaw, rollNeck); + HashMap map = new HashMap<>(); + Optional.ofNullable(neck).ifPresent(value -> map.put("neck", value)); + Optional.ofNullable(rothead).ifPresent(value -> map.put("rothead", value)); + Optional.ofNullable(eyeX).ifPresent(value -> map.put("eyeX", value)); + Optional.ofNullable(eyeY).ifPresent(value -> map.put("eyeY", value)); + Optional.ofNullable(jaw).ifPresent(value -> map.put("jaw", value)); + Optional.ofNullable(rollNeck).ifPresent(value -> map.put("rollNeck", value)); + invoke("publishMoveHead", map); } public void moveHead(Integer neck, Integer rothead, Integer rollNeck) { @@ -820,8 +918,11 @@ public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer } public void moveTorso(Double topStom, Double midStom, Double lowStom) { - // the "right" way - invoke("publishMoveTorso", topStom, midStom, lowStom); + HashMap map = new HashMap<>(); + Optional.ofNullable(topStom).ifPresent(value -> map.put("topStom", value)); + Optional.ofNullable(midStom).ifPresent(value -> map.put("midStom", value)); + Optional.ofNullable(lowStom).ifPresent(value -> map.put("lowStom", value)); + invoke("publishMoveTorso", map); } public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { @@ -829,10 +930,119 @@ public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { sendToPeer("torso", "moveToBlocking", topStom, midStom, lowStom); } + /** + * At boot all services specified through configuration have started, or if no + * configuration has started minimally the InMoov2 service has started. During + * the processing of config and starting other services data will have + * accumulated, and at boot, some of data may now be inspected and processed + * in a synchronous single threaded way. With reporting after startup, vs + * during, other peer services are not needed (e.g. audioPlayer is no longer + * needed to be started "before" InMoov2 because when boot is called + * everything that is wanted has been started. + * + */ + synchronized public void onBoot() { + + // thinking you shouldn't "boot" twice ? + if (hasBooted) { + log.warn("will not boot again"); + return; + } + + List services = Runtime.getServices(); + for (ServiceInterface si : services) { + if ("Servo".equals(si.getSimpleName())) { + send(si.getFullName(), "setAutoDisable", true); + } + } + + // FIXME - standardize multi-config examples should be available + // moved from startService to allow more simple control + // FIXME standard FileIO copyIfNotExists(src, dst) + try { + // copy config if it doesn't already exist + String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); + List files = FileIO.getFileList(resourceBotDir); + for (File f : files) { + String botDir = "data/config/" + f.getName(); + File bDir = new File(botDir); + if (bDir.exists() || !f.isDirectory()) { + log.info("skipping data/config/{}", botDir); + } else { + log.info("will copy new data/config/{}", botDir); + try { + FileIO.copy(f.getAbsolutePath(), botDir); + } catch (Exception e) { + error(e); + } + } + } + } catch (Exception e) { + error(e); + } + + // FIXME - find good way of running an animation "through" a state + if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { + NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); + if (neoPixel != null) { + invoke("publishPlayAnimation", config.bootAnimation); + } + } + + if (config.startupSound && getPeer("audioPlayer") != null) { + ((AudioFile) getPeer("audioPlayer")).playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); + } + + if (config.systemEventsOnBoot) { + // reporting on all services and config started + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG STARTED %s", bootedConfig); + } + + for (String peerKey : peersStarted) { + systemEvent("STARTED %s", peerKey); + } + + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG LOADED %s", bootedConfig); + } + } + + // FIXME - important to do invoke & fsm needs to be consistent order + + // if speaking then turn off animation + + // publish all the errors + + // switch off animations + + // start heartbeat + // say starting heartbeat + if (config.heartbeat) { + startHeartbeat(); + } else { + stopHeartbeat(); + } + + // say finished booting + + fsm.fire("wake"); + + // if (getPeer("mouth") != null) { + // AbstractSpeechSynthesis mouth = + // (AbstractSpeechSynthesis)getPeer("mouth"); + // mouth.setMute(wasMute); + // } + + hasBooted = true; + } + public PredicateEvent onChangePredicate(PredicateEvent event) { log.error("onChangePredicate {}", event); if (event.name.equals("topic")) { - invoke("publishEvent", String.format("TOPIC CHANGED TO %s", event.value)); + systemEvent("TOPIC CHANGED TO %s", event.value); } // depending on configuration .... // call python ? @@ -841,6 +1051,12 @@ public PredicateEvent onChangePredicate(PredicateEvent event) { return event; } + public void onConfigFinished(String configName) { + log.info("onConfigFinished"); + configStarted = false; + invoke("publishBoot"); + } + /** * comes in from runtime which owns the config list * @@ -857,12 +1073,6 @@ public void onCreated(String fullname) { log.info("{} created", fullname); } - public void onFinishedConfig(String configName) { - log.info("onFinishedConfig"); - // invoke("publishEvent", "configFinished"); - invoke("publishFinishedConfig", configName); - } - public void onGestureStatus(Status status) { if (!status.equals(Status.success()) && !status.equals(Status.warn("Python process killed !"))) { error("I cannot execute %s, please check logs", lastGestureExecuted); @@ -872,6 +1082,86 @@ public void onGestureStatus(Status status) { unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } + /** + * A generalized recurring event which can preform checks and various other + * methods or tasks. Heartbeats will not start until after boot stage. + */ + public void onHeartbeat(String name) { + try { + // heartbeats can start before config is + // done processing - so the following should + // not be dependent on config + + if (!hasBooted) { + log.info("boot hasn't completed, will not process heartbeat"); + return; + } + + Long lastActivityTime = getLastActivityTime(); + + // FIXME lastActivityTime != 0 is bogus - the value should be null if + // never set + if (config.stateIdleInterval != null && lastActivityTime != null && lastActivityTime != 0 + && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { + stateLastIdleTime = lastActivityTime; + } + + if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) { + fsm.fire("idle"); + stateLastIdleTime = System.currentTimeMillis(); + } + + // interval event firing + if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { + // fsm.fire("random"); + stateLastRandomTime = System.currentTimeMillis(); + } + + } catch (Exception e) { + error(e); + } + + if (config.pirOnFlash && isPeerStarted("pir") && isPirOn) { + flash("pir"); + } + + if (config.batteryInSystem) { + double batteryLevel = Runtime.getBatteryLevel(); + invoke("publishBatteryLevel", batteryLevel); + // FIXME - thresholding should always have old value or state + // so we don't pump endless errors + if (batteryLevel < 5) { + error("battery level < 5 percent"); + // systemEvent(BATTERY ERROR) + } else if (batteryLevel < 10) { + warn("battery level < 10 percent"); + // systemEvent(BATTERY WARN) + } + } + + // flash error until errors are cleared + if (config.flashOnErrors) { + if (errors.size() > 0) { + invoke("publishFlash", "error"); + } else { + invoke("publishFlash", "heartbeat"); + } + } + + } + + public void onInactivity() { + log.info("onInactivity"); + + // powerDown ? + + } + + /** + * Central hub of input motion control. Potentially, all input from + * joysticks, quest2 controllers and headset, or any IK service could + * be sent here + */ @Override public void onJointAngles(Map angleMap) { log.debug("onJointAngles {}", angleMap); @@ -892,23 +1182,75 @@ public void onJoystickInput(JoystickData input) throws Exception { invoke("publishEvent", "joystick"); } - public String onNewState(String state) { - log.error("onNewState {}", state); + /** + * Centralized logging system will have all logging from all services, + * including lower level logs that do not propegate as statuses + * + * @param log + * - flushed log from Log service + */ + public void onLogEvents(List log) { + // scan for warn or errors + for (LogEntry entry : log) { + if ("ERROR".equals(entry.level) && errors.size() < 100) { + errors.add(entry); + } + } + } + + public void onMoveHead(Map map) { + InMoov2Head head = (InMoov2Head) getPeer("head"); + if (head != null) { + head.onMove(map); + } + } + + public void onMoveLeftArm(Map map) { + InMoov2Arm leftArm = (InMoov2Arm) getPeer("leftArm"); + if (leftArm != null) { + leftArm.onMove(map); + } + } - // put configurable filter here ! + public void onMoveLeftHand(Map map) { + InMoov2Hand leftHand = (InMoov2Hand) getPeer("leftHand"); + if (leftHand != null) { + leftHand.onMove(map); + } + } - // state substitutions ? - // let python subscribe directly to fsm.publishNewState - // if - invoke(state); - // depending on configuration .... - // call python ? - // fire fsm events ? - // do defaults ? - return state; + // public Message publishPython(String method, Object...data) { + // return Message.createMessage(getName(), getName(), method, data); + // } + + public void onMoveRightArm(Map map) { + InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); + if (rightArm != null) { + rightArm.onMove(map); + } + } + + public void onMoveRightHand(Map map) { + InMoov2Hand rightHand = (InMoov2Hand) getPeer("rightHand"); + if (rightHand != null) { + rightHand.onMove(map); + } + } + + public void onMoveTorso(Map map) { + InMoov2Torso torso = (InMoov2Torso) getPeer("torso"); + if (torso != null) { + torso.onMove(map); + } } + + +// public Message publishPython(String method, Object...data) { +// return Message.createMessage(getName(), getName(), method, data); +// } + public OpenCVData onOpenCVData(OpenCVData data) { // FIXME - publish event with or without data ? String file reference return data; @@ -927,19 +1269,9 @@ public void onPeak(double volume) { } } - /** - * initial callback for Pir sensor Default behavior will be: send fsm event - * onPirOn flash neopixel - */ public void onPirOn() { - led.action = "flash"; - led.red = 50; - led.green = 100; - led.blue = 150; - led.count = 5; - led.interval = 500; // FIXME flash on config.flashOnBoot - invoke("publishFlash"); + invoke("publishFlash", "pir"); ProgramAB chatBot = (ProgramAB)getPeer("chatBot"); if (chatBot != null) { String botState = chatBot.getPredicate("botState"); @@ -949,19 +1281,12 @@ public void onPirOn() { } } - // GOOD GOOD GOOD - LOOPBACK - flexible and replacable by python - // yet provides a stable default, which can be fully replaced - // Works using common pub/sub rules - // TODO - this is a loopback power up - // its replaceable by typical subscription rules - public void onPowerUp() { - // CON - type aware - NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); - // CON - necessary NPE checking - if (neoPixel != null) { - neoPixel.setColor(0, 130, 0); - neoPixel.playAnimation("Larson Scanner"); - } + /** + * Pir off callback - FIXME NEEDS WORK + */ + public void onPirOff() { + isPirOn = false; + fsm.fire("sleep"); } @Override @@ -980,23 +1305,13 @@ public boolean onSense(boolean b) { // setEvent("pir-sense-on" .. also sets it in config ? // config.handledEvents["pir-sense-on"] if (b) { - invoke("publishEvent", "PIR ON"); + systemEvent("PIR ON"); } else { - invoke("publishEvent", "PIR OFF"); + systemEvent("PIR OFF"); } return b; } - /** - * runtime re-publish relay - * - * @param configName - */ - public void onStartConfig(String configName) { - log.info("onStartConfig"); - invoke("publishStartConfig", configName); - } - /** * Part of service life cycle - a new servo has been started * @@ -1006,121 +1321,16 @@ public void onStartConfig(String configName) { */ @Override public void onStarted(String name) { - InMoov2Config c = (InMoov2Config) config; - - log.info("onStarted {}", name); try { - Runtime runtime = Runtime.getInstance(); log.info("onStarted {}", name); - // BAD IDEA - better to ask for a system report or an error report - // if (runtime.isProcessingConfig()) { - // invoke("publishEvent", "CONFIG STARTED"); - // } - String peerKey = getPeerKey(name); - if (peerKey == null) { - // service not a peer - return; + if (peerKey != null) { + peersStarted.add(peerKey); } - if (runtime.isProcessingConfig() && !configStarted) { - invoke("publishEvent", "CONFIG STARTED " + runtime.getConfigName()); - configStarted = true; - } - - invoke("publishEvent", "STARTED " + peerKey); - - switch (peerKey) { - case "audioPlayer": - break; - case "chatBot": - ProgramAB chatBot = (ProgramAB) Runtime.getService(name); - chatBot.attachTextListener(getPeerName("htmlFilter")); - startPeer("htmlFilter"); - break; - case "controller3": - break; - case "controller4": - break; - case "ear": - AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) Runtime.getService(name); - ear.attachTextListener(getPeerName("chatBot")); - break; - case "eyeTracking": - break; - case "fsm": - break; - case "gpt3": - break; - case "head": - addListener("publishMoveHead", name); - break; - case "headTracking": - break; - case "htmlFilter": - TextPublisher htmlFilter = (TextPublisher) Runtime.getService(name); - htmlFilter.attachTextListener(getPeerName("mouth")); - break; - case "imageDisplay": - break; - case "leap": - break; - case "left": - break; - case "leftArm": - addListener("publishMoveLeftArm", name, "onMoveArm"); - break; - case "leftHand": - addListener("publishMoveLeftHand", name, "onMoveHand"); - break; - case "mouth": - mouth = (AbstractSpeechSynthesis) Runtime.getService(name); - mouth.attachSpeechListener(getPeerName("ear")); - break; - case "mouthControl": - break; - case "neoPixel": - break; - case "opencv": - subscribeTo(name, "publishOpenCVData"); - break; - case "openni": - break; - case "openWeatherMap": - break; - case "pid": - break; - case "pir": - break; - case "random": - break; - case "right": - break; - case "rightArm": - addListener("publishMoveRightArm", name, "onMoveArm"); - break; - case "rightHand": - addListener("publishMoveRightHand", name, "onMoveHand"); - break; - case "servoMixer": - break; - case "simulator": - break; - case "torso": - addListener("publishMoveTorso", name); - break; - case "ultrasonicRight": - break; - case "ultrasonicLeft": - break; - default: - log.warn("unknown peer %s not hanled in onStarted", peerKey); - break; - } - - // type processing for Servo + // new servo ServiceInterface si = Runtime.getService(name); if ("Servo".equals(si.getSimpleName())) { log.info("sending setAutoDisable true to {}", name); @@ -1133,8 +1343,84 @@ public void onStarted(String name) { } } + /** + * The integration between the FiniteStateMachine (fsm) and the InMoov2 + * service and potentially other services (Python, ProgramAB) happens here. + * + * After boot all state changes get published here. + * + * Some InMoov2 service methods will be called here for "default + * implemenation" of states. If a user doesn't want to have that default + * implementation, they can change it by changing the definition of the state + * machine, and have a new state which will call a Python inmoov2 library + * callback. Overriding, appending, or completely transforming the behavior is + * all easily accomplished by managing the fsm and python inmoov2 library + * callbacks. + * + * Python inmoov2 callbacks ProgramAB topic switching + * + * Depending on config: + * + * + * @param stateChange + * @return + */ + public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) { + try { + log.error("onStateChange {}", stateChange); + + String current = stateChange.current; + String last = stateChange.last; + + // leaving random state + if ("random".equals(last) && !"random".equals(current) && isPeerStarted("random")) { + Random random = (Random) getPeer("random"); + random.disable(); + } + + if ("wake".equals(last)) { + invoke("publishStopAnimation"); + } + + if (config.systemEventStateChange) { + systemEvent("ON STATE %s", current); + } + + if (config.customSounds && customSoundMap.containsKey(current)) { + invoke("publishPlayAudioFile", customSoundMap.get(current)); + } + + // TODO - only a few InMoov2 state defaults will be called here + if (stateDefaults.contains(current)) { + invoke(current); + } + + // FIXME add topic changes to AIML here ! + // FIXME add clallbacks to inmmoov2 library + + // put configurable filter here ! + + // state substitutions ? + // let python subscribe directly to fsm.publishStateChange + + // if python && configured to do python inmoov2 library callbacks + // do a callback ... default NOOPs should be in library + + // if + // invoke(state); + // depending on configuration .... + // call python ? + // fire fsm events ? + // do defaults ? + } catch (Exception e) { + error(e); + } + return stateChange; + } + @Override public void onStopped(String name) { + log.info("service {} has stopped"); // using release peer for peer releasing // FIXME - auto remove subscriptions of peers? } @@ -1151,34 +1437,24 @@ public void onText(String text) { // TODO FIX/CHECK this, migrate from python land public void powerDown() { + // publishFlash(maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds) rest(); - purgeTasks(); + purgeTasks(); // including heartbeat disable(); - if (ear != null) { - ear.lockOutAllGrammarExcept("power up"); + if (chatBot != null) { + chatBot.sleep(); } - // FIXME - DO NOT DO THIS !!!! SIMPLY PUBLISH A POWER DOWN EVENT AND PYTHON - // CAN SUBSCRIBE - // AND MAINTAIN A SET OF onPowerDown: callback methods - python.execMethod("power_down"); - } - - // TODO FIX/CHECK this, migrate from python land - // FIXME - defaultPowerUp switchable + override - public void powerUp() { - enable(); - rest(); - if (ear != null) { - ear.clearLock(); + // FIXME - bad remove it - what is needed ? + // i think this is legacy wake word + ear.lockOutAllGrammarExcept("power up"); } - beginCheckingOnInactivity(); - - python.execMethod("power_up"); } /** @@ -1199,10 +1475,24 @@ public String publishStartConfig(String configName) { return configName; } - public String publishFinishedConfig(String configName) { - info("config %s finished", configName); - invoke("publishEvent", "CONFIG LOADED " + configName); + public double publishBatteryLevel(double d) { + return d; + } + /** + * A heartbeat that continues to check status, and fire events to the FSM. + * Checks battery, flashes leds and processes all the configured checks in + * onHeartbeat at a regular interval + * */ + public void publishHeartbeat() { + log.info("publishHeartbeat"); + } + + public void publishBoot() { + log.info("publishBoot"); + } + + public String publishConfigFinished(String configName) { return configName; } @@ -1217,34 +1507,35 @@ public List publishConfigList() { } /** - * event publisher for the fsm - although other services potentially can - * consume and filter this event channel - * - * @param event + * publishes a name for NeoPixel.onFlash to consume + * @param name * @return */ - public String publishEvent(String event) { - return String.format("SYSTEM_EVENT %s", event); + public String publishFlash(String name) { + return name; } - + /** - * used to configure a flashing event - could use configuration to signal - * different colors and states - * + * publishes a name for NeoPixel.onFlash to consume, + * in a seperate channel to potentially be used by + * "speaking only" leds + * @param name * @return */ - public LedDisplayData publishFlash() { - return led; + public String publishSpeakingFlash(String name) { + return name; + } + + /** + * if inactivityTime configured, this event is published after there has not + * been in activity since. + */ + public String publishFlash(String flashName) { + return flashName; } public String publishHeartbeat() { - led.action = "flash"; - led.red = 180; - led.green = 10; - led.blue = 30; - led.count = 1; - led.interval = 50; - invoke("publishFlash"); + invoke("publishFlash", "heartbeat"); return getName(); } @@ -1259,94 +1550,60 @@ public Message publishMessage(Message msg) { return msg; } - public HashMap publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); - if ("left".equals(which)) { - invoke("publishMoveLeftArm", bicep, rotate, shoulder, omoplate); - } else { - invoke("publishMoveRightArm", bicep, rotate, shoulder, omoplate); - } + public Map publishMoveHead(Map map) { return map; } - public HashMap publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("which", which); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - if ("left".equals(which)) { - invoke("publishMoveLeftHand", thumb, index, majeure, ringFinger, pinky, wrist); - } else { - invoke("publishMoveRightHand", thumb, index, majeure, ringFinger, pinky, wrist); - } + public Map publishMoveLeftArm(Map map) { return map; } - public HashMap publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { - HashMap map = new HashMap<>(); - map.put("neck", neck); - map.put("rothead", rothead); - map.put("eyeX", eyeX); - map.put("eyeY", eyeY); - map.put("jaw", jaw); - map.put("rollNeck", rollNeck); + public Map publishMoveLeftHand(Map map) { return map; } - public HashMap publishMoveLeftArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); + public Map publishMoveRightArm(Map map) { return map; } - public HashMap publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); + public Map publishMoveRightHand(Map map) { return map; } - public HashMap publishMoveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); + public Map publishMoveTorso(Map map) { return map; } - public HashMap publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - return map; + public String publishPlayAudioFile(String filename) { + return filename; } - public HashMap publishMoveTorso(Double topStom, Double midStom, Double lowStom) { - HashMap map = new HashMap<>(); - map.put("topStom", topStom); - map.put("midStom", midStom); - map.put("lowStom", lowStom); - return map; + public String publishPlayAnimation(String animation) { + return animation; + } + + /** + * stop animation event + */ + public void publishStopAnimation() { + } + + public FiniteStateMachine.StateChange publishStateChange(FiniteStateMachine.StateChange state) { + log.info("publishStateChange {}", state); + return state; + } + + /** + * event publisher for the fsm - although other services potentially can + * consume and filter this event channel + * + * @param event + * @return + */ + public String publishSystemEvent(String event) { + // well, it turned out underscore was a goofy selection, as underscore in + // aiml is wildcard ... duh + return String.format("SYSTEM_EVENT %s", event); } /** @@ -1357,11 +1614,21 @@ public String publishText(String text) { return text; } + /** + * default this will come from idle after some configurable time period + */ + public void random() { + Random random = (Random) getPeer("random"); + if (random != null) { + random.enable(); + } + } + @Override public void releasePeer(String peerKey) { super.releasePeer(peerKey); if (peerKey != null) { - invoke("publishEvent", "STOPPED " + peerKey); + systemEvent("STOPPED %s", peerKey); } } @@ -1534,7 +1801,7 @@ public void setOpenCV(OpenCV opencv) { } public boolean setPirPlaySounds(boolean b) { - getTypedConfig().pirPlaySounds = b; + config.pirPlaySounds = b; return b; } @@ -1589,10 +1856,17 @@ public void setVoice(String name) { } } - // ----------------------------------------------------------------------------- - // These are methods added that were in InMoov1 that we no longer had in - // InMoov2. - // From original InMoov1 so we don't loose the + public void shutdown() { + log.info("shutdown"); + Runtime.shutdown(); + } + + /** + * ear still listening pir still active + */ + public void sleep() { + log.info("sleep"); + } public void sleeping() { log.error("sleeping"); @@ -1640,29 +1914,7 @@ public void speakBlocking(String format, Object... args) { } } - public void startAll() throws Exception { - startAll(null, null); - } - - public void startAll(String leftPort, String rightPort) throws Exception { - startMouth(); - startChatBot(); - - // startHeadTracking(); - // startEyesTracking(); - // startOpenCV(); - startEar(); - - startServos(); - // startMouthControl(head.jaw, mouth); - - speakBlocking(get("STARTINGSEQUENCE")); - } - - public void startBrain() { - startChatBot(); - } - + @Deprecated /* This needs to be removed ! */ public ProgramAB startChatBot() { try { @@ -1711,10 +1963,12 @@ public ProgramAB startChatBot() { if (chatBot.getPredicate("default", "firstinit").isEmpty() || chatBot.getPredicate("default", "firstinit").equals("unknown") || chatBot.getPredicate("default", "firstinit").equals("started")) { chatBot.startSession(chatBot.getPredicate("default", "lastUsername")); - invoke("publishEvent", "FIRST INIT"); + // probably not necessary - state change events should be enough + systemEvent("FIRST INIT"); } else { chatBot.startSession(chatBot.getPredicate("default", "lastUsername")); - invoke("publishEvent", "WAKE UP"); + // probably not necessary - state change events should be enough + systemEvent("WAKE UP"); } } catch (Exception e) { speak("could not load chatBot"); @@ -1749,7 +2003,8 @@ public void startedGesture(String nameOfGesture) { } public void startHeartbeat() { - addTask(1000, "publishHeartbeat"); + addTask(config.heartbeatInterval, "publishHeartbeat"); + config.heartbeat = true; } // TODO - general objective "might" be to reduce peers down to something @@ -1776,6 +2031,7 @@ public SpeechSynthesis startMouth() { } mouth.attachSpeechRecognizer(ear); + // mouth.attach(htmlFilter); // same as chatBot not needed // this.attach((Attachable) mouth); @@ -1811,8 +2067,9 @@ public ServiceInterface startPeer(String peer) { @Override public void startService() { super.startService(); + fsm = (FiniteStateMachine) startPeer("fsm"); + fsm.init(); - InMoov2Config c = (InMoov2Config) config; Runtime runtime = Runtime.getInstance(); // get service start and release life cycle events @@ -1831,38 +2088,32 @@ public void startService() { addListener(getName(), "powerUp"); subscribe("runtime", "publishConfigList"); - if (runtime.isProcessingConfig()) { - invoke("publishEvent", "configStarted"); - } - subscribe("runtime", "publishStartConfig"); - subscribe("runtime", "publishFinishedConfig"); - // chatbot getresponse attached to publishEvent - addListener("publishEvent", getPeerName("chatBot"), "getResponse"); + // subscribe to config processing events + // runtime callbacks publish the same a local + subscribe("runtime", "publishConfigStarted", "publishConfigStarted"); + subscribe("runtime", "publishConfigFinished", "publishConfigFinished"); - try { - // copy config if it doesn't already exist - String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); - List files = FileIO.getFileList(resourceBotDir); - for (File f : files) { - String botDir = "data/config/" + f.getName(); - File bDir = new File(botDir); - if (bDir.exists() || !f.isDirectory()) { - log.info("skipping data/config/{}", botDir); - } else { - log.info("will copy new data/config/{}", botDir); - try { - FileIO.copy(f.getAbsolutePath(), botDir); - } catch (Exception e) { - error(e); - } - } + runtime.invoke("publishConfigList"); + + // iterate through existing started service + // add them to peers booted + for (String name : Runtime.getServiceNames()) { + String peerKey = getPeerKey(name); + if (peerKey != null) { + peersStarted.add(peerKey); } - } catch (Exception e) { - error(e); } - runtime.invoke("publishConfigList"); + if (runtime.isProcessingConfig()) { + // if InMoov2 was started as part of a config set + // set here so boot can be delayed until the config + // set is done + configStarted = true; + bootedConfig = runtime.getConfigName(); + } else { + invoke("publishBoot"); + } } public void startServos() { @@ -1896,6 +2147,7 @@ public void stopGesture() { public void stopHeartbeat() { purgeTask("publishHeartbeat"); + config.heartbeat = false; } public void stopNeopixelAnimation() { @@ -1922,7 +2174,17 @@ public void systemCheck() { Platform platform = Runtime.getPlatform(); setPredicate("system version", platform.getVersion()); // ERROR buffer !!! - invoke("publishEvent", "systemCheckFinished"); + systemEvent("SYSTEMCHECKFINISHED"); // wtf is this? + } + + public String systemEvent(String eventMsg) { + invoke("publishSystemEvent", eventMsg); + return eventMsg; + } + + public String systemEvent(String format, Object... ags) { + String eventMsg = String.format(format, ags); + return systemEvent(eventMsg); } // FIXME - if this is really desired it will drive local references for all @@ -1975,6 +2237,154 @@ public boolean setSpeechType(String speechType) { } + public void closeRightHand() { + + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveRightHand", map); + + } + + public void openRightHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveRightHand", map); + } + + public void closeLeftHand() { + + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveLeftHand", map); + + } + + public void openLeftHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveLeftHand", map); + } + + public void openHands() { + openLeftHand(); + openRightHand(); + } + + public void closeHands() { + closeLeftHand(); + closeRightHand(); + } + + public void wake() { + log.info("wake"); + // do waking things - based on config + + // blink + + // wake gesture + // callback + // imoov2[{name}]["onWake"](this) + /** + *

+     i01.speakBlocking("I was sleeping")
+     lookrightside()
+     sleep(2)
+     lookleftside()
+     sleep(4)
+     relax()
+     ear.clearLock()
+     sleep(2)
+     i01.finishedGesture()
+     * 
+ */ + + /** + *
+     * // legacy
+     * enable();
+     * rest();
+     * 
+     * if (ear != null) {
+     *   ear.clearLock();
+     * }
+     * 
+     * // beginCheckingOnInactivity();
+     * // BAD BAD BAD !!!
+     * publishEvent("powerUp"); // before or after loopback
+     * 
+ **/ + // was a relax gesture .. might want to ask about it .. + + // if ear start listening + AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) getPeer("ear"); + if (ear != null) { + ear.startListening(); + } + + // attempt recognize where its at + + // attempt to recognize people + + // look for activity + + // say hello + + // start animation (configurable) + + rest(); + + // should "session be determined by recognition?" + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); + + if (chatBot != null) { + String firstinit = chatBot.getPredicate("firstinit"); + // wtf - "ok" really, for a boolean? + if (!"ok".equals(firstinit)) { + fsm.fire("firstInit"); + } + } + } + public static void main(String[] args) { try { @@ -2069,5 +2479,4 @@ public static void main(String[] args) { } } - } diff --git a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java index 05524a2630..48e5ab9a86 100644 --- a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java +++ b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java @@ -247,6 +247,8 @@ public void publishTelemetry(String name) { log.info("Servo : {} Angle : {}", jointName, angleMap.get(jointName)); } invoke("publishJointAngles", angleMap); + + // we want to publish the joint positions // this way we can render on the web gui.. double[][] jointPositionMap = createJointPositionMap(name); @@ -292,8 +294,7 @@ public static void main(String[] args) throws Exception { LoggingFactory.init("info"); String arm = "myArm"; - Runtime.createAndStart("python", "Python"); - Runtime.createAndStart("gui", "SwingGui"); + Runtime.start("python", "Python"); InverseKinematics3D inversekinematics = (InverseKinematics3D) Runtime.start("ik3d", "InverseKinematics3D"); // InverseKinematics3D inversekinematics = new InverseKinematics3D("iksvc"); diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index e13daf273d..259cd258df 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -28,9 +28,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.Set; +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; @@ -38,58 +40,15 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.NeoPixelConfig; -import org.myrobotlab.service.config.ServiceConfig; +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 { - - /** - * Thread to do animations Java side and push the changing of pixels to the - * neopixel - */ - private class AnimationRunner implements Runnable { - - boolean running = false; - - private transient Thread thread = null; - - @Override - public void run() { - try { - running = true; - - while (running) { - equalizer(); - Double wait_ms_per_frame = fpsToWaitMs(speedFps); - sleep(wait_ms_per_frame.intValue()); - } - } catch (Exception e) { - error(e); - stop(); - } - } - - // 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; - thread = null; - } - } - - @Override - public void releaseService() { - super.releaseService(); - clear(); - } +public class NeoPixel extends Service implements NeoPixelControl, AudioListener { public static class Pixel { public int address; @@ -122,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; @@ -138,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; } @@ -146,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 AnimationRunner animationRunner; + 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 @@ -166,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 @@ -190,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 */ @@ -212,34 +329,31 @@ 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; - - final Map animations = new HashMap<>(); + 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); registerForInterfaceChange(NeoPixelController.class); - animationRunner = new AnimationRunner(); + worker = new Worker(); animations.put("Stop", 1); animations.put("Color Wipe", 2); animations.put("Larson Scanner", 3); @@ -249,14 +363,64 @@ public NeoPixel(String n, String id) { animations.put("Rainbow Cycle", 7); animations.put("Flash Random", 8); animations.put("Ironman", 9); - // > 99 is java side animations - animations.put("Equalizer", 100); + } + + 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; } @@ -289,40 +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 java animations - animationRunner.stop(); - // 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() { @@ -361,154 +500,165 @@ public void detachNeoPixelController(NeoPixelController neoCntrlr) { broadcastState(); } - public void equalizer() { - equalizer(null, null); + public void fill(int r, int g, int b) { + fill(0, pixelCount, r, g, b, null); } - public void equalizer(Long wait_ms_per_frame, Integer range) { + public void fill(int beginAddress, int count, int r, int g, int b) { + fill(beginAddress, count, r, g, b, null); + } - if (controller == null) { - log.warn("controller not set"); - return; + 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); + } - if (wait_ms_per_frame == null) { - wait_ms_per_frame = 25L; + public void fill(String color) { + int rgb[] = CodecUtils.getColor(color); + if (rgb == null) { + error("could not get color %s", color); + return; } + fill(rgb[0], rgb[1], rgb[2]); + } - if (range == null) { - range = 25; - } - - Random rand = new Random(); - int c = rand.nextInt(range); - - fillMatrix(red, green, blue, white); + public void fillMatrix(int r, int g, int b) { + fillMatrix(r, g, b, 0); + } - if (c < 18) { - setMatrix(0, 0, 0, 0); - setMatrix(7, 0, 0, 0); + public void fillMatrix(int r, int g, int b, int w) { + PixelSet ps = getPixelSet(); + for (Pixel p : ps.pixels) { + p.red = r; + p.green = g; + p.blue = b; + p.white = w; } + } - fillMatrix(red, green, blue, white); + public void flash() { + flash(red, green, blue, 1, 300, 300); + } - if (c < 16) { - setMatrix(0, 0, 0, 0); - setMatrix(7, 0, 0, 0); - } + public void flash(int r, int g, int b) { + flash(r, g, b, 1, 300, 300); + } - if (c < 12) { - setMatrix(1, 0, 0, 0); - setMatrix(6, 0, 0, 0); - } + public void flash(int r, int g, int b, int count) { + flash(r, g, b, count, 300, 300); + } - if (c < 8) { - setMatrix(2, 0, 0, 0); - setMatrix(5, 0, 0, 0); + 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); + } - writeMatrix(); - + public void flash(int r, int g, int b, long timeOn, long timeOff) { + flash(r, g, b, 1, timeOn, timeOff); } - - public void onLedDisplay(LedDisplayData data) { - - if ("flash".equals(data.action)) { - flash(data.count, data.interval, data.red, data.green, data.blue); + + /** + * Invokes a flash from the flashMap + * + * @param name + */ + public void flash(String name) { + if (config.flashMap == null) { + error("flash map is null"); + return; } - - } - public void flash(int count, long interval, int r, int g, int b) { - long delay = 0; - for (int i = 0; i < count; ++i) { - addTask(getName()+"fill-"+System.currentTimeMillis(), true, 0, delay, "fill", r, g, b); - delay+= interval/2; - addTask(getName()+"clear-"+System.currentTimeMillis(), true, 0, delay, "clear"); - delay+= interval/2; + 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) { - NeoPixelConfig c = (NeoPixelConfig)config; - - // FIXME - these need to be moved into config -// int count = 2; -// int interval = 75; - setBrightness((int)brightNess); - fill(red, green, blue); - -// long delay = 0; -// for (int i = 0; i < count; ++i) { -// addTask(getName()+"fill-"+System.currentTimeMillis(), true, 0, delay, "fill", red, green, blue); -// delay+= interval/2; -// addTask(getName()+"clear-"+System.currentTimeMillis(), true, 0, delay, "clear"); -// delay+= interval/2; -// } - - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } - + 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); } - public void fill(int r, int g, int b) { - fill(0, pixelCount, r, g, b, null); + // 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 void fill(int beginAddress, int count, int r, int g, int b) { - fill(beginAddress, count, r, g, b, null); + public Set getAnimations() { + return animations.keySet(); } - public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { - NeoPixelConfig c = (NeoPixelConfig)config; - - if (w == null) { - w = 0; - } - - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot setPixel controller not set", getName()); - return; - } - np2.neoPixelFill(getName(), beginAddress, count, r, g, b, w); - - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } - + public int getBlue() { + return blue; } - public void fillMatrix(int r, int g, int b) { - fillMatrix(r, g, b, 0); + /** + * get the list of hex defined colors + * + * @return + */ + public List getColorNames() { + return CodecUtils.getColorNames(); } - public void fillMatrix(int r, int g, int b, int w) { - PixelSet ps = getPixelSet(); - for (Pixel p : ps.pixels) { - p.red = r; - p.green = g; - p.blue = b; - p.white = w; - } - - } + @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; - public int getBlue() { - return blue; + 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; } @@ -579,59 +729,103 @@ public int getRed() { } @Override - public void playAnimation(String animation) { + public boolean isAttached(Attachable instance) { + return instance.getName().equals(controller); + } - if (animations.containsKey(animation)) { - currentAnimation = animation; - if (animations.get(animation) < 99) { - setAnimation(animations.get(animation), red, green, blue, speedFps); - } else { - // only 1 java side animation at the moment - equalizer(); - animationRunner.start(); - } - } else { - error("could not find animation %s", animation); + /** + * 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"); } - broadcastState(); } - public void stopAnimation() { - setAnimation(1, red, green, blue, speedFps); + /** + * 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 setAnimation(int animation, int red, int green, int blue, int speedFps) { - if (speedFps > maxFps) { - speedFps = maxFps; - } + synchronized public void playAnimation(String animation) { - this.speedFps = speedFps; + log.debug("playAnimation {} {} {} {} {}", animation, red, green, blue, speedFps); - if (controller == null) { - error("%s could not set animation no attached controller", getName()); + if (animation == null || animation.equals("Stop")) { + log.info("clearing animation"); + clear(); 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; - animationRunner.stop(); + + if (animation.equals(currentAnimation)) { + log.info("already playing {}", currentAnimation); + return; + } + + if (animations.containsKey(animation)) { + + if (speedFps > maxFps) { + speedFps = maxFps; + } + + LedDisplayData data = new LedDisplayData("animation"); + data.animation = animation; + addDisplayTask(data); + } else { + error("could not find animation %s", animation); } - broadcastState(); } - // 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; + public void playIronman() { + setColor(170, 170, 255); + setSpeed(50); + playAnimation("Ironman"); + } + + @Override + public void releaseService() { + super.releaseService(); + clear(); + } + + @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 @@ -653,22 +847,32 @@ public void setBlue(int blue) { } public void setBrightness(int value) { - NeoPixelConfig c = (NeoPixelConfig)config; - - 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); - - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); + LedDisplayData data = new LedDisplayData("brightness"); + data.brightness = value; + addDisplayTask(data); + } + + 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) { @@ -693,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 */ @@ -718,7 +935,6 @@ public void setPixel(int address, int red, int green, int blue, int white) { * @param delayMs */ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int red, int green, int blue, int white, Integer delayMs) { - NeoPixelConfig c = (NeoPixelConfig)config; // get and update memory cache PixelSet ps = getPixelSet(matrixName, pixelSetIndex); @@ -737,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 (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } + LedDisplayData data = new LedDisplayData("writeMatrix"); + addDisplayTask(data); } public int setPixelCount(int pixelCount) { @@ -770,67 +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 startAnimation() { - startAnimation(currentMatrix); - } - - /** - * handle both user defined, java defined, and controller on board animations - * FIXME - make "settings" separate call - * - * @param name - */ - public void startAnimation(String name) { - animationRunner.start(); - } - - 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() { - NeoPixelConfig c = (NeoPixelConfig)config; - - 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 (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } - - - } - /** * extremely rough fps * @@ -849,155 +995,51 @@ public void setSpeed(Integer speed) { } } - public void playIronman() { - setColor(170, 170, 255); - setSpeed(50); - playAnimation("Ironman"); + 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); + } } @Override - public ServiceConfig getConfig() { - - NeoPixelConfig config = (NeoPixelConfig)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 void startService() { + super.startService(); + worker.start(); } - @Override - public ServiceConfig apply(ServiceConfig c) { - NeoPixelConfig config = (NeoPixelConfig) 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; + synchronized public void stopAnimation() { + clear(); } - public String onStarted(String name) { - return name; + public void stopService() { + super.stopService(); + worker.stop(); + 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) { - NeoPixelConfig c = (NeoPixelConfig)config; - c.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/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index e17cd987b9..bf4a155d1d 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -32,7 +32,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Queue; import java.util.Random; import java.util.Set; import java.util.TimeZone; @@ -70,7 +69,6 @@ import org.myrobotlab.framework.repo.IvyWrapper; import org.myrobotlab.framework.repo.Repo; import org.myrobotlab.framework.repo.ServiceData; -import org.myrobotlab.framework.repo.ServiceDependency; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.AppenderType; import org.myrobotlab.logging.LoggerFactory; @@ -134,7 +132,7 @@ public class Runtime extends Service implements MessageListener, * a registry of all services regardless of which environment they came from - * each must have a unique name */ - static private final Map registry = new TreeMap<>(); + static private final Map registry = new LinkedHashMap<>(); /** * A plan is a request to runtime to change the system. Typically its to ask @@ -200,12 +198,6 @@ public class Runtime extends Service implements MessageListener, */ boolean processingConfig = false; - /** - * The one config directory where all config is managed the {default} is the - * current configuration set - */ - // protected String configDir = "data" + fs + "config"; - /** *
    * The set of client connections to this mrl instance Some of the connections
@@ -437,9 +429,9 @@ synchronized private static Map createServicesFromPlan
         runtime.error("could not get %s from plan", service);
         continue;
       }
-      sc.state = "CREATING";
+      // sc.state = "CREATING";
       ServiceInterface si = createService(service, sc.type, null);
-      sc.state = "CREATED";
+      // sc.state = "CREATED";
       // process  the base listeners/subscription of ServiceConfig
       si.addConfigListeners(sc);
       if (si instanceof ConfigurableService) {
@@ -1552,7 +1544,8 @@ public void run() {
             r.getRepo().install(serviceType);
           }
         } catch (Exception e) {
-          r.error(e);
+          r.error("dependencies failed - install error", e);
+          throw new RuntimeException(String.format("dependencies failed - install error %s", e.getMessage()));
         }
       }
     };
@@ -1912,13 +1905,6 @@ public synchronized static boolean releaseService(String inName) {
     if (si.isLocal()) {
       si.purgeTasks();
       si.stopService();
-      Plan plan = Runtime.getPlan();
-      ServiceConfig sc = plan.get(inName);
-      if (sc == null) {
-        log.debug("service config not available for {}", inName);
-      } else {
-        sc.state = "STOPPED";
-      }
     } else {
       if (runtime != null) {
         runtime.send(name, "releaseService");
@@ -1927,33 +1913,6 @@ public synchronized static boolean releaseService(String inName) {
     // FOR remote this isn't correct - it should wait for
     // a message from the other runtime to say that its released
     unregister(name);
-    Plan plan = Runtime.getPlan();
-    ServiceConfig sc = plan.get(inName);
-
-    if (sc != null) {
-      sc.state = "RELEASED";
-      // FIXME - TODO RELEASE PEERS ! which is any inName.* !!!
-
-      // iterate through peers
-      // if (sc.autoStartPeers) {
-      // // get peers from meta data
-      // MetaData md = MetaData.get(sc.type);
-      // Map peers = md.getPeers();
-      // log.info("auto start peers and {} of type {} has {} peers", inName,
-      // sc.type, peers.size());
-      // // RECURSE ! - if we found peers and autoStartPeers is true - we start
-      // // all
-      // // the children up
-      // for (String peer : peers.keySet()) {
-      // // get actual Name
-      // String actualPeerName = getPeerName(peer, sc, peers, inName);
-      // if (actualPeerName != null && isStarted(actualPeerName) &&
-      // si.autoStartedPeersContains(actualPeerName)) {
-      // release(actualPeerName);
-      // }
-      // }
-      // }
-    }
 
     return true;
   }
@@ -2585,8 +2544,8 @@ static public void startConfig(String configName) {
     setConfig(configName);
     Runtime runtime = Runtime.getInstance();
     runtime.processingConfig = true; // multiple inbox threads not available
-    runtime.invoke("publishStartConfig", configName);
-    RuntimeConfig rtConfig = runtime.readServiceConfig(runtime.getConfigName(), "runtime", new StaticType<>() {});
+    runtime.invoke("publishConfigStarted", configName);
+    RuntimeConfig rtConfig = (RuntimeConfig) runtime.readServiceConfig(runtime.getConfigName(), "runtime");
     if (rtConfig == null) {
       runtime.error("cannot find %s%s%s", runtime.getConfigName(), fs, "runtime.yml");
       return;
@@ -2597,6 +2556,7 @@ static public void startConfig(String configName) {
     // for every service listed in runtime registry - load it
     // FIXME - regex match on filesystem matches on *.yml
     for (String service : rtConfig.getRegistry()) {
+            
       if ("runtime".equals(service) || Runtime.isStarted(service)) {
         continue;
       }
@@ -2626,12 +2586,12 @@ static public void startConfig(String configName) {
     }
 
     runtime.processingConfig = false; // multiple inbox threads not available
-    runtime.invoke("publishFinishedConfig", configName);
+    runtime.invoke("publishConfigFinished", configName);
 
   }
 
-  public String publishStartConfig(String configName) {
-    log.info("publishStartConfig {}", configName);
+  public String publishConfigStarted(String configName) {
+    log.info("publishConfigStarted {}", configName);
     // Make Note: done inline, because the thread actually doing the config
     // processing
     // would need to be finished with it before this thread could be invoked
@@ -2640,8 +2600,8 @@ public String publishStartConfig(String configName) {
     return configName;
   }
 
-  public String publishFinishedConfig(String configName) {
-    log.info("publishFinishedConfig {}", configName);
+  public String publishConfigFinished(String configName) {
+    log.info("publishConfigFinished {}", configName);
     // Make Note: done inline, because the thread actually doing the config
     // processing
     // would need to be finished with it before this thread could be invoked
@@ -2781,21 +2741,6 @@ public Runtime(String n, String id) {
         // fist and only time....
         runtime = this;
         repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper");
-
-        // resolve serviceData MetaTypes for the repo
-
-        for (MetaData metaData : serviceData.getServiceTypes()) {
-          if (metaData.getSimpleName().equals("OpenCV")) {
-            log.warn("here");
-          }
-          Set deps = repo.getUnfulfilledDependencies(metaData.getType());
-          if (deps.size() == 0) {
-            metaData.installed = true;
-          } else {
-            log.warn("{} not installed", metaData.getSimpleName());
-          }
-        }
-
       }
     }
 
@@ -3536,7 +3481,7 @@ static public String execute(String... args) {
    * @return The programs stderr and stdout output
    */
   static public String execute(String program, List args, String workingDir, Map additionalEnv, boolean block) {
-    log.info("execToString(\"{} {}\")", program, args);
+    log.debug("execToString(\"{} {}\")", program, args);
 
     List command = new ArrayList<>();
     command.add(program);
@@ -3572,14 +3517,15 @@ static public String execute(String program, List args, String workingDi
         if (block) {
             int exitValue = handle.waitFor();
             outputBuilder.append("Exit Value: ").append(exitValue);
-            log.info("Command exited with exit value: {}", exitValue);
+            log.debug("Command exited with exit value: {}", exitValue);
         } else {
-            log.info("Command started");
+            log.debug("Command started");
         }
 
         return outputBuilder.toString();
     } catch (IOException e) {
         log.error("Error executing command", e);
+        Runtime.getInstance().error(e.getMessage());
         return e.getMessage();
     } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
@@ -3631,13 +3577,14 @@ public static Double getBatteryLevel() {
       } else if (platform.isLinux()) {
         // TODO This is incorrect, will not work when unplugged
         // and acpitool output is different than expected,
-        // at least on Ubuntu 22.04
-        String ret = Runtime.execute("acpitool");
-        int pos0 = ret.indexOf("Charging, ");
+        // at least on Ubuntu 22.04 - consider oshi library
+        String ret = Runtime.execute("acpi");
+        int pos0 = ret.indexOf("%");
+
         if (pos0 != -1) {
-          pos0 = pos0 + 10;
-          int pos1 = ret.indexOf("%", pos0);
-          String dble = ret.substring(pos0, pos1).trim();
+          int pos1 = ret.lastIndexOf(" ", pos0);
+          // int pos1 = ret.indexOf("%", pos0);
+          String dble = ret.substring(pos1, pos0).trim();
           try {
             r = Double.parseDouble(dble);
           } catch (Exception e) {
@@ -4820,9 +4767,15 @@ synchronized private Plan loadService(Plan plan, String name, String type, boole
 
     return plan;
   }
-
+  
+  /**
+   * read a service's configuration, in the context
+   * of current config set name or default
+   * @param name
+   * @return
+   */
   public ServiceConfig readServiceConfig(String name) {
-    return readServiceConfig(name, new StaticType<>() {});
+    return readServiceConfig(null, name);
   }
   
   /**
diff --git a/src/main/java/org/myrobotlab/service/config/CalikoConfig.java b/src/main/java/org/myrobotlab/service/config/CalikoConfig.java
new file mode 100644
index 0000000000..d1c3ff9554
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/config/CalikoConfig.java
@@ -0,0 +1,36 @@
+package org.myrobotlab.service.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.myrobotlab.service.Pid.PidData;
+
+public class CalikoConfig extends ServiceConfig {
+
+  public Map data = new HashMap<>();
+  
+  public boolean use3dDemo;
+
+  public int demoNumber;
+
+  public boolean drawConstraints;
+
+  public Object rotateBasesMode;
+
+  public boolean drawLines;
+
+  public boolean drawModels;
+
+  public boolean fixedBaseMode;
+
+  public boolean drawAxes;
+
+  public boolean paused;
+
+  public boolean leftMouseButtonDown;
+
+  public int windowWidth;
+
+  public int windowHeight;
+
+}
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index af6c68c7bf..d90bd6851d 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -1,49 +1,88 @@
 package org.myrobotlab.service.config;
 
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.myrobotlab.framework.Plan;
 import org.myrobotlab.jme3.UserDataConfig;
 import org.myrobotlab.math.MapperLinear;
+import org.myrobotlab.math.MapperSimple;
 import org.myrobotlab.service.Pid.PidData;
 import org.myrobotlab.service.Runtime;
 import org.myrobotlab.service.config.FiniteStateMachineConfig.Transition;
 import org.myrobotlab.service.config.RandomConfig.RandomMessageConfig;
 
+/**
+ * InMoov2Config - configuration for InMoov2 service
+ *  - this is a "default" configuration 
+ * If its configuration which will directly affect another service the naming
+ * pattern should be {peerName}{propertyName}
+ * e.g. neoPixelErrorRed
+ * 
+ * FIXME make a color map that can be overridden
+ * 
+ * @author GroG
+ *
+ */
 public class InMoov2Config extends ServiceConfig {
 
-  public int analogPinFromSoundCard = 53;
-  
-  public int audioPollsBySeconds = 2;
-  
-  public boolean audioSignalProcessing=false;
-  
+  /**
+   * When the healthCheck is operating, it will check the battery level.
+   * If the battery level is < 5% it will publishFlash with red at regular interval 
+   */
   public boolean batteryInSystem = false;
-  
-  public boolean customSound=false;
+
+  /**
+   * enable custom sound map for state changes
+   */
+  public boolean customSound = false;
+
   
   public boolean forceMicroOnIfSleeping = true;
   
-  public boolean healthCheckActivated = false;
-  
-  public int healthCheckTimerMs = 60000;
-  
-  public boolean heartbeat = false;
+  /**
+   * flashes if error has occurred - requires heartbeat
+   */
+  public boolean healthCheckFlash = true;
   
 
   /**
-   * idle time measures the time the fsm is in an idle state
+   * flashes the neopixel every time a health check is preformed.
+   * green == good
+   * red == battery < 5%
    */
-  public boolean idleTimer = true;
+  public boolean heartbeatFlash = false;
 
+  /**
+   * Single heartbeat to drive InMoov2 .. it can check status, healthbeat,
+   * and fire events to the FSM.
+   * Checks battery level and sends a heartbeat flash on publishHeartbeat
+   * and onHeartbeat at a regular interval
+   */
+  public boolean heartbeat = true;
+  
+  /**
+   * interval heath check processes in milliseconds
+   */
+  public long heartbeatInterval = 3000;
+  
+  /**
+   * loads all python gesture files in the gesture directory
+   */
   public boolean loadGestures = true;
 
+  /**
+   * executes all scripts in the init directory on startup 
+   */
+  public boolean loadInitScripts = true;
+  
   /**
    * default to null - allow the OS to set it, unless explicilty set
    */
   public String locale = null; // = "en-US";
 
-  public boolean neoPixelBootGreen=true;
+  public boolean neoPixelBootGreen = true;
 
   public boolean neoPixelDownloadBlue = true;
 
@@ -54,38 +93,71 @@ public class InMoov2Config extends ServiceConfig {
   public boolean openCVFaceRecognizerActivated=true;
     
   public boolean pirEnableTracking = false;
-  
+
+  public boolean pirOnFlash = true;
+
   /**
-   * play pir sounds when pir switching states
-   * sound located in data/InMoov2/sounds/pir-activated.mp3
-   * sound located in data/InMoov2/sounds/pir-deactivated.mp3
+   * play pir sounds when pir switching states sound located in
+   * data/InMoov2/sounds/pir-activated.mp3 sound located in
+   * data/InMoov2/sounds/pir-deactivated.mp3
    */
   public boolean pirPlaySounds = true;
-  
+
   public boolean pirWakeUp = true;
-    
-  public boolean robotCanMoveHeadWhileSpeaking = true;
-  
   
+  public boolean robotCanMoveHeadWhileSpeaking = true;
+
   /**
    * startup and shutdown will pause inmoov - set the speed to this value then
    * attempt to move to rest
    */
   public double shutdownStartupSpeed = 50;
+
+  /**
+   * Sleep 5 minutes after last presence detected
+   */
+  public int sleepTimeoutMs = 300000;
+
+  public boolean startupSound = true;
   
   /**
-   * Sleep 5 minutes after last presence detected 
+   * 
+   */
+  public boolean stateChangeIsMute = true; 
+    
+  /**
+   * Interval in seconds for a idle state event to fire off.
+   * If the fsm is in a state which will allow transitioning, the InMoov2
+   * state will transition to idle. Heartbeat will fire the event.
    */
-  public int sleepTimeoutMs=300000;
+  public Integer stateIdleInterval = 120;
   
-  public boolean startupSound = true;
-
-  public int trackingTimeoutMs=10000;
   
+  /**
+   * Interval in seconds for a random state event to fire off.
+   * If the fsm is in a state which will allow transitioning, the InMoov2
+   * state will transition to random. Heartbeat will fire the event.
+   */
+  public Integer stateRandomInterval = 120;
+
+  /**
+   * Determines if InMoov2 publish system events during boot state
+   */
+  public boolean systemEventsOnBoot = false;
+
+  /**
+   * Publish system event when state changes
+   */
+  public boolean systemEventStateChange = true;
+
+  public int trackingTimeoutMs = 10000;
+
   public String unlockInsult = "forgive me";
-  
+
   public boolean virtual = false;
 
+  public String bootAnimation = "Theater Chase";
+
   public InMoov2Config() {
   }
 
@@ -110,7 +182,10 @@ public Plan getDefault(Plan plan, String name) {
     addDefaultPeerConfig(plan, name, "left", "Arduino", false);
     addDefaultPeerConfig(plan, name, "leftArm", "InMoov2Arm", false);
     addDefaultPeerConfig(plan, name, "leftHand", "InMoov2Hand", false);
+    addDefaultPeerConfig(plan, name, "log", "Log", false);
     addDefaultPeerConfig(plan, name, "mouth", "MarySpeech", false);
+    // a first !
+    addDefaultPeerConfig(plan, name, "mouth.audioFile", "AudioFile", false);
     addDefaultPeerConfig(plan, name, "mouthControl", "MouthControl", false);
     addDefaultPeerConfig(plan, name, "neoPixel", "NeoPixel", false);
     addDefaultPeerConfig(plan, name, "opencv", "OpenCV", false);
@@ -127,10 +202,29 @@ public Plan getDefault(Plan plan, String name) {
     addDefaultPeerConfig(plan, name, "torso", "InMoov2Torso", false);
     addDefaultPeerConfig(plan, name, "ultrasonicRight", "UltrasonicSensor", false);
     addDefaultPeerConfig(plan, name, "ultrasonicLeft", "UltrasonicSensor", false);
-
+    addDefaultPeerConfig(plan, name, "vertx", "Vertx", false);
+    addDefaultPeerConfig(plan, name, "webxr", "WebXR", false);
+    
+    WebXRConfig webxr = (WebXRConfig)plan.get(getPeerName("webxr"));
+    
+    Map map = new HashMap<>();
+    MapperSimple mapper = new MapperSimple(-0.5, 0.5, 0, 180);
+    map.put("i01.head.neck", mapper);
+    webxr.controllerMappings.put("head.orientation.pitch", map);
+
+    map = new HashMap<>();
+    mapper = new MapperSimple(-0.5, 0.5, 0, 180);
+    map.put("i01.head.rothead", mapper);
+    webxr.controllerMappings.put("head.orientation.yaw", map);
+
+    map = new HashMap<>();
+    mapper = new MapperSimple(-0.5, 0.5, 0, 180);
+    map.put("i01.head.roll", mapper);
+    webxr.controllerMappings.put("head.orientation.roll", map);
+    
     MouthControlConfig mouthControl = (MouthControlConfig) plan.get(getPeerName("mouthControl"));
 
-    // setup name references to different services
+    // setup name references to different services FIXME getPeerName("head").getPeerName("jaw")
     mouthControl.jaw = name + ".head.jaw";
     String i01Name = name;
     int index = name.indexOf(".");
@@ -139,7 +233,7 @@ public Plan getDefault(Plan plan, String name) {
     }
 
     mouthControl.mouth = i01Name + ".mouth";
-
+    
     ProgramABConfig chatBot = (ProgramABConfig) plan.get(getPeerName("chatBot"));
     Runtime runtime = Runtime.getInstance();
     String[] bots = new String[] { "cn-ZH", "en-US", "fi-FI", "hi-IN", "nl-NL", "ru-RU", "de-DE", "es-ES", "fr-FR", "it-IT", "pt-PT", "tr-TR" };
@@ -154,14 +248,20 @@ public Plan getDefault(Plan plan, String name) {
         }
       }
     }
-    
+
     chatBot.currentUserName = "human";
-    
+
     // chatBot.textListeners = new String[] { name + ".htmlFilter" };
     if (chatBot.listeners == null) {
       chatBot.listeners = new ArrayList<>();
     }
     chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText"));
+    
+    
+    Gpt3Config gpt3 = (Gpt3Config) plan.get(getPeerName("gpt3"));
+    gpt3.listeners = new ArrayList<>();
+    gpt3.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText"));
+    
 
     HtmlFilterConfig htmlFilter = (HtmlFilterConfig) plan.get(getPeerName("htmlFilter"));
     // htmlFilter.textListeners = new String[] { name + ".mouth" };
@@ -181,17 +281,18 @@ public Plan getDefault(Plan plan, String name) {
     ServoMixerConfig servoMixer = (ServoMixerConfig) plan.get(getPeerName("servoMixer"));
     servoMixer.listeners = new ArrayList<>();
     servoMixer.listeners.add(new Listener("publishText", name + ".mouth", "onText"));
-    //servoMixer.listeners.add(new Listener("publishText", name + ".chatBot", "onText"));
+    // servoMixer.listeners.add(new Listener("publishText", name + ".chatBot",
+    // "onText"));
 
     // == Peer - ear =============================
     // setup name references to different services
     WebkitSpeechRecognitionConfig ear = (WebkitSpeechRecognitionConfig) plan.get(getPeerName("ear"));
-    ear.listeners = new ArrayList<>(); 
+    ear.listeners = new ArrayList<>();
     ear.listeners.add(new Listener("publishText", name + ".chatBot", "onText"));
     ear.listening = true;
     // remove, should only need ServiceConfig.listeners
-    ear.textListeners = new String[]{name + ".chatBot"};
-
+    ear.textListeners = new String[] { name + ".chatBot" };
+    
     JMonkeyEngineConfig simulator = (JMonkeyEngineConfig) plan.get(getPeerName("simulator"));
 
     simulator.multiMapped.put(name + ".leftHand.index", new String[] { name + ".leftHand.index", name + ".leftHand.index2", name + ".leftHand.index3" });
@@ -259,24 +360,30 @@ public Plan getDefault(Plan plan, String name) {
     simulator.cameraLookAt = name + ".torso.lowStom";
 
     FiniteStateMachineConfig fsm = (FiniteStateMachineConfig) plan.get(getPeerName("fsm"));
-    // TODO - events easily gotten from InMoov data ?? auto callbacks in python if exists ?
+    // TODO - events easily gotten from InMoov data ?? auto callbacks in python
+    // if exists ?
+    fsm.listeners = new ArrayList<>();
     fsm.current = "boot";
-    fsm.transitions.add(new Transition("boot", "configStarted", "applyingConfig"));
-    fsm.transitions.add(new Transition("applyingConfig", "getUserInfo", "getUserInfo"));
-    fsm.transitions.add(new Transition("applyingConfig", "systemCheck", "systemCheck"));
-    fsm.transitions.add(new Transition("applyingConfig", "wake", "awake"));
-    fsm.transitions.add(new Transition("getUserInfo", "systemCheck", "systemCheck"));
-    fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake"));
-    fsm.transitions.add(new Transition("awake", "sleep", "sleeping"));
+    fsm.transitions.add(new Transition("boot", "wake", "wake"));
+    fsm.transitions.add(new Transition("wake", "idle", "idle"));
+    fsm.transitions.add(new Transition("firstInit", "idle", "idle"));
+    fsm.transitions.add(new Transition("idle", "random", "random"));
+    fsm.transitions.add(new Transition("random", "idle", "idle"));
+    fsm.transitions.add(new Transition("idle", "sleep", "sleep"));
+    fsm.transitions.add(new Transition("sleep", "wake", "wake"));
+    fsm.transitions.add(new Transition("idle", "powerDown", "powerDown"));
+    fsm.transitions.add(new Transition("wake", "firstInit", "firstInit"));
+    // powerDown to shutdown
+//    fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake"));
+//    fsm.transitions.add(new Transition("awake", "sleep", "sleeping"));
 
-    
-    
     PirConfig pir = (PirConfig) plan.get(getPeerName("pir"));
-    pir.pin = "23";
+    pir.pin = "D23";
     pir.controller = name + ".left";
     pir.listeners = new ArrayList<>();
-    pir.listeners.add(new Listener("publishPirOn", name, "onPirOn"));
-    
+    pir.listeners.add(new Listener("publishPirOn", name));
+    pir.listeners.add(new Listener("publishPirOff", name));
+
     // == Peer - random =============================
     RandomConfig random = (RandomConfig) plan.get(getPeerName("random"));
     random.enabled = false;
@@ -388,16 +495,87 @@ public Plan getDefault(Plan plan, String name) {
     plan.remove(name + ".eyeTracking.controller.serial");
     plan.remove(name + ".eyeTracking.cv");
     
+    // LOOPBACK PUBLISHING - ITS A GREAT WAY TO SUPPORT
+    // EXTENSIBLE AND OVERRIDABLE BEHAVIORS
+    
     // inmoov2 default listeners
     listeners = new ArrayList<>();
     // FIXME - should be getPeerName("neoPixel")
-    listeners.add(new Listener("publishFlash", name + ".neoPixel", "onLedDisplay"));
-
-    listeners.add(new Listener("publishEvent", name + ".fsm"));
         
-    // remove the auto-added starts in the plan's runtime RuntimConfig.registry
-    plan.removeStartsWith(name + ".");
+    // loopbacks allow user to override or extend with python
+    listeners.add(new Listener("publishBoot", name));
+    listeners.add(new Listener("publishHeartbeat", name));
+    listeners.add(new Listener("publishConfigFinished", name));
+    listeners.add(new Listener("publishStateChange", name));
+    
+//    listeners.add(new Listener("publishPowerUp", name));
+//    listeners.add(new Listener("publishPowerDown", name));
+//    listeners.add(new Listener("publishError", name));
+    
+    listeners.add(new Listener("publishMoveHead", name));
+    listeners.add(new Listener("publishMoveRightArm", name));
+    listeners.add(new Listener("publishMoveLeftArm", name));
+    listeners.add(new Listener("publishMoveRightHand", name));
+    listeners.add(new Listener("publishMoveLeftHand", name));
+    listeners.add(new Listener("publishMoveTorso", name));
+
+
+    
+    LogConfig log = (LogConfig) plan.get(getPeerName("log"));
+    log.listeners = new ArrayList<>();
+    log.listeners.add(new Listener("publishLogEvents", name));
+    
+//    mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name));
+//    mouth_audioFile.listeners.add(new Listener("publishAudioStart", name));
+    
+    // InMoov2 --to--> service 
+    listeners.add(new Listener("publishFlash", getPeerName("neoPixel")));
+    listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse"));
+    listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer")));
+    
+    listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel")));
+    listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel")));
+    
+//    listeners.add(new Listener("publishPowerUp", name));
+//    listeners.add(new Listener("publishPowerDown", name));
+//    listeners.add(new Listener("publishError", name));
     
+    listeners.add(new Listener("publishMoveHead", name));
+    listeners.add(new Listener("publishMoveRightArm", name));
+    listeners.add(new Listener("publishMoveLeftArm", name));
+    listeners.add(new Listener("publishMoveRightHand", name));
+    listeners.add(new Listener("publishMoveLeftHand", name));
+    listeners.add(new Listener("publishMoveTorso", name));
+
+   
+    // service --to--> InMoov2
+    AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));
+    mouth_audioFile.listeners = new ArrayList<>();
+    mouth_audioFile.listeners.add(new Listener("publishPeak", name));
+    fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange"));
+    
+    webxr.listeners = new ArrayList<>();
+    webxr.listeners.add(new Listener("publishJointAngles", name));
+    
+//    mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name));
+//    mouth_audioFile.listeners.add(new Listener("publishAudioStart", name));
+    
+    // InMoov2 --to--> service 
+    listeners.add(new Listener("publishFlash", getPeerName("neoPixel"), "onLedDisplay"));
+    listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse"));
+    listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer")));
+    
+    
+    // service --to--> service
+    ServoMixerConfig servoMixer = (ServoMixerConfig) plan.get(getPeerName("servoMixer"));
+    servoMixer.listeners = new ArrayList<>();
+    servoMixer.listeners.add(new Listener("publishText", getPeerName("mouth"), "onText"));
+    
+    
+    
+    // remove the auto-added starts in the plan's runtime RuntimConfig.registry
+    // plan.removeStartsWith(name + ".");
+
     // rtConfig.add(name); // <-- adding i01 / not needed
 
     return plan;
diff --git a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java
index c2ad476697..5cb8fc6cd5 100644
--- a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java
@@ -1,20 +1,137 @@
 package org.myrobotlab.service.config;
 
+import java.util.HashMap;
+import java.util.Map;
+
+import org.myrobotlab.framework.Plan;
+
 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;
+  /**
+   * initial fill
+   */
   public boolean fill = false;
-  // auto clears flashes
-  public boolean autoClear = true;
-  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.
+   */
+  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
+   */
+  public Plan getDefault(Plan plan, String name) {
+    super.getDefault(plan, name);
+
+    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/config/ServiceConfig.java b/src/main/java/org/myrobotlab/service/config/ServiceConfig.java
index 233e0c29b8..524ee47291 100755
--- a/src/main/java/org/myrobotlab/service/config/ServiceConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ServiceConfig.java
@@ -79,12 +79,7 @@ public String toString() {
    * simple type name of service defined for this config
    */
   public String type;
-
-  // FIXME - change to enum ! 
-  // FIXME - remove this - its not used 
-  // heh non transient makes it easy to debug !
-  transient public String state = "INIT"; // INIT | LOADED | CREATED | STARTED |
-                                          // STOPPED | RELEASED
+  
 
   public String getPath(String name, String peerKey) {
     if (name == null) {
diff --git a/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java b/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java
index a49520c1e4..c84c4dca65 100644
--- a/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java
@@ -8,6 +8,7 @@ public class SpeechSynthesisConfig extends ServiceConfig {
 
   public boolean mute = false;
   public boolean blocking = false;
+  @Deprecated /* :(  ... this is already in listeners ! */
   public String[] speechRecognizers;
   public Map substitutions;
   public String voice;
diff --git a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java
index 92ce88ba38..b70e081c52 100644
--- a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java
+++ b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java
@@ -1,28 +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;
-    public int green;
-    public int blue;
-    //public int white?;
-    
-    /**
-     * number of flashes
-     */
-    public int count = 5;
-    /**
-     * interval of flash in ms
-     */
-    public long interval = 500;
-    
-    
+  /**
+   * required action field may be
+   *  fill | flash | play animation | stop | clear
+   */
+  public String action; 
+  
+  /**
+   * name of animation
+   */
+  public String animation = null;
+
+  /**
+   * flash definition
+   */
+  public List flashes = new ArrayList<>();
+  
+  /**
+   * if set overrides default brightness
+   */  
+  public Integer brightness = null;
+
+  /**
+   * begin fill address
+   */
+  public int beginAddress;
+
+  /**
+   * fill count
+   */
+  public int onCount; 
+
+
+  public LedDisplayData(String action) {
+    this.action = action;
+  }
+
+  public String toString() {
+    return String.format("%s, %s", action, animation);
+  }
 }
diff --git a/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java b/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java
index 003b956f34..33e9b416ac 100644
--- a/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java
@@ -2,6 +2,7 @@
 
 public interface AudioControl {
 
+  // FIXME - should be onVolume(volume)
   public void setVolume(double volume);
 
   public double getVolume();
@@ -10,7 +11,7 @@ public interface AudioControl {
    * plays an audiofile - is a listener function for publishAudioFile
    * @param file
    */
-  public void onPlayAudioFile(String file);
+  public void onPlayAudioFile(String dir);
   
   /**
    * must be a directory, will play one of the audio files within that directory
@@ -18,7 +19,7 @@ public interface AudioControl {
    */
   public void onPlayRandomAudioFile(String dir);
 
-  // pause
-  // resume
-  // interrupt ?
+  // onPause
+  // onResume
+  // onInterrupt ?
 }
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/java/org/myrobotlab/service/meta/CalikoMeta.java b/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java
new file mode 100644
index 0000000000..5e98d56744
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java
@@ -0,0 +1,41 @@
+package org.myrobotlab.service.meta;
+
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.meta.abstracts.MetaData;
+import org.slf4j.Logger;
+
+public class CalikoMeta extends MetaData {
+  private static final long serialVersionUID = 1L;
+  public final static Logger log = LoggerFactory.getLogger(CalikoMeta.class);
+
+  /**
+   * This class is contains all the meta data details of a service. It's peers,
+   * dependencies, and all other meta data related to the service.
+   * 
+   */
+  public CalikoMeta() {
+
+    // add a cool description
+    addDescription("used as a general template");
+
+    // false will prevent it being seen in the ui
+    setAvailable(true);
+
+    // add dependencies if necessary
+    // for the solver
+    addDependency("au.edu.federation.caliko", "caliko", "1.3.8");
+
+    // for the ui
+    addDependency("au.edu.federation.caliko.visualisation", "caliko-visualisation", "1.3.8");
+    addDependency("au.edu.federation.caliko.demo", "caliko-demo", "1.3.8");
+      
+    // add it to one or many categories
+    addCategory("ik", "inverse kinematics");
+
+    // add a sponsor to this service
+    // the person who will do maintenance
+    // setSponsor("GroG");
+
+  }
+
+}
diff --git a/src/main/resources/resource/BoofCV/basket_depth.png b/src/main/resources/resource/BoofCV/basket_depth.png
new file mode 100644
index 0000000000..1477305355
Binary files /dev/null and b/src/main/resources/resource/BoofCV/basket_depth.png differ
diff --git a/src/main/resources/resource/BoofCV/basket_rgb.png b/src/main/resources/resource/BoofCV/basket_rgb.png
new file mode 100644
index 0000000000..0bf3d57021
Binary files /dev/null and b/src/main/resources/resource/BoofCV/basket_rgb.png differ
diff --git a/src/main/resources/resource/BoofCV/intrinsic.yaml b/src/main/resources/resource/BoofCV/intrinsic.yaml
new file mode 100644
index 0000000000..db7c7f9ca9
--- /dev/null
+++ b/src/main/resources/resource/BoofCV/intrinsic.yaml
@@ -0,0 +1,19 @@
+# Pinhole camera model with radial and tangential distortion
+# (fx,fy) = focal length, (cx,cy) = principle point, (width,height) = image shape
+# radial = radial distortion, (t1,t2) = tangential distortion
+
+pinhole:
+  fx: 529.74137370586
+  fy: 529.5715453060717
+  cx: 312.57382117058427
+  cy: 257.05061008728114
+  width: 640
+  height: 480
+  skew: 0.0
+model: pinhole_radial_tangential
+radial_tangential:
+  radial:
+  - 0.17889353480851655
+  - -0.32301207366192053
+  t1: 0.0
+  t2: 0.0
diff --git a/src/main/resources/resource/BoofCV/visualdepth.yaml b/src/main/resources/resource/BoofCV/visualdepth.yaml
new file mode 100644
index 0000000000..b911c9a497
--- /dev/null
+++ b/src/main/resources/resource/BoofCV/visualdepth.yaml
@@ -0,0 +1,20 @@
+# RGB Depth Camera Calibration
+model: visual_depth
+max_depth: 10000
+no_depth: 0
+intrinsic:
+  pinhole:
+    fx: 529.74137370586
+    fy: 529.5715453060717
+    cx: 312.57382117058427
+    cy: 257.05061008728114
+    width: 640
+    height: 480
+    skew: 0.0
+  model: pinhole_radial_tangential
+  radial_tangential:
+    radial:
+    - 0.17889353480851655
+    - -0.32301207366192053
+    t1: 0.0
+    t2: 0.0
\ No newline at end of file
diff --git a/src/main/resources/resource/Caliko.png b/src/main/resources/resource/Caliko.png
new file mode 100644
index 0000000000..3ddbc83695
Binary files /dev/null and b/src/main/resources/resource/Caliko.png differ
diff --git a/src/main/resources/resource/Caliko/pyramid.obj b/src/main/resources/resource/Caliko/pyramid.obj
new file mode 100644
index 0000000000..797d3ac9fe
--- /dev/null
+++ b/src/main/resources/resource/Caliko/pyramid.obj
@@ -0,0 +1,56 @@
+# Triangle consisting of 8 lines: A square at the bottom then from each vertex up to the tip
+
+
+# Top Left Back
+# v -0.5f 0.5f 0.0f
+# Top Right Back
+# v 0.5f 0.5f 0.0f
+# Bottom Left Back
+# v -0.5f -0.5f 0.0f
+# Bottom Right Back
+# v 0.5f -0.5f 0.0f
+# Center Front
+# v 0.0f 0.0f 1.0f
+
+# Top Left Back
+v -0.5f 0.5f 0.0f
+# Bottom Left Back
+v -0.5f -0.5f 0.0f
+
+# Top Left Back
+v -0.5f 0.5f 0.0f
+# Top Right Back
+v 0.5f 0.5f 0.0f
+
+# Top Right Back
+v 0.5f 0.5f 0.0f
+# Bottom Right Back
+v 0.5f -0.5f 0.0f
+
+# Bottom Right Back
+v 0.5f -0.5f 0.0f
+# Bottom Left Back
+v -0.5f -0.5f 0.0f
+
+
+
+# SQUARE
+# Top Left Back
+v -0.5f 0.5f 0.0f
+# Center Front
+v 0.0f 0.0f 1.0f
+
+# Top Right Back
+v 0.5f 0.5f 0.0f
+# Center Front
+v 0.0f 0.0f 1.0f
+
+# Bottom Left Back
+v -0.5f -0.5f 0.0f
+# Center Front
+v 0.0f 0.0f 1.0f
+
+# Bottom Right Back
+v 0.5f -0.5f 0.0f
+# Center Front
+v 0.0f 0.0f 1.0f
\ No newline at end of file
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 05f67ef4ea..c527bcd0e1 100644
--- a/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js
@@ -11,7 +11,7 @@ angular.module('mrlapp.service.NeoPixelGui', []).controller('NeoPixelGuiCtrl', [
     $scope.pins = []
     $scope.speeds = []
     $scope.types = ['RGB', 'RGBW']
-    $scope.animations = ['No animation', 'Stop', 'Color Wipe', 'Larson Scanner', 'Theater Chase', 'Theater Chase Rainbow', 'Rainbow', 'Rainbow Cycle', 'Flash Random', 'Ironman', 'equalizer']
+    $scope.animations = ['Stop', 'Color Wipe', 'Larson Scanner', 'Theater Chase', 'Theater Chase Rainbow', 'Rainbow', 'Rainbow Cycle', 'Flash Random', 'Ironman']
     $scope.pixelCount = null
 
     // set pixel position
@@ -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
         }
 
diff --git a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js
index 4f452e9834..143f739d3f 100644
--- a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js
@@ -365,7 +365,9 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$
     }
 
      $scope.saveConfig = function() {
-          $scope.service.selectedOption = 'current'
+        $scope.service.includePeers = false
+        $scope.service.selectedOption = "current"
+         
           var modalInstance = $uibModal.open({
             templateUrl: 'saveConfig.html',
             scope: $scope,
diff --git a/src/main/resources/resource/WebGui/app/service/tab-header.html b/src/main/resources/resource/WebGui/app/service/tab-header.html
index 4abedeb490..4557717bdc 100644
--- a/src/main/resources/resource/WebGui/app/service/tab-header.html
+++ b/src/main/resources/resource/WebGui/app/service/tab-header.html
@@ -55,7 +55,7 @@
                         
                         
  • - +   subscriptions diff --git a/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html b/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html index ec43324484..d3cef04291 100644 --- a/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html @@ -10,7 +10,7 @@

    {{address}} {{color}}

    pixel count   - + diff --git a/src/main/resources/resource/WebXR.png b/src/main/resources/resource/WebXR.png index 2432b77c92..dcf18c9242 100644 Binary files a/src/main/resources/resource/WebXR.png and b/src/main/resources/resource/WebXR.png differ diff --git a/src/test/java/org/myrobotlab/service/ClockTest.java b/src/test/java/org/myrobotlab/service/ClockTest.java index b9761ac743..ad086ff345 100644 --- a/src/test/java/org/myrobotlab/service/ClockTest.java +++ b/src/test/java/org/myrobotlab/service/ClockTest.java @@ -37,7 +37,7 @@ public void testService() throws Exception { assertEquals(interval, clock.getInterval()); clock.startClock(); - Service.sleep(10); + Service.sleep(100); assertTrue(clock.isClockRunning()); clock.stopClock(); diff --git a/src/test/java/org/myrobotlab/service/ServoMixerTest.java b/src/test/java/org/myrobotlab/service/ServoMixerTest.java new file mode 100644 index 0000000000..da5c35683b --- /dev/null +++ b/src/test/java/org/myrobotlab/service/ServoMixerTest.java @@ -0,0 +1,31 @@ +package org.myrobotlab.service; + +import org.junit.Test; +import org.myrobotlab.kinematics.Gesture; +import org.myrobotlab.kinematics.GesturePart; +import org.myrobotlab.test.AbstractTest; + +public class ServoMixerTest extends AbstractTest { + + @Test + public void testService() throws Exception { + + Servo s1 = (Servo)Runtime.start("s1", "Servo"); + Servo s2 = (Servo)Runtime.start("s2", "Servo"); + + ServoMixer mixer = (ServoMixer)Runtime.start("mixer", "ServoMixer"); + String gestureFileName = "mixerTest-1"; + // mixer.addNewGestureFile(gestureFileName); + Gesture gesture = new Gesture(); + + s1.moveTo(5); + s2.moveTo(5); + mixer.savePose("pose-1"); + + GesturePart part1 = new GesturePart(); + + gesture.getParts().add(null); + + } + +} \ No newline at end of file