From 70a2157b7e94579e86a8d229d491f6d9bb922b20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:57:36 +0000 Subject: [PATCH 01/18] Initial plan From 470ca7d8d85d65ef5c3fb126c6c68751d9f431c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:22:22 +0000 Subject: [PATCH 02/18] Restructure modules into self-contained directories with config, docs, and tests Co-authored-by: danic85 <6583012+danic85@users.noreply.github.com> --- config/braillespeak.yml | 5 - config/pitemperature.yml | 4 - main.py | 2 +- module_loader.py | 23 +- modules/actuators/__init__.py | 0 modules/actuators/bus_servo/README.md | 72 ++++++ modules/actuators/bus_servo/__init__.py | 0 .../actuators/bus_servo/config.yml | 0 modules/actuators/bus_servo/tests/__init__.py | 0 .../bus_servo/tests/test_bus_servo.py | 28 +++ modules/actuators/piservo/README.md | 28 +++ modules/actuators/piservo/__init__.py | 0 .../actuators/piservo/config.yml | 2 +- modules/actuators/{ => piservo}/piservo.py | 0 modules/actuators/piservo/tests/__init__.py | 0 .../actuators/piservo/tests/test_piservo.py | 25 ++ modules/actuators/servo/README.md | 28 +++ modules/actuators/servo/__init__.py | 0 .../actuators/servo/config.yml | 2 +- modules/actuators/{ => servo}/servo.py | 0 modules/actuators/servo/tests/__init__.py | 0 modules/actuators/servo/tests/test_servo.py | 29 +++ modules/actuators/tests/__init__.py | 0 modules/animate/README.md | 28 +++ modules/animate/__init__.py | 0 modules/{ => animate}/animate.py | 0 .../animate.yml => modules/animate/config.yml | 2 +- modules/animate/tests/__init__.py | 0 modules/animate/tests/test_animate.py | 30 +++ .../scripts/modules/tests/__init__.py | 0 modules/audio/__init__.py | 0 modules/audio/braillespeak/README.md | 38 +++ modules/audio/braillespeak/__init__.py | 0 .../audio/{ => braillespeak}/braillespeak.py | 0 modules/audio/braillespeak/config.yml | 5 + modules/audio/braillespeak/tests/__init__.py | 0 .../braillespeak/tests/test_braillespeak.py | 20 ++ modules/audio/buzzer/README.md | 28 +++ modules/audio/buzzer/__init__.py | 0 modules/audio/{ => buzzer}/buzzer.py | 0 .../audio/buzzer/config.yml | 2 +- modules/audio/buzzer/tests/__init__.py | 0 modules/audio/buzzer/tests/test_buzzer.py | 26 ++ modules/audio/speechinput/README.md | 30 +++ modules/audio/speechinput/__init__.py | 0 .../audio/speechinput/config.yml | 2 +- .../audio/{ => speechinput}/speechinput.py | 0 modules/audio/speechinput/tests/__init__.py | 0 .../speechinput/tests/test_speechinput.py | 30 +++ modules/audio/ttsmodule/README.md | 28 +++ modules/audio/ttsmodule/__init__.py | 0 .../audio/ttsmodule/config.yml | 2 +- modules/audio/ttsmodule/tests/__init__.py | 0 .../audio/ttsmodule/tests/test_ttsmodule.py | 25 ++ modules/audio/{ => ttsmodule}/ttsmodule.py | 0 modules/chatgpt/README.md | 73 ++++++ modules/chatgpt/__init__.py | 0 modules/{ => chatgpt}/chatgpt.py | 0 .../chatgpt.yml => modules/chatgpt/config.yml | 2 +- modules/chatgpt/tests/__init__.py | 0 modules/chatgpt/tests/test_chatgpt.py | 32 +++ modules/controller_handler/README.md | 32 +++ modules/controller_handler/__init__.py | 0 .../controller_handler/config.yml | 2 +- .../controller_handler.py | 0 modules/controller_handler/tests/__init__.py | 0 .../tests/test_controller_handler.py | 31 +++ modules/display/__init__.py | 0 modules/display/tft_display_eye/README.md | 110 +++++++++ modules/display/tft_display_eye/__init__.py | 0 .../display/tft_display_eye/config.yml | 2 +- .../display/tft_display_eye/tests/__init__.py | 0 .../tests/test_tft_display_eye.py | 33 +++ .../{ => tft_display_eye}/tft_display_eye.py | 0 modules/display/waveshare_oled/README.md | 28 +++ modules/display/waveshare_oled/__init__.py | 0 .../display/waveshare_oled/config.yml | 2 +- .../display/waveshare_oled/tests/__init__.py | 0 .../tests/test_waveshare_oled.py | 32 +++ .../{ => waveshare_oled}/waveshare_oled.py | 0 modules/gpio/__init__.py | 0 modules/gpio/laser/README.md | 28 +++ modules/gpio/laser/__init__.py | 0 .../gpio/laser/config.yml | 2 +- modules/gpio/{ => laser}/laser.py | 0 modules/gpio/laser/tests/__init__.py | 0 modules/gpio/laser/tests/test_laser.py | 30 +++ modules/gpio/motion/README.md | 75 ++++++ modules/gpio/motion/__init__.py | 0 .../gpio/motion/config.yml | 2 +- modules/gpio/{ => motion}/motion.py | 0 modules/gpio/motion/tests/__init__.py | 0 modules/gpio/motion/tests/test_motion.py | 24 ++ modules/gpio/read/README.md | 28 +++ modules/gpio/read/__init__.py | 0 .../gpio/read/config.yml | 2 +- modules/gpio/{ => read}/read.py | 0 modules/gpio/read/tests/__init__.py | 0 modules/gpio/read/tests/test_read.py | 22 ++ modules/i2c_servo/README.md | 60 +++++ modules/i2c_servo/__init__.py | 0 .../i2c_servo/config.yml | 2 +- modules/{ => i2c_servo}/i2c_servo.py | 0 modules/i2c_servo/tests/__init__.py | 0 modules/i2c_servo/tests/test_i2c_servo.py | 18 ++ modules/input_recorder/README.md | 28 +++ modules/input_recorder/__init__.py | 0 .../input_recorder/config.yml | 2 +- .../{ => input_recorder}/input_recorder.py | 0 modules/input_recorder/tests/__init__.py | 0 .../tests/test_input_recorder.py | 33 +++ modules/logwrapper/README.md | 28 +++ modules/logwrapper/__init__.py | 0 .../logwrapper/config.yml | 2 +- modules/{ => logwrapper}/logwrapper.py | 0 modules/logwrapper/tests/__init__.py | 0 modules/logwrapper/tests/test_logwrapper.py | 28 +++ modules/neopixel/__init__.py | 0 modules/neopixel/emotion_analysis/README.md | 28 +++ modules/neopixel/emotion_analysis/__init__.py | 0 .../neopixel/emotion_analysis/config.yml | 2 +- .../emotion_analysis.py | 0 .../emotion_analysis/tests/__init__.py | 0 .../tests/test_emotion_analysis.py | 24 ++ modules/neopixel/neopx/README.md | 0 modules/neopixel/neopx/__init__.py | 0 .../neopixel/neopx/config.yml | 2 +- modules/neopixel/{ => neopx}/neopx.py | 0 modules/neopixel/neopx/tests/__init__.py | 0 modules/neopixel/neopx/tests/test_neopx.py | 32 +++ modules/network/__init__.py | 0 modules/network/arduinoserial/README.md | 30 +++ modules/network/arduinoserial/__init__.py | 0 .../{ => arduinoserial}/arduinoserial.py | 0 .../network/arduinoserial/config.yml | 2 +- .../network/arduinoserial/tests/__init__.py | 0 .../arduinoserial/tests/test_arduinoserial.py | 27 +++ modules/network/messaging_service/README.md | 119 ++++++++++ modules/network/messaging_service/__init__.py | 0 .../network/messaging_service/config.yml | 2 +- .../messaging_service.py | 0 .../messaging_service/tests/__init__.py | 0 .../tests/test_messaging_service.py | 34 +++ modules/network/rtlsdr/README.md | 95 ++++++++ modules/network/rtlsdr/__init__.py | 0 .../network/rtlsdr/config.yml | 2 +- modules/network/{ => rtlsdr}/rtlsdr.py | 0 modules/network/rtlsdr/tests/__init__.py | 0 modules/network/rtlsdr/tests/test_rtlsdr.py | 21 ++ modules/network/telegrambot/README.md | 111 +++++++++ modules/network/telegrambot/__init__.py | 0 .../network/telegrambot/config.yml | 2 +- .../network/{ => telegrambot}/telegrambot.py | 0 modules/network/telegrambot/tests/__init__.py | 0 .../telegrambot/tests/test_telegrambot.py | 23 ++ modules/personality/README.md | 217 +++++++++++++++++ modules/personality/__init__.py | 0 .../personality/config.yml | 2 +- modules/{ => personality}/personality.py | 0 modules/personality/tests/__init__.py | 0 modules/personality/tests/test_personality.py | 27 +++ modules/pitemperature/README.md | 28 +++ modules/pitemperature/__init__.py | 0 modules/pitemperature/config.yml | 4 + modules/{ => pitemperature}/pitemperature.py | 0 modules/pitemperature/tests/__init__.py | 0 .../pitemperature/tests/test_pitemperature.py | 39 +++ modules/sensor/__init__.py | 0 modules/sensor/imu/__init__.py | 0 modules/sensor/imu/bno055/README.md | 77 ++++++ modules/sensor/imu/bno055/__init__.py | 0 modules/sensor/imu/{ => bno055}/bno055.py | 0 .../sensor/imu/bno055/config.yml | 2 +- modules/sensor/imu/bno055/tests/__init__.py | 0 .../sensor/imu/bno055/tests/test_bno055.py | 36 +++ modules/sensor/imu/mpu6050/README.md | 86 +++++++ modules/sensor/imu/mpu6050/__init__.py | 0 .../sensor/imu/mpu6050/config.yml | 2 +- modules/sensor/imu/{ => mpu6050}/mpu6050.py | 0 modules/sensor/imu/mpu6050/tests/__init__.py | 0 .../sensor/imu/mpu6050/tests/test_mpu6050.py | 23 ++ modules/translator/README.md | 28 +++ modules/translator/__init__.py | 0 .../translator/config.yml | 2 +- modules/translator/tests/__init__.py | 0 modules/translator/tests/test_translator.py | 34 +++ modules/{ => translator}/translator.py | 0 modules/vision/__init__.py | 0 modules/vision/imx500/__init__.py | 0 modules/vision/imx500/tracking/README.md | 28 +++ modules/vision/imx500/tracking/__init__.py | 0 .../vision/imx500/tracking/config.yml | 2 +- .../vision/imx500/tracking/tests/__init__.py | 0 .../imx500/tracking/tests/test_tracking.py | 36 +++ .../vision/imx500/{ => tracking}/tracking.py | 0 modules/vision/imx500/vision/README.md | 28 +++ modules/vision/imx500/vision/__init__.py | 0 .../vision/imx500/vision/config.yml | 2 +- .../vision/imx500/vision/tests/__init__.py | 0 .../vision/imx500/vision/tests/test_vision.py | 29 +++ modules/vision/imx500/{ => vision}/vision.py | 0 modules/vision/opencv/__init__.py | 0 modules/vision/opencv/vision/README.md | 28 +++ modules/vision/opencv/vision/__init__.py | 0 .../vision/opencv/vision/config.yml | 2 +- .../vision/opencv/vision/tests/__init__.py | 0 .../vision/opencv/vision/tests/test_vision.py | 22 ++ modules/vision/opencv/{ => vision}/vision.py | 0 modules/xbox_controller/README.md | 224 ++++++++++++++++++ modules/xbox_controller/__init__.py | 0 .../xbox_controller/config.yml | 2 +- modules/xbox_controller/tests/__init__.py | 0 .../tests/test_xbox_controller.py | 41 ++++ .../{ => xbox_controller}/xbox_controller.py | 0 test.sh | 3 +- tests/test_bno055.py | 4 +- tests/test_controller.py | 2 +- tests/test_input_recorder.py | 2 +- tests/test_logwrapper.py | 12 +- tests/test_module_loader.py | 38 +-- tests/test_pitemperature.py | 25 +- tests/test_sensor.py | 2 +- 222 files changed, 2943 insertions(+), 89 deletions(-) delete mode 100644 config/braillespeak.yml delete mode 100644 config/pitemperature.yml create mode 100644 modules/actuators/__init__.py create mode 100644 modules/actuators/bus_servo/README.md create mode 100644 modules/actuators/bus_servo/__init__.py rename config/servo_bus.yml => modules/actuators/bus_servo/config.yml (100%) mode change 100755 => 100644 create mode 100644 modules/actuators/bus_servo/tests/__init__.py create mode 100644 modules/actuators/bus_servo/tests/test_bus_servo.py create mode 100644 modules/actuators/piservo/README.md create mode 100644 modules/actuators/piservo/__init__.py rename config/piservo.yml => modules/actuators/piservo/config.yml (79%) rename modules/actuators/{ => piservo}/piservo.py (100%) create mode 100644 modules/actuators/piservo/tests/__init__.py create mode 100644 modules/actuators/piservo/tests/test_piservo.py create mode 100644 modules/actuators/servo/README.md create mode 100644 modules/actuators/servo/__init__.py rename config/servos.yml => modules/actuators/servo/config.yml (92%) rename modules/actuators/{ => servo}/servo.py (100%) create mode 100644 modules/actuators/servo/tests/__init__.py create mode 100644 modules/actuators/servo/tests/test_servo.py create mode 100644 modules/actuators/tests/__init__.py create mode 100644 modules/animate/README.md create mode 100644 modules/animate/__init__.py rename modules/{ => animate}/animate.py (100%) rename config/animate.yml => modules/animate/config.yml (52%) mode change 100755 => 100644 create mode 100644 modules/animate/tests/__init__.py create mode 100644 modules/animate/tests/test_animate.py create mode 100644 modules/archived/scripts/modules/tests/__init__.py create mode 100644 modules/audio/__init__.py create mode 100644 modules/audio/braillespeak/README.md create mode 100644 modules/audio/braillespeak/__init__.py rename modules/audio/{ => braillespeak}/braillespeak.py (100%) create mode 100644 modules/audio/braillespeak/config.yml create mode 100644 modules/audio/braillespeak/tests/__init__.py create mode 100644 modules/audio/braillespeak/tests/test_braillespeak.py create mode 100644 modules/audio/buzzer/README.md create mode 100644 modules/audio/buzzer/__init__.py rename modules/audio/{ => buzzer}/buzzer.py (100%) rename config/buzzer.yml => modules/audio/buzzer/config.yml (71%) create mode 100644 modules/audio/buzzer/tests/__init__.py create mode 100644 modules/audio/buzzer/tests/test_buzzer.py create mode 100644 modules/audio/speechinput/README.md create mode 100644 modules/audio/speechinput/__init__.py rename config/speechinput.yml => modules/audio/speechinput/config.yml (89%) rename modules/audio/{ => speechinput}/speechinput.py (100%) create mode 100644 modules/audio/speechinput/tests/__init__.py create mode 100644 modules/audio/speechinput/tests/test_speechinput.py create mode 100644 modules/audio/ttsmodule/README.md create mode 100644 modules/audio/ttsmodule/__init__.py rename config/ttsmodule.yml => modules/audio/ttsmodule/config.yml (84%) create mode 100644 modules/audio/ttsmodule/tests/__init__.py create mode 100644 modules/audio/ttsmodule/tests/test_ttsmodule.py rename modules/audio/{ => ttsmodule}/ttsmodule.py (100%) create mode 100644 modules/chatgpt/README.md create mode 100644 modules/chatgpt/__init__.py rename modules/{ => chatgpt}/chatgpt.py (100%) rename config/chatgpt.yml => modules/chatgpt/config.yml (91%) create mode 100644 modules/chatgpt/tests/__init__.py create mode 100644 modules/chatgpt/tests/test_chatgpt.py create mode 100644 modules/controller_handler/README.md create mode 100644 modules/controller_handler/__init__.py rename config/controller_handler.yml => modules/controller_handler/config.yml (98%) rename modules/{ => controller_handler}/controller_handler.py (100%) create mode 100644 modules/controller_handler/tests/__init__.py create mode 100644 modules/controller_handler/tests/test_controller_handler.py create mode 100644 modules/display/__init__.py create mode 100644 modules/display/tft_display_eye/README.md create mode 100644 modules/display/tft_display_eye/__init__.py rename config/tft_display.yml => modules/display/tft_display_eye/config.yml (90%) create mode 100644 modules/display/tft_display_eye/tests/__init__.py create mode 100644 modules/display/tft_display_eye/tests/test_tft_display_eye.py rename modules/display/{ => tft_display_eye}/tft_display_eye.py (100%) create mode 100644 modules/display/waveshare_oled/README.md create mode 100644 modules/display/waveshare_oled/__init__.py rename config/oled_display.yml => modules/display/waveshare_oled/config.yml (85%) create mode 100644 modules/display/waveshare_oled/tests/__init__.py create mode 100644 modules/display/waveshare_oled/tests/test_waveshare_oled.py rename modules/display/{ => waveshare_oled}/waveshare_oled.py (100%) create mode 100644 modules/gpio/__init__.py create mode 100644 modules/gpio/laser/README.md create mode 100644 modules/gpio/laser/__init__.py rename config/gpio_laser.yml => modules/gpio/laser/config.yml (76%) rename modules/gpio/{ => laser}/laser.py (100%) create mode 100644 modules/gpio/laser/tests/__init__.py create mode 100644 modules/gpio/laser/tests/test_laser.py create mode 100644 modules/gpio/motion/README.md create mode 100644 modules/gpio/motion/__init__.py rename config/gpio_motion.yml => modules/gpio/motion/config.yml (75%) rename modules/gpio/{ => motion}/motion.py (100%) create mode 100644 modules/gpio/motion/tests/__init__.py create mode 100644 modules/gpio/motion/tests/test_motion.py create mode 100644 modules/gpio/read/README.md create mode 100644 modules/gpio/read/__init__.py rename config/gpio_read.yml => modules/gpio/read/config.yml (75%) rename modules/gpio/{ => read}/read.py (100%) create mode 100644 modules/gpio/read/tests/__init__.py create mode 100644 modules/gpio/read/tests/test_read.py create mode 100644 modules/i2c_servo/README.md create mode 100644 modules/i2c_servo/__init__.py rename config/i2c_servo.yml => modules/i2c_servo/config.yml (80%) rename modules/{ => i2c_servo}/i2c_servo.py (100%) create mode 100644 modules/i2c_servo/tests/__init__.py create mode 100644 modules/i2c_servo/tests/test_i2c_servo.py create mode 100644 modules/input_recorder/README.md create mode 100644 modules/input_recorder/__init__.py rename config/input_recorder.yml => modules/input_recorder/config.yml (57%) mode change 100755 => 100644 rename modules/{ => input_recorder}/input_recorder.py (100%) create mode 100644 modules/input_recorder/tests/__init__.py create mode 100644 modules/input_recorder/tests/test_input_recorder.py create mode 100644 modules/logwrapper/README.md create mode 100644 modules/logwrapper/__init__.py rename config/logwrapper.yml => modules/logwrapper/config.yml (82%) rename modules/{ => logwrapper}/logwrapper.py (100%) create mode 100644 modules/logwrapper/tests/__init__.py create mode 100644 modules/logwrapper/tests/test_logwrapper.py create mode 100644 modules/neopixel/__init__.py create mode 100644 modules/neopixel/emotion_analysis/README.md create mode 100644 modules/neopixel/emotion_analysis/__init__.py rename config/neopixel_emotions.yml => modules/neopixel/emotion_analysis/config.yml (99%) rename modules/neopixel/{ => emotion_analysis}/emotion_analysis.py (100%) create mode 100644 modules/neopixel/emotion_analysis/tests/__init__.py create mode 100644 modules/neopixel/emotion_analysis/tests/test_emotion_analysis.py create mode 100644 modules/neopixel/neopx/README.md create mode 100644 modules/neopixel/neopx/__init__.py rename config/neopixel.yml => modules/neopixel/neopx/config.yml (95%) rename modules/neopixel/{ => neopx}/neopx.py (100%) create mode 100644 modules/neopixel/neopx/tests/__init__.py create mode 100644 modules/neopixel/neopx/tests/test_neopx.py create mode 100644 modules/network/__init__.py create mode 100644 modules/network/arduinoserial/README.md create mode 100644 modules/network/arduinoserial/__init__.py rename modules/network/{ => arduinoserial}/arduinoserial.py (100%) rename config/serial.yml => modules/network/arduinoserial/config.yml (54%) create mode 100644 modules/network/arduinoserial/tests/__init__.py create mode 100644 modules/network/arduinoserial/tests/test_arduinoserial.py create mode 100644 modules/network/messaging_service/README.md create mode 100644 modules/network/messaging_service/__init__.py rename config/messaging_service.yml => modules/network/messaging_service/config.yml (82%) mode change 100755 => 100644 rename modules/network/{ => messaging_service}/messaging_service.py (100%) create mode 100644 modules/network/messaging_service/tests/__init__.py create mode 100644 modules/network/messaging_service/tests/test_messaging_service.py create mode 100644 modules/network/rtlsdr/README.md create mode 100644 modules/network/rtlsdr/__init__.py rename config/rtlsdr.yml => modules/network/rtlsdr/config.yml (78%) rename modules/network/{ => rtlsdr}/rtlsdr.py (100%) create mode 100644 modules/network/rtlsdr/tests/__init__.py create mode 100644 modules/network/rtlsdr/tests/test_rtlsdr.py create mode 100644 modules/network/telegrambot/README.md create mode 100644 modules/network/telegrambot/__init__.py rename config/telegram.yml => modules/network/telegrambot/config.yml (76%) rename modules/network/{ => telegrambot}/telegrambot.py (100%) create mode 100644 modules/network/telegrambot/tests/__init__.py create mode 100644 modules/network/telegrambot/tests/test_telegrambot.py create mode 100644 modules/personality/README.md create mode 100644 modules/personality/__init__.py rename config/personality.yml => modules/personality/config.yml (72%) rename modules/{ => personality}/personality.py (100%) create mode 100644 modules/personality/tests/__init__.py create mode 100644 modules/personality/tests/test_personality.py create mode 100644 modules/pitemperature/README.md create mode 100644 modules/pitemperature/__init__.py create mode 100644 modules/pitemperature/config.yml rename modules/{ => pitemperature}/pitemperature.py (100%) create mode 100644 modules/pitemperature/tests/__init__.py create mode 100644 modules/pitemperature/tests/test_pitemperature.py create mode 100644 modules/sensor/__init__.py create mode 100644 modules/sensor/imu/__init__.py create mode 100644 modules/sensor/imu/bno055/README.md create mode 100644 modules/sensor/imu/bno055/__init__.py rename modules/sensor/imu/{ => bno055}/bno055.py (100%) rename config/bno055.yml => modules/sensor/imu/bno055/config.yml (86%) create mode 100644 modules/sensor/imu/bno055/tests/__init__.py create mode 100644 modules/sensor/imu/bno055/tests/test_bno055.py create mode 100644 modules/sensor/imu/mpu6050/README.md create mode 100644 modules/sensor/imu/mpu6050/__init__.py rename config/mpu6050.yml => modules/sensor/imu/mpu6050/config.yml (67%) rename modules/sensor/imu/{ => mpu6050}/mpu6050.py (100%) create mode 100644 modules/sensor/imu/mpu6050/tests/__init__.py create mode 100644 modules/sensor/imu/mpu6050/tests/test_mpu6050.py create mode 100644 modules/translator/README.md create mode 100644 modules/translator/__init__.py rename config/translator.yml => modules/translator/config.yml (74%) create mode 100644 modules/translator/tests/__init__.py create mode 100644 modules/translator/tests/test_translator.py rename modules/{ => translator}/translator.py (100%) create mode 100644 modules/vision/__init__.py create mode 100644 modules/vision/imx500/__init__.py create mode 100644 modules/vision/imx500/tracking/README.md create mode 100644 modules/vision/imx500/tracking/__init__.py rename config/tracking.yml => modules/vision/imx500/tracking/config.yml (61%) create mode 100644 modules/vision/imx500/tracking/tests/__init__.py create mode 100644 modules/vision/imx500/tracking/tests/test_tracking.py rename modules/vision/imx500/{ => tracking}/tracking.py (100%) create mode 100644 modules/vision/imx500/vision/README.md create mode 100644 modules/vision/imx500/vision/__init__.py rename config/vision_imx500.yml => modules/vision/imx500/vision/config.yml (82%) create mode 100644 modules/vision/imx500/vision/tests/__init__.py create mode 100644 modules/vision/imx500/vision/tests/test_vision.py rename modules/vision/imx500/{ => vision}/vision.py (100%) create mode 100644 modules/vision/opencv/__init__.py create mode 100644 modules/vision/opencv/vision/README.md create mode 100644 modules/vision/opencv/vision/__init__.py rename config/vision_opencv.yml => modules/vision/opencv/vision/config.yml (53%) create mode 100644 modules/vision/opencv/vision/tests/__init__.py create mode 100644 modules/vision/opencv/vision/tests/test_vision.py rename modules/vision/opencv/{ => vision}/vision.py (100%) create mode 100644 modules/xbox_controller/README.md create mode 100644 modules/xbox_controller/__init__.py rename config/controller.yml => modules/xbox_controller/config.yml (91%) mode change 100755 => 100644 create mode 100644 modules/xbox_controller/tests/__init__.py create mode 100644 modules/xbox_controller/tests/test_xbox_controller.py rename modules/{ => xbox_controller}/xbox_controller.py (100%) diff --git a/config/braillespeak.yml b/config/braillespeak.yml deleted file mode 100644 index 45fa6dfe..00000000 --- a/config/braillespeak.yml +++ /dev/null @@ -1,5 +0,0 @@ -braillespeak: - enabled: false - path: modules.audio.braillespeak.BrailleSpeak - config: - pin: 27 \ No newline at end of file diff --git a/config/pitemperature.yml b/config/pitemperature.yml deleted file mode 100644 index 4d389c99..00000000 --- a/config/pitemperature.yml +++ /dev/null @@ -1,4 +0,0 @@ -pitemperature: - enabled: true - environment: robot - path: modules.pitemperature.PiTemperature \ No newline at end of file diff --git a/main.py b/main.py index 283e6148..b076203f 100644 --- a/main.py +++ b/main.py @@ -17,7 +17,7 @@ def main(): env = args.env # Dynamically load and initialize modules, passing env - loader = ModuleLoader(config_folder="config", environment=env) + loader = ModuleLoader(config_folder="modules", environment=env) module_instances = loader.load_modules() # Set messaging service for all modules diff --git a/module_loader.py b/module_loader.py index 1430d36c..e03b1446 100755 --- a/module_loader.py +++ b/module_loader.py @@ -4,17 +4,22 @@ from pubsub import pub class ModuleLoader: - def __init__(self, config_folder='config', environment=None): + def __init__(self, config_folder='modules', environment=None): """ ModuleLoader class - :param config_folder: folder containing the module configuration files + :param config_folder: root folder to search for module config.yml files (searched recursively) - Example config file: - config/modules.yml + Each module lives in its own directory containing: + - .py (Python implementation) + - config.yml (module configuration) + - README.md (documentation) + - tests/ (unit tests) + + Example config.yml: --- buzzer: enabled: true # Required - path: "modules.audio.buzzer.Buzzer" # Required + path: "modules.audio.buzzer.buzzer.Buzzer" # Required config: # Passed as **kwargs to the module's __init__ method pin: 27 name: 'buzzer' @@ -32,8 +37,12 @@ def __init__(self, config_folder='config', environment=None): self.modules = self.load_yaml_files() def load_yaml_files(self): - """Load and parse YAML files from the config folder.""" - config_files = [os.path.join(self.config_folder, f) for f in os.listdir(self.config_folder) if f.endswith('.yml')] + """Recursively search config_folder for config.yml files and load module configurations.""" + config_files = [] + for root, dirs, files in os.walk(self.config_folder): + for f in files: + if f == 'config.yml': + config_files.append(os.path.join(root, f)) loaded_modules = [] for file_path in config_files: with open(file_path, 'r') as stream: diff --git a/modules/actuators/__init__.py b/modules/actuators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/actuators/bus_servo/README.md b/modules/actuators/bus_servo/README.md new file mode 100644 index 00000000..d48394d8 --- /dev/null +++ b/modules/actuators/bus_servo/README.md @@ -0,0 +1,72 @@ +# BaseModule Documentation + +## Overview + +Serial bus servos allow for efficient communication and control of multiple servos over a single bus. The `Servo` class provides an interface to manage these servos, including setting positions, speeds, and handling configurations. + +## Configuration + +The `config/servo_bus.yml` file contains the configuration for each bus servo. The `instances` section defines the servos, their IDs, and their initial positions. The `poses` section within each instance defines various poses that can be used to set servo positions. + +## Getting Started / Calibration + +To calibrate the servos, each must have their ID set individually. To achieve this, connect one servo to the driver board and run `modules/actuators/bus_servo/change_id.py`. This script will prompt you to enter the ID for the connected servo, which will then be saved permanently on the servo. + +If you have challenges understanding the current ID of the servo, run `modules/actuators/bus_servo/STServo_examples/read_all.py` or `modules/actuators/bus_servo/SCServo_examples/read_all.py` depending on the servo type. This script will read and display the current ID of the connected servo. + +Note: if there is an issue with dependencies, you may need to run the script from within the `modules/actuators/bus_servo` directory. + +Once the ID has been set for all servos, you can use the `Servo` class to control them by enabling it in the above yaml file. + +### Centering +The servos range is between 0-4095 for ST servos and 0-1024 for SC servos. The position readout can wrap around (from 0 to 4095), so the servos should be set to the midpoint before mounting them in the robot otherwise it can result in the servo passing the wrong way through the range of values, which could damage the robot. + +The `change_id.py` script will set the servo to the midpoint when changing the ID. The servo should then be mounted in the robot at roughly the midpoint position. + +You can also enable `center_on_boot` for each servo in the configuration file, which will move the servo to the center of its range on initialization. + + +### Calibration +To calibrate the servo positions, set the flag `calibrate_on_boot` to `true` in the configuration file for each instance (servo). This will cause the servo to output it's current position in the debug log, which can then be copied into the start position, or range. Servos can be manually moved to any position to identify their range or certain poses. + +Note: `calibrate_on_boot` is enabled by default to help with initial configuration. + +Once the start position and range are set, move the flag to the next servo and repeat the process until all servos are calibrated. + +You can then copy the output values directly from the log into the configuration file. + +Finally, set `calibrate_on_boot` to false and re-run the program to start using the servos with their configured positions. + +### Demonstration + +To demonstrate the servo movement on boot, set the flag `demonstrate_on_boot` to `true` in the configuration file for each instance (servo). This will cause the servo to move to its minimum and maximum positions once on initialization, allowing you to see the range of motion. + +## Subscriptions + +The `Servo` class subscribes to the following topics: +`servo::mv` - to move the servo to a specific position relative to it's current position. +`servo::mvabs` - to move the servo to a specific position with absolute values. + +## Smooth initialization + +Because the `Servo` class gets the current position of the servo on initialization, there is no danger of a servo jumping from an unknown position to the start position. This is especially useful when the servos are powered on in a random position and is an advantage over hobby servos used in previous versions of the modular biped. + +## SC vs ST servos + +The `Servo` class supports both ST and SC series servos from Waveshare and compatible servos. The type of servo is determined by the `model` variable in the configuration file. The class will automatically use the appropriate SDK for the specified servo type. + +There are some limitations to the SC servos as they do not support continuous rotation and have a lower rotational range compared to the ST servos. + +## Notes during testing + +- Speed: between 0 and 3000 for SC servos tested, 0 is max, then 1-3000 increases speed. +- Acceleration: between 0 and 3000 for SC servos tested, 0 is max but no significant difference observed between values. +- Overload error occurring fairly frequently on SC servos. However when testing with scheduled movements the issue is far less frequent. This may be due to sending commands too quickly in succession. Cycling torque off/on seems to resolve the error, otherwise you need to power cycle the servo. +- To disable torque on SC servos use: `self.packetHandler.write1ByteTxRx(self.portHandler, self.index, ADDR_TORQUE_ENABLE, 0)`, change the 0 to 1 to enable torque. +- One of the SC servos definitely seems more prone to overload errors than the others. It may be a faulty servo. This servo also doesn't always respect speed settings. It can be set to 3000 and move quickly on one movement then slow the next without a change in speed being commanded. This issue may be directional, as both the original SC servos show this behaviour in one direction only. +- In some cases the overload error does seem to be caused by load on the servo, in which case toggling the torque does not resolve this. The neck pan is an example of this where the SC servo needed to be replaced with the more powerful ST servo. + +## References +https://www.waveshare.com/wiki/ST3215_Servo +https://www.waveshare.com/wiki/SC09_Servo +https://www.waveshare.com/wiki/Bus_Servo_Adapter_(A) \ No newline at end of file diff --git a/modules/actuators/bus_servo/__init__.py b/modules/actuators/bus_servo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/servo_bus.yml b/modules/actuators/bus_servo/config.yml old mode 100755 new mode 100644 similarity index 100% rename from config/servo_bus.yml rename to modules/actuators/bus_servo/config.yml diff --git a/modules/actuators/bus_servo/tests/__init__.py b/modules/actuators/bus_servo/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/actuators/bus_servo/tests/test_bus_servo.py b/modules/actuators/bus_servo/tests/test_bus_servo.py new file mode 100644 index 00000000..68e2d5e4 --- /dev/null +++ b/modules/actuators/bus_servo/tests/test_bus_servo.py @@ -0,0 +1,28 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +# Mock the SDK modules +mock_st_sdk = MagicMock() +mock_sc_sdk = MagicMock() +sys.modules['modules.actuators.bus_servo.STservo_sdk'] = mock_st_sdk +sys.modules['modules.actuators.bus_servo.SCservo_sdk'] = mock_sc_sdk + +from modules.actuators.bus_servo.servo import Servo + +class TestBusServo(unittest.TestCase): + + def test_init(self): + servo = Servo(name='test', id=1, range=[0, 4095], model='ST3215') + self.assertEqual(servo.identifier, 'test') + self.assertEqual(servo.index, 1) + self.assertEqual(servo.range, [0, 4095]) + self.assertEqual(servo.model, 'ST3215') + + def test_init_defaults(self): + servo = Servo(name='test', id=1, range=[0, 1023]) + self.assertEqual(servo.baudrate, 1000000) + self.assertEqual(servo.port, '/dev/ttyAMA0') + +if __name__ == '__main__': + unittest.main() diff --git a/modules/actuators/piservo/README.md b/modules/actuators/piservo/README.md new file mode 100644 index 00000000..a83b4ffb --- /dev/null +++ b/modules/actuators/piservo/README.md @@ -0,0 +1,28 @@ +# PiServo + +## Overview + +Raspberry Pi GPIO servo controller. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +piservo: + enabled: false # Set to true to enable this module + path: modules.actuators.piservo.piservo.PiServo + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/actuators/piservo/__init__.py b/modules/actuators/piservo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/piservo.yml b/modules/actuators/piservo/config.yml similarity index 79% rename from config/piservo.yml rename to modules/actuators/piservo/config.yml index 84b596d9..044f810a 100644 --- a/config/piservo.yml +++ b/modules/actuators/piservo/config.yml @@ -1,6 +1,6 @@ piservo: enabled: false - path: modules.actuators.piservo.PiServo + path: modules.actuators.piservo.piservo.PiServo instances: - name: "ear" id: 0 diff --git a/modules/actuators/piservo.py b/modules/actuators/piservo/piservo.py similarity index 100% rename from modules/actuators/piservo.py rename to modules/actuators/piservo/piservo.py diff --git a/modules/actuators/piservo/tests/__init__.py b/modules/actuators/piservo/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/actuators/piservo/tests/test_piservo.py b/modules/actuators/piservo/tests/test_piservo.py new file mode 100644 index 00000000..f4baba4b --- /dev/null +++ b/modules/actuators/piservo/tests/test_piservo.py @@ -0,0 +1,25 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_gpiozero = MagicMock() +sys.modules['gpiozero'] = mock_gpiozero + +from modules.actuators.piservo.piservo import PiServo + +class TestPiServo(unittest.TestCase): + + def test_init(self): + servo = PiServo(pin=22, range=[-40, 40], start=0) + self.assertEqual(servo.pin, 22) + self.assertEqual(servo.range, [-40, 40]) + self.assertEqual(servo.start, 0) + + def test_setup_messaging(self): + servo = PiServo(pin=22, range=[-40, 40], start=0) + with patch.object(servo, 'subscribe') as mock_sub: + servo.setup_messaging() + mock_sub.assert_called_with('piservo/move', servo.move) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/actuators/servo/README.md b/modules/actuators/servo/README.md new file mode 100644 index 00000000..26e62a28 --- /dev/null +++ b/modules/actuators/servo/README.md @@ -0,0 +1,28 @@ +# Servo + +## Overview + +Arduino serial servo controller with range and buffer support. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +servos: + enabled: false # Set to true to enable this module + path: modules.actuators.servo.servo.Servo + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/actuators/servo/__init__.py b/modules/actuators/servo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/servos.yml b/modules/actuators/servo/config.yml similarity index 92% rename from config/servos.yml rename to modules/actuators/servo/config.yml index 1990f0b2..364f0e4b 100644 --- a/config/servos.yml +++ b/modules/actuators/servo/config.yml @@ -1,6 +1,6 @@ servos: enabled: false - path: "modules.actuators.servo.Servo" # Include class name here + path: "modules.actuators.servo.servo.Servo" # Include class name here instances: - name: "leg_l_hip" id: 0 diff --git a/modules/actuators/servo.py b/modules/actuators/servo/servo.py similarity index 100% rename from modules/actuators/servo.py rename to modules/actuators/servo/servo.py diff --git a/modules/actuators/servo/tests/__init__.py b/modules/actuators/servo/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/actuators/servo/tests/test_servo.py b/modules/actuators/servo/tests/test_servo.py new file mode 100644 index 00000000..5d7f1520 --- /dev/null +++ b/modules/actuators/servo/tests/test_servo.py @@ -0,0 +1,29 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_pigpio = MagicMock() +sys.modules['pigpio'] = mock_pigpio + +# Mock ArduinoSerial +mock_arduino = MagicMock() +sys.modules['modules.network.arduinoserial'] = MagicMock() +sys.modules['modules.network.arduinoserial.arduinoserial'] = MagicMock() + +from modules.actuators.servo.servo import Servo + +class TestServo(unittest.TestCase): + + def test_init(self): + servo = Servo(pin=1, name='test', range=[0, 180], start=50, serial=True) + self.assertEqual(servo.pin, 1) + self.assertEqual(servo.identifier, 'test') + self.assertEqual(servo.range, [0, 180]) + + def test_translate(self): + servo = Servo(name='test', range=[0, 200], start=50, serial=True) + # 50% of range [0,200] should be 100 + self.assertEqual(servo.translate(50), 100) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/actuators/tests/__init__.py b/modules/actuators/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/animate/README.md b/modules/animate/README.md new file mode 100644 index 00000000..ec7e1618 --- /dev/null +++ b/modules/animate/README.md @@ -0,0 +1,28 @@ +# Animate + +## Overview + +Animation playback module for controlling robot animations. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +animate: + enabled: false # Set to true to enable this module + path: modules.animate.animate.Animate + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/animate/__init__.py b/modules/animate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/animate.py b/modules/animate/animate.py similarity index 100% rename from modules/animate.py rename to modules/animate/animate.py diff --git a/config/animate.yml b/modules/animate/config.yml old mode 100755 new mode 100644 similarity index 52% rename from config/animate.yml rename to modules/animate/config.yml index fc97553e..edec2ad3 --- a/config/animate.yml +++ b/modules/animate/config.yml @@ -1,4 +1,4 @@ animate: enabled: true environment: 'robot' - path: modules.animate.Animate \ No newline at end of file + path: modules.animate.animate.Animate \ No newline at end of file diff --git a/modules/animate/tests/__init__.py b/modules/animate/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/animate/tests/test_animate.py b/modules/animate/tests/test_animate.py new file mode 100644 index 00000000..0574a096 --- /dev/null +++ b/modules/animate/tests/test_animate.py @@ -0,0 +1,30 @@ +import unittest +from unittest.mock import MagicMock, patch +import sys + +mock_gpiozero = MagicMock() +sys.modules['gpiozero'] = mock_gpiozero + +from modules.animate.animate import Animate + +class TestAnimate(unittest.TestCase): + + def test_init_default_path(self): + anim = Animate() + self.assertTrue(anim.path.endswith('/')) + + def test_animate_missing_file(self): + anim = Animate(path='/tmp') + anim.messaging_service = MagicMock() + anim.log = MagicMock() + with self.assertRaises(ValueError): + anim.animate('nonexistent') + + def test_setup_messaging(self): + anim = Animate(path='/tmp') + with patch.object(anim, 'subscribe') as mock_sub: + anim.setup_messaging() + mock_sub.assert_called_with('animate', anim.animate) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/archived/scripts/modules/tests/__init__.py b/modules/archived/scripts/modules/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/__init__.py b/modules/audio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/braillespeak/README.md b/modules/audio/braillespeak/README.md new file mode 100644 index 00000000..2bd2f772 --- /dev/null +++ b/modules/audio/braillespeak/README.md @@ -0,0 +1,38 @@ +# BrailleSpeak + +## Overview + +Braille-based speech output module. + +Communicate with tones, letters converted to tone pairs +Uses Buzzer module to play tones via pubsub + +:param kwargs: pin, duration + +Subscribes to 'speak' event + +Example: +self.publish('speak', msg="Hi") + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +braillespeak: + enabled: false # Set to true to enable this module + path: modules.audio.braillespeak.braillespeak.BrailleSpeak + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/audio/braillespeak/__init__.py b/modules/audio/braillespeak/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/braillespeak.py b/modules/audio/braillespeak/braillespeak.py similarity index 100% rename from modules/audio/braillespeak.py rename to modules/audio/braillespeak/braillespeak.py diff --git a/modules/audio/braillespeak/config.yml b/modules/audio/braillespeak/config.yml new file mode 100644 index 00000000..1e4ed653 --- /dev/null +++ b/modules/audio/braillespeak/config.yml @@ -0,0 +1,5 @@ +braillespeak: + enabled: false + path: modules.audio.braillespeak.braillespeak.BrailleSpeak + config: + pin: 27 \ No newline at end of file diff --git a/modules/audio/braillespeak/tests/__init__.py b/modules/audio/braillespeak/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/braillespeak/tests/test_braillespeak.py b/modules/audio/braillespeak/tests/test_braillespeak.py new file mode 100644 index 00000000..9cba4e42 --- /dev/null +++ b/modules/audio/braillespeak/tests/test_braillespeak.py @@ -0,0 +1,20 @@ +import unittest +from unittest.mock import MagicMock, patch +from modules.audio.braillespeak.braillespeak import BrailleSpeak + +class TestBrailleSpeak(unittest.TestCase): + + def test_init(self): + bs = BrailleSpeak(pin=27, duration=0.05) + self.assertEqual(bs.pin, 27) + self.assertEqual(bs.duration, 0.05) + self.assertEqual(len(bs.notes), 8) + + def test_setup_messaging(self): + bs = BrailleSpeak(pin=27) + with patch.object(bs, 'subscribe') as mock_sub: + bs.setup_messaging() + mock_sub.assert_called_with('speak', bs.send) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/audio/buzzer/README.md b/modules/audio/buzzer/README.md new file mode 100644 index 00000000..fe61614c --- /dev/null +++ b/modules/audio/buzzer/README.md @@ -0,0 +1,28 @@ +# Buzzer + +## Overview + +GPIO buzzer module for playing tones and melodies. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +buzzer: + enabled: false # Set to true to enable this module + path: modules.audio.buzzer.buzzer.Buzzer + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/audio/buzzer/__init__.py b/modules/audio/buzzer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/buzzer.py b/modules/audio/buzzer/buzzer.py similarity index 100% rename from modules/audio/buzzer.py rename to modules/audio/buzzer/buzzer.py diff --git a/config/buzzer.yml b/modules/audio/buzzer/config.yml similarity index 71% rename from config/buzzer.yml rename to modules/audio/buzzer/config.yml index c69a77e7..067212b0 100644 --- a/config/buzzer.yml +++ b/modules/audio/buzzer/config.yml @@ -1,6 +1,6 @@ buzzer: enabled: false - path: "modules.audio.buzzer.Buzzer" + path: "modules.audio.buzzer.buzzer.Buzzer" config: pin: 27 name: 'buzzer' diff --git a/modules/audio/buzzer/tests/__init__.py b/modules/audio/buzzer/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/buzzer/tests/test_buzzer.py b/modules/audio/buzzer/tests/test_buzzer.py new file mode 100644 index 00000000..081ec30a --- /dev/null +++ b/modules/audio/buzzer/tests/test_buzzer.py @@ -0,0 +1,26 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_gpiozero = MagicMock() +sys.modules['gpiozero'] = mock_gpiozero +sys.modules['gpiozero.tones'] = mock_gpiozero.tones + +from modules.audio.buzzer.buzzer import Buzzer + +class TestBuzzer(unittest.TestCase): + + def test_init(self): + buzzer = Buzzer(pin=27) + self.assertEqual(buzzer.pin, 27) + mock_gpiozero.TonalBuzzer.assert_called_with(27) + + def test_setup_messaging(self): + buzzer = Buzzer(pin=27) + with patch.object(buzzer, 'subscribe') as mock_sub: + buzzer.setup_messaging() + mock_sub.assert_any_call('play', buzzer.play_song) + mock_sub.assert_any_call('buzz', buzzer.buzz) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/audio/speechinput/README.md b/modules/audio/speechinput/README.md new file mode 100644 index 00000000..4029c4a8 --- /dev/null +++ b/modules/audio/speechinput/README.md @@ -0,0 +1,30 @@ +# SpeechInput + +## Overview + +Speech recognition input module. + +Use speech_recognition to detect and interpret audio + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +speechinput: + enabled: false # Set to true to enable this module + path: modules.audio.speechinput.speechinput.SpeechInput + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/audio/speechinput/__init__.py b/modules/audio/speechinput/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/speechinput.yml b/modules/audio/speechinput/config.yml similarity index 89% rename from config/speechinput.yml rename to modules/audio/speechinput/config.yml index 6cf5fbb7..12a1d8a8 100644 --- a/config/speechinput.yml +++ b/modules/audio/speechinput/config.yml @@ -1,6 +1,6 @@ speechinput: enabled: false - path: modules.audio.speechinput.SpeechInput + path: modules.audio.speechinput.speechinput.SpeechInput config: device_name: "pulse" start_on_boot: false diff --git a/modules/audio/speechinput.py b/modules/audio/speechinput/speechinput.py similarity index 100% rename from modules/audio/speechinput.py rename to modules/audio/speechinput/speechinput.py diff --git a/modules/audio/speechinput/tests/__init__.py b/modules/audio/speechinput/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/speechinput/tests/test_speechinput.py b/modules/audio/speechinput/tests/test_speechinput.py new file mode 100644 index 00000000..5e6f418c --- /dev/null +++ b/modules/audio/speechinput/tests/test_speechinput.py @@ -0,0 +1,30 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_speech_recognition = MagicMock() +mock_recognizer = MagicMock() +mock_microphone = MagicMock() +mock_speech_recognition.Recognizer.return_value = mock_recognizer +mock_speech_recognition.Microphone.return_value = mock_microphone +sys.modules['speech_recognition'] = mock_speech_recognition + +from modules.audio.speechinput.speechinput import SpeechInput + +class TestSpeechInput(unittest.TestCase): + + def test_init(self): + si = SpeechInput(device_name='test', sample_rate=16000) + self.assertEqual(si.device_name, 'test') + self.assertEqual(si.sample_rate, 16000) + self.assertFalse(si.listening) + + def test_setup_messaging(self): + si = SpeechInput() + si.log = MagicMock() + with patch.object(si, 'subscribe') as mock_sub: + si.setup_messaging() + mock_sub.assert_any_call('speech:listen', si.start) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/audio/ttsmodule/README.md b/modules/audio/ttsmodule/README.md new file mode 100644 index 00000000..33768c7a --- /dev/null +++ b/modules/audio/ttsmodule/README.md @@ -0,0 +1,28 @@ +# TTSModule + +## Overview + +Text-to-speech output module. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +ttsmodule: + enabled: false # Set to true to enable this module + path: modules.audio.ttsmodule.ttsmodule.TTSModule + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/audio/ttsmodule/__init__.py b/modules/audio/ttsmodule/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/ttsmodule.yml b/modules/audio/ttsmodule/config.yml similarity index 84% rename from config/ttsmodule.yml rename to modules/audio/ttsmodule/config.yml index 6b23ac55..7a22caa0 100644 --- a/config/ttsmodule.yml +++ b/modules/audio/ttsmodule/config.yml @@ -1,7 +1,7 @@ ttsmodule: enabled: true environment: robot - path: "modules.audio.ttsmodule.TTSModule" + path: "modules.audio.ttsmodule.ttsmodule.TTSModule" config: service: "elevenlabs" # or pyttsx3 voice_id: "pMsXgVXv3BLzUgSXRplE" diff --git a/modules/audio/ttsmodule/tests/__init__.py b/modules/audio/ttsmodule/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/audio/ttsmodule/tests/test_ttsmodule.py b/modules/audio/ttsmodule/tests/test_ttsmodule.py new file mode 100644 index 00000000..b1e9f47e --- /dev/null +++ b/modules/audio/ttsmodule/tests/test_ttsmodule.py @@ -0,0 +1,25 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_pyttsx3 = MagicMock() +mock_elevenlabs = MagicMock() +sys.modules['pyttsx3'] = mock_pyttsx3 +sys.modules['elevenlabs'] = mock_elevenlabs + +from modules.audio.ttsmodule.ttsmodule import TTSModule + +class TestTTSModule(unittest.TestCase): + + def test_init_pyttsx3(self): + tts = TTSModule(service='pyttsx3') + self.assertEqual(tts.service, 'pyttsx3') + + def test_setup_messaging(self): + tts = TTSModule(service='pyttsx3') + with patch.object(tts, 'subscribe') as mock_sub: + tts.setup_messaging() + mock_sub.assert_called_with('tts', tts.speak) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/audio/ttsmodule.py b/modules/audio/ttsmodule/ttsmodule.py similarity index 100% rename from modules/audio/ttsmodule.py rename to modules/audio/ttsmodule/ttsmodule.py diff --git a/modules/chatgpt/README.md b/modules/chatgpt/README.md new file mode 100644 index 00000000..8dbaadad --- /dev/null +++ b/modules/chatgpt/README.md @@ -0,0 +1,73 @@ +# ChatGPT Module Documentation + +## Overview + +The `ChatGPT` module is designed to interface with OpenAI's GPT models to provide conversational capabilities. It allows the robot to respond to speech inputs with text or predefined animations. This documentation outlines the setup and usage of the `ChatGPT` class. + +## Configuration + +Before using the `ChatGPT` module, you need to configure it in the `config/chatgpt.yml` file. Below is an explanation of the configuration options: + +```yaml +chatgpt: + enabled: false # Set to true to enable the ChatGPT module + path: 'modules.chatgpt.ChatGPT' # Path to the ChatGPT class + config: + model: gpt-4o-mini # Model to use for the chat + persona: "You are a helpful assistant robot. You respond with short phrases where possible. + Alternatively, you can respond with the following commands instead of text if you feel they are appropriate: + - animate:head_nod + - animate:head_shake" + dependencies: + python: + - openai + additional: + - https://platform.openai.com/api-keys +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +You must also obtain an API key from OpenAI to use the GPT models. The API key should be added as an environment variable OPENAI_API_KEY: + +```bash +export OPENAI_API_KEY="your-api-key" +``` + +Read here for config steps : https://platform.openai.com/docs/quickstart + +## Usage + +### Initializing the ChatGPT Module + +When the module is enabled in the config YAML, the `ChatGPT` class is automatically imported and initialized. + +### Chat Completion + +The `completion` method sends the input text to the OpenAI API and processes the response. It can publish responses as text-to-speech (TTS) messages or trigger animations. + +### Example Usage + +You can test the `ChatGPT` module by running the following code: + +```python +self.publish('speech', text='Hello, can you hear me?') +``` + +When the module is enabled in the config YAML, the `ChatGPT` class is automatically imported and initialized. + +You can also reference the `ChatGPT` class directly in the main.py: + +```python +module = module_instances['chatgpt'] +module.completion('Can you hear me?') +``` + +## Customization + +The `ChatGPT` module can be customized to meet specific requirements by modifying the persona, model, and response handling logic. You can update the configuration file to change the persona and model used for the chat. + +## Conclusion + +The `ChatGPT` module provides a powerful interface for adding conversational capabilities to the robot using OpenAI's GPT models. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the `ChatGPT` module in your projects. \ No newline at end of file diff --git a/modules/chatgpt/__init__.py b/modules/chatgpt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/chatgpt.py b/modules/chatgpt/chatgpt.py similarity index 100% rename from modules/chatgpt.py rename to modules/chatgpt/chatgpt.py diff --git a/config/chatgpt.yml b/modules/chatgpt/config.yml similarity index 91% rename from config/chatgpt.yml rename to modules/chatgpt/config.yml index 38f7c98d..f1d43486 100644 --- a/config/chatgpt.yml +++ b/modules/chatgpt/config.yml @@ -1,6 +1,6 @@ chatgpt: enabled: true - path: 'modules.chatgpt.ChatGPT' + path: 'modules.chatgpt.chatgpt.ChatGPT' config: model: gpt-4o-mini persona: "You are a helpful assistant robot. You respond with short phrases where possible. diff --git a/modules/chatgpt/tests/__init__.py b/modules/chatgpt/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/chatgpt/tests/test_chatgpt.py b/modules/chatgpt/tests/test_chatgpt.py new file mode 100644 index 00000000..b94e1b11 --- /dev/null +++ b/modules/chatgpt/tests/test_chatgpt.py @@ -0,0 +1,32 @@ +import sys +import os +import unittest +from unittest.mock import MagicMock, patch + +mock_openai = MagicMock() +sys.modules['openai'] = mock_openai + +from modules.chatgpt.chatgpt import ChatGPT + +class TestChatGPT(unittest.TestCase): + + def test_init_no_api_key(self): + env = {k: v for k, v in os.environ.items() if k != 'OPENAI_API_KEY'} + with patch.dict('os.environ', env, clear=True): + with self.assertRaises(RuntimeError): + ChatGPT() + + def test_init_with_api_key(self): + with patch.dict('os.environ', {'OPENAI_API_KEY': 'test_key'}): + bot = ChatGPT() + self.assertEqual(bot.model, 'gpt-4o-mini') + + def test_setup_messaging(self): + with patch.dict('os.environ', {'OPENAI_API_KEY': 'test_key'}): + bot = ChatGPT() + with patch.object(bot, 'subscribe') as mock_sub: + bot.setup_messaging() + mock_sub.assert_any_call('speech', bot.completion) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/controller_handler/README.md b/modules/controller_handler/README.md new file mode 100644 index 00000000..62a51c04 --- /dev/null +++ b/modules/controller_handler/README.md @@ -0,0 +1,32 @@ +# ControllerHandler + +## Overview + +Maps controller inputs to robot actions based on configurable mappings. + +Consume controller input and map to messaging events. + +Requires injection of a controller module that provides input data. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +controller: + enabled: false # Set to true to enable this module + path: modules.controller_handler.controller_handler.ControllerHandler + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/controller_handler/__init__.py b/modules/controller_handler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/controller_handler.yml b/modules/controller_handler/config.yml similarity index 98% rename from config/controller_handler.yml rename to modules/controller_handler/config.yml index 8d7adda2..15edb0f6 100644 --- a/config/controller_handler.yml +++ b/modules/controller_handler/config.yml @@ -1,6 +1,6 @@ controller: enabled: true - path: 'modules.controller_handler.ControllerHandler' + path: 'modules.controller_handler.controller_handler.ControllerHandler' config: debug: false modifier_buttons: diff --git a/modules/controller_handler.py b/modules/controller_handler/controller_handler.py similarity index 100% rename from modules/controller_handler.py rename to modules/controller_handler/controller_handler.py diff --git a/modules/controller_handler/tests/__init__.py b/modules/controller_handler/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/controller_handler/tests/test_controller_handler.py b/modules/controller_handler/tests/test_controller_handler.py new file mode 100644 index 00000000..83a6f297 --- /dev/null +++ b/modules/controller_handler/tests/test_controller_handler.py @@ -0,0 +1,31 @@ +import unittest +from unittest.mock import MagicMock, patch +from modules.controller_handler.controller_handler import ControllerHandler + +class TestControllerHandler(unittest.TestCase): + + def setUp(self): + self.handler = ControllerHandler( + mapping={'default': {}}, + modifier_buttons=[], + debug=False + ) + self.handler.messaging_service = MagicMock() + + def test_init(self): + self.assertIsNone(self.handler.controller) + self.assertFalse(self.handler.running) + + def test_start(self): + self.handler.log = MagicMock() + self.handler.start() + self.assertTrue(self.handler.running) + self.handler.messaging_service.subscribe.assert_called() + + def test_process_input_no_controller(self): + self.handler.log = MagicMock() + self.handler._process_input() + self.handler.log.assert_called() + +if __name__ == '__main__': + unittest.main() diff --git a/modules/display/__init__.py b/modules/display/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/display/tft_display_eye/README.md b/modules/display/tft_display_eye/README.md new file mode 100644 index 00000000..ab6c7ee7 --- /dev/null +++ b/modules/display/tft_display_eye/README.md @@ -0,0 +1,110 @@ +# TFTDisplay Module Documentation + +## Overview + +The `TFTDisplay` module is designed to interface with a TFT display using the SPI protocol. It provides functionalities to initialize the display, perform tests, and draw graphics on the screen. This documentation outlines the setup and usage of the `TFTDisplay` class. + +## Configuration + +Before using the `TFTDisplay` module, you need to configure it in the `config/tft_display.yml` file. Below is an explanation of the configuration options: + +```yaml +tft_display: + enabled: false # Set to true to enable the TFTDisplay module + path: modules.display.tft_display.TFTDisplay # Path to the TFTDisplay class + config: + bus: 0 # SPI bus number (0 for /dev/spidev0.0) + device: 0 # SPI device number (0 for /dev/spidev0.0) + rst_pin: 27 # GPIO pin for reset + dc_pin: 25 # GPIO pin for data/command + bl_pin: 18 # GPIO pin for backlight (not used if no backlight) + test_on_boot: true # Set to true to run a test on boot + dependencies: + unix: + - python3-pip + - python3-pil + - python3-numpy + python: + - spidev +``` + +### Dependencies + +After enabling the module, run ./install.sh to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +This module utilises the waveshare library. More information available here: https://www.waveshare.com/wiki/2.4inch_LCD_Module#Python + +## Usage + +### Display size + +There are multiple libraries available for working with TFT displays. The `TFTDisplay` module uses the `ST7735` library, which supports a resolution of 240x240 pixels. + +The import and initialization of the display must be adjusted for other devices. + +```python +from modules.display.lib import LCD_1inch28 +self.disp = LCD_1inch28.LCD_1inch28(spi=spi, spi_freq=10000000, rst=kwargs.get('rst_pin'), dc=kwargs.get('dc_pin') , bl=kwargs.get('bl_pin')) +``` + +This is not currently supported via the config YAML, but could be added in the future. + +### Initializing the TFTDisplay + +When the module is enabled in the config YAML, the `TFTDisplay` class is automatically imported and initialized. + +You can also reference the `TFTDisplay` class directly in the main.py: + +```python +module = module_instances['tft_display'] # Get the TFTDisplay module instance +module.test_display() # Run a test on the display +``` + +### Testing the Display + +If `test_on_boot` is set to `true` in the configuration, the display will automatically run a test upon initialization. You can also manually call the `test_display` method: + +```python +tft_display.test_display() +``` + +### Drawing on the Display + +The `TFTDisplay` class provides methods to draw shapes and images on the display. Here’s an example of how to draw an arc and lines: + +```python +from PIL import Image, ImageDraw + +# Create a blank image +image = Image.new("RGB", (tft_display.disp.width, tft_display.disp.height), "BLACK") +draw = ImageDraw.Draw(image) + +# Draw an arc +draw.arc((1, 1, 239, 239), 0, 360, fill=(0, 0, 255)) + +# Draw lines +draw.line([(120, 1), (120, 12)], fill=(128, 255, 128), width=4) + +# Display the image +tft_display.disp.ShowImage(image) +``` + +### Clearing the Display + +To clear the display, you can call the `clear` method: + +```python +tft_display.disp.clear() +``` + +### Setting the Backlight + +You can adjust the backlight brightness using the `bl_DutyCycle` method: + +```python +tft_display.disp.bl_DutyCycle(50) # Set backlight to 50% +``` + +## Conclusion + +The `TFTDisplay` module provides a straightforward interface for working with TFT displays in Python. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the TFT display in your projects. \ No newline at end of file diff --git a/modules/display/tft_display_eye/__init__.py b/modules/display/tft_display_eye/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/tft_display.yml b/modules/display/tft_display_eye/config.yml similarity index 90% rename from config/tft_display.yml rename to modules/display/tft_display_eye/config.yml index 26b159e0..6a686a74 100644 --- a/config/tft_display.yml +++ b/modules/display/tft_display_eye/config.yml @@ -1,7 +1,7 @@ tft_display: enabled: true environment: robot - path: modules.display.tft_display_eye.TFTDisplayEye + path: modules.display.tft_display_eye.tft_display_eye.TFTDisplayEye config: bus: 0 # Use `ls /dev/spi*` to identify. (/dev/spidev0.0) is bus 0, device 0 device: 0 # Use `ls /dev/spi*` to identify. (/dev/spidev0.0) is bus 0, device 0 diff --git a/modules/display/tft_display_eye/tests/__init__.py b/modules/display/tft_display_eye/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/display/tft_display_eye/tests/test_tft_display_eye.py b/modules/display/tft_display_eye/tests/test_tft_display_eye.py new file mode 100644 index 00000000..e2559f4d --- /dev/null +++ b/modules/display/tft_display_eye/tests/test_tft_display_eye.py @@ -0,0 +1,33 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_spi = MagicMock() +mock_pil = MagicMock() +mock_lcd = MagicMock() +mock_lcd_module = MagicMock() +sys.modules['spidev'] = mock_spi +sys.modules['PIL'] = mock_pil +sys.modules['PIL.Image'] = mock_pil.Image +sys.modules['PIL.ImageDraw'] = mock_pil.ImageDraw +sys.modules['modules.display.lib'] = mock_lcd +sys.modules['modules.display.lib.LCD_1inch28'] = mock_lcd_module + +# Mock the TFTDisplay parent +mock_tft_display_mod = MagicMock() +sys.modules['modules.display.tft_display'] = mock_tft_display_mod + +# Make TFTDisplay a usable base class +class MockTFTDisplay(MagicMock): + pass +mock_tft_display_mod.TFTDisplay = MockTFTDisplay + +from modules.display.tft_display_eye.tft_display_eye import TFTDisplayEye + +class TestTFTDisplayEye(unittest.TestCase): + + def test_class_exists(self): + self.assertTrue(hasattr(TFTDisplayEye, '__init__')) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/display/tft_display_eye.py b/modules/display/tft_display_eye/tft_display_eye.py similarity index 100% rename from modules/display/tft_display_eye.py rename to modules/display/tft_display_eye/tft_display_eye.py diff --git a/modules/display/waveshare_oled/README.md b/modules/display/waveshare_oled/README.md new file mode 100644 index 00000000..7e292912 --- /dev/null +++ b/modules/display/waveshare_oled/README.md @@ -0,0 +1,28 @@ +# WaveshareOLED + +## Overview + +Waveshare OLED display driver module. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +oled_display: + enabled: false # Set to true to enable this module + path: modules.display.waveshare_oled.waveshare_oled.WaveshareOLED + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/display/waveshare_oled/__init__.py b/modules/display/waveshare_oled/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/oled_display.yml b/modules/display/waveshare_oled/config.yml similarity index 85% rename from config/oled_display.yml rename to modules/display/waveshare_oled/config.yml index 69896cb2..3fb08bc8 100644 --- a/config/oled_display.yml +++ b/modules/display/waveshare_oled/config.yml @@ -1,7 +1,7 @@ oled_display: enabled: true environment: robot - path: modules.display.waveshare_oled.WaveshareOLED + path: modules.display.waveshare_oled.waveshare_oled.WaveshareOLED config: test_on_boot: false time_on_boot: false diff --git a/modules/display/waveshare_oled/tests/__init__.py b/modules/display/waveshare_oled/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/display/waveshare_oled/tests/test_waveshare_oled.py b/modules/display/waveshare_oled/tests/test_waveshare_oled.py new file mode 100644 index 00000000..d3779188 --- /dev/null +++ b/modules/display/waveshare_oled/tests/test_waveshare_oled.py @@ -0,0 +1,32 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_pil = MagicMock() +sys.modules['PIL'] = mock_pil +sys.modules['PIL.Image'] = mock_pil.Image +sys.modules['PIL.ImageDraw'] = mock_pil.ImageDraw +sys.modules['PIL.ImageFont'] = mock_pil.ImageFont + +mock_waveshare = MagicMock() +sys.modules['modules.display.lib.waveshare'] = mock_waveshare +sys.modules['modules.display.lib.waveshare.OLED_0in91'] = mock_waveshare.OLED_0in91 +mock_waveshare.OLED_0in91.OLED_0in91.return_value.width = 128 +mock_waveshare.OLED_0in91.OLED_0in91.return_value.height = 32 + +from modules.display.waveshare_oled.waveshare_oled import WaveshareOLED + +class TestWaveshareOLED(unittest.TestCase): + + def test_init(self): + oled = WaveshareOLED(test_on_boot=False) + # init should not raise + + def test_setup_messaging(self): + oled = WaveshareOLED() + with patch.object(oled, 'subscribe') as mock_sub: + oled.setup_messaging() + mock_sub.assert_any_call('system/exit', oled.exit) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/display/waveshare_oled.py b/modules/display/waveshare_oled/waveshare_oled.py similarity index 100% rename from modules/display/waveshare_oled.py rename to modules/display/waveshare_oled/waveshare_oled.py diff --git a/modules/gpio/__init__.py b/modules/gpio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/gpio/laser/README.md b/modules/gpio/laser/README.md new file mode 100644 index 00000000..5fe8d6c6 --- /dev/null +++ b/modules/gpio/laser/README.md @@ -0,0 +1,28 @@ +# Laser + +## Overview + +GPIO laser control module. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +gpio_laser: + enabled: false # Set to true to enable this module + path: modules.gpio.laser.laser.Laser + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/gpio/laser/__init__.py b/modules/gpio/laser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/gpio_laser.yml b/modules/gpio/laser/config.yml similarity index 76% rename from config/gpio_laser.yml rename to modules/gpio/laser/config.yml index 5a43a07b..4d9f1013 100644 --- a/config/gpio_laser.yml +++ b/modules/gpio/laser/config.yml @@ -1,7 +1,7 @@ motion: enabled: true environment: robot - path: 'modules.gpio.laser.Laser' + path: 'modules.gpio.laser.laser.Laser' config: pin: 6 test_on_boot: false diff --git a/modules/gpio/laser.py b/modules/gpio/laser/laser.py similarity index 100% rename from modules/gpio/laser.py rename to modules/gpio/laser/laser.py diff --git a/modules/gpio/laser/tests/__init__.py b/modules/gpio/laser/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/gpio/laser/tests/test_laser.py b/modules/gpio/laser/tests/test_laser.py new file mode 100644 index 00000000..af78f189 --- /dev/null +++ b/modules/gpio/laser/tests/test_laser.py @@ -0,0 +1,30 @@ +import unittest +from unittest import mock + +mock_gpiozero = mock.MagicMock() +with mock.patch.dict('sys.modules', {'gpiozero': mock_gpiozero}): + from modules.gpio.laser.laser import Laser + +class TestLaser(unittest.TestCase): + + def test_init(self): + laser = Laser(pin=17) + self.assertEqual(laser.pin, 17) + mock_gpiozero.LED.assert_called_with(17) + + def test_activate(self): + laser = Laser(pin=17) + laser.activate(True) + self.assertTrue(laser.state) + laser.activate(False) + self.assertFalse(laser.state) + + def test_toggle(self): + laser = Laser(pin=17, state=False) + laser.toggle() + self.assertTrue(laser.state) + laser.toggle() + self.assertFalse(laser.state) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/gpio/motion/README.md b/modules/gpio/motion/README.md new file mode 100644 index 00000000..9f56720b --- /dev/null +++ b/modules/gpio/motion/README.md @@ -0,0 +1,75 @@ +# Sensor Module Documentation + +## Overview + +The `Sensor` module is designed to interface with a motion sensor using the `gpiozero` library. It provides functionalities to initialize the sensor, read sensor data, and publish motion detection events. This documentation outlines the setup and usage of the `Sensor` class. + +## Configuration + +Before using the `Sensor` module, you need to configure it in the `config/sensor.yml` file. Below is an explanation of the configuration options: + +```yaml +sensor: + enabled: false # Set to true to enable the Sensor module + path: modules.sensor.Sensor # Path to the Sensor class + config: + pin: 4 # GPIO pin number connected to the sensor + test_on_boot: false # Set to true to run a test on boot + dependencies: + unix: + - python3-gpiozero + python: + - gpiozero +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +## Usage + +### Initializing the Sensor + +When the module is enabled in the config YAML, the `Sensor` class is automatically imported and initialized. + +You can also reference the `Sensor` class directly in the `main.py`: + +```python +module = module_instances['sensor'] # Get the Sensor module instance +module.read() # Read data from the sensor +``` + +### Reading Data + +The `Sensor` class provides methods to read data from the motion sensor. Here’s an example of how to read data: + +```python +def read(self): + self.value = self.sensor.motion_detected + return self.value +``` + +### Publishing Motion Events + +The `Sensor` class can publish motion detection events to the application via a pub/sub mechanism. Here’s an example of how to publish a motion event: + +```python +def loop(self): + if self.read(): + self.publish('motion') +``` + +### Testing the Sensor + +The `Sensor` class includes a test method to continuously read data from the sensor and print the results. Here’s an example of how to test the sensor: + +```python +def test(self): + while True: + print(self.read()) + sleep(1) +``` + +## Conclusion + +The `Sensor` module provides a straightforward interface for working with motion sensors in Python. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the motion sensor in your projects. \ No newline at end of file diff --git a/modules/gpio/motion/__init__.py b/modules/gpio/motion/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/gpio_motion.yml b/modules/gpio/motion/config.yml similarity index 75% rename from config/gpio_motion.yml rename to modules/gpio/motion/config.yml index a30d8a54..84283dec 100644 --- a/config/gpio_motion.yml +++ b/modules/gpio/motion/config.yml @@ -1,7 +1,7 @@ gpio_motion: enabled: true environment: robot - path: 'modules.gpio.motion.Motion' + path: 'modules.gpio.motion.motion.Motion' config: pin: 9 test_on_boot: false diff --git a/modules/gpio/motion.py b/modules/gpio/motion/motion.py similarity index 100% rename from modules/gpio/motion.py rename to modules/gpio/motion/motion.py diff --git a/modules/gpio/motion/tests/__init__.py b/modules/gpio/motion/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/gpio/motion/tests/test_motion.py b/modules/gpio/motion/tests/test_motion.py new file mode 100644 index 00000000..5d7c0632 --- /dev/null +++ b/modules/gpio/motion/tests/test_motion.py @@ -0,0 +1,24 @@ +import unittest +from unittest import mock + +mock_gpiozero = mock.MagicMock() +mock_gpiozero.MotionSensor = mock.MagicMock() +with mock.patch.dict('sys.modules', {'gpiozero': mock_gpiozero}): + from modules.gpio.motion.motion import Motion + +class TestMotion(unittest.TestCase): + + def test_init(self): + sensor = Motion(pin=17) + self.assertEqual(sensor.pin, 17) + mock_gpiozero.MotionSensor.assert_called_with(17) + + def test_read(self): + sensor = Motion(pin=17) + mock_gpiozero.MotionSensor.return_value.motion_detected = True + self.assertTrue(sensor.read()) + mock_gpiozero.MotionSensor.return_value.motion_detected = False + self.assertFalse(sensor.read()) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/gpio/read/README.md b/modules/gpio/read/README.md new file mode 100644 index 00000000..c5d98c95 --- /dev/null +++ b/modules/gpio/read/README.md @@ -0,0 +1,28 @@ +# Read + +## Overview + +General purpose GPIO input reader module. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +gpio_read: + enabled: false # Set to true to enable this module + path: modules.gpio.read.read.Read + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/gpio/read/__init__.py b/modules/gpio/read/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/gpio_read.yml b/modules/gpio/read/config.yml similarity index 75% rename from config/gpio_read.yml rename to modules/gpio/read/config.yml index 8c119ae5..b7c9a726 100644 --- a/config/gpio_read.yml +++ b/modules/gpio/read/config.yml @@ -1,6 +1,6 @@ motion: enabled: false - path: 'modules.gpio.read.Read' + path: 'modules.gpio.read.read.Read' config: pin: 5 test_on_boot: false diff --git a/modules/gpio/read.py b/modules/gpio/read/read.py similarity index 100% rename from modules/gpio/read.py rename to modules/gpio/read/read.py diff --git a/modules/gpio/read/tests/__init__.py b/modules/gpio/read/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/gpio/read/tests/test_read.py b/modules/gpio/read/tests/test_read.py new file mode 100644 index 00000000..47c1c42b --- /dev/null +++ b/modules/gpio/read/tests/test_read.py @@ -0,0 +1,22 @@ +import unittest +from unittest import mock + +mock_gpiozero = mock.MagicMock() +with mock.patch.dict('sys.modules', {'gpiozero': mock_gpiozero}): + from modules.gpio.read.read import Read + +class TestRead(unittest.TestCase): + + def test_init(self): + reader = Read(pin=17) + self.assertEqual(reader.pin, 17) + mock_gpiozero.InputDevice.assert_called_with(17) + + def test_read(self): + reader = Read(pin=17) + mock_gpiozero.InputDevice.return_value.value = 1 + result = reader.read() + self.assertEqual(reader.value, 1) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/i2c_servo/README.md b/modules/i2c_servo/README.md new file mode 100644 index 00000000..da3b4f60 --- /dev/null +++ b/modules/i2c_servo/README.md @@ -0,0 +1,60 @@ +# I2CServo Module Documentation + +## Overview + +The `I2CServo` module is designed to interface with servo motors using the I2C protocol. It provides functionalities to initialize the servos, perform tests, and move the servos to specified angles. This documentation outlines the setup and usage of the `I2CServo` class. + +## Configuration + +Before using the `I2CServo` module, you need to configure it in the `config/i2c_servo.yml` file. Below is an explanation of the configuration options: + +```yaml +i2c_servo: + enabled: false # Set to true to enable the I2CServo module + path: modules.i2c_servo.I2CServo # Path to the I2CServo class + config: + test_on_boot: false # Set to true to run a test on boot + servo_count: 16 # Number of servos connected + dependencies: + unix: + - python3-smbus + python: + - adafruit-circuitpython-servokit +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +## Usage + +### Initializing the I2CServo + +When the module is enabled in the config YAML, the `I2CServo` class is automatically imported and initialized. + +You can also reference the `I2CServo` class directly in the `main.py`: + +```python +module = module_instances['i2c_servo'] # Get the I2CServo module instance +module.test() # Run a test on the servos +``` + +### Testing the Servos + +If `test_on_boot` is set to `true` in the configuration, the servos will automatically run a test upon initialization. You can also manually call the `test` method: + +```python +i2c_servo.test() +``` + +### Moving the Servos + +The `I2CServo` class provides a method to move the servos to specified angles. Here’s an example of how to move a servo: + +```python +i2c_servo.moveServo(index=0, angle=90) # Move servo at index 0 to 90 degrees +``` + +## Conclusion + +The `I2CServo` module provides a straightforward interface for working with servo motors using the I2C protocol in Python. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the servo motors in your projects. \ No newline at end of file diff --git a/modules/i2c_servo/__init__.py b/modules/i2c_servo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/i2c_servo.yml b/modules/i2c_servo/config.yml similarity index 80% rename from config/i2c_servo.yml rename to modules/i2c_servo/config.yml index fab21bad..34b05e7b 100644 --- a/config/i2c_servo.yml +++ b/modules/i2c_servo/config.yml @@ -1,6 +1,6 @@ i2c_servo: enabled: false - path: modules.i2c_servo.I2CServo + path: modules.i2c_servo.i2c_servo.I2CServo config: test_on_boot: false servo_count: 16 diff --git a/modules/i2c_servo.py b/modules/i2c_servo/i2c_servo.py similarity index 100% rename from modules/i2c_servo.py rename to modules/i2c_servo/i2c_servo.py diff --git a/modules/i2c_servo/tests/__init__.py b/modules/i2c_servo/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/i2c_servo/tests/test_i2c_servo.py b/modules/i2c_servo/tests/test_i2c_servo.py new file mode 100644 index 00000000..d00b24ca --- /dev/null +++ b/modules/i2c_servo/tests/test_i2c_servo.py @@ -0,0 +1,18 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_adafruit = MagicMock() +sys.modules['adafruit_servokit'] = mock_adafruit + +from modules.i2c_servo.i2c_servo import I2CServo + +class TestI2CServo(unittest.TestCase): + + def test_init(self): + servo = I2CServo(servo_count=3) + self.assertEqual(servo.count, 3) + mock_adafruit.ServoKit.assert_called_with(channels=16) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/input_recorder/README.md b/modules/input_recorder/README.md new file mode 100644 index 00000000..1b6fbae4 --- /dev/null +++ b/modules/input_recorder/README.md @@ -0,0 +1,28 @@ +# InputRecorder + +## Overview + +Records and replays controller input events for animation creation. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +input_recorder: + enabled: false # Set to true to enable this module + path: modules.input_recorder.input_recorder.InputRecorder + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/input_recorder/__init__.py b/modules/input_recorder/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/input_recorder.yml b/modules/input_recorder/config.yml old mode 100755 new mode 100644 similarity index 57% rename from config/input_recorder.yml rename to modules/input_recorder/config.yml index 7950e732..c26f352e --- a/config/input_recorder.yml +++ b/modules/input_recorder/config.yml @@ -1,5 +1,5 @@ input_recorder: enabled: true - path: modules.input_recorder.InputRecorder + path: modules.input_recorder.input_recorder.InputRecorder config: animations_dir: animations/recorded diff --git a/modules/input_recorder.py b/modules/input_recorder/input_recorder.py similarity index 100% rename from modules/input_recorder.py rename to modules/input_recorder/input_recorder.py diff --git a/modules/input_recorder/tests/__init__.py b/modules/input_recorder/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/input_recorder/tests/test_input_recorder.py b/modules/input_recorder/tests/test_input_recorder.py new file mode 100644 index 00000000..682ec8e8 --- /dev/null +++ b/modules/input_recorder/tests/test_input_recorder.py @@ -0,0 +1,33 @@ +import unittest +from unittest.mock import MagicMock +from modules.input_recorder.input_recorder import InputRecorder + +class TestInputRecorder(unittest.TestCase): + + def setUp(self): + self.recorder = InputRecorder(animations_dir='/tmp') + self.recorder.log = lambda *a, **k: None + self.recorder.save_events = MagicMock() + + def test_toggle_recording(self): + self.recorder.toggle_recording(enable=True, filename='test.json') + self.assertTrue(self.recorder.recording) + self.recorder.toggle_recording(enable=False) + self.assertFalse(self.recorder.recording) + self.recorder.save_events.assert_called() + + def test_handle_event(self): + self.recorder.toggle_recording(enable=True, filename='test.json') + event = { + 'time_ms': 123456, + 'value': 1, + 'type': 1, + 'number': 7, + 'topics_args': [{'topic': 'tts', 'args': {'msg': 'Test'}}] + } + self.recorder.handle_event(event) + self.assertEqual(len(self.recorder.events), 1) + self.assertIn('timestamp', self.recorder.events[0]) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/logwrapper/README.md b/modules/logwrapper/README.md new file mode 100644 index 00000000..0ccb1114 --- /dev/null +++ b/modules/logwrapper/README.md @@ -0,0 +1,28 @@ +# LogWrapper + +## Overview + +Logging module that wraps Python logging with pub/sub integration. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +logwrapper: + enabled: false # Set to true to enable this module + path: modules.logwrapper.logwrapper.LogWrapper + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/logwrapper/__init__.py b/modules/logwrapper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/logwrapper.yml b/modules/logwrapper/config.yml similarity index 82% rename from config/logwrapper.yml rename to modules/logwrapper/config.yml index 2a766da2..92033ba8 100644 --- a/config/logwrapper.yml +++ b/modules/logwrapper/config.yml @@ -1,6 +1,6 @@ logwrapper: enabled: true # Highly recommended to enable this module - path: modules.logwrapper.LogWrapper + path: modules.logwrapper.logwrapper.LogWrapper config: filename: app.log log_level: 'debug' # debug, info, warning, error, critical diff --git a/modules/logwrapper.py b/modules/logwrapper/logwrapper.py similarity index 100% rename from modules/logwrapper.py rename to modules/logwrapper/logwrapper.py diff --git a/modules/logwrapper/tests/__init__.py b/modules/logwrapper/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/logwrapper/tests/test_logwrapper.py b/modules/logwrapper/tests/test_logwrapper.py new file mode 100644 index 00000000..fe2494e0 --- /dev/null +++ b/modules/logwrapper/tests/test_logwrapper.py @@ -0,0 +1,28 @@ +import unittest +from unittest import mock +import logging +from modules.logwrapper.logwrapper import LogWrapper + +class TestLogWrapper(unittest.TestCase): + + @mock.patch('modules.logwrapper.logwrapper.logging') + def test_init(self, mock_logging): + log_wrapper = LogWrapper(path='/tmp', filename='test.log', log_level='info', cli_level='debug') + self.assertEqual(log_wrapper.path, '/tmp') + self.assertEqual(log_wrapper.filename, 'test.log') + + @mock.patch('modules.logwrapper.logwrapper.logging') + def test_log(self, mock_logging): + log_wrapper = LogWrapper(path='/tmp', filename='test.log', log_level='info', cli_level='debug') + log_wrapper.log('Test message', type='info') + mock_logging.log.assert_called_with(logging.INFO, 'Test message') + + @mock.patch('modules.logwrapper.logwrapper.logging') + def test_setup_messaging(self, mock_logging): + log_wrapper = LogWrapper(path='/tmp', filename='test.log', log_level='info', cli_level='debug') + with mock.patch.object(log_wrapper, 'subscribe') as mock_subscribe: + log_wrapper.setup_messaging() + mock_subscribe.assert_any_call('log', log_wrapper.log) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/neopixel/__init__.py b/modules/neopixel/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/neopixel/emotion_analysis/README.md b/modules/neopixel/emotion_analysis/README.md new file mode 100644 index 00000000..63bbb185 --- /dev/null +++ b/modules/neopixel/emotion_analysis/README.md @@ -0,0 +1,28 @@ +# EmotionAnalysis + +## Overview + +Emotion analysis module that maps text sentiment to NeoPixel LED colors. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +neopixel_emotions: + enabled: false # Set to true to enable this module + path: modules.neopixel.emotion_analysis.emotion_analysis.EmotionAnalysis + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/neopixel/emotion_analysis/__init__.py b/modules/neopixel/emotion_analysis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/neopixel_emotions.yml b/modules/neopixel/emotion_analysis/config.yml similarity index 99% rename from config/neopixel_emotions.yml rename to modules/neopixel/emotion_analysis/config.yml index 12a63aee..43d40045 100644 --- a/config/neopixel_emotions.yml +++ b/modules/neopixel/emotion_analysis/config.yml @@ -1,6 +1,6 @@ neopixel_emotions: enabled: false - path: modules.neopixel.emotion_analysis.EmotionAnalysis + path: modules.neopixel.emotion_analysis.emotion_analysis.EmotionAnalysis dependencies: python: - tensorflow diff --git a/modules/neopixel/emotion_analysis.py b/modules/neopixel/emotion_analysis/emotion_analysis.py similarity index 100% rename from modules/neopixel/emotion_analysis.py rename to modules/neopixel/emotion_analysis/emotion_analysis.py diff --git a/modules/neopixel/emotion_analysis/tests/__init__.py b/modules/neopixel/emotion_analysis/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/neopixel/emotion_analysis/tests/test_emotion_analysis.py b/modules/neopixel/emotion_analysis/tests/test_emotion_analysis.py new file mode 100644 index 00000000..6731e6e7 --- /dev/null +++ b/modules/neopixel/emotion_analysis/tests/test_emotion_analysis.py @@ -0,0 +1,24 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_transformers = MagicMock() +sys.modules['transformers'] = mock_transformers + +from modules.neopixel.emotion_analysis.emotion_analysis import EmotionAnalysis + +class TestEmotionAnalysis(unittest.TestCase): + + def test_init(self): + ea = EmotionAnalysis(colors={'happy': 'green', 'sad': 'blue'}) + self.assertIsNotNone(ea.emotion_analyzer) + self.assertIsNotNone(ea.emotion_to_keyword) + + def test_setup_messaging(self): + ea = EmotionAnalysis() + with patch.object(ea, 'subscribe') as mock_sub: + ea.setup_messaging() + mock_sub.assert_called_with('speech', ea.analyze_text) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/neopixel/neopx/README.md b/modules/neopixel/neopx/README.md new file mode 100644 index 00000000..e69de29b diff --git a/modules/neopixel/neopx/__init__.py b/modules/neopixel/neopx/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/neopixel.yml b/modules/neopixel/neopx/config.yml similarity index 95% rename from config/neopixel.yml rename to modules/neopixel/neopx/config.yml index a12bea0f..84dcbc3c 100644 --- a/config/neopixel.yml +++ b/modules/neopixel/neopx/config.yml @@ -1,6 +1,6 @@ neopixel: enabled: false - path: 'modules.neopixel.neopx.NeoPx' + path: 'modules.neopixel.neopx.neopx.NeoPx' config: pin: 12 # Only used for GPIO. GPIO2 and 3 are use for i2c, GPIO10 is used for SPI, GPIO12 is used for GPIO protocol: 'I2C' # choose between GPIO, I2C and SPI diff --git a/modules/neopixel/neopx.py b/modules/neopixel/neopx/neopx.py similarity index 100% rename from modules/neopixel/neopx.py rename to modules/neopixel/neopx/neopx.py diff --git a/modules/neopixel/neopx/tests/__init__.py b/modules/neopixel/neopx/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/neopixel/neopx/tests/test_neopx.py b/modules/neopixel/neopx/tests/test_neopx.py new file mode 100644 index 00000000..39080da2 --- /dev/null +++ b/modules/neopixel/neopx/tests/test_neopx.py @@ -0,0 +1,32 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_board = MagicMock() +mock_colour = MagicMock() +mock_seesaw = MagicMock() +sys.modules['board'] = mock_board +sys.modules['colour'] = mock_colour +sys.modules['adafruit_seesaw'] = mock_seesaw + +# Mock Color class to return empty list for range_to +mock_colour.Color.return_value.range_to.return_value = [] + +from modules.neopixel.neopx.neopx import NeoPx + +class TestNeoPx(unittest.TestCase): + + def test_color_map_exists(self): + self.assertIn('red', NeoPx.COLOR_MAP) + self.assertIn('green', NeoPx.COLOR_MAP) + self.assertIn('blue', NeoPx.COLOR_MAP) + self.assertIn('off', NeoPx.COLOR_MAP) + + def test_setup_messaging(self): + px = NeoPx.__new__(NeoPx) + with patch.object(px, 'subscribe') as mock_sub: + px.setup_messaging() + mock_sub.assert_any_call('led', px.set) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/network/__init__.py b/modules/network/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/arduinoserial/README.md b/modules/network/arduinoserial/README.md new file mode 100644 index 00000000..7215baff --- /dev/null +++ b/modules/network/arduinoserial/README.md @@ -0,0 +1,30 @@ +# ArduinoSerial + +## Overview + +Arduino serial communication module using robust-serial protocol. + +Communicate with Arduino over Serial + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +serial: + enabled: false # Set to true to enable this module + path: modules.network.arduinoserial.arduinoserial.ArduinoSerial + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/network/arduinoserial/__init__.py b/modules/network/arduinoserial/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/arduinoserial.py b/modules/network/arduinoserial/arduinoserial.py similarity index 100% rename from modules/network/arduinoserial.py rename to modules/network/arduinoserial/arduinoserial.py diff --git a/config/serial.yml b/modules/network/arduinoserial/config.yml similarity index 54% rename from config/serial.yml rename to modules/network/arduinoserial/config.yml index 376c9e0b..c1543a5e 100644 --- a/config/serial.yml +++ b/modules/network/arduinoserial/config.yml @@ -1,6 +1,6 @@ serial: enabled: false - path: "modules.network.arduinoserial.ArduinoSerial" + path: "modules.network.arduinoserial.arduinoserial.ArduinoSerial" config: port: '/dev/ttyAMA0' baudrate: '115200' \ No newline at end of file diff --git a/modules/network/arduinoserial/tests/__init__.py b/modules/network/arduinoserial/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/arduinoserial/tests/test_arduinoserial.py b/modules/network/arduinoserial/tests/test_arduinoserial.py new file mode 100644 index 00000000..c000521c --- /dev/null +++ b/modules/network/arduinoserial/tests/test_arduinoserial.py @@ -0,0 +1,27 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_robust_serial = MagicMock() +sys.modules['modules.network.robust_serial'] = mock_robust_serial +sys.modules['modules.network.robust_serial.robust_serial'] = mock_robust_serial.robust_serial +sys.modules['modules.network.robust_serial.utils'] = mock_robust_serial.utils +mock_robust_serial.utils.open_serial_port.side_effect = Exception("No serial port") + +from modules.network.arduinoserial.arduinoserial import ArduinoSerial + +class TestArduinoSerial(unittest.TestCase): + + def test_init_no_port(self): + with self.assertRaises(Exception): + ArduinoSerial(port='/dev/ttyAMA0') + + def test_setup_messaging(self): + arduino = ArduinoSerial.__new__(ArduinoSerial) + arduino.serial_file = None + with patch.object(arduino, 'subscribe') as mock_sub: + arduino.setup_messaging() + mock_sub.assert_called_with('serial', arduino.send) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/network/messaging_service/README.md b/modules/network/messaging_service/README.md new file mode 100644 index 00000000..688a03b4 --- /dev/null +++ b/modules/network/messaging_service/README.md @@ -0,0 +1,119 @@ +# Messaging Service Documentation + +## Overview + +The `MessagingService` module provides an abstraction layer for different messaging protocols, including `pypubsub` and `MQTT`. It allows modules to publish and subscribe to messages using a unified interface. This documentation outlines the setup and usage of the `MessagingService` class and explains how to create a new module that utilizes the `BaseModule` class, which includes a messaging service instance. + +## Configuration + +Before using the `MessagingService` module, you need to configure it in the `config/messaging_service.yml` file. Below is an explanation of the configuration options: + +```yaml +messaging_service: + enabled: true # Set to true to enable the MessagingService module + path: modules.network.messaging_service.MessagingService # Path to the MessagingService class + config: + protocol: 'pubsub' # Protocol to use ('pubsub' or 'mqtt') + mqtt_host: 'localhost' # MQTT broker host (if using MQTT) + port: 1883 # MQTT broker port (if using MQTT) + dependencies: + python: + - paho-mqtt + - pypubsub +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +## Usage + +### Initializing the MessagingService + +When the module is enabled in the config YAML, the `MessagingService` class is automatically imported and initialized. + +You can also reference the `MessagingService` class directly in the `main.py`: + +```python +module = module_instances['messaging_service'] # Get the MessagingService module instance +module.publish('system/log', message="Hello, World!") # Publish a message +``` + +### Subscribing to Topics + +The `MessagingService` class provides a method to subscribe to topics. Here’s an example of how to subscribe to a topic: + +```python +def callback(message): + print(message) + +module.subscribe('system/log', callback) +``` + +### Publishing Messages + +The `MessagingService` class provides a method to publish messages to topics. Here’s an example of how to publish a message: + +```python +module.publish('system/log', message="Hello, World!") +``` + +## Creating a New Module + +To create a new module that utilizes the `BaseModule` class, follow these steps: + +1. Create a new Python file for your module (e.g., `my_module.py`). +2. Import the `BaseModule` class. +3. Define your module class and inherit from `BaseModule`. +4. Implement the necessary methods and messaging setup. + +### Example: MyModule + +```python +# filepath: /home/dan/projects/modular-biped/modules/my_module.py +from modules.base_module import BaseModule + +class MyModule(BaseModule): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.value = kwargs.get('value', 0) + + if kwargs.get('test_on_boot'): + self.test() + + def setup_messaging(self): + """Subscribe to necessary topics.""" + self.subscribe('system/loop/1', self.loop) + + def loop(self): + self.publish('my_module/value', value=self.value) + + def test(self): + print(f"Initial value: {self.value}") +``` + +### Configuration for MyModule + +Create a configuration file for your module (e.g., `config/my_module.yml`): + +```yaml +my_module: + enabled: true # Set to true to enable the MyModule module + path: modules.my_module.MyModule # Path to the MyModule class + config: + value: 42 # Initial value + test_on_boot: true # Set to true to run a test on boot +``` + +### Using MyModule in main.py + +Reference the `MyModule` class in the `main.py`: + +```python +module = module_instances['my_module'] # Get the MyModule module instance +module.publish('my_module/value', value=100) # Publish a value +``` + +## Conclusion + +The `MessagingService` module provides a unified interface for different messaging protocols, making it easy to publish and subscribe to messages in your application. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the messaging service in your projects. Additionally, you can create new modules that inherit from the `BaseModule` class and leverage the messaging service for communication. \ No newline at end of file diff --git a/modules/network/messaging_service/__init__.py b/modules/network/messaging_service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/messaging_service.yml b/modules/network/messaging_service/config.yml old mode 100755 new mode 100644 similarity index 82% rename from config/messaging_service.yml rename to modules/network/messaging_service/config.yml index 3c83aacf..4530927f --- a/config/messaging_service.yml +++ b/modules/network/messaging_service/config.yml @@ -1,6 +1,6 @@ messaging_service: enabled: true # Required for framework to function - path: modules.network.messaging_service.MessagingService + path: modules.network.messaging_service.messaging_service.MessagingService config: protocol: 'pubsub' # 'mqtt' or 'pubsub' - something is wrong with mqtt that prevents the loops from executing correctly mqtt_host: 'localhost' diff --git a/modules/network/messaging_service.py b/modules/network/messaging_service/messaging_service.py similarity index 100% rename from modules/network/messaging_service.py rename to modules/network/messaging_service/messaging_service.py diff --git a/modules/network/messaging_service/tests/__init__.py b/modules/network/messaging_service/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/messaging_service/tests/test_messaging_service.py b/modules/network/messaging_service/tests/test_messaging_service.py new file mode 100644 index 00000000..e4ee0e82 --- /dev/null +++ b/modules/network/messaging_service/tests/test_messaging_service.py @@ -0,0 +1,34 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_paho = MagicMock() +mock_pubsub = MagicMock() +sys.modules['paho'] = mock_paho +sys.modules['paho.mqtt'] = mock_paho.mqtt +sys.modules['paho.mqtt.client'] = mock_paho.mqtt.client +sys.modules['pubsub'] = mock_pubsub +sys.modules['pubsub.pub'] = mock_pubsub.pub + +from modules.network.messaging_service.messaging_service import MessagingService, PubSubMessagingService + +class TestMessagingService(unittest.TestCase): + + def test_init_pubsub(self): + ms = MessagingService(protocol='pubsub') + self.assertEqual(ms.protocol, 'pubsub') + self.assertIsInstance(ms.messaging_service, PubSubMessagingService) + + def test_init_invalid_protocol(self): + with self.assertRaises(ValueError): + MessagingService(protocol='invalid') + + def test_pubsub_subscribe_and_publish(self): + ms = PubSubMessagingService() + callback = MagicMock() + # Should not raise + ms.subscribe('test/topic', callback) + ms.publish('test/topic', message='hello') + +if __name__ == '__main__': + unittest.main() diff --git a/modules/network/rtlsdr/README.md b/modules/network/rtlsdr/README.md new file mode 100644 index 00000000..e5d0cfd4 --- /dev/null +++ b/modules/network/rtlsdr/README.md @@ -0,0 +1,95 @@ +# RTLSDR Module Documentation + +## Overview + +The `RTLSDR` module is designed to interface with RTL Software Defined Radio (SDR) using the `rtl_433` tool. It provides functionalities to start and stop the `rtl_433` process, listen to its HTTP streaming API, and handle JSON events. This documentation outlines the setup and usage of the `RTLSDR` class. + +## Configuration + +Before using the `RTLSDR` module, you need to configure it in the `config/rtlsdr.yml` file. Below is an explanation of the configuration options: + +```yaml +rtlsdr: + enabled: false # Set to true to enable the RTLSDR module + path: modules.network.rtlsdr.RTLSDR # Path to the RTLSDR class + config: + udp_host: "127.0.0.1" # Host for the UDP stream + udp_port: 8433 # Port for the UDP stream + timeout: 70 # Timeout for the HTTP connection + dependencies: + unix: + - rtl-sdr + python: + - requests +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +## Usage + +### Initializing the RTLSDR + +When the module is enabled in the config YAML, the `RTLSDR` class is automatically imported and initialized. + +You can also reference the `RTLSDR` class directly in the `main.py`: + +```python +module = module_instances['rtlsdr'] # Get the RTLSDR module instance +module.start_rtl_433() # Start the rtl_433 process +module.listen_once() # Listen to one chunk of the rtl_433 stream +``` + +### Starting and Stopping rtl_433 + +The `RTLSDR` class provides methods to start and stop the `rtl_433` process. Here’s an example of how to start and stop the process: + +```python +module.start_rtl_433() # Start the rtl_433 process +module.stop_rtl_433() # Stop the rtl_433 process +``` + +### Listening to the Stream + +The `RTLSDR` class provides methods to listen to the `rtl_433` HTTP stream and handle JSON events. Here’s an example of how to listen to the stream: + +```python +module.listen_once() # Listen to one chunk of the rtl_433 stream +module.rtl_433_listen() # Listen to rtl_433 messages in a loop until stopped +``` + +### Handling Events + +The `RTLSDR` class includes a method to handle JSON events from the `rtl_433` stream. Here’s an example of how to handle events: + +```python +def handle_event(self, line): + """Process each JSON line from rtl_433.""" + try: + data = json.loads(line) + print(data) + self.publish('sdr/data', data=data) + + # Additional custom handling below + # Example: print battery and temperature information + label = data.get("model", "Unknown") + if "channel" in data: + label += ".CH" + str(data["channel"]) + elif "id" in data: + label += ".ID" + str(data["id"]) + + if data.get("battery_ok") == 0: + print(f"{label} Battery empty!") + if "temperature_C" in data: + print(f"{label} Temperature: {data['temperature_C']}°C") + if "humidity" in data: + print(f"{label} Humidity: {data['humidity']}%") + + except json.JSONDecodeError: + print("Failed to decode JSON line:", line) +``` + +## Conclusion + +The `RTLSDR` module provides a straightforward interface for working with RTL Software Defined Radio (SDR) using the `rtl_433` tool. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the RTLSDR in your projects. \ No newline at end of file diff --git a/modules/network/rtlsdr/__init__.py b/modules/network/rtlsdr/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/rtlsdr.yml b/modules/network/rtlsdr/config.yml similarity index 78% rename from config/rtlsdr.yml rename to modules/network/rtlsdr/config.yml index 5612604e..127462f2 100644 --- a/config/rtlsdr.yml +++ b/modules/network/rtlsdr/config.yml @@ -1,6 +1,6 @@ rtl_sdr: enabled: false - path: modules.network.rtlsdr.RTLSDR + path: modules.network.rtlsdr.rtlsdr.RTLSDR config: udp_host: "127.0.0.1" udp_port: 8433 diff --git a/modules/network/rtlsdr.py b/modules/network/rtlsdr/rtlsdr.py similarity index 100% rename from modules/network/rtlsdr.py rename to modules/network/rtlsdr/rtlsdr.py diff --git a/modules/network/rtlsdr/tests/__init__.py b/modules/network/rtlsdr/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/rtlsdr/tests/test_rtlsdr.py b/modules/network/rtlsdr/tests/test_rtlsdr.py new file mode 100644 index 00000000..99103aeb --- /dev/null +++ b/modules/network/rtlsdr/tests/test_rtlsdr.py @@ -0,0 +1,21 @@ +import unittest +from unittest.mock import MagicMock, patch +from modules.network.rtlsdr.rtlsdr import RTLSDR + +class TestRTLSDR(unittest.TestCase): + + def test_init_defaults(self): + sdr = RTLSDR() + self.assertEqual(sdr.udp_host, '127.0.0.1') + self.assertEqual(sdr.udp_port, 8433) + self.assertIsNone(sdr.rtl_process) + + def test_setup_messaging(self): + sdr = RTLSDR() + with patch.object(sdr, 'subscribe') as mock_sub: + sdr.setup_messaging() + mock_sub.assert_any_call('sdr/start', sdr.start_rtl_433) + mock_sub.assert_any_call('sdr/listen', sdr.listen_once) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/network/telegrambot/README.md b/modules/network/telegrambot/README.md new file mode 100644 index 00000000..b2601b9a --- /dev/null +++ b/modules/network/telegrambot/README.md @@ -0,0 +1,111 @@ +# TelegramBot Module Documentation + +## Overview + +The `TelegramBot` module is designed to interface with Telegram using the `python-telegram-bot` library. It provides functionalities to initialize the bot, handle commands, and publish messages to the application via a pub/sub mechanism. This documentation outlines the setup and usage of the `TelegramBot` class. + +## Configuration + +Before using the `TelegramBot` module, you need to configure it in the `config/telegram_bot.yml` file. Below is an explanation of the configuration options: + +```yaml +telegram_bot: + enabled: false # Set to true to enable the TelegramBot module + path: modules.network.telegrambot.TelegramBot # Path to the TelegramBot class + config: + token: '' # Telegram bot token (can also be set as an environment variable) + user_whitelist: [] # List of user IDs allowed to interact with the bot + dependencies: + unix: + - python3-pip + python: + - python-telegram-bot +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +## Usage + +### Initializing the TelegramBot + +When the module is enabled in the config YAML, the `TelegramBot` class is automatically imported and initialized. + +You can also reference the `TelegramBot` class directly in the `main.py`: + +```python +module = module_instances['telegram_bot'] # Get the TelegramBot module instance +module.publish('telegram/respond', user_id=123456789, response="Hello, World!") # Send a message to a user +``` + +### Setting Up the Bot + +To set up the bot, follow these steps: + +1. Search for BotFather in Telegram. +2. Start the BotFather. +3. Type `/newbot`. +4. Follow the instructions. +5. Copy the token (DON'T SHARE IT WITH ANYONE). +6. Create an environment variable called `TELEGRAM_BOT_TOKEN` and set it to the token (can also be set in the config YAML). +7. Run the script. + +### Handling Commands + +The `TelegramBot` class provides methods to handle commands and messages. Here’s an example of how to handle the `/start` and `/help` commands: + +```python +@staticmethod +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Send a message when the command /start is issued.""" + user = update.effective_user + await update.message.reply_html( + rf"Hi {user.mention_html()}!", + reply_markup=ForceReply(selective=True), + ) + +@staticmethod +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Send a message when the command /help is issued.""" + await update.message.reply_text("Help!") +``` + +### Publishing Messages + +The `TelegramBot` class can publish messages to the application via a pub/sub mechanism. Here’s an example of how to publish a message: + +```python +async def publish(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Publish the user's message to the application via pubsub.""" + user_id = update.effective_user.id + message = update.message.text + + # Save the update for response handling + self.update = update + + # Publish the message to other parts of the application + self.publish('telegram/received', user_id=user_id, message=message) + print(f"Published message from user {user_id}: {message} on topic telegram/received") +``` + +### Handling Responses + +The `TelegramBot` class can handle responses from the application and send them back to the user. Here’s an example of how to handle a response: + +```python +async def handle(self, user_id: int, response: str) -> None: + """Handle responses from the application and send them back to the user.""" + if (user_id not in self.user_whitelist): + print(f"User {user_id} not in whitelist, skipping response") + return + print(f"Handling response for user {user_id}: {response}") + + # Send the response back to the user on Telegram + if self.update and self.update.effective_user.id == user_id: + await self.update.message.reply_text(response) +``` + +## Conclusion + +The `TelegramBot` module provides a straightforward interface for working with Telegram bots in Python. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the Telegram bot in your projects. \ No newline at end of file diff --git a/modules/network/telegrambot/__init__.py b/modules/network/telegrambot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/telegram.yml b/modules/network/telegrambot/config.yml similarity index 76% rename from config/telegram.yml rename to modules/network/telegrambot/config.yml index 43085d53..e9236864 100644 --- a/config/telegram.yml +++ b/modules/network/telegrambot/config.yml @@ -1,6 +1,6 @@ telegram: enabled: true - path: modules.network.telegrambot.TelegramBot + path: modules.network.telegrambot.telegrambot.TelegramBot config: user_whitelist: [] dependencies: diff --git a/modules/network/telegrambot.py b/modules/network/telegrambot/telegrambot.py similarity index 100% rename from modules/network/telegrambot.py rename to modules/network/telegrambot/telegrambot.py diff --git a/modules/network/telegrambot/tests/__init__.py b/modules/network/telegrambot/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/telegrambot/tests/test_telegrambot.py b/modules/network/telegrambot/tests/test_telegrambot.py new file mode 100644 index 00000000..7d232b21 --- /dev/null +++ b/modules/network/telegrambot/tests/test_telegrambot.py @@ -0,0 +1,23 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_telegram = MagicMock() +sys.modules['telegram'] = mock_telegram +sys.modules['telegram.ext'] = mock_telegram.ext + +from modules.network.telegrambot.telegrambot import TelegramBot + +class TestTelegramBot(unittest.TestCase): + + def test_init_no_token(self): + with patch.dict('os.environ', {}, clear=True): + with self.assertRaises(RuntimeError): + TelegramBot() + + def test_init_with_token(self): + bot = TelegramBot(token='test_token_123') + self.assertEqual(bot.token, 'test_token_123') + +if __name__ == '__main__': + unittest.main() diff --git a/modules/personality/README.md b/modules/personality/README.md new file mode 100644 index 00000000..fd98a986 --- /dev/null +++ b/modules/personality/README.md @@ -0,0 +1,217 @@ +# Personality Module Documentation + +## Overview + +The `Personality` module is designed to add personality and interactive behaviors to the robot. It subscribes to various events such as motion detection, serial communication, and vision detections, and responds with actions like animations, LED changes, and sounds. This documentation outlines the current functionality and how it interacts with other modules. + +## Configuration + +Before using the `Personality` module, you need to configure it in the `config/personality.yml` file. Below is an explanation of the configuration options: + +```yaml +personality: + enabled: true # Set to true to enable the Personality module + path: modules.personality.Personality # Path to the Personality class + config: + min_interval: 20 # Minimum interval between actions in seconds + max_interval: 60 # Maximum interval between actions in seconds +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +## Usage + +### Initializing the Personality Module + +When the module is enabled in the config YAML, the `Personality` class is automatically imported and initialized. + +### Subscribing to Topics + +The `Personality` module subscribes to various topics to receive events and trigger actions. Here’s an example of how to subscribe to topics: + +```python +def setup_messaging(self): + """Subscribe to necessary topics.""" + self.subscribe('system/loop/1', self.loop) + self.subscribe('vision/detections', self.handle_vision_detections) + self.subscribe('motion', self.update_motion_time) + self.subscribe('serial', self.track_serial_idle) +``` + +### Loop Method + +The `loop` method is called periodically to handle ongoing reactions and trigger new actions based on the current state and timing intervals. + +```python +def loop(self): + # Handle ongoing object reaction + if self.object_reaction_end_time and datetime.now() >= self.object_reaction_end_time: + self.publish('led', identifiers=[ + 'right', 'top_right', 'top_left', 'left', + 'bottom_left', 'bottom_right' + ], color="off") + self.object_reaction_end_time = None + + # Update the middle eye LED based on conditions + self.update_middle_eye_led() + + self.random_neopixel_status() + + # Check if it's time for the next action + if datetime.now() >= self.next_action_time: + action = choice(self.actions) + action() + self.next_action_time = self.calculate_next_action_time() + + # If serial has been idle for more than 10 seconds, call random_animation() + if self.last_serial_time and datetime.now() - self.last_serial_time > timedelta(seconds=10): + self.random_animation() +``` + +### Actions + +The `Personality` module defines various actions that can be triggered, such as animations, sounds, and LED changes. + +#### Random Animation + +```python +def random_animation(self): + animations = [ + 'head_shake', + 'head_left', + 'head_right', + 'wake', + 'look_down', + 'look_up', + 'celebrate' + ] + animation = choice(animations) + self.log(f"Random animation triggered: {animation}") + self.publish('animate', action=animation) +``` + +#### Braillespeak + +```python +def braillespeak(self): + messages = ["Hi", "Hello", "Hai", "Hey"] + msg = choice(messages) + self.publish('speak', msg=msg) + self.log(f"Braillespeak triggered: {msg}") +``` + +#### Buzzer Tone + +```python +def buzzer_tone(self): + frequency = randint(300, 1000) # Random frequency between 300Hz and 1000Hz + length = round(randint(1, 5) / 10, 1) # Random length between 0.1s and 0.5s + self.publish('buzz', frequency=frequency, length=length) + self.log(f"Buzzer tone triggered: {frequency}Hz for {length}s") +``` + +#### Buzzer Song + +```python +def buzzer_song(self): + songs = ["happy birthday", "merry christmas"] + song = choice(songs) + self.publish('play', song=song) + self.log(f"Buzzer song triggered: {song}") +``` + +#### Random Neopixel Status + +```python +def random_neopixel_status(self): + if not self.last_status_time or datetime.now() - self.last_status_time > timedelta(seconds=3): + self.last_status_time = datetime.now() + color = choice(["red", "green", "blue", "white_dim", "purple", "yellow", "orange", "pink"]) + self.publish('led', identifiers=[0], color=color) + for i in range 4, 0, -1): + if i+1 < 5: + self.led_colors[i] = self.led_colors[i-1] + for i in range(1, 5): + self.publish('led', identifiers=[i], color=self.led_colors[i]) + self.led_colors[0] = color + self.log(message=f"Neopixel status triggered set to {color}", level='debug') +``` + +#### Random Neopixel Eye + +```python +def random_neopixel_eye(self): + positions = [ + 'right', 'top_right', 'top_left', 'left', + 'bottom_left', 'bottom_right' + ] + position = choice(positions) + color = choice(["white_dim"]) + self.publish('led', identifiers=positions, color=color) + self.log(f"Neopixel eye ring set to {color}") +``` + +#### Move Antenna + +```python +def move_antenna(self): + angle = randint(-40, 40) + self.publish('piservo/move', angle=angle) + self.log(f"Antenna moved to angle: {angle}") +``` + +### Handling Vision Detections + +The `handle_vision_detections` method processes detected objects and triggers reactions. + +```python +def handle_vision_detections(self, matches): + if matches and len(matches) > 0 and (self.object_reaction_end_time is None or datetime.now() >= self.object_reaction_end_time): + self.log(f"Vision detected objects: {matches}") + self.last_vision_time = datetime.now() + self.random_neopixel_eye() + self.object_reaction_end_time = datetime.now() + timedelta(seconds=3) +``` + +### Updating Motion Time + +The `update_motion_time` method updates the last motion time. + +```python +def update_motion_time(self): + self.last_motion_time = datetime.now() +``` + +### Updating Middle Eye LED + +The `update_middle_eye_led` method updates the middle eye LED based on the current state. + +```python +def update_middle_eye_led(self): + now = datetime.now() + if self.last_vision_time and now - self.last_vision_time <= timedelta(seconds=30): + self.publish('led', identifiers='middle', color='green') + elif now - self.last_motion_time > timedelta(seconds=30): + self.publish('led', identifiers='middle', color='red') + else: + self.publish('led', identifiers='middle', color='blue') +``` + +### Tracking Serial Idle + +The `track_serial_idle` method tracks the last serial communication time. + +```python +def track_serial_idle(self, type, identifier, message): + self.last_serial_time = datetime.now() +``` + +## Customization + +The `Personality` module can be modified to meet specific requirements for different implementations. You can add new actions, change the intervals, or modify the existing behaviors to suit your needs. + +## Conclusion + +The `Personality` module provides a comprehensive framework for adding personality and interactive behaviors to the robot. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and customize the `Personality` module in your projects. \ No newline at end of file diff --git a/modules/personality/__init__.py b/modules/personality/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/personality.yml b/modules/personality/config.yml similarity index 72% rename from config/personality.yml rename to modules/personality/config.yml index 1329e186..6cf3a828 100644 --- a/config/personality.yml +++ b/modules/personality/config.yml @@ -1,6 +1,6 @@ personality: enabled: true - path: modules.personality.Personality + path: modules.personality.personality.Personality config: mode: 6 # Config.MODE_LIVE sleep_timeout: 60 # Time in seconds before entering sleep mode \ No newline at end of file diff --git a/modules/personality.py b/modules/personality/personality.py similarity index 100% rename from modules/personality.py rename to modules/personality/personality.py diff --git a/modules/personality/tests/__init__.py b/modules/personality/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/personality/tests/test_personality.py b/modules/personality/tests/test_personality.py new file mode 100644 index 00000000..4d0acd61 --- /dev/null +++ b/modules/personality/tests/test_personality.py @@ -0,0 +1,27 @@ +import unittest +from unittest.mock import MagicMock, patch +from modules.personality.personality import Personality + +class TestPersonality(unittest.TestCase): + + def test_init_defaults(self): + p = Personality() + self.assertEqual(p.eye, 'blue') + self.assertEqual(p.min_interval, 20) + self.assertEqual(p.max_interval, 60) + self.assertEqual(p.balance_enabled, True) + self.assertIsNotNone(p.next_action_time) + + def test_init_custom(self): + p = Personality(min_interval=10, max_interval=30, balance_enabled=False) + self.assertEqual(p.min_interval, 10) + self.assertEqual(p.max_interval, 30) + self.assertFalse(p.balance_enabled) + + def test_calculate_next_action_time(self): + p = Personality(min_interval=5, max_interval=10) + t = p.calculate_next_action_time() + self.assertIsNotNone(t) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/pitemperature/README.md b/modules/pitemperature/README.md new file mode 100644 index 00000000..ba8c3b89 --- /dev/null +++ b/modules/pitemperature/README.md @@ -0,0 +1,28 @@ +# PiTemperature + +## Overview + +Monitors Raspberry Pi CPU temperature and throttles/sleeps the system when needed. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +pitemperature: + enabled: false # Set to true to enable this module + path: modules.pitemperature.pitemperature.PiTemperature + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/pitemperature/__init__.py b/modules/pitemperature/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/pitemperature/config.yml b/modules/pitemperature/config.yml new file mode 100644 index 00000000..21a8124f --- /dev/null +++ b/modules/pitemperature/config.yml @@ -0,0 +1,4 @@ +pitemperature: + enabled: true + environment: robot + path: modules.pitemperature.pitemperature.PiTemperature \ No newline at end of file diff --git a/modules/pitemperature.py b/modules/pitemperature/pitemperature.py similarity index 100% rename from modules/pitemperature.py rename to modules/pitemperature/pitemperature.py diff --git a/modules/pitemperature/tests/__init__.py b/modules/pitemperature/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/pitemperature/tests/test_pitemperature.py b/modules/pitemperature/tests/test_pitemperature.py new file mode 100644 index 00000000..0ed84c8f --- /dev/null +++ b/modules/pitemperature/tests/test_pitemperature.py @@ -0,0 +1,39 @@ +import unittest +from unittest import mock +from modules.pitemperature.pitemperature import PiTemperature + +class TestPiTemperature(unittest.TestCase): + + @mock.patch('modules.pitemperature.pitemperature.os.popen') + def test_monitor_normal_temp(self, mock_popen): + mock_popen.return_value.readline.return_value = "temp=50'C" + temp_module = PiTemperature() + with mock.patch('modules.pitemperature.pitemperature.threading.Thread'): + temp_module.messaging_service = mock.MagicMock() + with mock.patch.object(temp_module, 'publish') as mock_publish: + temp_module.monitor() + mock_publish.assert_any_call('system/temperature', value='50') + mock_publish.assert_any_call('system/wake', requestor='PiTemperature') + + @mock.patch('modules.pitemperature.pitemperature.os.popen') + def test_monitor_high_temp(self, mock_popen): + mock_popen.return_value.readline.return_value = "temp=81'C" + temp_module = PiTemperature() + with mock.patch('modules.pitemperature.pitemperature.threading.Thread'): + temp_module.messaging_service = mock.MagicMock() + with mock.patch.object(temp_module, 'publish') as mock_publish: + temp_module.monitor() + mock_publish.assert_any_call('system/throttle', requestor='PiTemperature') + + @mock.patch('modules.pitemperature.pitemperature.os.popen') + def test_monitor_critical_temp(self, mock_popen): + mock_popen.return_value.readline.return_value = "temp=86'C" + temp_module = PiTemperature() + with mock.patch('modules.pitemperature.pitemperature.threading.Thread'): + temp_module.messaging_service = mock.MagicMock() + with mock.patch.object(temp_module, 'publish') as mock_publish: + temp_module.monitor() + mock_publish.assert_any_call('system/sleep', requestor='PiTemperature') + +if __name__ == '__main__': + unittest.main() diff --git a/modules/sensor/__init__.py b/modules/sensor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/sensor/imu/__init__.py b/modules/sensor/imu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/sensor/imu/bno055/README.md b/modules/sensor/imu/bno055/README.md new file mode 100644 index 00000000..8ec199ec --- /dev/null +++ b/modules/sensor/imu/bno055/README.md @@ -0,0 +1,77 @@ +# BNO055 Module Documentation + +## Overview + +The `BNO055` module interfaces with the BNO055 9-DOF sensor using the I2C protocol. It provides initialization, data reading (Euler angles, acceleration, gyroscope, etc.), and publishes sensor data via the pubsub system. This documentation outlines setup and usage of the `BNO055` class. + +## Configuration + +Configure the module in `config/bno055.yml`: + +```yaml +bno055: + enabled: true + path: modules.sensor.imu.bno055.BNO055 + instances: + - name: "imu_head" + test_on_boot: false + bus: 1 + address: 0x28 # Default + - name: "imu_body" + test_on_boot: false + bus: 1 + line_offset: 1 # For debugging second sensor on same bus, to display both at the same time + address: 0x29 # Alternative address if second sensor is used + dependencies: + python: + - adafruit-circuitpython-bno055 + - adafruit-blinka +``` + +### Dependencies + +After enabling, run `./install.sh` to install dependencies. Ensure `adafruit-circuitpython-bno055` and `adafruit-blinka` are installed. + +## Usage + +### Initializing the BNO055 + +When enabled in the config YAML, the `BNO055` class is automatically imported and initialized. + + +You can also reference the `BNO055` class directly in your code: + +```python +module = module_instances['imu_head'] # Get the BNO055 module instance +module.read_data() # Print sensor data to console +euler = module.get_euler() # Get Euler angles as a tuple +``` + +### Reading and Publishing Data + +- `read_data()`: Prints all sensor values (temperature, acceleration, gyro, euler angles, etc.) to the console. +- `get_euler()`: Returns the Euler angles as a tuple. +- `publish_changed_data()`: Publishes sensor data to the pubsub topic `imu//data` when significant changes are detected. This is only called if you invoke it manually or if `test_on_boot` is set to `true` in the config (which causes continuous reading/publishing on boot). + +### Example Data Output + +```python +{ + 'temperature': 19, + 'acceleration': (0.17, -9.74, -0.99), + 'magnetic': (12.19, 63.75, 32.25), + 'gyro': (0.001, 0.002, -0.002), + 'euler': (359.94, 0.94, 95.75), + 'quaternion': (0.67, -0.74, -0.01, 0.0), + 'linear_acceleration': (0.01, 0.02, 0.03), + 'gravity': (0.16, -9.75, -0.98) +} +``` + + +## Notes + +- The module inherits from `BaseModule` and uses the pubsub system for messaging. +- The `test_on_boot` option will continuously read and print data on startup if set to `true`. +- Data is only published when significant changes are detected to reduce message flooding. +- The module does not automatically subscribe to a loop topic for periodic publishing; you must call `publish_changed_data()` yourself if you want regular updates. diff --git a/modules/sensor/imu/bno055/__init__.py b/modules/sensor/imu/bno055/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/sensor/imu/bno055.py b/modules/sensor/imu/bno055/bno055.py similarity index 100% rename from modules/sensor/imu/bno055.py rename to modules/sensor/imu/bno055/bno055.py diff --git a/config/bno055.yml b/modules/sensor/imu/bno055/config.yml similarity index 86% rename from config/bno055.yml rename to modules/sensor/imu/bno055/config.yml index 399dee41..da48689b 100644 --- a/config/bno055.yml +++ b/modules/sensor/imu/bno055/config.yml @@ -1,7 +1,7 @@ bno055: enabled: true environment: robot - path: modules.sensor.imu.bno055.BNO055 + path: modules.sensor.imu.bno055.bno055.BNO055 instances: - name: "imu_head" test_on_boot: false diff --git a/modules/sensor/imu/bno055/tests/__init__.py b/modules/sensor/imu/bno055/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/sensor/imu/bno055/tests/test_bno055.py b/modules/sensor/imu/bno055/tests/test_bno055.py new file mode 100644 index 00000000..9bf4ef07 --- /dev/null +++ b/modules/sensor/imu/bno055/tests/test_bno055.py @@ -0,0 +1,36 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_board = MagicMock() +mock_adafruit_bno055 = MagicMock() +sys.modules['board'] = mock_board +sys.modules['adafruit_bno055'] = mock_adafruit_bno055 + +from modules.sensor.imu.bno055.bno055 import BNO055 + +def make_sensor(**kwargs): + mock_board.I2C.reset_mock() + mock_adafruit_bno055.BNO055_I2C.reset_mock() + return BNO055(**kwargs) + +class TestBNO055(unittest.TestCase): + + def test_init_defaults(self): + sensor = make_sensor() + self.assertEqual(sensor.name, 'bno055') + self.assertEqual(sensor.address, 0x28) + mock_board.I2C.assert_called_once() + + def test_init_custom(self): + sensor = make_sensor(name='myimu', address=0x29) + self.assertEqual(sensor.name, 'myimu') + self.assertEqual(sensor.address, 0x29) + + def test_get_euler(self): + sensor = make_sensor() + sensor.sensor.euler = (10.0, 2.0, 45.0) + self.assertEqual(sensor.get_euler(), (10.0, 2.0, 45.0)) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/sensor/imu/mpu6050/README.md b/modules/sensor/imu/mpu6050/README.md new file mode 100644 index 00000000..ea59a74b --- /dev/null +++ b/modules/sensor/imu/mpu6050/README.md @@ -0,0 +1,86 @@ +# MPU6050 Module Documentation + +## Overview + +The `MPU6050` module is designed to interface with the MPU6050 sensor using the I2C protocol. It provides functionalities to initialize the sensor, read raw data from the accelerometer and gyroscope, and process this data. This documentation outlines the setup and usage of the `MPU6050` class. + +## Configuration + +Before using the `MPU6050` module, you need to configure it in the `config/mpu6050.yml` file. Below is an explanation of the configuration options: + +```yaml +mpu6050: + enabled: false # Set to true to enable the MPU6050 module + path: modules.sensor.imu.mpu6050.MPU6050 # Path to the MPU6050 class + config: + test_on_boot: false # Set to true to run a test on boot + dependencies: + unix: + - python3-smbus + python: + - smbus +``` + +### Dependencies + +After enabling the module, run `./install.sh` to install the required dependencies. The dependencies are listed under `dependencies` in the configuration file. Check the output to ensure that the dependencies are installed correctly. + +## Usage + +### Initializing the MPU6050 + +When the module is enabled in the config YAML, the `MPU6050` class is automatically imported and initialized. + +You can also reference the `MPU6050` class directly in the `main.py`: + +```python +module = module_instances['mpu6050'] # Get the MPU6050 module instance +module.read_data() # Start reading data from the sensor +``` + +### Reading Data + +The `MPU6050` class provides methods to read raw data from the accelerometer and gyroscope. Here’s an example of how to read data: + +```python +def read_data(self): + print("Reading Data of Gyroscope and Accelerometer") + while True: + try: + # Read Accelerometer raw value + acc_x = self.read_raw_data(ACCEL_XOUT_H) + acc_y = self.read_raw_data(ACCEL_YOUT_H) + acc_z = self.read_raw_data(ACCEL_ZOUT_H) + + # Read Gyroscope raw value + gyro_x = self.read_raw_data(GYRO_XOUT_H) + gyro_y = self.read_raw_data(GYRO_YOUT_H) + gyro_z = self.read_raw_data(GYRO_ZOUT_H) + except OSError: + continue + + # Full scale range +/- 250 degree/C as per sensitivity scale factor + Ax = acc_x / 16384.0 + Ay = acc_y / 16384.0 + Az = acc_z / 16384.0 + + Gx = gyro_x / 131.0 + Gy = gyro_y / 131.0 + Gz = gyro_z / 131.0 + + print(f"Gx={Gx:.2f}°/s\tGy={Gy:.2f}°/s\tGz={Gz:.2f}°/s\tAx={Ax:.2f} g\tAy={Ay:.2f} g\tAz={Az:.2f} g") + sleep(0.1) +``` + +### Handling Errors + +The `MPU6050` class includes error handling for I2C communication errors. If an `OSError` occurs, it will be caught and ignored, allowing the script to continue running: + +```python +except OSError: + continue +``` + +## Conclusion + +The `MPU6050` module provides a straightforward interface for working with the MPU6050 sensor in Python. By following the configuration and usage instructions outlined in this documentation, you can effectively integrate and utilize the MPU6050 sensor in your projects. \ No newline at end of file diff --git a/modules/sensor/imu/mpu6050/__init__.py b/modules/sensor/imu/mpu6050/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/mpu6050.yml b/modules/sensor/imu/mpu6050/config.yml similarity index 67% rename from config/mpu6050.yml rename to modules/sensor/imu/mpu6050/config.yml index 84163170..353b41ed 100644 --- a/config/mpu6050.yml +++ b/modules/sensor/imu/mpu6050/config.yml @@ -1,6 +1,6 @@ mpu6050: enabled: false - path: modules.sensor.imu.mpu6050.MPU6050 + path: modules.sensor.imu.mpu6050.mpu6050.MPU6050 config: test_on_boot: true dependencies: diff --git a/modules/sensor/imu/mpu6050.py b/modules/sensor/imu/mpu6050/mpu6050.py similarity index 100% rename from modules/sensor/imu/mpu6050.py rename to modules/sensor/imu/mpu6050/mpu6050.py diff --git a/modules/sensor/imu/mpu6050/tests/__init__.py b/modules/sensor/imu/mpu6050/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/sensor/imu/mpu6050/tests/test_mpu6050.py b/modules/sensor/imu/mpu6050/tests/test_mpu6050.py new file mode 100644 index 00000000..9ca4818b --- /dev/null +++ b/modules/sensor/imu/mpu6050/tests/test_mpu6050.py @@ -0,0 +1,23 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +mock_smbus = MagicMock() +mock_pubsub = MagicMock() +sys.modules['smbus'] = mock_smbus +sys.modules['pubsub'] = mock_pubsub +sys.modules['pubsub.pub'] = mock_pubsub.pub + +with patch('time.sleep', return_value=None): + from modules.sensor.imu.mpu6050.mpu6050 import MPU6050 + +class TestMPU6050(unittest.TestCase): + + def test_init(self): + with patch('time.sleep', return_value=None): + sensor = MPU6050() + self.assertEqual(sensor.Device_Address, 0x68) + mock_smbus.SMBus.assert_called_with(1) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/translator/README.md b/modules/translator/README.md new file mode 100644 index 00000000..f3fa9f8c --- /dev/null +++ b/modules/translator/README.md @@ -0,0 +1,28 @@ +# Translator + +## Overview + +Text translation module. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +translator: + enabled: false # Set to true to enable this module + path: modules.translator.translator.Translator + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/translator/__init__.py b/modules/translator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/translator.yml b/modules/translator/config.yml similarity index 74% rename from config/translator.yml rename to modules/translator/config.yml index 852b1071..09d6cb12 100644 --- a/config/translator.yml +++ b/modules/translator/config.yml @@ -1,6 +1,6 @@ translator: enabled: false - path: modules.translator.Translator + path: modules.translator.translator.Translator config: default: src: "en" diff --git a/modules/translator/tests/__init__.py b/modules/translator/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/translator/tests/test_translator.py b/modules/translator/tests/test_translator.py new file mode 100644 index 00000000..2e2899fb --- /dev/null +++ b/modules/translator/tests/test_translator.py @@ -0,0 +1,34 @@ +import unittest +from unittest.mock import MagicMock, patch + +mock_googletrans = MagicMock() +mock_translator_instance = MagicMock() +mock_googletrans.Translator.return_value = mock_translator_instance + +import sys +sys.modules['googletrans'] = mock_googletrans + +from modules.translator.translator import Translator + +class TestTranslator(unittest.TestCase): + + def test_init_defaults(self): + t = Translator() + self.assertEqual(t.src, 'en') + self.assertEqual(t.dest, 'en') + + def test_same_language_no_translation(self): + t = Translator(src='en', dest='en') + result = t.request('hello') + self.assertEqual(result, 'hello') + + def test_request(self): + t = Translator(src='en', dest='es') + mock_translation = MagicMock() + mock_translation.text = 'hola' + mock_translator_instance.translate.return_value = mock_translation + result = t.request('hello') + self.assertEqual(result, 'hola') + +if __name__ == '__main__': + unittest.main() diff --git a/modules/translator.py b/modules/translator/translator.py similarity index 100% rename from modules/translator.py rename to modules/translator/translator.py diff --git a/modules/vision/__init__.py b/modules/vision/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/vision/imx500/__init__.py b/modules/vision/imx500/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/vision/imx500/tracking/README.md b/modules/vision/imx500/tracking/README.md new file mode 100644 index 00000000..b4ebefe4 --- /dev/null +++ b/modules/vision/imx500/tracking/README.md @@ -0,0 +1,28 @@ +# Tracking + +## Overview + +IMX500 AI camera tracking module for object tracking. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +tracking: + enabled: false # Set to true to enable this module + path: modules.vision.imx500.tracking.tracking.Tracking + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/vision/imx500/tracking/__init__.py b/modules/vision/imx500/tracking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/tracking.yml b/modules/vision/imx500/tracking/config.yml similarity index 61% rename from config/tracking.yml rename to modules/vision/imx500/tracking/config.yml index c4746d76..d6b1c7db 100644 --- a/config/tracking.yml +++ b/modules/vision/imx500/tracking/config.yml @@ -1,6 +1,6 @@ tracking: enabled: false - path: 'modules.vision.imx500.tracking.Tracking' + path: 'modules.vision.imx500.tracking.tracking.Tracking' config: active: true name: 'tracking' diff --git a/modules/vision/imx500/tracking/tests/__init__.py b/modules/vision/imx500/tracking/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/vision/imx500/tracking/tests/test_tracking.py b/modules/vision/imx500/tracking/tests/test_tracking.py new file mode 100644 index 00000000..c3896fd2 --- /dev/null +++ b/modules/vision/imx500/tracking/tests/test_tracking.py @@ -0,0 +1,36 @@ +import unittest +from unittest.mock import MagicMock, patch +from modules.vision.imx500.tracking.tracking import Tracking + +class TestTracking(unittest.TestCase): + + def test_init_defaults(self): + t = Tracking() + self.assertTrue(t.active) + self.assertFalse(t.moving) + self.assertEqual(t.filter, 'person') + + def test_init_custom(self): + t = Tracking(active=False, filter='car') + self.assertFalse(t.active) + self.assertEqual(t.filter, 'car') + + def test_set_state(self): + t = Tracking() + t.set_state(False) + self.assertFalse(t.active) + + def test_unfreeze(self): + t = Tracking() + t.moving = True + t.unfreeze() + self.assertFalse(t.moving) + + def test_setup_messaging(self): + t = Tracking() + with patch.object(t, 'subscribe') as mock_sub: + t.setup_messaging() + mock_sub.assert_any_call('vision/detections', t.handle) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/vision/imx500/tracking.py b/modules/vision/imx500/tracking/tracking.py similarity index 100% rename from modules/vision/imx500/tracking.py rename to modules/vision/imx500/tracking/tracking.py diff --git a/modules/vision/imx500/vision/README.md b/modules/vision/imx500/vision/README.md new file mode 100644 index 00000000..474091de --- /dev/null +++ b/modules/vision/imx500/vision/README.md @@ -0,0 +1,28 @@ +# Vision + +## Overview + +IMX500 AI camera vision module for object detection. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +vision_imx500: + enabled: false # Set to true to enable this module + path: modules.vision.imx500.vision.vision.Vision + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/vision/imx500/vision/__init__.py b/modules/vision/imx500/vision/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/vision_imx500.yml b/modules/vision/imx500/vision/config.yml similarity index 82% rename from config/vision_imx500.yml rename to modules/vision/imx500/vision/config.yml index 3e8207c2..cc74f192 100644 --- a/config/vision_imx500.yml +++ b/modules/vision/imx500/vision/config.yml @@ -1,7 +1,7 @@ vision_imx500: enabled: true environment: robot - path: 'modules.vision.imx500.vision.Vision' + path: 'modules.vision.imx500.vision.vision.Vision' config: preview: false dependencies: diff --git a/modules/vision/imx500/vision/tests/__init__.py b/modules/vision/imx500/vision/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/vision/imx500/vision/tests/test_vision.py b/modules/vision/imx500/vision/tests/test_vision.py new file mode 100644 index 00000000..41393016 --- /dev/null +++ b/modules/vision/imx500/vision/tests/test_vision.py @@ -0,0 +1,29 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +# Mock hardware-specific modules +for mod in ['cv2', 'numpy', 'picamera2', 'libcamera', 'argparse']: + if mod not in sys.modules: + sys.modules[mod] = MagicMock() +sys.modules['picamera2.devices'] = MagicMock() +sys.modules['picamera2.devices.imx500'] = MagicMock() +sys.modules['picamera2.devices.imx500.imx500_postprocess'] = MagicMock() +sys.modules['numpy'] = MagicMock() + +from modules.vision.imx500.vision.vision import Vision + +class TestIMX500Vision(unittest.TestCase): + + def test_class_exists(self): + self.assertTrue(hasattr(Vision, '__init__')) + + def test_setup_messaging(self): + v = Vision.__new__(Vision) + with patch.object(v, 'subscribe') as mock_sub, \ + patch.object(v, 'publish') as mock_pub: + v.setup_messaging() + # Should subscribe to system topics + +if __name__ == '__main__': + unittest.main() diff --git a/modules/vision/imx500/vision.py b/modules/vision/imx500/vision/vision.py similarity index 100% rename from modules/vision/imx500/vision.py rename to modules/vision/imx500/vision/vision.py diff --git a/modules/vision/opencv/__init__.py b/modules/vision/opencv/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/vision/opencv/vision/README.md b/modules/vision/opencv/vision/README.md new file mode 100644 index 00000000..d53700a2 --- /dev/null +++ b/modules/vision/opencv/vision/README.md @@ -0,0 +1,28 @@ +# Vision + +## Overview + +OpenCV-based vision module for computer vision processing. + +## Configuration + +Configuration is defined in `config.yml` within this directory: + +```yaml +vision_opencv: + enabled: false # Set to true to enable this module + path: modules.vision.opencv.vision.vision.Vision + config: + # See config.yml for available options +``` + +## Usage + +Enable the module in `config.yml` and it will be automatically loaded by the `ModuleLoader`. + +## Testing + +Run the module tests with: +```bash +python3 -m unittest discover -s modules -p 'test_*.py' +``` diff --git a/modules/vision/opencv/vision/__init__.py b/modules/vision/opencv/vision/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/vision_opencv.yml b/modules/vision/opencv/vision/config.yml similarity index 53% rename from config/vision_opencv.yml rename to modules/vision/opencv/vision/config.yml index 92bc0a54..663d7e3e 100644 --- a/config/vision_opencv.yml +++ b/modules/vision/opencv/vision/config.yml @@ -1,5 +1,5 @@ vision_opencv: enabled: false - path: 'modules.vision.opencv.vision.Vision' + path: 'modules.vision.opencv.vision.vision.Vision' config: preview: false diff --git a/modules/vision/opencv/vision/tests/__init__.py b/modules/vision/opencv/vision/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/vision/opencv/vision/tests/test_vision.py b/modules/vision/opencv/vision/tests/test_vision.py new file mode 100644 index 00000000..81a209de --- /dev/null +++ b/modules/vision/opencv/vision/tests/test_vision.py @@ -0,0 +1,22 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +for mod in ['cv2', 'numpy', 'picamera2', 'argparse']: + if mod not in sys.modules: + sys.modules[mod] = MagicMock() + +from modules.vision.opencv.vision.vision import Vision + +class TestOpenCVVision(unittest.TestCase): + + def test_class_exists(self): + self.assertTrue(hasattr(Vision, '__init__')) + + def test_setup_messaging(self): + v = Vision.__new__(Vision) + with patch.object(v, 'subscribe') as mock_sub: + v.setup_messaging() + +if __name__ == '__main__': + unittest.main() diff --git a/modules/vision/opencv/vision.py b/modules/vision/opencv/vision/vision.py similarity index 100% rename from modules/vision/opencv/vision.py rename to modules/vision/opencv/vision/vision.py diff --git a/modules/xbox_controller/README.md b/modules/xbox_controller/README.md new file mode 100644 index 00000000..0f04fb01 --- /dev/null +++ b/modules/xbox_controller/README.md @@ -0,0 +1,224 @@ +# XboxController & ControllerHandler Documentation + +## Overview + +The controller system consists of two main classes: +- `XboxController` (in `modules/xbox_controller.py`): Handles low-level gamepad input, normalizes values, and exposes current input status. +- `ControllerHandler` (in `modules/controller_handler.py`): Consumes input from a controller module, maps it to actions/topics, and publishes events to control the robot. + +Configuration is provided via YAML files, allowing flexible mapping of controller inputs to robot actions, including support for modifiers, scaling, and multiple mapping sets. + +--- + +## XboxController + +**File:** `modules/xbox_controller.py` + +### Purpose +- Reads input from a gamepad (e.g., Xbox controller) via `/dev/input/js0`. +- Normalizes button and axis values. +- Exposes current status for consumption by higher-level modules. + +### Key Features +- Button and axis names are configurable via YAML. +- Normalized status can be queried at any time. +- Designed to be injected into a handler (e.g., `ControllerHandler`). + +--- + +## ControllerHandler + +**File:** `modules/controller_handler.py` + +### Purpose +- Maps controller input to robot actions using a YAML-defined mapping. +- Supports modifier buttons for alternate mapping sets. +- Publishes events to the robot's messaging system. + +### Key Features +- Supports multiple mapping sets (e.g., `default`, `BTN_TL`, `BTN_TR`), selected by holding modifier buttons. +- Each mapping can define multiple actions per input. +- Actions can include topics, arguments, and modifiers (e.g., scaling). + +--- + +## YAML Configuration + +### Example Structure + +```yaml +controller: + enabled: true + path: 'modules.controller_handler.ControllerHandler' + config: + debug: false + modifier_buttons: + - BTN_TL + - BTN_TR + mapping: + default: + BTN_A: + actions: + - topic: 'tts' + args: {msg: 'Hello!'} + ABS_X: + actions: + - topic: 'servo:neck_pan:mv' + args: {} + modifier: + scale: 0.1 + max_delta: 200 + BTN_TL: + BTN_X: + actions: + - topic: 'servo:neck_pan:mv' + args: {delta: 50} +``` + +### Mapping Sets +- The top-level keys under `mapping` (e.g., `default`, `BTN_TL`, `BTN_TR`) define different mapping sets. +- The `default` set is used when no modifier buttons are held. +- If one or more modifier buttons are held, the corresponding set is used (e.g., holding `BTN_TL` uses the `BTN_TL` mapping). + +### Actions List +- Each input (button or axis) maps to an `actions` list. +- Each action is a dictionary with at least a `topic` (the event to publish) and optional `args` (arguments for the topic). +- Example: + ```yaml + BTN_A: + actions: + - topic: 'tts' + args: {msg: 'Hello!'} + ``` + +### Modifiers +- Each action can include a `modifier` dictionary to adjust arguments dynamically. +- Common modifier options: + - `scale`: Multiplies the input value (useful for axes). + - `mapping`: Maps the input value to a new range (e.g., [0, 240]). +- Example: + ```yaml + ABS_X: + actions: + - topic: 'servo:neck_pan:mv' + args: {} + modifier: + scale: 0.1 + - topic: 'servo:neck_pan:mv' + args: {} + modifier: + mapping: [-120, 120] + ``` + +### Overarching Options +- For axes, you can specify an overarching `start` value at the same level as `actions`: + ```yaml + ABS_RY: + start: -1.0 + actions: + - topic: 'servo:leg_r_ankle:mv' + args: {} + ``` +- The handler will use this `start` value to adjust the input before applying thresholds or modifiers. This is useful for inputs where the default start position is not zero. + +--- + +## How It Works + +1. The `XboxController` reads and normalizes input, exposing the current status. +2. The `ControllerHandler` subscribes to a loop event and, on each tick: + - Reads the current status directly from the controller (this dependency is injected after initialization in main.py). + - Determines which mapping set to use based on active modifier buttons. + - For each active input, looks up the corresponding actions. + - Publishes each action's topic with its arguments (applying modifiers as needed). + +--- + +## Adding/Customizing Mappings + +- To add a new button or axis mapping, add a new key under the desired mapping set: + ```yaml + BTN_B: + actions: + - topic: 'tts' + args: {msg: 'Button B pressed!'} + ``` +- To add multiple actions for a single input, add more items to the `actions` list. +- To use scaling or other modifiers, add a `modifier` dictionary to the action. + +--- + +## Tips +- Use `debug: true` in the config to enable verbose logging for troubleshooting. +- Modifier buttons allow for complex control schemes (e.g., holding `BTN_TL` changes the function of other buttons). Multiple buttons can be combined, e.g. `BTN_TL+BTN_TR`. +- Always test your mappings to ensure the robot responds as expected. + + +### Enable controller on ubuntu: + +I'm not sure what helped with this situation, but I had immense difficulty getting bluetooth working for the Xbox Series S/X controller. I had to upgrade the firmware using this tool on windows 10: https://apps.microsoft.com/detail/9nblggh30xj3?hl=en-GB&gl=GB + +In addition, I installed xpadneo: + +``` +sudo apt install dkms linux-headers-$(uname -r) +git clone https://github.com/atar-axis/xpadneo.git +cd xpadneo +sudo ./install.sh +``` + +Disabling ERTM may also have been required: +``` +sudo nano /etc/modprobe.d/bluetooth.conf + +# Add this line and save +options bluetooth disable_ertm=Y + +sudo reboot +``` + +Then use bluetoothctl to test: +``` +sudo bluetoothctl +# Turn on agent and set as default +agent on +default-agent + +# Start scanning for devices +scan on + +# Replace XX:XX:XX:XX:XX:XX with your controller's MAC address +pair XX:XX:XX:XX:XX:XX +trust XX:XX:XX:XX:XX:XX +connect XX:XX:XX:XX:XX:X + +exit +``` + +Further adjustments that may have helped: + +``` +# edit /etc/bluetooth/main.conf +[General] +ControllerMode = dual +JustWorksRepairing = confirm +``` + +The package xboxdrv was also suggested, but I couldn't get that to detect the controller, even once the script was working... + +Testing with `jstest /dev/input/js0` (requires `sudo apt-get install joystick`) showed the controller worked, but I could not get the python modules `inputs` or `pygame` to recognise any input from the controller. + +In the end I modified the module to poll the /dev/input/js0 stream into python. + +--- + +## Related Files +- `modules/xbox_controller.py`: Low-level controller input. +- `modules/controller_handler.py`: Input-to-action mapping and event publishing. +- `config/controller_handler.yml`: Main YAML configuration for mappings. +- `config/controller.yml`: (If used) Additional controller configuration. + +--- + +## See Also +- `BaseModule.md` for base class features and messaging system details. diff --git a/modules/xbox_controller/__init__.py b/modules/xbox_controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/config/controller.yml b/modules/xbox_controller/config.yml old mode 100755 new mode 100644 similarity index 91% rename from config/controller.yml rename to modules/xbox_controller/config.yml index 6832ce27..47cf68d3 --- a/config/controller.yml +++ b/modules/xbox_controller/config.yml @@ -1,6 +1,6 @@ controller: enabled: true - path: 'modules.xbox_controller.XboxController' + path: 'modules.xbox_controller.xbox_controller.XboxController' config: normalised: true debug: false diff --git a/modules/xbox_controller/tests/__init__.py b/modules/xbox_controller/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/xbox_controller/tests/test_xbox_controller.py b/modules/xbox_controller/tests/test_xbox_controller.py new file mode 100644 index 00000000..a57f50ab --- /dev/null +++ b/modules/xbox_controller/tests/test_xbox_controller.py @@ -0,0 +1,41 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch +sys.modules['yaml'] = MagicMock() +from modules.xbox_controller.xbox_controller import XboxController + +class TestXboxController(unittest.TestCase): + + def setUp(self): + self.log_patch = patch('modules.base_module.BaseModule.log', lambda *a, **k: None) + self.log_patch.start() + self.addCleanup(self.log_patch.stop) + self.config = { + 'button_names': {0: 'BTN_A', 1: 'BTN_B'}, + 'axis_names': {0: 'ABS_X', 1: 'ABS_Y'}, + 'axis_min': -32767.0, + 'axis_max': 32767.0, + 'deadzone': 4000, + 'device': '/dev/input/js0', + 'normalised': True, + 'debug': False + } + + def test_init_requires_button_and_axis_names(self): + with self.assertRaises(ValueError): + XboxController(axis_names={}) + with self.assertRaises(ValueError): + XboxController(button_names={}) + + def test_get_name(self): + ctrl = XboxController(**self.config) + self.assertEqual(ctrl._get_name(ctrl.JS_EVENT_BUTTON, 0), 'BTN_A') + self.assertEqual(ctrl._get_name(ctrl.JS_EVENT_AXIS, 0), 'ABS_X') + + def test_button_event(self): + ctrl = XboxController(**self.config) + ctrl._handle_button_event(0, ctrl.JS_EVENT_BUTTON, 1, 0) + self.assertEqual(ctrl.current_status_normalized['BTN_A'], 1.0) + +if __name__ == '__main__': + unittest.main() diff --git a/modules/xbox_controller.py b/modules/xbox_controller/xbox_controller.py similarity index 100% rename from modules/xbox_controller.py rename to modules/xbox_controller/xbox_controller.py diff --git a/test.sh b/test.sh index ab45c77c..8e8104a6 100755 --- a/test.sh +++ b/test.sh @@ -1 +1,2 @@ -python3 -m unittest discover -s tests -p "test_*.py" \ No newline at end of file +python3 -m unittest discover -s tests -p "test_*.py" +python3 -m unittest discover -s modules -p "test_*.py" -t . diff --git a/tests/test_bno055.py b/tests/test_bno055.py index e43ed298..a8c32934 100644 --- a/tests/test_bno055.py +++ b/tests/test_bno055.py @@ -14,7 +14,7 @@ sys.modules['pubsub'] = mock_pubsub sys.modules['pubsub.pub'] = mock_pubsub.pub -from modules.sensor.imu.bno055 import BNO055 +from modules.sensor.imu.bno055.bno055 import BNO055 def make_sensor(**kwargs): @@ -75,7 +75,7 @@ def test_init_test_on_boot(self): def fake_read_data(self_inner): call_count['n'] += 1 - with patch('modules.sensor.imu.bno055.time.sleep', side_effect=StopIteration), \ + with patch('modules.sensor.imu.bno055.bno055.time.sleep', side_effect=StopIteration), \ patch.object(BNO055, 'read_data', fake_read_data): with self.assertRaises(StopIteration): make_sensor(test_on_boot=True) diff --git a/tests/test_controller.py b/tests/test_controller.py index 785bb06a..1c597d18 100755 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch sys.modules['yaml'] = MagicMock() sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from modules.xbox_controller import XboxController +from modules.xbox_controller.xbox_controller import XboxController class TestXboxController(unittest.TestCase): diff --git a/tests/test_input_recorder.py b/tests/test_input_recorder.py index bae721f3..ccd3c9e6 100755 --- a/tests/test_input_recorder.py +++ b/tests/test_input_recorder.py @@ -4,7 +4,7 @@ class TestInputRecorder(unittest.TestCase): def setUp(self): - from modules.input_recorder import InputRecorder + from modules.input_recorder.input_recorder import InputRecorder self.recorder = InputRecorder(animations_dir='/tmp') self.recorder.log = lambda *a, **k: None self.recorder.save_events = MagicMock() diff --git a/tests/test_logwrapper.py b/tests/test_logwrapper.py index 42ab2bff..59903022 100644 --- a/tests/test_logwrapper.py +++ b/tests/test_logwrapper.py @@ -2,11 +2,11 @@ from unittest import mock import os import logging -from modules.logwrapper import LogWrapper +from modules.logwrapper.logwrapper import LogWrapper class TestLogWrapper(unittest.TestCase): - @mock.patch('modules.logwrapper.logging') + @mock.patch('modules.logwrapper.logwrapper.logging') def test_init(self, mock_logging): log_wrapper = LogWrapper(path='/tmp', filename='test.log', log_level='info', cli_level='debug') self.assertEqual(log_wrapper.path, '/tmp') @@ -21,19 +21,19 @@ def test_init(self, mock_logging): datefmt='%Y/%m/%d %I:%M:%S %p' ) - @mock.patch('modules.logwrapper.logging') + @mock.patch('modules.logwrapper.logwrapper.logging') def test_log(self, mock_logging): log_wrapper = LogWrapper(path='/tmp', filename='test.log', log_level='info', cli_level='debug') log_wrapper.log('Test message', type='info') mock_logging.log.assert_called_with(logging.INFO, 'Test message') - @mock.patch('modules.logwrapper.logging') + @mock.patch('modules.logwrapper.logwrapper.logging') def test_log_json_message(self, mock_logging): log_wrapper = LogWrapper(path='/tmp', filename='test.log', log_level='info', cli_level='debug') log_wrapper.log('{"message": "Test JSON message"}', type='info') mock_logging.log.assert_called_with(logging.INFO, 'Test JSON message') - @mock.patch('modules.logwrapper.logging') + @mock.patch('modules.logwrapper.logwrapper.logging') def test_log_with_translator(self, mock_logging): mock_translator = mock.Mock() mock_translator.request.return_value = 'Translated message' @@ -42,7 +42,7 @@ def test_log_with_translator(self, mock_logging): mock_translator.request.assert_called_with('Test message') mock_logging.log.assert_called_with(logging.INFO, 'Translated message') - @mock.patch('modules.logwrapper.logging') + @mock.patch('modules.logwrapper.logwrapper.logging') def test_setup_messaging(self, mock_logging): log_wrapper = LogWrapper(path='/tmp', filename='test.log', log_level='info', cli_level='debug') with mock.patch.object(log_wrapper, 'subscribe') as mock_subscribe: diff --git a/tests/test_module_loader.py b/tests/test_module_loader.py index 47645b94..f9802b27 100644 --- a/tests/test_module_loader.py +++ b/tests/test_module_loader.py @@ -19,12 +19,12 @@ class TestModuleLoader(unittest.TestCase): """Test cases for ModuleLoader class""" def setUp(self): - self.patcher_os_listdir = patch('os.listdir') + self.patcher_os_walk = patch('os.walk') self.patcher_open = patch('builtins.open', create=True) self.patcher_yaml = patch('yaml.safe_load') self.patcher_importlib_spec = patch('importlib.util.spec_from_file_location') self.patcher_importlib_mod = patch('importlib.util.module_from_spec') - self.mock_listdir = self.patcher_os_listdir.start() + self.mock_walk = self.patcher_os_walk.start() self.mock_open = self.patcher_open.start() self.mock_yaml = self.patcher_yaml.start() self.mock_spec = self.patcher_importlib_spec.start() @@ -47,58 +47,58 @@ def _setup_module_config(self, enabled=True, env=None, name='TestModule', path=' def test_initialization_default_env(self): # Should default to 'robot' environment - self.mock_listdir.return_value = [] + self.mock_walk.return_value = [] from module_loader import ModuleLoader loader = ModuleLoader(config_folder='config') self.assertEqual(loader.environment, 'robot') def test_load_yaml_files_enabled_and_env_string(self): # Only enabled modules with matching env are loaded - self.mock_listdir.return_value = ['mod1.yml'] + self.mock_walk.return_value = [('modules/test', [], ['config.yml'])] mock_file = MagicMock() self.mock_open.return_value.__enter__.return_value = mock_file self.mock_yaml.return_value = self._setup_module_config(enabled=True, env='robot') from module_loader import ModuleLoader - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') modules = loader.load_yaml_files() self.assertEqual(len(modules), 1) # Should skip if env does not match self.mock_yaml.return_value = self._setup_module_config(enabled=True, env='dev') - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') modules = loader.load_yaml_files() self.assertEqual(len(modules), 0) def test_load_yaml_files_env_list(self): - self.mock_listdir.return_value = ['mod1.yml'] + self.mock_walk.return_value = [('modules/test', [], ['config.yml'])] mock_file = MagicMock() self.mock_open.return_value.__enter__.return_value = mock_file self.mock_yaml.return_value = self._setup_module_config(enabled=True, env=['robot', 'dev']) from module_loader import ModuleLoader - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') modules = loader.load_yaml_files() self.assertEqual(len(modules), 1) - loader = ModuleLoader(config_folder='config', environment='other') + loader = ModuleLoader(config_folder='modules', environment='other') modules = loader.load_yaml_files() self.assertEqual(len(modules), 0) def test_load_yaml_files_disabled(self): - self.mock_listdir.return_value = ['mod1.yml'] + self.mock_walk.return_value = [('modules/test', [], ['config.yml'])] mock_file = MagicMock() self.mock_open.return_value.__enter__.return_value = mock_file self.mock_yaml.return_value = self._setup_module_config(enabled=False) from module_loader import ModuleLoader - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') modules = loader.load_yaml_files() self.assertEqual(len(modules), 0) def test_load_yaml_files_yaml_error(self): import yaml - self.mock_listdir.return_value = ['mod1.yml'] + self.mock_walk.return_value = [('modules/test', [], ['config.yml'])] mock_file = MagicMock() self.mock_open.return_value.__enter__.return_value = mock_file self.mock_yaml.side_effect = yaml.YAMLError('YAML error') from module_loader import ModuleLoader - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') # Should not raise, just print error modules = loader.load_yaml_files() self.assertEqual(modules, []) @@ -106,7 +106,7 @@ def test_load_yaml_files_yaml_error(self): def test_load_modules_success_and_instance_naming(self): # Setup for dynamic loading - self.mock_listdir.return_value = ['mod1.yml'] + self.mock_walk.return_value = [('modules/test', [], ['config.yml'])] mock_file = MagicMock() self.mock_open.return_value.__enter__.return_value = mock_file # One module, with two instances @@ -127,7 +127,7 @@ def __init__(self, **kwargs): setattr(mock_mod, 'TestModule', Dummy) mock_spec.loader.exec_module.side_effect = lambda mod: mod.__setattr__('TestModule', Dummy) from module_loader import ModuleLoader - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') loader.modules = loader.load_yaml_files() # Patch module_from_spec to return our mock_mod self.mock_mod.return_value = mock_mod @@ -141,7 +141,7 @@ def __init__(self, **kwargs): self.assertEqual(instances_dict['TestModule_foo'].kwargs['name'], 'foo') def test_load_modules_no_config(self): - self.mock_listdir.return_value = ['mod1.yml'] + self.mock_walk.return_value = [('modules/test', [], ['config.yml'])] mock_file = MagicMock() self.mock_open.return_value.__enter__.return_value = mock_file # config is None @@ -155,7 +155,7 @@ def __init__(self, **kwargs): setattr(mock_mod, 'TestModule', Dummy) mock_spec.loader.exec_module.side_effect = lambda mod: mod.__setattr__('TestModule', Dummy) from module_loader import ModuleLoader - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') loader.modules = loader.load_yaml_files() with patch('importlib.util.module_from_spec', return_value=mock_mod): instances_dict = loader.load_modules() @@ -164,7 +164,7 @@ def __init__(self, **kwargs): self.assertIsInstance(instances_dict['TestModule_TestModule'], Dummy) def test_load_modules_import_error(self): - self.mock_listdir.return_value = ['mod1.yml'] + self.mock_walk.return_value = [('modules/test', [], ['config.yml'])] mock_file = MagicMock() self.mock_open.return_value.__enter__.return_value = mock_file self.mock_yaml.return_value = self._setup_module_config(enabled=True) @@ -177,7 +177,7 @@ def raise_import(mod): raise Exception('Import error') mock_spec.loader.exec_module.side_effect = raise_import from module_loader import ModuleLoader - loader = ModuleLoader(config_folder='config', environment='robot') + loader = ModuleLoader(config_folder='modules', environment='robot') loader.modules = loader.load_yaml_files() with patch('importlib.util.module_from_spec', return_value=mock_mod): # Should not raise, just print error diff --git a/tests/test_pitemperature.py b/tests/test_pitemperature.py index 759cbc66..4c06445a 100644 --- a/tests/test_pitemperature.py +++ b/tests/test_pitemperature.py @@ -1,15 +1,15 @@ import unittest from unittest import mock -from modules.pitemperature import PiTemperature +from modules.pitemperature.pitemperature import PiTemperature class TestPiTemperature(unittest.TestCase): - - @mock.patch('modules.pitemperature.os.popen') + @mock.patch('modules.pitemperature.pitemperature.os.popen') def test_monitor_critical_temp(self, mock_popen): mock_popen.return_value.readline.return_value = "temp=86'C" temp_module = PiTemperature() - temp_module.messaging_service = mock.MagicMock() + with mock.patch('modules.pitemperature.pitemperature.threading.Thread'): + temp_module.messaging_service = mock.MagicMock() with mock.patch.object(temp_module, 'publish') as mock_publish, \ mock.patch.object(temp_module, 'log') as mock_log: temp_module.monitor() @@ -20,11 +20,12 @@ def test_monitor_critical_temp(self, mock_popen): mock_log.assert_called_with('Temperature is critical: 86°C', 'critical') - @mock.patch('modules.pitemperature.os.popen') + @mock.patch('modules.pitemperature.pitemperature.os.popen') def test_monitor_high_temp(self, mock_popen): mock_popen.return_value.readline.return_value = "temp=81'C" temp_module = PiTemperature() - temp_module.messaging_service = mock.MagicMock() + with mock.patch('modules.pitemperature.pitemperature.threading.Thread'): + temp_module.messaging_service = mock.MagicMock() with mock.patch.object(temp_module, 'publish') as mock_publish, \ mock.patch.object(temp_module, 'log') as mock_log: temp_module.monitor() @@ -35,11 +36,12 @@ def test_monitor_high_temp(self, mock_popen): mock_log.assert_called_with('Temperature is high: 81°C', 'warning') - @mock.patch('modules.pitemperature.os.popen') + @mock.patch('modules.pitemperature.pitemperature.os.popen') def test_monitor_normal_temp(self, mock_popen): mock_popen.return_value.readline.return_value = "temp=50'C" temp_module = PiTemperature() - temp_module.messaging_service = mock.MagicMock() + with mock.patch('modules.pitemperature.pitemperature.threading.Thread'): + temp_module.messaging_service = mock.MagicMock() with mock.patch.object(temp_module, 'publish') as mock_publish, \ mock.patch.object(temp_module, 'log') as mock_log: temp_module.monitor() @@ -49,12 +51,13 @@ def test_monitor_normal_temp(self, mock_popen): # Should log only the debug message mock_log.assert_called_with('Temperature: 50°C', 'debug') - @mock.patch('modules.pitemperature.os.popen') - @mock.patch('modules.pitemperature.sys.exit') + @mock.patch('modules.pitemperature.pitemperature.os.popen') + @mock.patch('modules.pitemperature.pitemperature.sys.exit') def test_monitor_shutdown_temp(self, mock_exit, mock_popen): mock_popen.return_value.readline.return_value = "temp=91'C" temp_module = PiTemperature() - temp_module.messaging_service = mock.MagicMock() + with mock.patch('modules.pitemperature.pitemperature.threading.Thread'): + temp_module.messaging_service = mock.MagicMock() with mock.patch.object(temp_module, 'publish') as mock_publish, \ mock.patch.object(temp_module, 'log') as mock_log: temp_module.monitor() diff --git a/tests/test_sensor.py b/tests/test_sensor.py index d90011ab..7be111a7 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -5,7 +5,7 @@ mock_gpiozero = mock.MagicMock() mock_gpiozero.MotionSensor = mock.MagicMock() with mock.patch.dict('sys.modules', {'gpiozero': mock_gpiozero}): - from modules.gpio.motion import Motion as Sensor + from modules.gpio.motion.motion import Motion as Sensor class TestSensor(unittest.TestCase): From 98ececaf08586e2d9127124b7f4451e5fa06b40c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:41:05 +0000 Subject: [PATCH 03/18] Add enabled.yml, infer module paths, integrate DiscordBot from develop Co-authored-by: danic85 <6583012+danic85@users.noreply.github.com> --- module_loader.py | 82 +- .../bus_servo/{servo.py => bus_servo.py} | 0 modules/actuators/bus_servo/config.yml | 279 +- .../bus_servo/tests/test_bus_servo.py | 2 +- modules/actuators/piservo/config.yml | 19 +- modules/actuators/servo/config.yml | 127 +- modules/animate/config.yml | 4 +- modules/audio/braillespeak/config.yml | 5 +- modules/audio/buzzer/config.yml | 7 +- modules/audio/speechinput/config.yml | 24 +- modules/audio/ttsmodule/config.yml | 20 +- modules/chatgpt/config.yml | 17 +- modules/controller_handler/config.yml | 232 +- modules/display/tft_display_eye/config.yml | 18 +- modules/display/waveshare_oled/config.yml | 16 +- modules/enabled.yml | 117 + modules/gpio/laser/config.yml | 8 +- modules/gpio/motion/config.yml | 6 +- modules/gpio/read/config.yml | 7 +- modules/i2c_servo/config.yml | 7 +- modules/input_recorder/config.yml | 3 +- modules/logwrapper/config.yml | 7 +- modules/neopixel/emotion_analysis/config.yml | 4659 ++++++++--------- modules/neopixel/neopx/config.yml | 69 +- modules/network/arduinoserial/config.yml | 7 +- modules/network/discordbot/README.md | 202 + modules/network/discordbot/__init__.py | 0 modules/network/discordbot/config.yml | 27 + modules/network/discordbot/discordbot.py | 393 ++ modules/network/discordbot/tests/__init__.py | 0 .../discordbot/tests/test_discordbot.py | 644 +++ modules/network/messaging_service/config.yml | 14 +- modules/network/rtlsdr/config.yml | 9 +- modules/network/telegrambot/config.yml | 7 +- modules/personality/config.yml | 7 +- modules/pitemperature/config.yml | 4 +- modules/sensor/imu/bno055/config.yml | 22 +- modules/sensor/imu/mpu6050/config.yml | 5 +- modules/translator/config.yml | 9 +- modules/vision/imx500/tracking/config.yml | 7 +- modules/vision/imx500/vision/config.yml | 10 +- modules/vision/opencv/vision/config.yml | 3 +- modules/xbox_controller/config.yml | 54 +- tests/test_discordbot.py | 644 +++ tests/test_module_loader.py | 73 +- 45 files changed, 4992 insertions(+), 2884 deletions(-) rename modules/actuators/bus_servo/{servo.py => bus_servo.py} (100%) create mode 100644 modules/enabled.yml create mode 100644 modules/network/discordbot/README.md create mode 100644 modules/network/discordbot/__init__.py create mode 100644 modules/network/discordbot/config.yml create mode 100644 modules/network/discordbot/discordbot.py create mode 100644 modules/network/discordbot/tests/__init__.py create mode 100644 modules/network/discordbot/tests/test_discordbot.py create mode 100644 tests/test_discordbot.py diff --git a/module_loader.py b/module_loader.py index e03b1446..49667368 100755 --- a/module_loader.py +++ b/module_loader.py @@ -8,51 +8,76 @@ def __init__(self, config_folder='modules', environment=None): """ ModuleLoader class :param config_folder: root folder to search for module config.yml files (searched recursively) - + Each module lives in its own directory containing: - - .py (Python implementation) - - config.yml (module configuration) + - .py (Python implementation, filename matches directory name) + - config.yml (module configuration with 'class' key for the Python class name) - README.md (documentation) - tests/ (unit tests) - - Example config.yml: + + Which modules are enabled is controlled by a single file: + /enabled.yml + + Example module config.yml: --- buzzer: - enabled: true # Required - path: "modules.audio.buzzer.buzzer.Buzzer" # Required - config: # Passed as **kwargs to the module's __init__ method + class: Buzzer # Python class name (required) + config: # Passed as **kwargs to __init__ (optional) pin: 27 name: 'buzzer' - + + Example enabled.yml: + --- + buzzer: + enabled: true + messaging_service: + enabled: true + Example: loader = ModuleLoader() modules = loader.load_modules() - + Reference module once loaded: - translator_inst = modules['Translator'] + translator_inst = modules['Translator'] """ self.config_folder = config_folder self.environment = environment or 'robot' print(f"[ModuleLoader] Loading modules for environment: {self.environment}") self.modules = self.load_yaml_files() + def _load_enabled_config(self): + """Load enabled.yml from the config folder to determine which modules are active.""" + enabled_file = os.path.join(self.config_folder, 'enabled.yml') + if not os.path.exists(enabled_file): + return {} + with open(enabled_file, 'r') as f: + try: + return yaml.safe_load(f) or {} + except yaml.YAMLError as e: + print(f"Error loading {enabled_file}: {e}") + return {} + def load_yaml_files(self): - """Recursively search config_folder for config.yml files and load module configurations.""" + """Recursively search config_folder for config.yml files and load enabled module configurations.""" + enabled = self._load_enabled_config() + config_files = [] for root, dirs, files in os.walk(self.config_folder): for f in files: if f == 'config.yml': config_files.append(os.path.join(root, f)) + loaded_modules = [] for file_path in config_files: with open(file_path, 'r') as stream: try: config = yaml.safe_load(stream) for module_name, module_config in config.items(): - if not module_config.get('enabled', False): + # Enabled status and environment come from enabled.yml + enabled_entry = enabled.get(module_name, {}) + if not enabled_entry.get('enabled', False): continue - # If 'environment' is specified, filter by self.environment - env_field = module_config.get('environment') + env_field = enabled_entry.get('environment') print(f"[ModuleLoader] Found module: {module_name} with environment filter: {env_field}") if env_field is not None: if isinstance(env_field, str): @@ -61,6 +86,11 @@ def load_yaml_files(self): elif isinstance(env_field, list): if self.environment not in env_field: continue + # Infer the Python file path from the config.yml directory + dir_path = os.path.dirname(file_path) + dir_name = os.path.basename(dir_path) + module_config['_py_file'] = os.path.join(dir_path, f"{dir_name}.py") + module_config['_class'] = module_config.get('class') loaded_modules.append(module_config) except yaml.YAMLError as e: print(f"Error loading {file_path}: {e}") @@ -68,10 +98,7 @@ def load_yaml_files(self): def set_messaging_service(self, module_instances, messaging_service): """Set the messaging service for the modules.""" - # Iterate through the module instances, extract name and module for name, module in module_instances.items(): - # get module name from object - # if module is not messaging_service: if 'MessagingService' in name: continue module.messaging_service = messaging_service @@ -80,33 +107,32 @@ def load_modules(self): """Dynamically load and instantiate the modules based on the config.""" instances = {} # Use a dictionary to store instances for easy access for module in self.modules: - print(f"Enabling {module['path']}") - module_path = module['path'].rsplit('.', 1)[0].replace('.', '/') - module_name = module['path'].split('.')[-1] + py_file = module['_py_file'] + class_name = module['_class'] + print(f"Enabling {class_name} from {py_file}") instances_config = module.get('instances', [module.get('config')]) shared_config = module.get('config', {}) if instances_config[0] is None: instances_config = [{}] - # Dynamically load the module - spec = importlib.util.spec_from_file_location(module_name, f"{module_path}.py") + # Dynamically load the module from its inferred file path + spec = importlib.util.spec_from_file_location(class_name, py_file) mod = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(mod) except Exception as e: - print(f"Error loading module {module_name}: {e}") + print(f"Error loading module {class_name}: {e}") - # If multiple instances, pass shared config to each instance + # If multiple instances, merge shared config into each instance config multiple_instances = 'instances' in module and isinstance(module['instances'], list) and len(module['instances']) > 0 for instance_config in instances_config: - # Merge shared config if multiple instances if multiple_instances: # Avoid overwriting instance-specific keys with shared config merged_config = {**shared_config, **instance_config} else: merged_config = instance_config - instance_name = module_name + '_' + instance_config.get('name') if instance_config.get('name') is not None else module_name - instance = getattr(mod, module_name)(**merged_config) + instance_name = class_name + '_' + instance_config.get('name') if instance_config.get('name') is not None else class_name + instance = getattr(mod, class_name)(**merged_config) instances[instance_name] = instance print("All modules loaded") diff --git a/modules/actuators/bus_servo/servo.py b/modules/actuators/bus_servo/bus_servo.py similarity index 100% rename from modules/actuators/bus_servo/servo.py rename to modules/actuators/bus_servo/bus_servo.py diff --git a/modules/actuators/bus_servo/config.yml b/modules/actuators/bus_servo/config.yml index d15c4653..328707d3 100644 --- a/modules/actuators/bus_servo/config.yml +++ b/modules/actuators/bus_servo/config.yml @@ -1,105 +1,180 @@ -servos: - enabled: true - environment: robot - path: "modules.actuators.bus_servo.servo.Servo" # Include class name here +bus_servo: config: poses: - - legs_forward: {'leg_r_tilt': 2806, 'leg_r_hip': 1989, 'leg_r_knee': 1349, 'leg_r_ankle': 2754, 'neck_pan': 1698, 'neck_tilt': 118, 'leg_l_tilt': 1619, 'leg_l_hip': 1028, 'leg_l_knee': 2634, 'leg_l_ankle': 1567} - - sit: {'leg_r_tilt': 2806, 'leg_r_hip': 2063, 'leg_r_knee': 1840, 'leg_r_ankle': 3641, 'neck_pan': 1697, 'neck_tilt': 118, 'leg_l_tilt': 1621, 'leg_l_hip': 853, 'leg_l_knee': 2161, 'leg_l_ankle': 614} - - wave_1: {'leg_r_tilt': 2809, 'leg_r_hip': 2063, 'leg_r_knee': 1840, 'leg_r_ankle': 3641, 'neck_pan': 1697, 'neck_tilt': 118, 'leg_l_tilt': 1618, 'leg_l_hip': 855, 'leg_l_knee': 2614, 'leg_l_ankle': 1337} - - wave_2: {'leg_r_tilt': 2807, 'leg_r_hip': 2063, 'leg_r_knee': 1840, 'leg_r_ankle': 3641, 'neck_pan': 1697, 'neck_tilt': 118, 'leg_l_tilt': 1618, 'leg_l_hip': 855, 'leg_l_knee': 2614, 'leg_l_ankle': 874} - - stand_low: {'leg_r_tilt': 2790, 'leg_r_hip': 422, 'leg_r_knee': 717, 'leg_r_ankle': 3174, 'neck_pan': 1697, 'neck_tilt': 119, 'leg_l_tilt': 1578, 'leg_l_hip': 2668, 'leg_l_knee': 3342, 'leg_l_ankle': 1107} - - stand_high: {'leg_r_tilt': 2790, 'leg_r_hip': 692, 'leg_r_knee': 1322, 'leg_r_ankle': 2861, 'neck_pan': 1700, 'neck_tilt': 123, 'leg_l_tilt': 1578, 'leg_l_hip': 2510, 'leg_l_knee': 2712, 'leg_l_ankle': 1560} - - sit_edge: {'leg_r_tilt': 2790, 'leg_r_hip': 1918, 'leg_r_knee': 2768, 'leg_r_ankle': 2854, 'neck_pan': 1704, 'neck_tilt': 124, 'leg_l_tilt': 1577, 'leg_l_hip': 1183, 'leg_l_knee': 1332, 'leg_l_ankle': 1562} - - sit_edge_swing_l: {'leg_r_tilt': 2790, 'leg_r_hip': 1918, 'leg_r_knee': 2768, 'leg_r_ankle': 2854, 'neck_pan': 1704, 'neck_tilt': 124, 'leg_l_tilt': 1577, 'leg_l_hip': 1185, 'leg_l_knee': 1789, 'leg_l_ankle': 1988} - - sit_edge_swing_r: {'leg_r_tilt': 2790, 'leg_r_hip': 1918, 'leg_r_knee': 2222, 'leg_r_ankle': 2496, 'neck_pan': 1704, 'neck_tilt': 124, 'leg_l_tilt': 1577, 'leg_l_hip': 1182, 'leg_l_knee': 1345, 'leg_l_ankle': 1511} + - legs_forward: + leg_r_tilt: 2806 + leg_r_hip: 1989 + leg_r_knee: 1349 + leg_r_ankle: 2754 + neck_pan: 1698 + neck_tilt: 118 + leg_l_tilt: 1619 + leg_l_hip: 1028 + leg_l_knee: 2634 + leg_l_ankle: 1567 + - sit: + leg_r_tilt: 2806 + leg_r_hip: 2063 + leg_r_knee: 1840 + leg_r_ankle: 3641 + neck_pan: 1697 + neck_tilt: 118 + leg_l_tilt: 1621 + leg_l_hip: 853 + leg_l_knee: 2161 + leg_l_ankle: 614 + - wave_1: + leg_r_tilt: 2809 + leg_r_hip: 2063 + leg_r_knee: 1840 + leg_r_ankle: 3641 + neck_pan: 1697 + neck_tilt: 118 + leg_l_tilt: 1618 + leg_l_hip: 855 + leg_l_knee: 2614 + leg_l_ankle: 1337 + - wave_2: + leg_r_tilt: 2807 + leg_r_hip: 2063 + leg_r_knee: 1840 + leg_r_ankle: 3641 + neck_pan: 1697 + neck_tilt: 118 + leg_l_tilt: 1618 + leg_l_hip: 855 + leg_l_knee: 2614 + leg_l_ankle: 874 + - stand_low: + leg_r_tilt: 2790 + leg_r_hip: 422 + leg_r_knee: 717 + leg_r_ankle: 3174 + neck_pan: 1697 + neck_tilt: 119 + leg_l_tilt: 1578 + leg_l_hip: 2668 + leg_l_knee: 3342 + leg_l_ankle: 1107 + - stand_high: + leg_r_tilt: 2790 + leg_r_hip: 692 + leg_r_knee: 1322 + leg_r_ankle: 2861 + neck_pan: 1700 + neck_tilt: 123 + leg_l_tilt: 1578 + leg_l_hip: 2510 + leg_l_knee: 2712 + leg_l_ankle: 1560 + - sit_edge: + leg_r_tilt: 2790 + leg_r_hip: 1918 + leg_r_knee: 2768 + leg_r_ankle: 2854 + neck_pan: 1704 + neck_tilt: 124 + leg_l_tilt: 1577 + leg_l_hip: 1183 + leg_l_knee: 1332 + leg_l_ankle: 1562 + - sit_edge_swing_l: + leg_r_tilt: 2790 + leg_r_hip: 1918 + leg_r_knee: 2768 + leg_r_ankle: 2854 + neck_pan: 1704 + neck_tilt: 124 + leg_l_tilt: 1577 + leg_l_hip: 1185 + leg_l_knee: 1789 + leg_l_ankle: 1988 + - sit_edge_swing_r: + leg_r_tilt: 2790 + leg_r_hip: 1918 + leg_r_knee: 2222 + leg_r_ankle: 2496 + neck_pan: 1704 + neck_tilt: 124 + leg_l_tilt: 1577 + leg_l_hip: 1182 + leg_l_knee: 1345 + leg_l_ankle: 1511 instances: - - name: "leg_r_tilt" - model: 'ST3215' - id: 1 - range: [2511, 3944] # 1433 total range (4095 = 360 degrees) - range_degrees: 125.9 - start: 2810 - # calibrate_on_boot: true - - name: "leg_r_hip" - model: 'ST3215' - id: 3 - range: [0, 2063] - range_degrees: 181.3 - # calibrate_on_boot: true - # start: 1688 - - name: "leg_r_knee" - model: 'ST3215' - id: 2 - range: [717, 3297] - range_degrees: 226.2 - # start: 1345 - - name: "leg_r_ankle" - model: 'ST3215' - id: 6 - range: [2459, 3872] - range_degrees: 123.7 - # start: 2762 - - name: "neck_pan" - model: 'ST3215' - id: 12 - range: [0, 3412] # 300 degrees = 3412, 360 degrees = 4095 - range_degrees: 299.7 - start: 1706 - # calibrate_on_boot: true - # demonstrate_on_boot: true - # - name: "neck_pan" - # model: 'SC09' - # baudrate: 115200 - # id: 4 - # range: [0, 1023] - # range_degrees: 300 - # start: 511 - # calibrate_on_boot: true - # demonstrate_on_boot: true - # speed: 60 # tested up to 600 - - name: "neck_tilt" - model: 'SC09' - baudrate: 115200 - id: 11 - range: [30, 220] - range_degrees: 55.6 - start: 120 - speed: 60 # max=0, 1 - 3000 tested - # acceleration: 0 # max (no significant difference between this and 1 - 3000) - # demonstrate_on_boot: true - # - name: "neck_tilt2" - # model: 'SC09' - # baudrate: 115200 - # id: 5 - # range: [0, 610] # 180 degree (may need reducing) - # start: 305 - # speed: 300 - # acceleration: 1 - # demonstrate_on_boot: true - - name: "leg_l_tilt" - model: 'ST3215' - id: 7 - range: [8, 1796] # 1788 total range (4095 = 360 degrees) - range_degrees: 156.9 - start: 1617 - # calibrate_on_boot: true - - name: "leg_l_hip" - model: 'ST3215' - id: 8 - range: [853, 3098] - range_degrees: 197.3 - # start: 2451 - - name: "leg_l_knee" - model: 'ST3215' - id: 9 - range: [1332, 3404] - range_degrees: 181.1 - # start: 2643 - - name: "leg_l_ankle" - model: 'ST3215' - id: 10 - range: [614, 2270] - range_degrees: 145.2 - # start: 1566 - # demonstrate_on_boot: true \ No newline at end of file + - name: leg_r_tilt + model: ST3215 + id: 1 + range: + - 2511 + - 3944 + range_degrees: 125.9 + start: 2810 + - name: leg_r_hip + model: ST3215 + id: 3 + range: + - 0 + - 2063 + range_degrees: 181.3 + - name: leg_r_knee + model: ST3215 + id: 2 + range: + - 717 + - 3297 + range_degrees: 226.2 + - name: leg_r_ankle + model: ST3215 + id: 6 + range: + - 2459 + - 3872 + range_degrees: 123.7 + - name: neck_pan + model: ST3215 + id: 12 + range: + - 0 + - 3412 + range_degrees: 299.7 + start: 1706 + - name: neck_tilt + model: SC09 + baudrate: 115200 + id: 11 + range: + - 30 + - 220 + range_degrees: 55.6 + start: 120 + speed: 60 + - name: leg_l_tilt + model: ST3215 + id: 7 + range: + - 8 + - 1796 + range_degrees: 156.9 + start: 1617 + - name: leg_l_hip + model: ST3215 + id: 8 + range: + - 853 + - 3098 + range_degrees: 197.3 + - name: leg_l_knee + model: ST3215 + id: 9 + range: + - 1332 + - 3404 + range_degrees: 181.1 + - name: leg_l_ankle + model: ST3215 + id: 10 + range: + - 614 + - 2270 + range_degrees: 145.2 + class: Servo diff --git a/modules/actuators/bus_servo/tests/test_bus_servo.py b/modules/actuators/bus_servo/tests/test_bus_servo.py index 68e2d5e4..9e90d3dc 100644 --- a/modules/actuators/bus_servo/tests/test_bus_servo.py +++ b/modules/actuators/bus_servo/tests/test_bus_servo.py @@ -8,7 +8,7 @@ sys.modules['modules.actuators.bus_servo.STservo_sdk'] = mock_st_sdk sys.modules['modules.actuators.bus_servo.SCservo_sdk'] = mock_sc_sdk -from modules.actuators.bus_servo.servo import Servo +from modules.actuators.bus_servo.bus_servo import Servo class TestBusServo(unittest.TestCase): diff --git a/modules/actuators/piservo/config.yml b/modules/actuators/piservo/config.yml index 044f810a..a7bcc131 100644 --- a/modules/actuators/piservo/config.yml +++ b/modules/actuators/piservo/config.yml @@ -1,13 +1,14 @@ piservo: - enabled: false - path: modules.actuators.piservo.piservo.PiServo instances: - - name: "ear" - id: 0 - pin: 22 - range: [-40, 40] - start: 0 - test_on_boot: false + - name: ear + id: 0 + pin: 22 + range: + - -40 + - 40 + start: 0 + test_on_boot: false dependencies: python: - - gpiozero + - gpiozero + class: PiServo diff --git a/modules/actuators/servo/config.yml b/modules/actuators/servo/config.yml index 364f0e4b..2145ed8b 100644 --- a/modules/actuators/servo/config.yml +++ b/modules/actuators/servo/config.yml @@ -1,57 +1,76 @@ -servos: - enabled: false - path: "modules.actuators.servo.servo.Servo" # Include class name here +servo: instances: - - name: "leg_l_hip" - id: 0 - pin: 9 - range: [0, 180] - start: 40 - - name: "leg_l_knee" - id: 1 - pin: 10 - range: [0, 180] - start: 10 - - name: "leg_l_ankle" - id: 2 - pin: 11 - range: [5, 165] - start: 100 - - name: "leg_r_hip" - id: 3 - pin: 6 - range: [0, 180] - start: 60 - - name: "leg_r_knee" - id: 4 - pin: 7 - range: [0, 180] - start: 90 - - name: "leg_r_ankle" - id: 5 - pin: 8 - range: [10, 180] - start: 0 - - name: "tilt" - id: 6 - pin: 2 - range: [60, 120] - start: 50 - - name: "pan" - id: 7 - pin: 3 - range: [30, 150] - start: 50 - - name: "hip_l" - id: 8 - pin: 4 - range: [0, 180] - start: 100 - - name: "hip_r" - id: 9 - pin: 5 - range: [0, 180] - start: 0 + - name: leg_l_hip + id: 0 + pin: 9 + range: + - 0 + - 180 + start: 40 + - name: leg_l_knee + id: 1 + pin: 10 + range: + - 0 + - 180 + start: 10 + - name: leg_l_ankle + id: 2 + pin: 11 + range: + - 5 + - 165 + start: 100 + - name: leg_r_hip + id: 3 + pin: 6 + range: + - 0 + - 180 + start: 60 + - name: leg_r_knee + id: 4 + pin: 7 + range: + - 0 + - 180 + start: 90 + - name: leg_r_ankle + id: 5 + pin: 8 + range: + - 10 + - 180 + start: 0 + - name: tilt + id: 6 + pin: 2 + range: + - 60 + - 120 + start: 50 + - name: pan + id: 7 + pin: 3 + range: + - 30 + - 150 + start: 50 + - name: hip_l + id: 8 + pin: 4 + range: + - 0 + - 180 + start: 100 + - name: hip_r + id: 9 + pin: 5 + range: + - 0 + - 180 + start: 0 dependencies: python: - - pigpio \ No newline at end of file + - pigpio + class: Servo diff --git a/modules/animate/config.yml b/modules/animate/config.yml index edec2ad3..eea7b08f 100644 --- a/modules/animate/config.yml +++ b/modules/animate/config.yml @@ -1,4 +1,2 @@ animate: - enabled: true - environment: 'robot' - path: modules.animate.animate.Animate \ No newline at end of file + class: Animate diff --git a/modules/audio/braillespeak/config.yml b/modules/audio/braillespeak/config.yml index 1e4ed653..3fb20c16 100644 --- a/modules/audio/braillespeak/config.yml +++ b/modules/audio/braillespeak/config.yml @@ -1,5 +1,4 @@ braillespeak: - enabled: false - path: modules.audio.braillespeak.braillespeak.BrailleSpeak config: - pin: 27 \ No newline at end of file + pin: 27 + class: BrailleSpeak diff --git a/modules/audio/buzzer/config.yml b/modules/audio/buzzer/config.yml index 067212b0..b58c4ece 100644 --- a/modules/audio/buzzer/config.yml +++ b/modules/audio/buzzer/config.yml @@ -1,9 +1,8 @@ buzzer: - enabled: false - path: "modules.audio.buzzer.buzzer.Buzzer" config: pin: 27 - name: 'buzzer' + name: buzzer dependencies: python: - - gpiozero + - gpiozero + class: Buzzer diff --git a/modules/audio/speechinput/config.yml b/modules/audio/speechinput/config.yml index 12a1d8a8..b5900864 100644 --- a/modules/audio/speechinput/config.yml +++ b/modules/audio/speechinput/config.yml @@ -1,23 +1,15 @@ speechinput: - enabled: false - path: modules.audio.speechinput.speechinput.SpeechInput config: - device_name: "pulse" + device_name: pulse start_on_boot: false dependencies: unix: - - flac - - python3-pyaudio - # - matplotlib - # - scipy - - ladspa-sdk + - flac + - python3-pyaudio + - ladspa-sdk python: - - SpeechRecognition - # - adafruit-python-shell - # - libportaudio0 - # - libportaudio2 - # - libportaudiocpp0 - # - portaudio19-dev + - SpeechRecognition additional: - - installers/install-mics.sh - - installers/install-speech-recognition.sh \ No newline at end of file + - installers/install-mics.sh + - installers/install-speech-recognition.sh + class: SpeechInput diff --git a/modules/audio/ttsmodule/config.yml b/modules/audio/ttsmodule/config.yml index 7a22caa0..36be8926 100644 --- a/modules/audio/ttsmodule/config.yml +++ b/modules/audio/ttsmodule/config.yml @@ -1,17 +1,15 @@ ttsmodule: - enabled: true - environment: robot - path: "modules.audio.ttsmodule.ttsmodule.TTSModule" config: - service: "elevenlabs" # or pyttsx3 - voice_id: "pMsXgVXv3BLzUgSXRplE" + service: elevenlabs + voice_id: pMsXgVXv3BLzUgSXRplE dependencies: python: - - pyttsx3 - - elevenlabs + - pyttsx3 + - elevenlabs unix: - - espeak - - ffmpeg - - libespeak1 + - espeak + - ffmpeg + - libespeak1 additional: - - installers/install-amp.sh \ No newline at end of file + - installers/install-amp.sh + class: TTSModule diff --git a/modules/chatgpt/config.yml b/modules/chatgpt/config.yml index f1d43486..756ef472 100644 --- a/modules/chatgpt/config.yml +++ b/modules/chatgpt/config.yml @@ -1,16 +1,13 @@ chatgpt: - enabled: true - path: 'modules.chatgpt.chatgpt.ChatGPT' config: model: gpt-4o-mini - persona: "You are a helpful assistant robot. You respond with short phrases where possible. - Alternatively, you can respond with the following commands instead of text if you feel they are appropriate: - - animate/head_nod - - animate/head_shake - - gpio/laser/on - - gpio/laser/off" + persona: 'You are a helpful assistant robot. You respond with short phrases where + possible. Alternatively, you can respond with the following commands instead + of text if you feel they are appropriate: - animate/head_nod - animate/head_shake + - gpio/laser/on - gpio/laser/off' dependencies: python: - - openai + - openai additional: - - https://platform.openai.com/api-keys \ No newline at end of file + - https://platform.openai.com/api-keys + class: ChatGPT diff --git a/modules/controller_handler/config.yml b/modules/controller_handler/config.yml index 15edb0f6..5cb8e55a 100644 --- a/modules/controller_handler/config.yml +++ b/modules/controller_handler/config.yml @@ -1,168 +1,182 @@ -controller: - enabled: true - path: 'modules.controller_handler.controller_handler.ControllerHandler' +controller_handler: config: debug: false modifier_buttons: - - BTN_TL - - BTN_TR + - BTN_TL + - BTN_TR mapping: default: BTN_TL: actions: - - topic: '' - args: {} + - topic: '' + args: {} BTN_TR: actions: - - topic: '' - args: {} + - topic: '' + args: {} BTN_SELECT: actions: - - topic: 'record/stop' - args: {} + - topic: record/stop + args: {} BTN_START: actions: - - topic: 'record/start' - args: {} - KEY_RECORD: # Only on Xbox S/X remotes + - topic: record/start + args: {} + KEY_RECORD: actions: - - topic: '' - args: {} - BTN_MODE: # Xbox Logo + - topic: '' + args: {} + BTN_MODE: actions: - - topic: '' - args: {} - ABS_HAT0X: # DPAD X + - topic: '' + args: {} + ABS_HAT0X: actions: - - topic: '' - args: {} - ABS_HAT0Y: # DPAD Y + - topic: '' + args: {} + ABS_HAT0Y: actions: - - topic: '' - args: {} + - topic: '' + args: {} BTN_A: actions: - - topic: 'tts' - args: {msg: 'Test Default'} + - topic: tts + args: + msg: Test Default BTN_X: actions: - - topic: 'eye/blink' - args: {} + - topic: eye/blink + args: {} BTN_Y: actions: - - topic: 'gpio/laser/toggle' - args: {} + - topic: gpio/laser/toggle + args: {} BTN_B: actions: - - topic: 'vision/toggle' - args: {} + - topic: vision/toggle + args: {} ABS_RX: actions: - - topic: 'servo:neck_pan:mv' - args: {} - modifier: - scale: 50 # max speed per event + - topic: servo:neck_pan:mv + args: {} + modifier: + scale: 50 ABS_RZ: actions: - - topic: 'servo:neck_tilt:mv' - args: {} - modifier: - scale: -50 - - topic: 'servo:neck_tilt2:mv' - args: {} - modifier: - scale: 50 - ABS_RY: # Right trigger + - topic: servo:neck_tilt:mv + args: {} + modifier: + scale: -50 + - topic: servo:neck_tilt2:mv + args: {} + modifier: + scale: 50 + ABS_RY: start: -1.0 actions: - - topic: 'servo:leg_r_ankle:mv' - args: {} - modifier: - scale: -50 - ABS_Z: # Left trigger + - topic: servo:leg_r_ankle:mv + args: {} + modifier: + scale: -50 + ABS_Z: start: -1.0 actions: - - topic: 'servo:leg_l_ankle:mv' - args: {} - modifier: - scale: -50 + - topic: servo:leg_l_ankle:mv + args: {} + modifier: + scale: -50 ABS_X: actions: - - topic: 'eye/move' - args: {axis: 'y'} - modifier: - mapping: [0, 240] + - topic: eye/move + args: + axis: y + modifier: + mapping: + - 0 + - 240 ABS_Y: actions: - - topic: 'eye/move' - args: {axis: 'x'} - modifier: - mapping: [240, 0] + - topic: eye/move + args: + axis: x + modifier: + mapping: + - 240 + - 0 BTN_TL: BTN_X: actions: - - topic: 'servo:neck_pan:mv' - args: {delta: -50} + - topic: servo:neck_pan:mv + args: + delta: -50 BTN_Y: actions: - - topic: 'servo:neck_tilt:mv' - args: {delta: -50} - - topic: 'servo:neck_tilt2:mv' - args: {delta: 50} + - topic: servo:neck_tilt:mv + args: + delta: -50 + - topic: servo:neck_tilt2:mv + args: + delta: 50 BTN_B: actions: - - topic: 'servo:neck_pan:mv' - args: {delta: 50} + - topic: servo:neck_pan:mv + args: + delta: 50 BTN_A: actions: - - topic: 'servo:neck_tilt:mv' - args: {delta: 50} - - topic: 'servo:neck_tilt2:mv' - args: {delta: -50} + - topic: servo:neck_tilt:mv + args: + delta: 50 + - topic: servo:neck_tilt2:mv + args: + delta: -50 BTN_TR: BTN_X: actions: - - topic: 'servo:neck_tilt:mv' - args: {delta: 50} + - topic: servo:neck_tilt:mv + args: + delta: 50 BTN_Y: actions: - - topic: 'servo:neck_tilt:mv' - args: {delta: -50} + - topic: servo:neck_tilt:mv + args: + delta: -50 BTN_B: actions: - - topic: 'servo:neck_tilt2:mv' - args: {delta: 50} + - topic: servo:neck_tilt2:mv + args: + delta: 50 BTN_A: actions: - - topic: 'servo:neck_tilt2:mv' - args: {delta: -50} + - topic: servo:neck_tilt2:mv + args: + delta: -50 ABS_HAT0Y: actions: - - topic: 'servo:neck_tilt:mvabs' - args: { - position: 511 - } - - topic: 'servo:neck_tilt2:mvabs' - args: { - position: 511 - } + - topic: servo:neck_tilt:mvabs + args: + position: 511 + - topic: servo:neck_tilt2:mvabs + args: + position: 511 ABS_X: actions: - - topic: 'servo:leg_l_ankle:mv' - modifier: - scale: 50 - - topic: 'servo:leg_r_ankle:mv' - modifier: - scale: -50 - - topic: 'servo:leg_l_knee:mv' - modifier: - scale: 50 - - topic: 'servo:leg_r_knee:mv' - modifier: - scale: -50 - - topic: 'servo:leg_l_hip:mv' - modifier: - scale: 50 - - topic: 'servo:leg_r_hip:mv' - modifier: - scale: -50 + - topic: servo:leg_l_ankle:mv + modifier: + scale: 50 + - topic: servo:leg_r_ankle:mv + modifier: + scale: -50 + - topic: servo:leg_l_knee:mv + modifier: + scale: 50 + - topic: servo:leg_r_knee:mv + modifier: + scale: -50 + - topic: servo:leg_l_hip:mv + modifier: + scale: 50 + - topic: servo:leg_r_hip:mv + modifier: + scale: -50 + class: ControllerHandler diff --git a/modules/display/tft_display_eye/config.yml b/modules/display/tft_display_eye/config.yml index 6a686a74..fe44a311 100644 --- a/modules/display/tft_display_eye/config.yml +++ b/modules/display/tft_display_eye/config.yml @@ -1,13 +1,10 @@ tft_display: - enabled: true - environment: robot - path: modules.display.tft_display_eye.tft_display_eye.TFTDisplayEye config: - bus: 0 # Use `ls /dev/spi*` to identify. (/dev/spidev0.0) is bus 0, device 0 - device: 0 # Use `ls /dev/spi*` to identify. (/dev/spidev0.0) is bus 0, device 0 + bus: 0 + device: 0 rst_pin: 27 dc_pin: 25 - bl_pin: 18 # Not used if no backlight pin on device + bl_pin: 18 rotation: -90.0 test_on_boot: true colors: @@ -21,8 +18,9 @@ tft_display: dark_gray: (64, 64, 64) dependencies: unix: - - python3-pip - - python3-pil - - python3-numpy + - python3-pip + - python3-pil + - python3-numpy python: - - spidev \ No newline at end of file + - spidev + class: TFTDisplayEye diff --git a/modules/display/waveshare_oled/config.yml b/modules/display/waveshare_oled/config.yml index 3fb08bc8..68e036c0 100644 --- a/modules/display/waveshare_oled/config.yml +++ b/modules/display/waveshare_oled/config.yml @@ -1,16 +1,14 @@ oled_display: - enabled: true - environment: robot - path: modules.display.waveshare_oled.waveshare_oled.WaveshareOLED config: test_on_boot: false time_on_boot: false - reset_pin: 26 # Changed directly in waveshare config.py to avoid conflict with TFT display - dc_pin: 24 # Changed directly in waveshare config.py to avoid conflict with TFT display + reset_pin: 26 + dc_pin: 24 dependencies: unix: - - python3-pip - - python3-pil - - python3-numpy + - python3-pip + - python3-pil + - python3-numpy python: - - RPi.GPIO \ No newline at end of file + - RPi.GPIO + class: WaveshareOLED diff --git a/modules/enabled.yml b/modules/enabled.yml new file mode 100644 index 00000000..2046b35f --- /dev/null +++ b/modules/enabled.yml @@ -0,0 +1,117 @@ +# Module enable/disable configuration +# Set enabled: true to load a module at startup +# Use environment: to restrict to a specific environment (e.g. robot, laptop) +# Module keys must match the top-level key in each module's config.yml + +animate: + enabled: true + environment: robot + +braillespeak: + enabled: false + +bus_servo: + enabled: true + environment: robot + +buzzer: + enabled: false + +bno055: + enabled: true + environment: robot + +chatgpt: + enabled: true + +controller_handler: + enabled: true + +discordbot: + enabled: true + environment: laptop + +gpio_motion: + enabled: true + environment: robot + +gpio_read: + enabled: false + +i2c_servo: + enabled: false + +input_recorder: + enabled: true + +laser: + enabled: true + environment: robot + +logwrapper: + enabled: true + +messaging_service: + enabled: true + +mpu6050: + enabled: false + +neopixel: + enabled: false + +neopixel_emotions: + enabled: false + +oled_display: + enabled: true + environment: robot + +personality: + enabled: true + +piservo: + enabled: false + +pitemperature: + enabled: true + environment: robot + +rtl_sdr: + enabled: false + +serial: + enabled: false + +servo: + enabled: false + +speechinput: + enabled: false + +telegram: + enabled: true + +tft_display: + enabled: true + environment: robot + +tracking: + enabled: false + +translator: + enabled: false + +ttsmodule: + enabled: true + environment: robot + +vision_imx500: + enabled: true + environment: robot + +vision_opencv: + enabled: false + +xbox_controller: + enabled: true diff --git a/modules/gpio/laser/config.yml b/modules/gpio/laser/config.yml index 4d9f1013..6a25b5fa 100644 --- a/modules/gpio/laser/config.yml +++ b/modules/gpio/laser/config.yml @@ -1,10 +1,8 @@ -motion: - enabled: true - environment: robot - path: 'modules.gpio.laser.laser.Laser' +laser: config: pin: 6 test_on_boot: false dependencies: python: - - gpiozero \ No newline at end of file + - gpiozero + class: Laser diff --git a/modules/gpio/motion/config.yml b/modules/gpio/motion/config.yml index 84283dec..7ee10587 100644 --- a/modules/gpio/motion/config.yml +++ b/modules/gpio/motion/config.yml @@ -1,10 +1,8 @@ gpio_motion: - enabled: true - environment: robot - path: 'modules.gpio.motion.motion.Motion' config: pin: 9 test_on_boot: false dependencies: python: - - gpiozero \ No newline at end of file + - gpiozero + class: Motion diff --git a/modules/gpio/read/config.yml b/modules/gpio/read/config.yml index b7c9a726..b127d730 100644 --- a/modules/gpio/read/config.yml +++ b/modules/gpio/read/config.yml @@ -1,9 +1,8 @@ -motion: - enabled: false - path: 'modules.gpio.read.read.Read' +gpio_read: config: pin: 5 test_on_boot: false dependencies: python: - - gpiozero \ No newline at end of file + - gpiozero + class: Read diff --git a/modules/i2c_servo/config.yml b/modules/i2c_servo/config.yml index 34b05e7b..39a8d937 100644 --- a/modules/i2c_servo/config.yml +++ b/modules/i2c_servo/config.yml @@ -1,11 +1,10 @@ i2c_servo: - enabled: false - path: modules.i2c_servo.i2c_servo.I2CServo config: test_on_boot: false servo_count: 16 dependencies: unix: - - python3-smbus + - python3-smbus python: - - adafruit-circuitpython-servokit \ No newline at end of file + - adafruit-circuitpython-servokit + class: I2CServo diff --git a/modules/input_recorder/config.yml b/modules/input_recorder/config.yml index c26f352e..a3329912 100644 --- a/modules/input_recorder/config.yml +++ b/modules/input_recorder/config.yml @@ -1,5 +1,4 @@ input_recorder: - enabled: true - path: modules.input_recorder.input_recorder.InputRecorder config: animations_dir: animations/recorded + class: InputRecorder diff --git a/modules/logwrapper/config.yml b/modules/logwrapper/config.yml index 92033ba8..0002d730 100644 --- a/modules/logwrapper/config.yml +++ b/modules/logwrapper/config.yml @@ -1,7 +1,6 @@ logwrapper: - enabled: true # Highly recommended to enable this module - path: modules.logwrapper.logwrapper.LogWrapper config: filename: app.log - log_level: 'debug' # debug, info, warning, error, critical - cli_level: 'info' # debug, info, warning, error, critical + log_level: debug + cli_level: info + class: LogWrapper diff --git a/modules/neopixel/emotion_analysis/config.yml b/modules/neopixel/emotion_analysis/config.yml index 43d40045..39fd305b 100644 --- a/modules/neopixel/emotion_analysis/config.yml +++ b/modules/neopixel/emotion_analysis/config.yml @@ -1,2358 +1,2357 @@ neopixel_emotions: - enabled: false - path: modules.neopixel.emotion_analysis.emotion_analysis.EmotionAnalysis dependencies: python: - - tensorflow - - torch - - transformers + - tensorflow + - torch + - transformers config: colors: grief: - COLOR_ABBEY: '(76, 79, 86)' - COLOR_AFFAIR: '(113, 70, 147)' - COLOR_ARSENIC: '(59, 68, 75)' - COLOR_BISMARK: '(73, 113, 131)' - COLOR_BLACK_CORAL: '(84, 98, 111)' - COLOR_BLUE_BAYOUX: '(73, 102, 121)' - COLOR_BLUE_MAGENTA_VIOLET: '(85, 53, 146)' - COLOR_BOSSANOVA: '(78, 42, 90)' - COLOR_BRIGHT_GRAY: '(60, 65, 81)' - COLOR_BUTTERFLY_BUSH: '(98, 78, 154)' - COLOR_CADET: '(83, 104, 114)' - COLOR_CASAL: '(47, 97, 104)' - COLOR_CHAMBRAY: '(53, 78, 140)' - COLOR_CHARCOAL: '(54, 69, 79)' - COLOR_COMET: '(92, 93, 117)' - COLOR_CYBER_GRAPE: '(88, 66, 124)' - COLOR_DARK_LAVENDER: '(115, 79, 150)' - COLOR_DARK_SLATE_BLUE: '(72, 61, 139)' - COLOR_DEEP_KOAMARU: '(51, 51, 102)' - COLOR_DEEP_SPACE_SPARKLE: '(74, 100, 108)' - COLOR_DOLPHIN: '(100, 96, 119)' - COLOR_EAST_BAY: '(65, 76, 125)' - COLOR_FIORD: '(64, 81, 105)' - COLOR_GIGAS: '(82, 60, 148)' - COLOR_GRAVEL: '(74, 68, 75)' - COLOR_GUN_POWDER: '(65, 66, 87)' - COLOR_GUNMETAL: '(42, 52, 57)' - COLOR_IMPERIAL: '(96, 47, 107)' - COLOR_INDEPENDENCE: '(76, 81, 109)' - COLOR_JAPANESE_INDIGO: '(38, 67, 72)' - COLOR_KASHMIR_BLUE: '(80, 112, 150)' - COLOR_LIBERTY: '(84, 90, 167)' - COLOR_LIMED_SPRUCE: '(57, 72, 81)' - COLOR_MAKO: '(68, 73, 84)' - COLOR_MARTINIQUE: '(54, 48, 80)' - COLOR_MID_GRAY: '(95, 95, 110)' - COLOR_MINSK: '(63, 48, 127)' - COLOR_MORTAR: '(80, 67, 81)' - COLOR_MULLED_WINE: '(78, 69, 98)' - COLOR_NEVADA: '(100, 110, 117)' - COLOR_OCEAN_BLUE: '(79, 66, 181)' - COLOR_OLD_HELIOTROPE: '(86, 60, 92)' - COLOR_ONYX: '(53, 56, 57)' - COLOR_ORACLE: '(55, 116, 117)' - COLOR_OUTER_SPACE: '(65, 74, 76)' - COLOR_PAYNES_GREY: '(83, 104, 120)' - COLOR_PICKLED_BLUEWOOD: '(49, 68, 89)' - COLOR_PLUMP_PURPLE: '(89, 70, 178)' - COLOR_PURPLE_NAVY: '(78, 81, 128)' - COLOR_QUEEN_BLUE: '(67, 107, 149)' - COLOR_RHINO: '(46, 63, 98)' - COLOR_RIVER_BED: '(67, 76, 89)' - COLOR_SALT_BOX: '(104, 94, 110)' - COLOR_SAN_JUAN: '(48, 75, 106)' - COLOR_SAN_MARINO: '(69, 108, 172)' - COLOR_SCARPA_FLOW: '(88, 85, 98)' - COLOR_SHIP_GRAY: '(62, 58, 68)' - COLOR_SHUTTLE_GRAY: '(95, 102, 114)' - COLOR_SMOKY: '(96, 91, 115)' - COLOR_STORMCLOUD: '(79, 102, 106)' - COLOR_TROUT: '(74, 78, 90)' - COLOR_TUNA: '(53, 53, 66)' - COLOR_UCLA_BLUE: '(83, 104, 149)' - COLOR_VICTORIA: '(83, 68, 145)' - COLOR_VOODOO: '(83, 52, 85)' - COLOR_WILLIAM: '(58, 104, 108)' - COLOR_ARMADILLO: '(67, 62, 55)' - COLOR_AU_CHICO: '(151, 96, 93)' - COLOR_CAMELOT: '(137, 52, 86)' - COLOR_CANNON_PINK: '(137, 67, 103)' - COLOR_CHICAGO: '(93, 92, 88)' - COLOR_COSMIC: '(118, 57, 93)' - COLOR_COYOTE_BROWN: '(129, 97, 62)' - COLOR_CROCODILE: '(115, 109, 88)' - COLOR_DARK_BROWN_TANGELO: '(136, 101, 78)' - COLOR_DARK_BYZANTIUM: '(93, 57, 84)' - COLOR_DARK_LIVER: '(83, 75, 79)' - COLOR_DARK_PUCE: '(79, 58, 60)' - COLOR_DEEP_RUBY: '(132, 63, 91)' - COLOR_DEEP_TAUPE: '(126, 94, 96)' - COLOR_DEEP_TUSCAN_RED: '(102, 66, 77)' - COLOR_DON_JUAN: '(93, 76, 81)' - COLOR_DORADO: '(107, 87, 85)' - COLOR_DOVE_GRAY: '(109, 108, 108)' - COLOR_DUNE: '(56, 53, 51)' - COLOR_EGGPLANT: '(97, 64, 81)' - COLOR_EMPEROR: '(81, 70, 73)' - COLOR_FALCON: '(127, 98, 109)' - COLOR_FEDORA: '(121, 106, 120)' - COLOR_FERRA: '(112, 79, 80)' - COLOR_FINN: '(105, 45, 84)' - COLOR_FLINT: '(111, 106, 97)' - COLOR_FRENCH_BISTRE: '(133, 109, 77)' - COLOR_FUSCOUS_GRAY: '(84, 83, 77)' - COLOR_GO_BEN: '(114, 109, 78)' - COLOR_GOLD_FUSION: '(133, 117, 78)' - COLOR_HEMLOCK: '(94, 93, 59)' - COLOR_HIPPIE_PINK: '(174, 69, 96)' - COLOR_IRONSIDE_GRAY: '(103, 102, 98)' - COLOR_JAPANESE_VIOLET: '(91, 50, 86)' - COLOR_KABUL: '(94, 72, 62)' - COLOR_KOKODA: '(110, 109, 87)' - COLOR_LIVER: '(103, 76, 71)' - COLOR_MAGENTA_HAZE: '(159, 69, 118)' - COLOR_MASALA: '(64, 59, 56)' - COLOR_MATTERHORN: '(78, 59, 65)' - COLOR_MAUVE_TAUPE: '(145, 95, 109)' - COLOR_MEDIUM_RUBY: '(170, 64, 105)' - COLOR_MERLIN: '(65, 60, 55)' - COLOR_OLD_LAVENDER: '(121, 104, 120)' - COLOR_OLD_MOSS_GREEN: '(134, 126, 54)' - COLOR_PABLO: '(119, 111, 97)' - COLOR_PASTEL_BROWN: '(131, 105, 83)' - COLOR_PEAT: '(113, 107, 86)' - COLOR_PINE_CONE: '(109, 94, 84)' - COLOR_PLUM: '(142, 69, 133)' - COLOR_PURPLE_TAUPE: '(80, 64, 77)' - COLOR_QUARTZ: '(81, 72, 79)' - COLOR_QUINACRIDONE_MAGENTA: '(142, 58, 89)' - COLOR_RAW_UMBER: '(130, 102, 68)' - COLOR_REDWOOD: '(164, 90, 82)' - COLOR_ROMAN_COFFEE: '(121, 93, 76)' - COLOR_ROSE_EBONY: '(103, 72, 70)' - COLOR_ROSE_TAUPE: '(144, 93, 93)' - COLOR_RUSSETT: '(117, 90, 87)' - COLOR_SANDSTONE: '(121, 109, 98)' - COLOR_SCORPION: '(105, 95, 98)' - COLOR_SOYA_BEAN: '(106, 96, 81)' - COLOR_SPICY_MIX: '(139, 95, 77)' - COLOR_SUGAR_PLUM: '(145, 78, 117)' - COLOR_SYCAMORE: '(144, 141, 57)' - COLOR_TOBACCO_BROWN: '(113, 93, 71)' - COLOR_TUATARA: '(54, 53, 52)' - COLOR_TUNDORA: '(74, 66, 68)' - COLOR_TUSCAN_RED: '(124, 72, 72)' - COLOR_TWILIGHT_LAVENDER: '(138, 73, 107)' - COLOR_UMBER: '(99, 81, 71)' - COLOR_VIN_ROUGE: '(152, 61, 97)' - COLOR_WENGE: '(100, 84, 82)' - COLOR_WINE_DREGS: '(103, 49, 71)' - COLOR_YELLOW_METAL: '(113, 99, 56)' - COLOR_ZAMBEZI: '(104, 85, 88)' - COLOR_AMAZON: '(59, 122, 87)' - COLOR_APPLE: '(79, 168, 61)' - COLOR_AXOLOTL: '(78, 102, 73)' - COLOR_BLACK_OLIVE: '(59, 60, 54)' - COLOR_CABBAGE_PONT: '(63, 76, 58)' - COLOR_CACTUS: '(88, 113, 86)' - COLOR_CAPE_COD: '(60, 68, 67)' - COLOR_CHALET_GREEN: '(81, 110, 61)' - COLOR_CHATEAU_GREEN: '(64, 168, 96)' - COLOR_COMO: '(81, 124, 102)' - COLOR_CORDUROY: '(96, 110, 104)' - COLOR_CUTTY_SARK: '(80, 118, 114)' - COLOR_DARK_OLIVE_GREEN: '(85, 107, 47)' - COLOR_DINGLEY: '(93, 119, 71)' - COLOR_EBONY: '(85, 93, 80)' - COLOR_FADED_JADE: '(66, 121, 119)' - COLOR_FELDGRAU: '(77, 93, 83)' - COLOR_FERN_GREEN: '(79, 121, 66)' - COLOR_FINCH: '(98, 102, 73)' - COLOR_FINLANDIA: '(85, 109, 86)' - COLOR_FRUIT_SALAD: '(79, 157, 93)' - COLOR_GLADE_GREEN: '(97, 132, 95)' - COLOR_GOBLIN: '(61, 125, 82)' - COLOR_GRAY_ASPARAGUS: '(70, 89, 69)' - COLOR_HIPPIE_GREEN: '(83, 130, 75)' - COLOR_HOOKERS_GREEN: '(73, 121, 107)' - COLOR_HUNTER_GREEN: '(53, 94, 59)' - COLOR_KELP: '(69, 73, 54)' - COLOR_KILLARNEY: '(58, 106, 71)' - COLOR_KOMBU_GREEN: '(53, 66, 48)' - COLOR_LIMA: '(118, 189, 23)' - COLOR_LIMED_ASH: '(116, 125, 99)' - COLOR_LUNAR_GREEN: '(60, 73, 58)' - COLOR_MAY_GREEN: '(76, 145, 65)' - COLOR_MINERAL_GREEN: '(63, 93, 83)' - COLOR_NANDOR: '(75, 93, 82)' - COLOR_NICKEL: '(114, 116, 114)' - COLOR_OLIVE_DRAB: '(107, 142, 35)' - COLOR_PLANTATION: '(39, 80, 75)' - COLOR_RYB_GREEN: '(102, 176, 50)' - COLOR_RIFLE_GREEN: '(68, 76, 56)' - COLOR_SAP_GREEN: '(80, 125, 42)' - COLOR_SIAM: '(100, 106, 84)' - COLOR_SPECTRA: '(47, 90, 87)' - COLOR_SPRING_LEAVES: '(87, 131, 99)' - COLOR_STROMBOLI: '(50, 93, 82)' - COLOR_TOM_THUMB: '(63, 88, 59)' - COLOR_VIRIDIAN: '(64, 130, 109)' - COLOR_WILLOW_GROVE: '(101, 116, 93)' - COLOR_WINTERGREEN_DREAM: '(86, 136, 125)' - COLOR_DAVYS_GREY: '(85, 85, 85)' - COLOR_DIM_GRAY: '(105, 105, 105)' - COLOR_GRANITE_GRAY: '(103, 103, 103)' - COLOR_JET: '(52, 52, 52)' - COLOR_MINE_SHAFT: '(50, 50, 50)' - COLOR_STORM_DUST: '(100, 100, 99)' - COLOR_DARK_SLATE_GRAY: '(47, 79, 79)' + COLOR_ABBEY: (76, 79, 86) + COLOR_AFFAIR: (113, 70, 147) + COLOR_ARSENIC: (59, 68, 75) + COLOR_BISMARK: (73, 113, 131) + COLOR_BLACK_CORAL: (84, 98, 111) + COLOR_BLUE_BAYOUX: (73, 102, 121) + COLOR_BLUE_MAGENTA_VIOLET: (85, 53, 146) + COLOR_BOSSANOVA: (78, 42, 90) + COLOR_BRIGHT_GRAY: (60, 65, 81) + COLOR_BUTTERFLY_BUSH: (98, 78, 154) + COLOR_CADET: (83, 104, 114) + COLOR_CASAL: (47, 97, 104) + COLOR_CHAMBRAY: (53, 78, 140) + COLOR_CHARCOAL: (54, 69, 79) + COLOR_COMET: (92, 93, 117) + COLOR_CYBER_GRAPE: (88, 66, 124) + COLOR_DARK_LAVENDER: (115, 79, 150) + COLOR_DARK_SLATE_BLUE: (72, 61, 139) + COLOR_DEEP_KOAMARU: (51, 51, 102) + COLOR_DEEP_SPACE_SPARKLE: (74, 100, 108) + COLOR_DOLPHIN: (100, 96, 119) + COLOR_EAST_BAY: (65, 76, 125) + COLOR_FIORD: (64, 81, 105) + COLOR_GIGAS: (82, 60, 148) + COLOR_GRAVEL: (74, 68, 75) + COLOR_GUN_POWDER: (65, 66, 87) + COLOR_GUNMETAL: (42, 52, 57) + COLOR_IMPERIAL: (96, 47, 107) + COLOR_INDEPENDENCE: (76, 81, 109) + COLOR_JAPANESE_INDIGO: (38, 67, 72) + COLOR_KASHMIR_BLUE: (80, 112, 150) + COLOR_LIBERTY: (84, 90, 167) + COLOR_LIMED_SPRUCE: (57, 72, 81) + COLOR_MAKO: (68, 73, 84) + COLOR_MARTINIQUE: (54, 48, 80) + COLOR_MID_GRAY: (95, 95, 110) + COLOR_MINSK: (63, 48, 127) + COLOR_MORTAR: (80, 67, 81) + COLOR_MULLED_WINE: (78, 69, 98) + COLOR_NEVADA: (100, 110, 117) + COLOR_OCEAN_BLUE: (79, 66, 181) + COLOR_OLD_HELIOTROPE: (86, 60, 92) + COLOR_ONYX: (53, 56, 57) + COLOR_ORACLE: (55, 116, 117) + COLOR_OUTER_SPACE: (65, 74, 76) + COLOR_PAYNES_GREY: (83, 104, 120) + COLOR_PICKLED_BLUEWOOD: (49, 68, 89) + COLOR_PLUMP_PURPLE: (89, 70, 178) + COLOR_PURPLE_NAVY: (78, 81, 128) + COLOR_QUEEN_BLUE: (67, 107, 149) + COLOR_RHINO: (46, 63, 98) + COLOR_RIVER_BED: (67, 76, 89) + COLOR_SALT_BOX: (104, 94, 110) + COLOR_SAN_JUAN: (48, 75, 106) + COLOR_SAN_MARINO: (69, 108, 172) + COLOR_SCARPA_FLOW: (88, 85, 98) + COLOR_SHIP_GRAY: (62, 58, 68) + COLOR_SHUTTLE_GRAY: (95, 102, 114) + COLOR_SMOKY: (96, 91, 115) + COLOR_STORMCLOUD: (79, 102, 106) + COLOR_TROUT: (74, 78, 90) + COLOR_TUNA: (53, 53, 66) + COLOR_UCLA_BLUE: (83, 104, 149) + COLOR_VICTORIA: (83, 68, 145) + COLOR_VOODOO: (83, 52, 85) + COLOR_WILLIAM: (58, 104, 108) + COLOR_ARMADILLO: (67, 62, 55) + COLOR_AU_CHICO: (151, 96, 93) + COLOR_CAMELOT: (137, 52, 86) + COLOR_CANNON_PINK: (137, 67, 103) + COLOR_CHICAGO: (93, 92, 88) + COLOR_COSMIC: (118, 57, 93) + COLOR_COYOTE_BROWN: (129, 97, 62) + COLOR_CROCODILE: (115, 109, 88) + COLOR_DARK_BROWN_TANGELO: (136, 101, 78) + COLOR_DARK_BYZANTIUM: (93, 57, 84) + COLOR_DARK_LIVER: (83, 75, 79) + COLOR_DARK_PUCE: (79, 58, 60) + COLOR_DEEP_RUBY: (132, 63, 91) + COLOR_DEEP_TAUPE: (126, 94, 96) + COLOR_DEEP_TUSCAN_RED: (102, 66, 77) + COLOR_DON_JUAN: (93, 76, 81) + COLOR_DORADO: (107, 87, 85) + COLOR_DOVE_GRAY: (109, 108, 108) + COLOR_DUNE: (56, 53, 51) + COLOR_EGGPLANT: (97, 64, 81) + COLOR_EMPEROR: (81, 70, 73) + COLOR_FALCON: (127, 98, 109) + COLOR_FEDORA: (121, 106, 120) + COLOR_FERRA: (112, 79, 80) + COLOR_FINN: (105, 45, 84) + COLOR_FLINT: (111, 106, 97) + COLOR_FRENCH_BISTRE: (133, 109, 77) + COLOR_FUSCOUS_GRAY: (84, 83, 77) + COLOR_GO_BEN: (114, 109, 78) + COLOR_GOLD_FUSION: (133, 117, 78) + COLOR_HEMLOCK: (94, 93, 59) + COLOR_HIPPIE_PINK: (174, 69, 96) + COLOR_IRONSIDE_GRAY: (103, 102, 98) + COLOR_JAPANESE_VIOLET: (91, 50, 86) + COLOR_KABUL: (94, 72, 62) + COLOR_KOKODA: (110, 109, 87) + COLOR_LIVER: (103, 76, 71) + COLOR_MAGENTA_HAZE: (159, 69, 118) + COLOR_MASALA: (64, 59, 56) + COLOR_MATTERHORN: (78, 59, 65) + COLOR_MAUVE_TAUPE: (145, 95, 109) + COLOR_MEDIUM_RUBY: (170, 64, 105) + COLOR_MERLIN: (65, 60, 55) + COLOR_OLD_LAVENDER: (121, 104, 120) + COLOR_OLD_MOSS_GREEN: (134, 126, 54) + COLOR_PABLO: (119, 111, 97) + COLOR_PASTEL_BROWN: (131, 105, 83) + COLOR_PEAT: (113, 107, 86) + COLOR_PINE_CONE: (109, 94, 84) + COLOR_PLUM: (142, 69, 133) + COLOR_PURPLE_TAUPE: (80, 64, 77) + COLOR_QUARTZ: (81, 72, 79) + COLOR_QUINACRIDONE_MAGENTA: (142, 58, 89) + COLOR_RAW_UMBER: (130, 102, 68) + COLOR_REDWOOD: (164, 90, 82) + COLOR_ROMAN_COFFEE: (121, 93, 76) + COLOR_ROSE_EBONY: (103, 72, 70) + COLOR_ROSE_TAUPE: (144, 93, 93) + COLOR_RUSSETT: (117, 90, 87) + COLOR_SANDSTONE: (121, 109, 98) + COLOR_SCORPION: (105, 95, 98) + COLOR_SOYA_BEAN: (106, 96, 81) + COLOR_SPICY_MIX: (139, 95, 77) + COLOR_SUGAR_PLUM: (145, 78, 117) + COLOR_SYCAMORE: (144, 141, 57) + COLOR_TOBACCO_BROWN: (113, 93, 71) + COLOR_TUATARA: (54, 53, 52) + COLOR_TUNDORA: (74, 66, 68) + COLOR_TUSCAN_RED: (124, 72, 72) + COLOR_TWILIGHT_LAVENDER: (138, 73, 107) + COLOR_UMBER: (99, 81, 71) + COLOR_VIN_ROUGE: (152, 61, 97) + COLOR_WENGE: (100, 84, 82) + COLOR_WINE_DREGS: (103, 49, 71) + COLOR_YELLOW_METAL: (113, 99, 56) + COLOR_ZAMBEZI: (104, 85, 88) + COLOR_AMAZON: (59, 122, 87) + COLOR_APPLE: (79, 168, 61) + COLOR_AXOLOTL: (78, 102, 73) + COLOR_BLACK_OLIVE: (59, 60, 54) + COLOR_CABBAGE_PONT: (63, 76, 58) + COLOR_CACTUS: (88, 113, 86) + COLOR_CAPE_COD: (60, 68, 67) + COLOR_CHALET_GREEN: (81, 110, 61) + COLOR_CHATEAU_GREEN: (64, 168, 96) + COLOR_COMO: (81, 124, 102) + COLOR_CORDUROY: (96, 110, 104) + COLOR_CUTTY_SARK: (80, 118, 114) + COLOR_DARK_OLIVE_GREEN: (85, 107, 47) + COLOR_DINGLEY: (93, 119, 71) + COLOR_EBONY: (85, 93, 80) + COLOR_FADED_JADE: (66, 121, 119) + COLOR_FELDGRAU: (77, 93, 83) + COLOR_FERN_GREEN: (79, 121, 66) + COLOR_FINCH: (98, 102, 73) + COLOR_FINLANDIA: (85, 109, 86) + COLOR_FRUIT_SALAD: (79, 157, 93) + COLOR_GLADE_GREEN: (97, 132, 95) + COLOR_GOBLIN: (61, 125, 82) + COLOR_GRAY_ASPARAGUS: (70, 89, 69) + COLOR_HIPPIE_GREEN: (83, 130, 75) + COLOR_HOOKERS_GREEN: (73, 121, 107) + COLOR_HUNTER_GREEN: (53, 94, 59) + COLOR_KELP: (69, 73, 54) + COLOR_KILLARNEY: (58, 106, 71) + COLOR_KOMBU_GREEN: (53, 66, 48) + COLOR_LIMA: (118, 189, 23) + COLOR_LIMED_ASH: (116, 125, 99) + COLOR_LUNAR_GREEN: (60, 73, 58) + COLOR_MAY_GREEN: (76, 145, 65) + COLOR_MINERAL_GREEN: (63, 93, 83) + COLOR_NANDOR: (75, 93, 82) + COLOR_NICKEL: (114, 116, 114) + COLOR_OLIVE_DRAB: (107, 142, 35) + COLOR_PLANTATION: (39, 80, 75) + COLOR_RYB_GREEN: (102, 176, 50) + COLOR_RIFLE_GREEN: (68, 76, 56) + COLOR_SAP_GREEN: (80, 125, 42) + COLOR_SIAM: (100, 106, 84) + COLOR_SPECTRA: (47, 90, 87) + COLOR_SPRING_LEAVES: (87, 131, 99) + COLOR_STROMBOLI: (50, 93, 82) + COLOR_TOM_THUMB: (63, 88, 59) + COLOR_VIRIDIAN: (64, 130, 109) + COLOR_WILLOW_GROVE: (101, 116, 93) + COLOR_WINTERGREEN_DREAM: (86, 136, 125) + COLOR_DAVYS_GREY: (85, 85, 85) + COLOR_DIM_GRAY: (105, 105, 105) + COLOR_GRANITE_GRAY: (103, 103, 103) + COLOR_JET: (52, 52, 52) + COLOR_MINE_SHAFT: (50, 50, 50) + COLOR_STORM_DUST: (100, 100, 99) + COLOR_DARK_SLATE_GRAY: (47, 79, 79) disgust: - COLOR_ABSOLUTE_ZERO: '(0, 72, 186)' - COLOR_ALLPORTS: '(0, 118, 163)' - COLOR_ASTRAL: '(50, 125, 160)' - COLOR_ATOLL: '(10, 111, 117)' - COLOR_BAHAMA_BLUE: '(2, 99, 149)' - COLOR_BALL_BLUE: '(33, 171, 205)' - COLOR_BDAZZLED_BLUE: '(46, 88, 148)' - COLOR_BLUE_CHILL: '(12, 137, 144)' - COLOR_BLUE_DIANNE: '(32, 72, 82)' - COLOR_BLUE_GREEN: '(13, 152, 186)' - COLOR_BLUE_SAPPHIRE: '(18, 97, 128)' - COLOR_BLUE_STONE: '(1, 97, 98)' - COLOR_BLUMINE: '(24, 88, 122)' - COLOR_BONDI_BLUE: '(0, 149, 182)' - COLOR_BOSTON_BLUE: '(59, 145, 180)' - COLOR_BRIGHT_CERULEAN: '(29, 172, 214)' - COLOR_BRIGHT_NAVY_BLUE: '(25, 116, 210)' - COLOR_CG_BLUE: '(0, 122, 165)' - COLOR_CALYPSO: '(49, 114, 141)' - COLOR_CERULEAN: '(0, 123, 167)' - COLOR_CERULEAN_BLUE: '(42, 82, 190)' - COLOR_CHATHAMS_BLUE: '(23, 85, 121)' - COLOR_COBALT_BLUE: '(0, 71, 171)' - COLOR_CONGRESS_BLUE: '(2, 71, 142)' - COLOR_CURIOUS_BLUE: '(37, 150, 209)' - COLOR_CYAN_COBALT_BLUE: '(40, 88, 156)' - COLOR_CYAN_CORNFLOWER_BLUE: '(24, 139, 194)' - COLOR_DARK_CERULEAN: '(8, 69, 126)' - COLOR_DARK_TURQUOISE: '(0, 206, 209)' - COLOR_DEEP_SEA_GREEN: '(9, 88, 89)' - COLOR_DENIM: '(21, 96, 189)' - COLOR_DENIM_BLUE: '(34, 67, 182)' - COLOR_EAGLE_GREEN: '(0, 73, 83)' - COLOR_EASTERN_BLUE: '(30, 154, 176)' - COLOR_ELM: '(28, 124, 125)' - COLOR_ENDEAVOUR: '(0, 86, 167)' - COLOR_FRENCH_BLUE: '(0, 114, 187)' - COLOR_FUN_BLUE: '(25, 89, 168)' - COLOR_GREEN_BLUE: '(17, 100, 180)' - COLOR_HONOLULU_BLUE: '(0, 109, 176)' - COLOR_LAPIS_LAZULI: '(38, 97, 156)' - COLOR_LOCHMARA: '(0, 126, 199)' - COLOR_MARINER: '(40, 106, 205)' - COLOR_MATISSE: '(27, 101, 157)' - COLOR_MEDIUM_ELECTRIC_BLUE: '(3, 80, 150)' - COLOR_METALLIC_SEAWEED: '(10, 126, 140)' - COLOR_MING: '(54, 116, 125)' - COLOR_MOSQUE: '(3, 106, 110)' - COLOR_MUNSELL_BLUE: '(0, 147, 175)' - COLOR_NCS_BLUE: '(0, 135, 189)' - COLOR_NEW_CAR: '(33, 79, 198)' - COLOR_OCEAN_BOAT_BLUE: '(0, 119, 190)' - COLOR_ORIENT: '(1, 94, 133)' - COLOR_PACIFIC_BLUE: '(28, 169, 201)' - COLOR_PARADISO: '(49, 125, 130)' - COLOR_PELOROUS: '(62, 171, 191)' - COLOR_PROCESS_CYAN: '(0, 183, 235)' - COLOR_RICH_ELECTRIC_BLUE: '(8, 146, 208)' - COLOR_SAPPHIRE: '(15, 82, 186)' - COLOR_SAPPHIRE_BLUE: '(0, 103, 165)' - COLOR_SCIENCE_BLUE: '(0, 102, 204)' - COLOR_SEA_BLUE: '(0, 105, 148)' - COLOR_SHERPA_BLUE: '(0, 73, 80)' - COLOR_SPANISH_BLUE: '(0, 112, 184)' - COLOR_SPANISH_SKY_BLUE: '(0, 170, 228)' - COLOR_ST_TROPAZ: '(45, 86, 155)' - COLOR_STAR_COMMAND_BLUE: '(0, 123, 184)' - COLOR_TEAL_BLUE: '(54, 117, 136)' - COLOR_TORY_BLUE: '(20, 80, 170)' - COLOR_TRUE_BLUE: '(0, 115, 207)' - COLOR_USAFA_BLUE: '(0, 79, 152)' - COLOR_VENICE_BLUE: '(5, 89, 137)' - COLOR_VIOLET_BLUE: '(50, 74, 178)' - COLOR_VIRIDIAN_GREEN: '(0, 150, 152)' - COLOR_VIVID_CERULEAN: '(0, 170, 238)' - COLOR_YALE_BLUE: '(15, 77, 146)' - COLOR_AQUA_DEEP: '(1, 75, 67)' - COLOR_BOTTLE_GREEN: '(0, 106, 78)' - COLOR_CARIBBEAN_GREEN: '(0, 204, 153)' - COLOR_CELADON_GREEN: '(47, 132, 124)' - COLOR_CRAYOLA_GREEN: '(28, 172, 120)' - COLOR_DARK_SPRING_GREEN: '(23, 114, 69)' - COLOR_DEEP_GREEN_CYAN_TURQUOISE: '(14, 124, 97)' - COLOR_DEEP_JUNGLE_GREEN: '(0, 75, 73)' - COLOR_DEEP_SEA: '(1, 130, 107)' - COLOR_EDEN: '(16, 88, 82)' - COLOR_ELF_GREEN: '(8, 131, 112)' - COLOR_EVENING_SEA: '(2, 78, 70)' - COLOR_GO_GREEN: '(0, 171, 102)' - COLOR_GENERIC_VIRIDIAN: '(0, 127, 102)' - COLOR_GENOA: '(21, 115, 107)' - COLOR_GOSSAMER: '(6, 155, 129)' - COLOR_GREEN_CYAN: '(0, 153, 102)' - COLOR_GREEN_HAZE: '(1, 163, 104)' - COLOR_GREEN_PEA: '(29, 97, 66)' - COLOR_ILLUMINATING_EMERALD: '(49, 145, 119)' - COLOR_JADE: '(0, 168, 107)' - COLOR_JUNGLE_GREEN: '(41, 171, 135)' - COLOR_KEPPEL: '(58, 176, 158)' - COLOR_LIGHT_SEA_GREEN: '(32, 178, 170)' - COLOR_LOCHINVAR: '(44, 140, 132)' - COLOR_MEDIUM_SEA_GREEN: '(60, 179, 113)' - COLOR_MEDIUM_SPRING_GREEN: '(0, 250, 154)' - COLOR_MINT: '(62, 180, 137)' - COLOR_MOUNTAIN_MEADOW: '(48, 186, 143)' - COLOR_MUNSELL_GREEN: '(0, 168, 119)' - COLOR_MYRTLE_GREEN: '(49, 120, 115)' - COLOR_NCS_GREEN: '(0, 159, 107)' - COLOR_NIAGARA: '(6, 161, 137)' - COLOR_OBSERVATORY: '(2, 134, 111)' - COLOR_PANTONE_GREEN: '(0, 173, 67)' - COLOR_PAOLO_VERONESE_GREEN: '(0, 155, 125)' - COLOR_PEARL_MYSTIC_TURQUOISE: '(50, 198, 166)' - COLOR_PERSIAN_GREEN: '(0, 166, 147)' - COLOR_PIGMENT_GREEN: '(0, 165, 80)' - COLOR_PINE_GREEN: '(1, 121, 111)' - COLOR_PUERTO_RICO: '(63, 193, 170)' - COLOR_SALEM: '(9, 127, 75)' - COLOR_SEA_GREEN: '(46, 139, 87)' - COLOR_SHAMROCK: '(51, 204, 153)' - COLOR_SHAMROCK_GREEN: '(0, 158, 96)' - COLOR_SPANISH_GREEN: '(0, 145, 80)' - COLOR_SPANISH_VIRIDIAN: '(0, 127, 92)' - COLOR_SURFIE_GREEN: '(12, 122, 121)' - COLOR_TEAL_GREEN: '(0, 130, 127)' - COLOR_TIFFANY_BLUE: '(10, 186, 181)' - COLOR_TROPICAL_RAIN_FOREST: '(0, 117, 94)' - COLOR_UFO_GREEN: '(60, 208, 112)' - COLOR_WATERCOURSE: '(5, 111, 87)' - COLOR_ZOMP: '(57, 167, 142)' - COLOR_DARK_CYAN: '(0, 139, 139)' - COLOR_JAVA: '(31, 194, 194)' - COLOR_ROBIN_EGG_BLUE: '(0, 204, 204)' - COLOR_SKOBELOFF: '(0, 116, 116)' - COLOR_TEAL: '(0, 128, 128)' - COLOR_WARM_BLACK: '(0, 66, 66)' + COLOR_ABSOLUTE_ZERO: (0, 72, 186) + COLOR_ALLPORTS: (0, 118, 163) + COLOR_ASTRAL: (50, 125, 160) + COLOR_ATOLL: (10, 111, 117) + COLOR_BAHAMA_BLUE: (2, 99, 149) + COLOR_BALL_BLUE: (33, 171, 205) + COLOR_BDAZZLED_BLUE: (46, 88, 148) + COLOR_BLUE_CHILL: (12, 137, 144) + COLOR_BLUE_DIANNE: (32, 72, 82) + COLOR_BLUE_GREEN: (13, 152, 186) + COLOR_BLUE_SAPPHIRE: (18, 97, 128) + COLOR_BLUE_STONE: (1, 97, 98) + COLOR_BLUMINE: (24, 88, 122) + COLOR_BONDI_BLUE: (0, 149, 182) + COLOR_BOSTON_BLUE: (59, 145, 180) + COLOR_BRIGHT_CERULEAN: (29, 172, 214) + COLOR_BRIGHT_NAVY_BLUE: (25, 116, 210) + COLOR_CG_BLUE: (0, 122, 165) + COLOR_CALYPSO: (49, 114, 141) + COLOR_CERULEAN: (0, 123, 167) + COLOR_CERULEAN_BLUE: (42, 82, 190) + COLOR_CHATHAMS_BLUE: (23, 85, 121) + COLOR_COBALT_BLUE: (0, 71, 171) + COLOR_CONGRESS_BLUE: (2, 71, 142) + COLOR_CURIOUS_BLUE: (37, 150, 209) + COLOR_CYAN_COBALT_BLUE: (40, 88, 156) + COLOR_CYAN_CORNFLOWER_BLUE: (24, 139, 194) + COLOR_DARK_CERULEAN: (8, 69, 126) + COLOR_DARK_TURQUOISE: (0, 206, 209) + COLOR_DEEP_SEA_GREEN: (9, 88, 89) + COLOR_DENIM: (21, 96, 189) + COLOR_DENIM_BLUE: (34, 67, 182) + COLOR_EAGLE_GREEN: (0, 73, 83) + COLOR_EASTERN_BLUE: (30, 154, 176) + COLOR_ELM: (28, 124, 125) + COLOR_ENDEAVOUR: (0, 86, 167) + COLOR_FRENCH_BLUE: (0, 114, 187) + COLOR_FUN_BLUE: (25, 89, 168) + COLOR_GREEN_BLUE: (17, 100, 180) + COLOR_HONOLULU_BLUE: (0, 109, 176) + COLOR_LAPIS_LAZULI: (38, 97, 156) + COLOR_LOCHMARA: (0, 126, 199) + COLOR_MARINER: (40, 106, 205) + COLOR_MATISSE: (27, 101, 157) + COLOR_MEDIUM_ELECTRIC_BLUE: (3, 80, 150) + COLOR_METALLIC_SEAWEED: (10, 126, 140) + COLOR_MING: (54, 116, 125) + COLOR_MOSQUE: (3, 106, 110) + COLOR_MUNSELL_BLUE: (0, 147, 175) + COLOR_NCS_BLUE: (0, 135, 189) + COLOR_NEW_CAR: (33, 79, 198) + COLOR_OCEAN_BOAT_BLUE: (0, 119, 190) + COLOR_ORIENT: (1, 94, 133) + COLOR_PACIFIC_BLUE: (28, 169, 201) + COLOR_PARADISO: (49, 125, 130) + COLOR_PELOROUS: (62, 171, 191) + COLOR_PROCESS_CYAN: (0, 183, 235) + COLOR_RICH_ELECTRIC_BLUE: (8, 146, 208) + COLOR_SAPPHIRE: (15, 82, 186) + COLOR_SAPPHIRE_BLUE: (0, 103, 165) + COLOR_SCIENCE_BLUE: (0, 102, 204) + COLOR_SEA_BLUE: (0, 105, 148) + COLOR_SHERPA_BLUE: (0, 73, 80) + COLOR_SPANISH_BLUE: (0, 112, 184) + COLOR_SPANISH_SKY_BLUE: (0, 170, 228) + COLOR_ST_TROPAZ: (45, 86, 155) + COLOR_STAR_COMMAND_BLUE: (0, 123, 184) + COLOR_TEAL_BLUE: (54, 117, 136) + COLOR_TORY_BLUE: (20, 80, 170) + COLOR_TRUE_BLUE: (0, 115, 207) + COLOR_USAFA_BLUE: (0, 79, 152) + COLOR_VENICE_BLUE: (5, 89, 137) + COLOR_VIOLET_BLUE: (50, 74, 178) + COLOR_VIRIDIAN_GREEN: (0, 150, 152) + COLOR_VIVID_CERULEAN: (0, 170, 238) + COLOR_YALE_BLUE: (15, 77, 146) + COLOR_AQUA_DEEP: (1, 75, 67) + COLOR_BOTTLE_GREEN: (0, 106, 78) + COLOR_CARIBBEAN_GREEN: (0, 204, 153) + COLOR_CELADON_GREEN: (47, 132, 124) + COLOR_CRAYOLA_GREEN: (28, 172, 120) + COLOR_DARK_SPRING_GREEN: (23, 114, 69) + COLOR_DEEP_GREEN_CYAN_TURQUOISE: (14, 124, 97) + COLOR_DEEP_JUNGLE_GREEN: (0, 75, 73) + COLOR_DEEP_SEA: (1, 130, 107) + COLOR_EDEN: (16, 88, 82) + COLOR_ELF_GREEN: (8, 131, 112) + COLOR_EVENING_SEA: (2, 78, 70) + COLOR_GO_GREEN: (0, 171, 102) + COLOR_GENERIC_VIRIDIAN: (0, 127, 102) + COLOR_GENOA: (21, 115, 107) + COLOR_GOSSAMER: (6, 155, 129) + COLOR_GREEN_CYAN: (0, 153, 102) + COLOR_GREEN_HAZE: (1, 163, 104) + COLOR_GREEN_PEA: (29, 97, 66) + COLOR_ILLUMINATING_EMERALD: (49, 145, 119) + COLOR_JADE: (0, 168, 107) + COLOR_JUNGLE_GREEN: (41, 171, 135) + COLOR_KEPPEL: (58, 176, 158) + COLOR_LIGHT_SEA_GREEN: (32, 178, 170) + COLOR_LOCHINVAR: (44, 140, 132) + COLOR_MEDIUM_SEA_GREEN: (60, 179, 113) + COLOR_MEDIUM_SPRING_GREEN: (0, 250, 154) + COLOR_MINT: (62, 180, 137) + COLOR_MOUNTAIN_MEADOW: (48, 186, 143) + COLOR_MUNSELL_GREEN: (0, 168, 119) + COLOR_MYRTLE_GREEN: (49, 120, 115) + COLOR_NCS_GREEN: (0, 159, 107) + COLOR_NIAGARA: (6, 161, 137) + COLOR_OBSERVATORY: (2, 134, 111) + COLOR_PANTONE_GREEN: (0, 173, 67) + COLOR_PAOLO_VERONESE_GREEN: (0, 155, 125) + COLOR_PEARL_MYSTIC_TURQUOISE: (50, 198, 166) + COLOR_PERSIAN_GREEN: (0, 166, 147) + COLOR_PIGMENT_GREEN: (0, 165, 80) + COLOR_PINE_GREEN: (1, 121, 111) + COLOR_PUERTO_RICO: (63, 193, 170) + COLOR_SALEM: (9, 127, 75) + COLOR_SEA_GREEN: (46, 139, 87) + COLOR_SHAMROCK: (51, 204, 153) + COLOR_SHAMROCK_GREEN: (0, 158, 96) + COLOR_SPANISH_GREEN: (0, 145, 80) + COLOR_SPANISH_VIRIDIAN: (0, 127, 92) + COLOR_SURFIE_GREEN: (12, 122, 121) + COLOR_TEAL_GREEN: (0, 130, 127) + COLOR_TIFFANY_BLUE: (10, 186, 181) + COLOR_TROPICAL_RAIN_FOREST: (0, 117, 94) + COLOR_UFO_GREEN: (60, 208, 112) + COLOR_WATERCOURSE: (5, 111, 87) + COLOR_ZOMP: (57, 167, 142) + COLOR_DARK_CYAN: (0, 139, 139) + COLOR_JAVA: (31, 194, 194) + COLOR_ROBIN_EGG_BLUE: (0, 204, 204) + COLOR_SKOBELOFF: (0, 116, 116) + COLOR_TEAL: (0, 128, 128) + COLOR_WARM_BLACK: (0, 66, 66) curiosity: - COLOR_AERO: '(124, 185, 232)' - COLOR_ANAKIWA: '(157, 229, 255)' - COLOR_AQUAMARINE_BLUE: '(113, 217, 226)' - COLOR_BABY_BLUE: '(137, 207, 240)' - COLOR_BABY_BLUE_EYES: '(161, 202, 241)' - COLOR_BEAU_BLUE: '(188, 212, 230)' - COLOR_BELGION: '(173, 216, 255)' - COLOR_BILOBA_FLOWER: '(178, 161, 234)' - COLOR_BLEU_DE_FRANCE: '(49, 140, 231)' - COLOR_BLIZZARD_BLUE: '(163, 227, 237)' - COLOR_BLUE_JEANS: '(93, 173, 236)' - COLOR_BLUE_LAGOON: '(172, 229, 238)' - COLOR_BLUEBERRY: '(79, 134, 247)' - COLOR_BRILLIANT_AZURE: '(51, 153, 255)' - COLOR_CAPRI: '(0, 191, 255)' - COLOR_CAROLINA_BLUE: '(86, 160, 211)' - COLOR_CELESTIAL_BLUE: '(73, 151, 208)' - COLOR_CHARLOTTE: '(186, 238, 249)' - COLOR_COLUMBIA_BLUE: '(196, 216, 226)' - COLOR_CORNFLOWER_BLUE: '(100, 149, 237)' - COLOR_DANUBE: '(96, 147, 209)' - COLOR_DARK_IMPERIAL_BLUE: '(110, 110, 249)' - COLOR_DARK_SKY_BLUE: '(140, 190, 214)' - COLOR_DIAMOND: '(185, 242, 255)' - COLOR_DODGER_BLUE: '(30, 144, 255)' - COLOR_ELECTRIC_BLUE: '(125, 249, 255)' - COLOR_FOUNTAIN_BLUE: '(86, 180, 190)' - COLOR_FRENCH_PASS: '(189, 237, 253)' - COLOR_FRENCH_SKY_BLUE: '(119, 181, 254)' - COLOR_FRESH_AIR: '(166, 231, 255)' - COLOR_HALF_BAKED: '(133, 196, 204)' - COLOR_HAVELOCK_BLUE: '(85, 144, 217)' - COLOR_ICEBERG: '(113, 166, 210)' - COLOR_JORDY_BLUE: '(138, 185, 241)' - COLOR_LIGHT_BLUE: '(173, 216, 230)' - COLOR_LIGHT_COBALT_BLUE: '(136, 172, 224)' - COLOR_LIGHT_CORNFLOWER_BLUE: '(147, 204, 234)' - COLOR_LIGHT_SKY_BLUE: '(135, 206, 250)' - COLOR_LIGHT_STEEL_BLUE: '(176, 196, 222)' - COLOR_LITTLE_BOY_BLUE: '(108, 160, 220)' - COLOR_MALIBU: '(125, 200, 247)' - COLOR_MAXIMUM_BLUE: '(71, 171, 204)' - COLOR_MAYA_BLUE: '(115, 194, 251)' - COLOR_MEDIUM_SKY_BLUE: '(128, 218, 235)' - COLOR_MEDIUM_SLATE_BLUE: '(123, 104, 238)' - COLOR_MELROSE: '(199, 193, 255)' - COLOR_MORNING_GLORY: '(158, 222, 224)' - COLOR_NON_PHOTO_BLUE: '(164, 221, 237)' - COLOR_PALE_CERULEAN: '(155, 196, 226)' - COLOR_PALE_CORNFLOWER_BLUE: '(171, 205, 239)' - COLOR_PALE_CYAN: '(135, 211, 248)' - COLOR_PERANO: '(169, 190, 242)' - COLOR_PERIWINKLE_GRAY: '(195, 205, 230)' - COLOR_PICTON_BLUE: '(69, 177, 232)' - COLOR_PORTAGE: '(139, 159, 238)' - COLOR_POWDER_BLUE: '(176, 224, 230)' - COLOR_REGENT_ST_BLUE: '(170, 214, 230)' - COLOR_SAIL: '(184, 224, 249)' - COLOR_SCOOTER: '(46, 191, 212)' - COLOR_SEA_SERPENT: '(75, 199, 207)' - COLOR_SEAGULL: '(128, 204, 234)' - COLOR_SHAKESPEARE: '(78, 171, 209)' - COLOR_SKY_BLUE: '(135, 206, 235)' - COLOR_SPINDLE: '(182, 209, 234)' - COLOR_SPIRO_DISCO_BALL: '(15, 192, 252)' - COLOR_SPRAY: '(121, 222, 236)' - COLOR_TROPICAL_BLUE: '(195, 221, 249)' - COLOR_UNITED_NATIONS_BLUE: '(91, 146, 229)' - COLOR_VERY_LIGHT_AZURE: '(116, 187, 251)' - COLOR_VERY_LIGHT_BLUE: '(102, 102, 255)' - COLOR_VIKING: '(100, 204, 219)' - COLOR_VISTA_BLUE: '(124, 158, 217)' - COLOR_VIVID_SKY_BLUE: '(0, 204, 255)' - COLOR_WATERSPOUT: '(164, 244, 249)' - COLOR_WINTER_WIZARD: '(160, 230, 255)' - COLOR_ZIGGURAT: '(191, 219, 226)' - COLOR_AQUA_ISLAND: '(161, 218, 215)' - COLOR_AQUAMARINE: '(127, 255, 212)' - COLOR_BERMUDA: '(125, 216, 198)' - COLOR_BRIGHT_TURQUOISE: '(8, 232, 222)' - COLOR_CRUISE: '(181, 236, 223)' - COLOR_DOWNY: '(111, 208, 197)' - COLOR_EUCALYPTUS: '(68, 215, 168)' - COLOR_ICE_COLD: '(177, 244, 231)' - COLOR_JAGGED_ICE: '(194, 232, 229)' - COLOR_MAGIC_MINT: '(170, 240, 209)' - COLOR_MEDIUM_TURQUOISE: '(72, 209, 204)' - COLOR_MINT_TULIP: '(196, 244, 235)' - COLOR_MONTE_CARLO: '(131, 208, 198)' - COLOR_PALE_ROBIN_EGG_BLUE: '(150, 222, 209)' - COLOR_PEARL_AQUA: '(136, 216, 192)' - COLOR_RIPTIDE: '(139, 230, 216)' - COLOR_SINBAD: '(159, 215, 211)' - COLOR_TURQUOISE: '(64, 224, 208)' - COLOR_TURQUOISE_BLUE: '(0, 255, 239)' - COLOR_WATER_LEAF: '(161, 233, 222)' - COLOR_AERO_BLUE: '(201, 255, 229)' - COLOR_HUMMING_BIRD: '(207, 249, 243)' - COLOR_ONAHAU: '(205, 244, 255)' - COLOR_PERIWINKLE: '(204, 204, 255)' - COLOR_SCANDAL: '(207, 250, 244)' - COLOR_CELESTE: '(178, 255, 255)' - COLOR_CYAN: '(0, 255, 255)' - COLOR_LIGHT_TURQUOISE: '(175, 238, 238)' + COLOR_AERO: (124, 185, 232) + COLOR_ANAKIWA: (157, 229, 255) + COLOR_AQUAMARINE_BLUE: (113, 217, 226) + COLOR_BABY_BLUE: (137, 207, 240) + COLOR_BABY_BLUE_EYES: (161, 202, 241) + COLOR_BEAU_BLUE: (188, 212, 230) + COLOR_BELGION: (173, 216, 255) + COLOR_BILOBA_FLOWER: (178, 161, 234) + COLOR_BLEU_DE_FRANCE: (49, 140, 231) + COLOR_BLIZZARD_BLUE: (163, 227, 237) + COLOR_BLUE_JEANS: (93, 173, 236) + COLOR_BLUE_LAGOON: (172, 229, 238) + COLOR_BLUEBERRY: (79, 134, 247) + COLOR_BRILLIANT_AZURE: (51, 153, 255) + COLOR_CAPRI: (0, 191, 255) + COLOR_CAROLINA_BLUE: (86, 160, 211) + COLOR_CELESTIAL_BLUE: (73, 151, 208) + COLOR_CHARLOTTE: (186, 238, 249) + COLOR_COLUMBIA_BLUE: (196, 216, 226) + COLOR_CORNFLOWER_BLUE: (100, 149, 237) + COLOR_DANUBE: (96, 147, 209) + COLOR_DARK_IMPERIAL_BLUE: (110, 110, 249) + COLOR_DARK_SKY_BLUE: (140, 190, 214) + COLOR_DIAMOND: (185, 242, 255) + COLOR_DODGER_BLUE: (30, 144, 255) + COLOR_ELECTRIC_BLUE: (125, 249, 255) + COLOR_FOUNTAIN_BLUE: (86, 180, 190) + COLOR_FRENCH_PASS: (189, 237, 253) + COLOR_FRENCH_SKY_BLUE: (119, 181, 254) + COLOR_FRESH_AIR: (166, 231, 255) + COLOR_HALF_BAKED: (133, 196, 204) + COLOR_HAVELOCK_BLUE: (85, 144, 217) + COLOR_ICEBERG: (113, 166, 210) + COLOR_JORDY_BLUE: (138, 185, 241) + COLOR_LIGHT_BLUE: (173, 216, 230) + COLOR_LIGHT_COBALT_BLUE: (136, 172, 224) + COLOR_LIGHT_CORNFLOWER_BLUE: (147, 204, 234) + COLOR_LIGHT_SKY_BLUE: (135, 206, 250) + COLOR_LIGHT_STEEL_BLUE: (176, 196, 222) + COLOR_LITTLE_BOY_BLUE: (108, 160, 220) + COLOR_MALIBU: (125, 200, 247) + COLOR_MAXIMUM_BLUE: (71, 171, 204) + COLOR_MAYA_BLUE: (115, 194, 251) + COLOR_MEDIUM_SKY_BLUE: (128, 218, 235) + COLOR_MEDIUM_SLATE_BLUE: (123, 104, 238) + COLOR_MELROSE: (199, 193, 255) + COLOR_MORNING_GLORY: (158, 222, 224) + COLOR_NON_PHOTO_BLUE: (164, 221, 237) + COLOR_PALE_CERULEAN: (155, 196, 226) + COLOR_PALE_CORNFLOWER_BLUE: (171, 205, 239) + COLOR_PALE_CYAN: (135, 211, 248) + COLOR_PERANO: (169, 190, 242) + COLOR_PERIWINKLE_GRAY: (195, 205, 230) + COLOR_PICTON_BLUE: (69, 177, 232) + COLOR_PORTAGE: (139, 159, 238) + COLOR_POWDER_BLUE: (176, 224, 230) + COLOR_REGENT_ST_BLUE: (170, 214, 230) + COLOR_SAIL: (184, 224, 249) + COLOR_SCOOTER: (46, 191, 212) + COLOR_SEA_SERPENT: (75, 199, 207) + COLOR_SEAGULL: (128, 204, 234) + COLOR_SHAKESPEARE: (78, 171, 209) + COLOR_SKY_BLUE: (135, 206, 235) + COLOR_SPINDLE: (182, 209, 234) + COLOR_SPIRO_DISCO_BALL: (15, 192, 252) + COLOR_SPRAY: (121, 222, 236) + COLOR_TROPICAL_BLUE: (195, 221, 249) + COLOR_UNITED_NATIONS_BLUE: (91, 146, 229) + COLOR_VERY_LIGHT_AZURE: (116, 187, 251) + COLOR_VERY_LIGHT_BLUE: (102, 102, 255) + COLOR_VIKING: (100, 204, 219) + COLOR_VISTA_BLUE: (124, 158, 217) + COLOR_VIVID_SKY_BLUE: (0, 204, 255) + COLOR_WATERSPOUT: (164, 244, 249) + COLOR_WINTER_WIZARD: (160, 230, 255) + COLOR_ZIGGURAT: (191, 219, 226) + COLOR_AQUA_ISLAND: (161, 218, 215) + COLOR_AQUAMARINE: (127, 255, 212) + COLOR_BERMUDA: (125, 216, 198) + COLOR_BRIGHT_TURQUOISE: (8, 232, 222) + COLOR_CRUISE: (181, 236, 223) + COLOR_DOWNY: (111, 208, 197) + COLOR_EUCALYPTUS: (68, 215, 168) + COLOR_ICE_COLD: (177, 244, 231) + COLOR_JAGGED_ICE: (194, 232, 229) + COLOR_MAGIC_MINT: (170, 240, 209) + COLOR_MEDIUM_TURQUOISE: (72, 209, 204) + COLOR_MINT_TULIP: (196, 244, 235) + COLOR_MONTE_CARLO: (131, 208, 198) + COLOR_PALE_ROBIN_EGG_BLUE: (150, 222, 209) + COLOR_PEARL_AQUA: (136, 216, 192) + COLOR_RIPTIDE: (139, 230, 216) + COLOR_SINBAD: (159, 215, 211) + COLOR_TURQUOISE: (64, 224, 208) + COLOR_TURQUOISE_BLUE: (0, 255, 239) + COLOR_WATER_LEAF: (161, 233, 222) + COLOR_AERO_BLUE: (201, 255, 229) + COLOR_HUMMING_BIRD: (207, 249, 243) + COLOR_ONAHAU: (205, 244, 255) + COLOR_PERIWINKLE: (204, 204, 255) + COLOR_SCANDAL: (207, 250, 244) + COLOR_CELESTE: (178, 255, 255) + COLOR_CYAN: (0, 255, 255) + COLOR_LIGHT_TURQUOISE: (175, 238, 238) disappointment: - COLOR_AFRICAN_VIOLET: '(178, 132, 190)' - COLOR_AIR_SUPERIORITY_BLUE: '(114, 160, 193)' - COLOR_ALUMINIUM: '(169, 172, 182)' - COLOR_AMETHYST: '(153, 102, 204)' - COLOR_AMETHYST_SMOKE: '(163, 151, 180)' - COLOR_BALI_HAI: '(133, 159, 175)' - COLOR_BLUE_BELL: '(162, 162, 208)' - COLOR_BLUE_GRAY: '(102, 153, 204)' - COLOR_BLUE_HAZE: '(191, 190, 216)' - COLOR_BOMBAY: '(175, 177, 184)' - COLOR_BRIGHT_LAVENDER: '(191, 148, 228)' - COLOR_CADET_GREY: '(145, 163, 176)' - COLOR_CASPER: '(173, 190, 209)' - COLOR_CEIL: '(146, 161, 207)' - COLOR_CERULEAN_FROST: '(109, 155, 195)' - COLOR_CHATELLE: '(189, 179, 199)' - COLOR_CHETWODE_BLUE: '(133, 129, 217)' - COLOR_COLD_PURPLE: '(171, 160, 217)' - COLOR_COOL_GREY: '(140, 146, 172)' - COLOR_DARK_PASTEL_BLUE: '(119, 158, 203)' - COLOR_DARK_PASTEL_PURPLE: '(150, 111, 214)' - COLOR_DULL_LAVENDER: '(168, 153, 230)' - COLOR_EAST_SIDE: '(172, 145, 206)' - COLOR_FRENCH_GRAY: '(189, 189, 198)' - COLOR_GLACIER: '(128, 179, 196)' - COLOR_GLOSSY_GRAPE: '(171, 146, 179)' - COLOR_GRAY_CHATEAU: '(162, 170, 179)' - COLOR_GRAY_SUIT: '(193, 190, 205)' - COLOR_GULL_GRAY: '(157, 172, 183)' - COLOR_GUMBO: '(124, 161, 166)' - COLOR_HEATHER: '(183, 195, 208)' - COLOR_HIT_GRAY: '(161, 173, 181)' - COLOR_JUNGLE_MIST: '(180, 207, 211)' - COLOR_LAVENDER: '(181, 126, 220)' - COLOR_LAVENDER_GRAY: '(196, 195, 208)' - COLOR_LAVENDER_INDIGO: '(148, 87, 235)' - COLOR_LAVENDER_PURPLE: '(150, 123, 182)' - COLOR_LENURPLE: '(186, 147, 216)' - COLOR_LIGHT_PASTEL_PURPLE: '(177, 156, 217)' - COLOR_LILAC_BUSH: '(152, 116, 211)' - COLOR_LOBLOLLY: '(189, 201, 206)' - COLOR_LOGAN: '(170, 169, 205)' - COLOR_LONDON_HUE: '(190, 166, 195)' - COLOR_MANATEE: '(151, 154, 170)' - COLOR_MEDIUM_PURPLE: '(147, 112, 219)' - COLOR_MOODY_BLUE: '(127, 118, 211)' - COLOR_MOONSTONE_BLUE: '(115, 169, 194)' - COLOR_MOUNTAIN_MIST: '(149, 147, 150)' - COLOR_NEPAL: '(142, 171, 193)' - COLOR_NEPTUNE: '(124, 183, 187)' - COLOR_PASTEL_BLUE: '(174, 198, 207)' - COLOR_PASTEL_PURPLE: '(179, 158, 181)' - COLOR_PEWTER_BLUE: '(139, 168, 183)' - COLOR_PIGEON_POST: '(175, 189, 217)' - COLOR_POLO_BLUE: '(141, 168, 204)' - COLOR_PURPLE_MOUNTAIN_MAJESTY: '(150, 120, 182)' - COLOR_RICH_LAVENDER: '(167, 107, 207)' - COLOR_ROCK_BLUE: '(158, 177, 205)' - COLOR_SANTAS_GRAY: '(159, 160, 177)' - COLOR_SILVER_SAND: '(191, 193, 194)' - COLOR_SPUN_PEARL: '(170, 171, 183)' - COLOR_SUBMARINE: '(186, 199, 201)' - COLOR_TOWER_GRAY: '(169, 189, 191)' - COLOR_TRUE_V: '(138, 115, 214)' - COLOR_UBE: '(136, 120, 195)' - COLOR_WELDON_BLUE: '(124, 152, 171)' - COLOR_WILD_BLUE_YONDER: '(162, 173, 208)' - COLOR_WISTERIA: '(201, 160, 220)' - COLOR_WISTFUL: '(164, 166, 211)' - COLOR_AKAROA: '(212, 196, 168)' - COLOR_ANTIQUE_BRASS: '(205, 149, 117)' - COLOR_APACHE: '(223, 190, 111)' - COLOR_ARYLIDE_YELLOW: '(233, 214, 107)' - COLOR_ASH: '(198, 195, 181)' - COLOR_BISON_HIDE: '(193, 183, 164)' - COLOR_BLACK_SHADOWS: '(191, 175, 178)' - COLOR_BOUQUET: '(174, 128, 158)' - COLOR_BRANDY: '(222, 193, 150)' - COLOR_BRANDY_ROSE: '(187, 137, 131)' - COLOR_BRONCO: '(171, 161, 150)' - COLOR_BROWN_YELLOW: '(204, 153, 102)' - COLOR_BURLYWOOD: '(222, 184, 135)' - COLOR_BURNING_SAND: '(217, 147, 118)' - COLOR_CAMEO: '(217, 185, 155)' - COLOR_CAN_CAN: '(213, 145, 164)' - COLOR_CAREYS_PINK: '(210, 158, 170)' - COLOR_CHENIN: '(223, 205, 111)' - COLOR_CHINO: '(206, 199, 167)' - COLOR_CLAM_SHELL: '(212, 182, 175)' - COLOR_CLOUD: '(199, 196, 191)' - COLOR_CLOUDY: '(172, 165, 159)' - COLOR_COLD_TURKEY: '(206, 186, 186)' - COLOR_CORAL_REEF: '(199, 188, 162)' - COLOR_COTTON_SEED: '(194, 189, 182)' - COLOR_DARK_KHAKI: '(189, 183, 107)' - COLOR_DARK_VANILLA: '(209, 190, 168)' - COLOR_DAWN: '(166, 162, 154)' - COLOR_DEL_RIO: '(176, 154, 149)' - COLOR_DUSTY_GRAY: '(168, 152, 155)' - COLOR_ECRU: '(194, 178, 128)' - COLOR_ENGLISH_LAVENDER: '(180, 131, 149)' - COLOR_EQUATOR: '(225, 188, 100)' - COLOR_EUNRY: '(207, 163, 157)' - COLOR_FALLOW: '(193, 154, 107)' - COLOR_FAWN: '(229, 170, 112)' - COLOR_FOGGY_GRAY: '(203, 202, 182)' - COLOR_GIMBLET: '(184, 181, 106)' - COLOR_GRAY_OLIVE: '(169, 164, 145)' - COLOR_GRULLO: '(169, 154, 134)' - COLOR_HEATHERED_GRAY: '(182, 176, 149)' - COLOR_HELIOTROPE_GRAY: '(170, 152, 169)' - COLOR_HILLARY: '(172, 165, 134)' - COLOR_KHAKI: '(195, 176, 145)' - COLOR_LASER: '(200, 181, 104)' - COLOR_LIGHT_FRENCH_BEIGE: '(200, 173, 127)' - COLOR_LIGHT_MEDIUM_ORCHID: '(211, 155, 203)' - COLOR_LILAC_LUSTER: '(174, 152, 170)' - COLOR_LILY: '(200, 170, 191)' - COLOR_MALTA: '(189, 178, 161)' - COLOR_MARTINI: '(175, 160, 158)' - COLOR_MISTY_MOSS: '(187, 180, 119)' - COLOR_MONGOOSE: '(181, 162, 127)' - COLOR_MY_PINK: '(214, 145, 136)' - COLOR_NAPA: '(172, 164, 148)' - COLOR_NOBEL: '(183, 177, 177)' - COLOR_NOMAD: '(186, 177, 162)' - COLOR_OLD_ROSE: '(192, 128, 129)' - COLOR_OLIVE_GREEN: '(181, 179, 92)' - COLOR_OPERA_MAUVE: '(183, 132, 167)' - COLOR_ORIENTAL_PINK: '(198, 145, 145)' - COLOR_PALE_SILVER: '(201, 192, 187)' - COLOR_PALE_SLATE: '(195, 191, 193)' - COLOR_PALE_TAUPE: '(188, 152, 126)' - COLOR_PASTEL_PINK: '(222, 165, 164)' - COLOR_PASTEL_VIOLET: '(203, 153, 201)' - COLOR_PAVLOVA: '(215, 196, 152)' - COLOR_PEARLY_PURPLE: '(183, 104, 162)' - COLOR_PETITE_ORCHID: '(219, 150, 144)' - COLOR_PINK_SWAN: '(190, 181, 183)' - COLOR_PUCE: '(204, 136, 153)' - COLOR_QUICKSAND: '(189, 151, 142)' - COLOR_ROB_ROY: '(234, 198, 116)' - COLOR_RODEO_DUST: '(201, 178, 155)' - COLOR_ROSY_BROWN: '(188, 143, 143)' - COLOR_SAGE: '(188, 184, 138)' - COLOR_SHADY_LADY: '(170, 165, 169)' - COLOR_SILK: '(189, 177, 168)' - COLOR_SILVER_PINK: '(196, 174, 173)' - COLOR_SOFT_AMBER: '(209, 198, 180)' - COLOR_SORRELL_BROWN: '(206, 185, 143)' - COLOR_STRAW: '(228, 217, 111)' - COLOR_SUNDANCE: '(201, 179, 91)' - COLOR_TACHA: '(214, 197, 98)' - COLOR_TALLOW: '(168, 165, 137)' - COLOR_TAN: '(210, 180, 140)' - COLOR_TEA: '(193, 186, 176)' - COLOR_THATCH: '(182, 157, 152)' - COLOR_THISTLE_GREEN: '(204, 202, 168)' - COLOR_TIDE: '(191, 184, 176)' - COLOR_TONYS_PINK: '(231, 159, 140)' - COLOR_TUMBLEWEED: '(222, 170, 136)' - COLOR_TUSCANY: '(192, 153, 153)' - COLOR_VEGAS_GOLD: '(197, 179, 88)' - COLOR_VIOLA: '(203, 143, 169)' - COLOR_WHISKEY: '(213, 154, 111)' - COLOR_WINTER_HAZEL: '(213, 209, 149)' - COLOR_YUMA: '(206, 194, 145)' - COLOR_ZORBA: '(165, 155, 145)' - COLOR_ACAPULCO: '(124, 176, 161)' - COLOR_ALGAE_GREEN: '(147, 223, 184)' - COLOR_ASH_GREY: '(178, 190, 181)' - COLOR_BOOGER_BUSTER: '(221, 226, 106)' - COLOR_BUD: '(168, 174, 156)' - COLOR_CAMBRIDGE_BLUE: '(163, 193, 173)' - COLOR_CASCADE: '(139, 169, 165)' - COLOR_CELADON: '(172, 225, 175)' - COLOR_CELERY: '(184, 194, 93)' - COLOR_CHINOOK: '(168, 227, 189)' - COLOR_CLAY_ASH: '(189, 200, 179)' - COLOR_CONIFER: '(172, 221, 77)' - COLOR_CORIANDER: '(196, 208, 176)' - COLOR_DARK_SEA_GREEN: '(143, 188, 143)' - COLOR_DE_YORK: '(122, 196, 136)' - COLOR_DECO: '(210, 218, 151)' - COLOR_EAGLE: '(182, 186, 164)' - COLOR_EDWARD: '(162, 174, 171)' - COLOR_ENVY: '(139, 166, 144)' - COLOR_ETON_BLUE: '(150, 200, 162)' - COLOR_FEIJOA: '(159, 221, 140)' - COLOR_FRINGY_FLOWER: '(177, 226, 193)' - COLOR_GRANNY_SMITH_APPLE: '(168, 228, 160)' - COLOR_GREEN_MIST: '(203, 211, 176)' - COLOR_GREEN_SMOKE: '(164, 175, 110)' - COLOR_GREEN_SPRING: '(184, 193, 177)' - COLOR_GULF_STREAM: '(128, 179, 174)' - COLOR_GUM_LEAF: '(182, 211, 191)' - COLOR_INCHWORM: '(178, 236, 93)' - COLOR_JET_STREAM: '(181, 210, 206)' - COLOR_JUNE_BUD: '(189, 218, 87)' - COLOR_KANGAROO: '(198, 200, 189)' - COLOR_LAUREL_GREEN: '(169, 186, 157)' - COLOR_LEMON_GRASS: '(155, 158, 143)' - COLOR_LIGHT_GREEN: '(144, 238, 144)' - COLOR_LIGHT_MOSS_GREEN: '(173, 223, 173)' - COLOR_LOCUST: '(168, 175, 142)' - COLOR_MADANG: '(183, 240, 190)' - COLOR_MEDIUM_AQUAMARINE: '(102, 221, 170)' - COLOR_MEDIUM_SPRING_BUD: '(201, 220, 135)' - COLOR_MINT_GREEN: '(152, 255, 152)' - COLOR_NORWAY: '(168, 189, 159)' - COLOR_OLIVINE: '(154, 185, 115)' - COLOR_OPAL: '(169, 198, 194)' - COLOR_PADUA: '(173, 230, 196)' - COLOR_PALE_GREEN: '(152, 251, 152)' - COLOR_PALE_LEAF: '(192, 211, 185)' - COLOR_PASTEL_GREEN: '(119, 221, 119)' - COLOR_PEWTER: '(150, 168, 161)' - COLOR_PINE_GLADE: '(199, 205, 144)' - COLOR_PISTACHIO: '(147, 197, 114)' - COLOR_PIXIE_GREEN: '(192, 216, 182)' - COLOR_POWDER_ASH: '(188, 201, 194)' - COLOR_PUMICE: '(194, 202, 196)' - COLOR_RAINEE: '(185, 200, 172)' - COLOR_REEF: '(201, 255, 162)' - COLOR_SCHIST: '(169, 180, 151)' - COLOR_SCREAMIN_GREEN: '(102, 255, 102)' - COLOR_SHADOW_GREEN: '(154, 194, 184)' - COLOR_SPRING_RAIN: '(172, 203, 177)' - COLOR_SPROUT: '(193, 215, 176)' - COLOR_SULU: '(193, 240, 124)' - COLOR_SUMMER_GREEN: '(150, 187, 171)' - COLOR_SURF: '(187, 215, 193)' - COLOR_SWAMP_GREEN: '(172, 183, 142)' - COLOR_TEAL_DEER: '(153, 230, 179)' - COLOR_TRADEWIND: '(95, 179, 172)' - COLOR_TURQUOISE_GREEN: '(160, 214, 180)' - COLOR_VERY_LIGHT_MALACHITE_GREEN: '(100, 233, 134)' - COLOR_WILD_WILLOW: '(185, 196, 106)' - COLOR_LIGHT_GRAYISH_MAGENTA: '(204, 153, 204)' - COLOR_LILAC: '(200, 162, 200)' - COLOR_DARK_MEDIUM_GRAY: '(169, 169, 169)' - COLOR_DELTA: '(164, 164, 157)' - COLOR_GRAY_NICKEL: '(195, 195, 189)' - COLOR_MIST_GRAY: '(196, 196, 188)' - COLOR_QUICK_SILVER: '(166, 166, 166)' - COLOR_SILVER: '(192, 192, 192)' - COLOR_SILVER_CHALICE: '(172, 172, 172)' - COLOR_SPANISH_GRAY: '(152, 152, 152)' - COLOR_STAR_DUST: '(159, 159, 156)' - COLOR_TIARA: '(195, 209, 209)' - COLOR_GRANNY_SMITH: '(132, 160, 160)' + COLOR_AFRICAN_VIOLET: (178, 132, 190) + COLOR_AIR_SUPERIORITY_BLUE: (114, 160, 193) + COLOR_ALUMINIUM: (169, 172, 182) + COLOR_AMETHYST: (153, 102, 204) + COLOR_AMETHYST_SMOKE: (163, 151, 180) + COLOR_BALI_HAI: (133, 159, 175) + COLOR_BLUE_BELL: (162, 162, 208) + COLOR_BLUE_GRAY: (102, 153, 204) + COLOR_BLUE_HAZE: (191, 190, 216) + COLOR_BOMBAY: (175, 177, 184) + COLOR_BRIGHT_LAVENDER: (191, 148, 228) + COLOR_CADET_GREY: (145, 163, 176) + COLOR_CASPER: (173, 190, 209) + COLOR_CEIL: (146, 161, 207) + COLOR_CERULEAN_FROST: (109, 155, 195) + COLOR_CHATELLE: (189, 179, 199) + COLOR_CHETWODE_BLUE: (133, 129, 217) + COLOR_COLD_PURPLE: (171, 160, 217) + COLOR_COOL_GREY: (140, 146, 172) + COLOR_DARK_PASTEL_BLUE: (119, 158, 203) + COLOR_DARK_PASTEL_PURPLE: (150, 111, 214) + COLOR_DULL_LAVENDER: (168, 153, 230) + COLOR_EAST_SIDE: (172, 145, 206) + COLOR_FRENCH_GRAY: (189, 189, 198) + COLOR_GLACIER: (128, 179, 196) + COLOR_GLOSSY_GRAPE: (171, 146, 179) + COLOR_GRAY_CHATEAU: (162, 170, 179) + COLOR_GRAY_SUIT: (193, 190, 205) + COLOR_GULL_GRAY: (157, 172, 183) + COLOR_GUMBO: (124, 161, 166) + COLOR_HEATHER: (183, 195, 208) + COLOR_HIT_GRAY: (161, 173, 181) + COLOR_JUNGLE_MIST: (180, 207, 211) + COLOR_LAVENDER: (181, 126, 220) + COLOR_LAVENDER_GRAY: (196, 195, 208) + COLOR_LAVENDER_INDIGO: (148, 87, 235) + COLOR_LAVENDER_PURPLE: (150, 123, 182) + COLOR_LENURPLE: (186, 147, 216) + COLOR_LIGHT_PASTEL_PURPLE: (177, 156, 217) + COLOR_LILAC_BUSH: (152, 116, 211) + COLOR_LOBLOLLY: (189, 201, 206) + COLOR_LOGAN: (170, 169, 205) + COLOR_LONDON_HUE: (190, 166, 195) + COLOR_MANATEE: (151, 154, 170) + COLOR_MEDIUM_PURPLE: (147, 112, 219) + COLOR_MOODY_BLUE: (127, 118, 211) + COLOR_MOONSTONE_BLUE: (115, 169, 194) + COLOR_MOUNTAIN_MIST: (149, 147, 150) + COLOR_NEPAL: (142, 171, 193) + COLOR_NEPTUNE: (124, 183, 187) + COLOR_PASTEL_BLUE: (174, 198, 207) + COLOR_PASTEL_PURPLE: (179, 158, 181) + COLOR_PEWTER_BLUE: (139, 168, 183) + COLOR_PIGEON_POST: (175, 189, 217) + COLOR_POLO_BLUE: (141, 168, 204) + COLOR_PURPLE_MOUNTAIN_MAJESTY: (150, 120, 182) + COLOR_RICH_LAVENDER: (167, 107, 207) + COLOR_ROCK_BLUE: (158, 177, 205) + COLOR_SANTAS_GRAY: (159, 160, 177) + COLOR_SILVER_SAND: (191, 193, 194) + COLOR_SPUN_PEARL: (170, 171, 183) + COLOR_SUBMARINE: (186, 199, 201) + COLOR_TOWER_GRAY: (169, 189, 191) + COLOR_TRUE_V: (138, 115, 214) + COLOR_UBE: (136, 120, 195) + COLOR_WELDON_BLUE: (124, 152, 171) + COLOR_WILD_BLUE_YONDER: (162, 173, 208) + COLOR_WISTERIA: (201, 160, 220) + COLOR_WISTFUL: (164, 166, 211) + COLOR_AKAROA: (212, 196, 168) + COLOR_ANTIQUE_BRASS: (205, 149, 117) + COLOR_APACHE: (223, 190, 111) + COLOR_ARYLIDE_YELLOW: (233, 214, 107) + COLOR_ASH: (198, 195, 181) + COLOR_BISON_HIDE: (193, 183, 164) + COLOR_BLACK_SHADOWS: (191, 175, 178) + COLOR_BOUQUET: (174, 128, 158) + COLOR_BRANDY: (222, 193, 150) + COLOR_BRANDY_ROSE: (187, 137, 131) + COLOR_BRONCO: (171, 161, 150) + COLOR_BROWN_YELLOW: (204, 153, 102) + COLOR_BURLYWOOD: (222, 184, 135) + COLOR_BURNING_SAND: (217, 147, 118) + COLOR_CAMEO: (217, 185, 155) + COLOR_CAN_CAN: (213, 145, 164) + COLOR_CAREYS_PINK: (210, 158, 170) + COLOR_CHENIN: (223, 205, 111) + COLOR_CHINO: (206, 199, 167) + COLOR_CLAM_SHELL: (212, 182, 175) + COLOR_CLOUD: (199, 196, 191) + COLOR_CLOUDY: (172, 165, 159) + COLOR_COLD_TURKEY: (206, 186, 186) + COLOR_CORAL_REEF: (199, 188, 162) + COLOR_COTTON_SEED: (194, 189, 182) + COLOR_DARK_KHAKI: (189, 183, 107) + COLOR_DARK_VANILLA: (209, 190, 168) + COLOR_DAWN: (166, 162, 154) + COLOR_DEL_RIO: (176, 154, 149) + COLOR_DUSTY_GRAY: (168, 152, 155) + COLOR_ECRU: (194, 178, 128) + COLOR_ENGLISH_LAVENDER: (180, 131, 149) + COLOR_EQUATOR: (225, 188, 100) + COLOR_EUNRY: (207, 163, 157) + COLOR_FALLOW: (193, 154, 107) + COLOR_FAWN: (229, 170, 112) + COLOR_FOGGY_GRAY: (203, 202, 182) + COLOR_GIMBLET: (184, 181, 106) + COLOR_GRAY_OLIVE: (169, 164, 145) + COLOR_GRULLO: (169, 154, 134) + COLOR_HEATHERED_GRAY: (182, 176, 149) + COLOR_HELIOTROPE_GRAY: (170, 152, 169) + COLOR_HILLARY: (172, 165, 134) + COLOR_KHAKI: (195, 176, 145) + COLOR_LASER: (200, 181, 104) + COLOR_LIGHT_FRENCH_BEIGE: (200, 173, 127) + COLOR_LIGHT_MEDIUM_ORCHID: (211, 155, 203) + COLOR_LILAC_LUSTER: (174, 152, 170) + COLOR_LILY: (200, 170, 191) + COLOR_MALTA: (189, 178, 161) + COLOR_MARTINI: (175, 160, 158) + COLOR_MISTY_MOSS: (187, 180, 119) + COLOR_MONGOOSE: (181, 162, 127) + COLOR_MY_PINK: (214, 145, 136) + COLOR_NAPA: (172, 164, 148) + COLOR_NOBEL: (183, 177, 177) + COLOR_NOMAD: (186, 177, 162) + COLOR_OLD_ROSE: (192, 128, 129) + COLOR_OLIVE_GREEN: (181, 179, 92) + COLOR_OPERA_MAUVE: (183, 132, 167) + COLOR_ORIENTAL_PINK: (198, 145, 145) + COLOR_PALE_SILVER: (201, 192, 187) + COLOR_PALE_SLATE: (195, 191, 193) + COLOR_PALE_TAUPE: (188, 152, 126) + COLOR_PASTEL_PINK: (222, 165, 164) + COLOR_PASTEL_VIOLET: (203, 153, 201) + COLOR_PAVLOVA: (215, 196, 152) + COLOR_PEARLY_PURPLE: (183, 104, 162) + COLOR_PETITE_ORCHID: (219, 150, 144) + COLOR_PINK_SWAN: (190, 181, 183) + COLOR_PUCE: (204, 136, 153) + COLOR_QUICKSAND: (189, 151, 142) + COLOR_ROB_ROY: (234, 198, 116) + COLOR_RODEO_DUST: (201, 178, 155) + COLOR_ROSY_BROWN: (188, 143, 143) + COLOR_SAGE: (188, 184, 138) + COLOR_SHADY_LADY: (170, 165, 169) + COLOR_SILK: (189, 177, 168) + COLOR_SILVER_PINK: (196, 174, 173) + COLOR_SOFT_AMBER: (209, 198, 180) + COLOR_SORRELL_BROWN: (206, 185, 143) + COLOR_STRAW: (228, 217, 111) + COLOR_SUNDANCE: (201, 179, 91) + COLOR_TACHA: (214, 197, 98) + COLOR_TALLOW: (168, 165, 137) + COLOR_TAN: (210, 180, 140) + COLOR_TEA: (193, 186, 176) + COLOR_THATCH: (182, 157, 152) + COLOR_THISTLE_GREEN: (204, 202, 168) + COLOR_TIDE: (191, 184, 176) + COLOR_TONYS_PINK: (231, 159, 140) + COLOR_TUMBLEWEED: (222, 170, 136) + COLOR_TUSCANY: (192, 153, 153) + COLOR_VEGAS_GOLD: (197, 179, 88) + COLOR_VIOLA: (203, 143, 169) + COLOR_WHISKEY: (213, 154, 111) + COLOR_WINTER_HAZEL: (213, 209, 149) + COLOR_YUMA: (206, 194, 145) + COLOR_ZORBA: (165, 155, 145) + COLOR_ACAPULCO: (124, 176, 161) + COLOR_ALGAE_GREEN: (147, 223, 184) + COLOR_ASH_GREY: (178, 190, 181) + COLOR_BOOGER_BUSTER: (221, 226, 106) + COLOR_BUD: (168, 174, 156) + COLOR_CAMBRIDGE_BLUE: (163, 193, 173) + COLOR_CASCADE: (139, 169, 165) + COLOR_CELADON: (172, 225, 175) + COLOR_CELERY: (184, 194, 93) + COLOR_CHINOOK: (168, 227, 189) + COLOR_CLAY_ASH: (189, 200, 179) + COLOR_CONIFER: (172, 221, 77) + COLOR_CORIANDER: (196, 208, 176) + COLOR_DARK_SEA_GREEN: (143, 188, 143) + COLOR_DE_YORK: (122, 196, 136) + COLOR_DECO: (210, 218, 151) + COLOR_EAGLE: (182, 186, 164) + COLOR_EDWARD: (162, 174, 171) + COLOR_ENVY: (139, 166, 144) + COLOR_ETON_BLUE: (150, 200, 162) + COLOR_FEIJOA: (159, 221, 140) + COLOR_FRINGY_FLOWER: (177, 226, 193) + COLOR_GRANNY_SMITH_APPLE: (168, 228, 160) + COLOR_GREEN_MIST: (203, 211, 176) + COLOR_GREEN_SMOKE: (164, 175, 110) + COLOR_GREEN_SPRING: (184, 193, 177) + COLOR_GULF_STREAM: (128, 179, 174) + COLOR_GUM_LEAF: (182, 211, 191) + COLOR_INCHWORM: (178, 236, 93) + COLOR_JET_STREAM: (181, 210, 206) + COLOR_JUNE_BUD: (189, 218, 87) + COLOR_KANGAROO: (198, 200, 189) + COLOR_LAUREL_GREEN: (169, 186, 157) + COLOR_LEMON_GRASS: (155, 158, 143) + COLOR_LIGHT_GREEN: (144, 238, 144) + COLOR_LIGHT_MOSS_GREEN: (173, 223, 173) + COLOR_LOCUST: (168, 175, 142) + COLOR_MADANG: (183, 240, 190) + COLOR_MEDIUM_AQUAMARINE: (102, 221, 170) + COLOR_MEDIUM_SPRING_BUD: (201, 220, 135) + COLOR_MINT_GREEN: (152, 255, 152) + COLOR_NORWAY: (168, 189, 159) + COLOR_OLIVINE: (154, 185, 115) + COLOR_OPAL: (169, 198, 194) + COLOR_PADUA: (173, 230, 196) + COLOR_PALE_GREEN: (152, 251, 152) + COLOR_PALE_LEAF: (192, 211, 185) + COLOR_PASTEL_GREEN: (119, 221, 119) + COLOR_PEWTER: (150, 168, 161) + COLOR_PINE_GLADE: (199, 205, 144) + COLOR_PISTACHIO: (147, 197, 114) + COLOR_PIXIE_GREEN: (192, 216, 182) + COLOR_POWDER_ASH: (188, 201, 194) + COLOR_PUMICE: (194, 202, 196) + COLOR_RAINEE: (185, 200, 172) + COLOR_REEF: (201, 255, 162) + COLOR_SCHIST: (169, 180, 151) + COLOR_SCREAMIN_GREEN: (102, 255, 102) + COLOR_SHADOW_GREEN: (154, 194, 184) + COLOR_SPRING_RAIN: (172, 203, 177) + COLOR_SPROUT: (193, 215, 176) + COLOR_SULU: (193, 240, 124) + COLOR_SUMMER_GREEN: (150, 187, 171) + COLOR_SURF: (187, 215, 193) + COLOR_SWAMP_GREEN: (172, 183, 142) + COLOR_TEAL_DEER: (153, 230, 179) + COLOR_TRADEWIND: (95, 179, 172) + COLOR_TURQUOISE_GREEN: (160, 214, 180) + COLOR_VERY_LIGHT_MALACHITE_GREEN: (100, 233, 134) + COLOR_WILD_WILLOW: (185, 196, 106) + COLOR_LIGHT_GRAYISH_MAGENTA: (204, 153, 204) + COLOR_LILAC: (200, 162, 200) + COLOR_DARK_MEDIUM_GRAY: (169, 169, 169) + COLOR_DELTA: (164, 164, 157) + COLOR_GRAY_NICKEL: (195, 195, 189) + COLOR_MIST_GRAY: (196, 196, 188) + COLOR_QUICK_SILVER: (166, 166, 166) + COLOR_SILVER: (192, 192, 192) + COLOR_SILVER_CHALICE: (172, 172, 172) + COLOR_SPANISH_GRAY: (152, 152, 152) + COLOR_STAR_DUST: (159, 159, 156) + COLOR_TIARA: (195, 209, 209) + COLOR_GRANNY_SMITH: (132, 160, 160) pride: - COLOR_AIR_FORCE_BLUE: '(0, 48, 143)' - COLOR_ARAPAWA: '(17, 12, 108)' - COLOR_ASTRONAUT: '(40, 58, 119)' - COLOR_ASTRONAUT_BLUE: '(1, 62, 98)' - COLOR_BAY_OF_MANY: '(39, 58, 129)' - COLOR_BIG_STONE: '(22, 42, 64)' - COLOR_BISCAY: '(27, 49, 98)' - COLOR_BLACK_ROCK: '(13, 3, 50)' - COLOR_BLACKCURRANT: '(50, 41, 58)' - COLOR_BLEACHED_CEDAR: '(44, 33, 51)' - COLOR_BLUE_DIAMOND: '(56, 4, 116)' - COLOR_BLUE_GEM: '(44, 14, 140)' - COLOR_BLUE_WHALE: '(4, 46, 76)' - COLOR_BLUE_ZODIAC: '(19, 38, 77)' - COLOR_BUNTING: '(21, 31, 76)' - COLOR_CATALINA_BLUE: '(6, 42, 120)' - COLOR_CELLO: '(30, 56, 91)' - COLOR_CHARADE: '(41, 41, 55)' - COLOR_CHERRY_PIE: '(42, 3, 89)' - COLOR_CHRISTALLE: '(51, 3, 107)' - COLOR_CLOUD_BURST: '(32, 46, 84)' - COLOR_COOL_BLACK: '(0, 46, 99)' - COLOR_COSMIC_COBALT: '(46, 45, 136)' - COLOR_CYPRUS: '(0, 62, 64)' - COLOR_DARK_BLUE: '(0, 0, 139)' - COLOR_DARK_MIDNIGHT_BLUE: '(0, 51, 102)' - COLOR_DARK_PURPLE: '(48, 25, 52)' - COLOR_DEEP_BLUE: '(34, 8, 120)' - COLOR_DEEP_COVE: '(5, 16, 64)' - COLOR_DEEP_SAPPHIRE: '(8, 37, 103)' - COLOR_DEEP_VIOLET: '(51, 0, 102)' - COLOR_DOWNRIVER: '(9, 34, 86)' - COLOR_DUKE_BLUE: '(0, 0, 156)' - COLOR_EBONY_CLAY: '(38, 40, 59)' - COLOR_EGYPTIAN_BLUE: '(16, 52, 166)' - COLOR_ELEPHANT: '(18, 52, 71)' - COLOR_GOVERNOR_BAY: '(47, 60, 179)' - COLOR_GREEN_VOGUE: '(3, 43, 82)' - COLOR_GULF_BLUE: '(5, 22, 87)' - COLOR_HAITI: '(27, 16, 53)' - COLOR_IMPERIAL_BLUE: '(0, 35, 149)' - COLOR_INDIGO_DYE: '(9, 31, 146)' - COLOR_INTERNATIONAL_KLEIN_BLUE: '(0, 47, 167)' - COLOR_JACARTA: '(58, 42, 106)' - COLOR_JACKSONS_PURPLE: '(32, 32, 141)' - COLOR_JAGGER: '(53, 14, 87)' - COLOR_KINGFISHER_DAISY: '(62, 4, 128)' - COLOR_LUCKY_POINT: '(26, 26, 104)' - COLOR_MADISON: '(9, 37, 93)' - COLOR_METEORITE: '(60, 31, 118)' - COLOR_MIDNIGHT_BLUE: '(25, 25, 112)' - COLOR_NAVY: '(0, 0, 128)' - COLOR_NILE_BLUE: '(25, 55, 81)' - COLOR_OXFORD_BLUE: '(0, 33, 71)' - COLOR_PANTONE_BLUE: '(0, 24, 168)' - COLOR_PARIS_M: '(38, 5, 106)' - COLOR_PAUA: '(38, 3, 104)' - COLOR_PERSIAN_BLUE: '(28, 57, 187)' - COLOR_PERSIAN_INDIGO: '(50, 18, 122)' - COLOR_PHTHALO_BLUE: '(0, 15, 137)' - COLOR_PIGMENT_BLUE: '(51, 51, 153)' - COLOR_PIXIE_POWDER: '(57, 18, 133)' - COLOR_PORT_GORE: '(37, 31, 79)' - COLOR_PRUSSIAN_BLUE: '(0, 49, 83)' - COLOR_REGAL_BLUE: '(1, 63, 106)' - COLOR_RESOLUTION_BLUE: '(0, 35, 135)' - COLOR_REVOLVER: '(44, 22, 50)' - COLOR_ROYAL_AZURE: '(0, 56, 168)' - COLOR_RUSSIAN_VIOLET: '(50, 23, 77)' - COLOR_SMALT: '(0, 51, 153)' - COLOR_SPACE_CADET: '(29, 41, 81)' - COLOR_ST_PATRICKS_BLUE: '(35, 41, 122)' - COLOR_STEEL_GRAY: '(38, 35, 53)' - COLOR_STRATOS: '(0, 7, 65)' - COLOR_TANGAROA: '(3, 22, 60)' - COLOR_TARAWERA: '(7, 58, 80)' - COLOR_TIBER: '(6, 53, 55)' - COLOR_TOLOPEA: '(27, 2, 69)' - COLOR_TOREA_BAY: '(15, 45, 158)' - COLOR_UA_BLUE: '(0, 51, 170)' - COLOR_VALENTINO: '(53, 14, 66)' - COLOR_VALHALLA: '(43, 25, 79)' - COLOR_VIOLENT_VIOLET: '(41, 12, 94)' - COLOR_WINDSOR: '(60, 8, 120)' - COLOR_YANKEES_BLUE: '(28, 40, 65)' - COLOR_ZAFFRE: '(0, 20, 168)' - COLOR_BALTIC_SEA: '(42, 38, 48)' - COLOR_BASTILLE: '(41, 33, 48)' - COLOR_BLACK: '(0, 0, 0)' - COLOR_BLACK_PEARL: '(4, 19, 34)' - COLOR_BLACK_RUSSIAN: '(10, 0, 28)' - COLOR_BLUE_CHARCOAL: '(1, 13, 26)' - COLOR_BUNKER: '(13, 17, 23)' - COLOR_CHARLESTON_GREEN: '(35, 43, 43)' - COLOR_CINDER: '(14, 14, 24)' - COLOR_COD_GRAY: '(11, 11, 11)' - COLOR_DAINTREE: '(1, 39, 49)' - COLOR_DARK_GUNMETAL: '(31, 38, 42)' - COLOR_EERIE_BLACK: '(27, 27, 27)' - COLOR_FIREFLY: '(14, 42, 48)' - COLOR_JAGUAR: '(8, 1, 16)' - COLOR_MIRAGE: '(22, 25, 40)' - COLOR_RAISIN_BLACK: '(36, 33, 36)' - COLOR_SHARK: '(37, 39, 44)' - COLOR_SWAMP: '(0, 27, 28)' - COLOR_VULCAN: '(16, 18, 29)' - COLOR_WOODSMOKE: '(12, 13, 15)' - COLOR_RICH_BLACK: '(0, 64, 64)' + COLOR_AIR_FORCE_BLUE: (0, 48, 143) + COLOR_ARAPAWA: (17, 12, 108) + COLOR_ASTRONAUT: (40, 58, 119) + COLOR_ASTRONAUT_BLUE: (1, 62, 98) + COLOR_BAY_OF_MANY: (39, 58, 129) + COLOR_BIG_STONE: (22, 42, 64) + COLOR_BISCAY: (27, 49, 98) + COLOR_BLACK_ROCK: (13, 3, 50) + COLOR_BLACKCURRANT: (50, 41, 58) + COLOR_BLEACHED_CEDAR: (44, 33, 51) + COLOR_BLUE_DIAMOND: (56, 4, 116) + COLOR_BLUE_GEM: (44, 14, 140) + COLOR_BLUE_WHALE: (4, 46, 76) + COLOR_BLUE_ZODIAC: (19, 38, 77) + COLOR_BUNTING: (21, 31, 76) + COLOR_CATALINA_BLUE: (6, 42, 120) + COLOR_CELLO: (30, 56, 91) + COLOR_CHARADE: (41, 41, 55) + COLOR_CHERRY_PIE: (42, 3, 89) + COLOR_CHRISTALLE: (51, 3, 107) + COLOR_CLOUD_BURST: (32, 46, 84) + COLOR_COOL_BLACK: (0, 46, 99) + COLOR_COSMIC_COBALT: (46, 45, 136) + COLOR_CYPRUS: (0, 62, 64) + COLOR_DARK_BLUE: (0, 0, 139) + COLOR_DARK_MIDNIGHT_BLUE: (0, 51, 102) + COLOR_DARK_PURPLE: (48, 25, 52) + COLOR_DEEP_BLUE: (34, 8, 120) + COLOR_DEEP_COVE: (5, 16, 64) + COLOR_DEEP_SAPPHIRE: (8, 37, 103) + COLOR_DEEP_VIOLET: (51, 0, 102) + COLOR_DOWNRIVER: (9, 34, 86) + COLOR_DUKE_BLUE: (0, 0, 156) + COLOR_EBONY_CLAY: (38, 40, 59) + COLOR_EGYPTIAN_BLUE: (16, 52, 166) + COLOR_ELEPHANT: (18, 52, 71) + COLOR_GOVERNOR_BAY: (47, 60, 179) + COLOR_GREEN_VOGUE: (3, 43, 82) + COLOR_GULF_BLUE: (5, 22, 87) + COLOR_HAITI: (27, 16, 53) + COLOR_IMPERIAL_BLUE: (0, 35, 149) + COLOR_INDIGO_DYE: (9, 31, 146) + COLOR_INTERNATIONAL_KLEIN_BLUE: (0, 47, 167) + COLOR_JACARTA: (58, 42, 106) + COLOR_JACKSONS_PURPLE: (32, 32, 141) + COLOR_JAGGER: (53, 14, 87) + COLOR_KINGFISHER_DAISY: (62, 4, 128) + COLOR_LUCKY_POINT: (26, 26, 104) + COLOR_MADISON: (9, 37, 93) + COLOR_METEORITE: (60, 31, 118) + COLOR_MIDNIGHT_BLUE: (25, 25, 112) + COLOR_NAVY: (0, 0, 128) + COLOR_NILE_BLUE: (25, 55, 81) + COLOR_OXFORD_BLUE: (0, 33, 71) + COLOR_PANTONE_BLUE: (0, 24, 168) + COLOR_PARIS_M: (38, 5, 106) + COLOR_PAUA: (38, 3, 104) + COLOR_PERSIAN_BLUE: (28, 57, 187) + COLOR_PERSIAN_INDIGO: (50, 18, 122) + COLOR_PHTHALO_BLUE: (0, 15, 137) + COLOR_PIGMENT_BLUE: (51, 51, 153) + COLOR_PIXIE_POWDER: (57, 18, 133) + COLOR_PORT_GORE: (37, 31, 79) + COLOR_PRUSSIAN_BLUE: (0, 49, 83) + COLOR_REGAL_BLUE: (1, 63, 106) + COLOR_RESOLUTION_BLUE: (0, 35, 135) + COLOR_REVOLVER: (44, 22, 50) + COLOR_ROYAL_AZURE: (0, 56, 168) + COLOR_RUSSIAN_VIOLET: (50, 23, 77) + COLOR_SMALT: (0, 51, 153) + COLOR_SPACE_CADET: (29, 41, 81) + COLOR_ST_PATRICKS_BLUE: (35, 41, 122) + COLOR_STEEL_GRAY: (38, 35, 53) + COLOR_STRATOS: (0, 7, 65) + COLOR_TANGAROA: (3, 22, 60) + COLOR_TARAWERA: (7, 58, 80) + COLOR_TIBER: (6, 53, 55) + COLOR_TOLOPEA: (27, 2, 69) + COLOR_TOREA_BAY: (15, 45, 158) + COLOR_UA_BLUE: (0, 51, 170) + COLOR_VALENTINO: (53, 14, 66) + COLOR_VALHALLA: (43, 25, 79) + COLOR_VIOLENT_VIOLET: (41, 12, 94) + COLOR_WINDSOR: (60, 8, 120) + COLOR_YANKEES_BLUE: (28, 40, 65) + COLOR_ZAFFRE: (0, 20, 168) + COLOR_BALTIC_SEA: (42, 38, 48) + COLOR_BASTILLE: (41, 33, 48) + COLOR_BLACK: (0, 0, 0) + COLOR_BLACK_PEARL: (4, 19, 34) + COLOR_BLACK_RUSSIAN: (10, 0, 28) + COLOR_BLUE_CHARCOAL: (1, 13, 26) + COLOR_BUNKER: (13, 17, 23) + COLOR_CHARLESTON_GREEN: (35, 43, 43) + COLOR_CINDER: (14, 14, 24) + COLOR_COD_GRAY: (11, 11, 11) + COLOR_DAINTREE: (1, 39, 49) + COLOR_DARK_GUNMETAL: (31, 38, 42) + COLOR_EERIE_BLACK: (27, 27, 27) + COLOR_FIREFLY: (14, 42, 48) + COLOR_JAGUAR: (8, 1, 16) + COLOR_MIRAGE: (22, 25, 40) + COLOR_RAISIN_BLACK: (36, 33, 36) + COLOR_SHARK: (37, 39, 44) + COLOR_SWAMP: (0, 27, 28) + COLOR_VULCAN: (16, 18, 29) + COLOR_WOODSMOKE: (12, 13, 15) + COLOR_RICH_BLACK: (0, 64, 64) neutral: - COLOR_AURO_METAL_SAURUS: '(110, 127, 128)' - COLOR_BERMUDA_GRAY: '(107, 139, 162)' - COLOR_BLUE_MARGUERITE: '(118, 102, 198)' - COLOR_BLUE_YONDER: '(80, 114, 167)' - COLOR_CADET_BLUE: '(95, 158, 160)' - COLOR_CHINESE_VIOLET: '(133, 96, 136)' - COLOR_CYAN_AZURE: '(78, 130, 180)' - COLOR_CYAN_BLUE_AZURE: '(70, 130, 191)' - COLOR_DARK_BLUE_GRAY: '(102, 102, 153)' - COLOR_DEEP_LILAC: '(153, 85, 187)' - COLOR_DELUGE: '(117, 99, 168)' - COLOR_FRENCH_LILAC: '(134, 96, 142)' - COLOR_FUCHSIA_BLUE: '(122, 88, 193)' - COLOR_GLAUCOUS: '(96, 130, 182)' - COLOR_GOTHIC: '(109, 146, 161)' - COLOR_HAN_BLUE: '(68, 108, 207)' - COLOR_HIPPIE_BLUE: '(88, 154, 175)' - COLOR_HOKI: '(101, 134, 159)' - COLOR_HORIZON: '(90, 135, 160)' - COLOR_IRIS: '(90, 79, 207)' - COLOR_JUMBO: '(124, 123, 130)' - COLOR_KIMBERLY: '(115, 108, 159)' - COLOR_LIGHT_SLATE_GRAY: '(119, 136, 153)' - COLOR_LYNCH: '(105, 126, 154)' - COLOR_MAJORELLE_BLUE: '(96, 80, 220)' - COLOR_MAMBA: '(142, 129, 144)' - COLOR_MOBSTER: '(127, 117, 137)' - COLOR_OSLO_GRAY: '(135, 141, 145)' - COLOR_PALE_SKY: '(110, 119, 131)' - COLOR_PURPLE_PLUM: '(156, 81, 182)' - COLOR_PURPUREUS: '(154, 78, 174)' - COLOR_RAVEN: '(114, 123, 137)' - COLOR_REGENT_GRAY: '(134, 148, 159)' - COLOR_RHYTHM: '(119, 118, 150)' - COLOR_ROLLING_STONE: '(116, 125, 131)' - COLOR_ROMAN_SILVER: '(131, 137, 150)' - COLOR_ROYAL_AIR_FORCE_BLUE: '(93, 138, 168)' - COLOR_ROYAL_BLUE: '(65, 105, 225)' - COLOR_ROYAL_PURPLE: '(120, 81, 169)' - COLOR_RUM: '(121, 105, 137)' - COLOR_SCAMPI: '(103, 95, 166)' - COLOR_SHADOW_BLUE: '(119, 139, 165)' - COLOR_SHIP_COVE: '(120, 139, 186)' - COLOR_SILVER_LAKE_BLUE: '(93, 137, 186)' - COLOR_SLATE_BLUE: '(106, 90, 205)' - COLOR_SLATE_GRAY: '(112, 128, 144)' - COLOR_SMALT_BLUE: '(81, 128, 143)' - COLOR_STEEL_BLUE: '(70, 130, 180)' - COLOR_STEEL_TEAL: '(95, 138, 139)' - COLOR_STORM_GRAY: '(113, 116, 134)' - COLOR_STUDIO: '(113, 74, 178)' - COLOR_TOOLBOX: '(116, 108, 192)' - COLOR_TRENDY_PINK: '(140, 100, 149)' - COLOR_TUFTS_BLUE: '(65, 125, 193)' - COLOR_WAIKAWA_GRAY: '(90, 110, 156)' - COLOR_WATERLOO_: '(123, 124, 148)' - COLOR_WEDGEWOOD: '(78, 127, 158)' - COLOR_ALMOND_FROST: '(144, 123, 113)' - COLOR_AMERICANO: '(135, 117, 110)' - COLOR_ANTIQUE_FUCHSIA: '(145, 92, 131)' - COLOR_ARROWTOWN: '(148, 135, 113)' - COLOR_BANDICOOT: '(133, 132, 112)' - COLOR_BARLEY_CORN: '(166, 139, 91)' - COLOR_BAZAAR: '(152, 119, 123)' - COLOR_BEAVER: '(159, 129, 112)' - COLOR_BLAST_OFF_BRONZE: '(165, 113, 100)' - COLOR_BRASS: '(181, 166, 66)' - COLOR_BROWN_SUGAR: '(175, 110, 77)' - COLOR_BURNISHED_BROWN: '(161, 122, 116)' - COLOR_CADILLAC: '(176, 76, 106)' - COLOR_CEMENT: '(141, 118, 98)' - COLOR_CHAMOISEE: '(160, 120, 90)' - COLOR_CHINA_ROSE: '(168, 81, 110)' - COLOR_CINEREOUS: '(152, 129, 123)' - COLOR_CLAY_CREEK: '(138, 131, 96)' - COLOR_CONCORD: '(124, 123, 122)' - COLOR_COPPER_PENNY: '(173, 111, 105)' - COLOR_COPPER_ROSE: '(153, 102, 102)' - COLOR_CORAL_TREE: '(168, 107, 107)' - COLOR_DARK_CHESTNUT: '(152, 105, 96)' - COLOR_DARK_TAN: '(145, 129, 81)' - COLOR_DEEP_PUCE: '(169, 92, 104)' - COLOR_DEER: '(186, 135, 89)' - COLOR_DIRT: '(155, 118, 83)' - COLOR_DOMINO: '(142, 119, 94)' - COLOR_DRIFTWOOD: '(175, 135, 81)' - COLOR_EMPRESS: '(129, 115, 119)' - COLOR_FRIAR_GRAY: '(128, 126, 121)' - COLOR_GIANTS_CLUB: '(176, 92, 82)' - COLOR_GRANITE_GREEN: '(141, 137, 116)' - COLOR_GURKHA: '(154, 149, 119)' - COLOR_HEMP: '(144, 120, 116)' - COLOR_HURRICANE: '(135, 124, 123)' - COLOR_HUSK: '(183, 164, 88)' - COLOR_IRRESISTIBLE: '(179, 68, 108)' - COLOR_LEATHER: '(150, 112, 89)' - COLOR_LIGHT_TAUPE: '(179, 139, 109)' - COLOR_LIMED_OAK: '(172, 138, 86)' - COLOR_LIVER_CHESTNUT: '(152, 116, 86)' - COLOR_MAKARA: '(137, 125, 109)' - COLOR_MATRIX: '(176, 93, 84)' - COLOR_MONSOON: '(138, 131, 137)' - COLOR_MOUNTBATTEN_PINK: '(153, 122, 141)' - COLOR_MUDDY_WATERS: '(183, 142, 92)' - COLOR_MUESLI: '(170, 139, 91)' - COLOR_MYSTIC_MAROON: '(173, 67, 121)' - COLOR_NATURAL_GRAY: '(139, 134, 128)' - COLOR_OLIVE_HAZE: '(139, 132, 112)' - COLOR_OPIUM: '(142, 111, 112)' - COLOR_PALE_BROWN: '(152, 118, 84)' - COLOR_PALE_OYSTER: '(152, 141, 119)' - COLOR_PHARLAP: '(163, 128, 123)' - COLOR_RAZZMIC_BERRY: '(141, 78, 133)' - COLOR_ROCKET_METALLIC: '(138, 127, 128)' - COLOR_ROSE_DUST: '(158, 94, 111)' - COLOR_ROSE_GOLD: '(183, 110, 121)' - COLOR_SANDAL: '(170, 141, 111)' - COLOR_SANDRIFT: '(171, 145, 122)' - COLOR_SANTA_FE: '(177, 109, 82)' - COLOR_SCHOONER: '(139, 132, 126)' - COLOR_SHADOW: '(138, 121, 93)' - COLOR_SPICY_PINK: '(129, 110, 113)' - COLOR_SQUIRREL: '(143, 129, 118)' - COLOR_STONEWALL: '(146, 133, 115)' - COLOR_STRIKEMASTER: '(149, 99, 135)' - COLOR_SUVA_GRAY: '(136, 131, 135)' - COLOR_TAPA: '(123, 120, 116)' - COLOR_TAPESTRY: '(176, 94, 129)' - COLOR_TAUPE_GRAY: '(139, 133, 137)' - COLOR_TEAK: '(177, 148, 97)' - COLOR_TOAST: '(154, 110, 97)' - COLOR_TURKISH_ROSE: '(181, 114, 129)' - COLOR_TUSCAN_TAN: '(166, 123, 91)' - COLOR_TWINE: '(194, 149, 93)' - COLOR_VENUS: '(146, 133, 144)' - COLOR_AMULET: '(123, 159, 128)' - COLOR_ANDROID_GREEN: '(164, 198, 57)' - COLOR_AQUA_FOREST: '(95, 167, 119)' - COLOR_ARTICHOKE: '(143, 151, 121)' - COLOR_ASPARAGUS: '(135, 169, 107)' - COLOR_BATTLESHIP_GRAY: '(130, 143, 114)' - COLOR_BAY_LEAF: '(125, 169, 141)' - COLOR_BITTER: '(134, 137, 116)' - COLOR_BLUE_SMOKE: '(116, 136, 129)' - COLOR_BREAKER_BAY: '(93, 161, 159)' - COLOR_BUD_GREEN: '(123, 182, 97)' - COLOR_CAMOUFLAGE_GREEN: '(120, 134, 107)' - COLOR_CHELSEA_CUCUMBER: '(131, 170, 93)' - COLOR_DOLLAR_BILL: '(133, 187, 101)' - COLOR_EMERALD: '(80, 200, 120)' - COLOR_FERN: '(99, 183, 108)' - COLOR_FLAX_SMOKE: '(123, 130, 101)' - COLOR_GREEN_SHEEN: '(110, 174, 161)' - COLOR_GUNSMOKE: '(130, 134, 133)' - COLOR_HIGHLAND: '(111, 142, 99)' - COLOR_LAUREL: '(116, 147, 120)' - COLOR_MANTIS: '(116, 195, 101)' - COLOR_MANTLE: '(139, 156, 144)' - COLOR_MOSS_GREEN: '(138, 154, 91)' - COLOR_MUMMYS_TOMB: '(130, 142, 132)' - COLOR_OCEAN_GREEN: '(72, 191, 145)' - COLOR_OXLEY: '(119, 158, 134)' - COLOR_PATINA: '(99, 154, 143)' - COLOR_POLISHED_PINE: '(93, 164, 147)' - COLOR_RUSSIAN_GREEN: '(103, 146, 103)' - COLOR_SEA_NYMPH: '(120, 163, 156)' - COLOR_SHINY_SHAMROCK: '(95, 167, 120)' - COLOR_SILVER_TREE: '(102, 181, 143)' - COLOR_SMOKE: '(115, 130, 118)' - COLOR_STACK: '(138, 143, 138)' - COLOR_SUSHI: '(135, 171, 57)' - COLOR_VERDIGRIS: '(67, 179, 174)' - COLOR_XANADU: '(115, 134, 120)' - COLOR_YELLOW_GREEN: '(154, 205, 50)' - COLOR_BOULDER: '(122, 122, 122)' - COLOR_GRAY: '(128, 128, 128)' - COLOR_OLD_SILVER: '(132, 132, 130)' - COLOR_SONIC_SILVER: '(117, 117, 117)' - COLOR_DESATURATED_CYAN: '(102, 153, 153)' - COLOR_JUNIPER: '(109, 146, 146)' - COLOR_SIROCCO: '(113, 128, 128)' + COLOR_AURO_METAL_SAURUS: (110, 127, 128) + COLOR_BERMUDA_GRAY: (107, 139, 162) + COLOR_BLUE_MARGUERITE: (118, 102, 198) + COLOR_BLUE_YONDER: (80, 114, 167) + COLOR_CADET_BLUE: (95, 158, 160) + COLOR_CHINESE_VIOLET: (133, 96, 136) + COLOR_CYAN_AZURE: (78, 130, 180) + COLOR_CYAN_BLUE_AZURE: (70, 130, 191) + COLOR_DARK_BLUE_GRAY: (102, 102, 153) + COLOR_DEEP_LILAC: (153, 85, 187) + COLOR_DELUGE: (117, 99, 168) + COLOR_FRENCH_LILAC: (134, 96, 142) + COLOR_FUCHSIA_BLUE: (122, 88, 193) + COLOR_GLAUCOUS: (96, 130, 182) + COLOR_GOTHIC: (109, 146, 161) + COLOR_HAN_BLUE: (68, 108, 207) + COLOR_HIPPIE_BLUE: (88, 154, 175) + COLOR_HOKI: (101, 134, 159) + COLOR_HORIZON: (90, 135, 160) + COLOR_IRIS: (90, 79, 207) + COLOR_JUMBO: (124, 123, 130) + COLOR_KIMBERLY: (115, 108, 159) + COLOR_LIGHT_SLATE_GRAY: (119, 136, 153) + COLOR_LYNCH: (105, 126, 154) + COLOR_MAJORELLE_BLUE: (96, 80, 220) + COLOR_MAMBA: (142, 129, 144) + COLOR_MOBSTER: (127, 117, 137) + COLOR_OSLO_GRAY: (135, 141, 145) + COLOR_PALE_SKY: (110, 119, 131) + COLOR_PURPLE_PLUM: (156, 81, 182) + COLOR_PURPUREUS: (154, 78, 174) + COLOR_RAVEN: (114, 123, 137) + COLOR_REGENT_GRAY: (134, 148, 159) + COLOR_RHYTHM: (119, 118, 150) + COLOR_ROLLING_STONE: (116, 125, 131) + COLOR_ROMAN_SILVER: (131, 137, 150) + COLOR_ROYAL_AIR_FORCE_BLUE: (93, 138, 168) + COLOR_ROYAL_BLUE: (65, 105, 225) + COLOR_ROYAL_PURPLE: (120, 81, 169) + COLOR_RUM: (121, 105, 137) + COLOR_SCAMPI: (103, 95, 166) + COLOR_SHADOW_BLUE: (119, 139, 165) + COLOR_SHIP_COVE: (120, 139, 186) + COLOR_SILVER_LAKE_BLUE: (93, 137, 186) + COLOR_SLATE_BLUE: (106, 90, 205) + COLOR_SLATE_GRAY: (112, 128, 144) + COLOR_SMALT_BLUE: (81, 128, 143) + COLOR_STEEL_BLUE: (70, 130, 180) + COLOR_STEEL_TEAL: (95, 138, 139) + COLOR_STORM_GRAY: (113, 116, 134) + COLOR_STUDIO: (113, 74, 178) + COLOR_TOOLBOX: (116, 108, 192) + COLOR_TRENDY_PINK: (140, 100, 149) + COLOR_TUFTS_BLUE: (65, 125, 193) + COLOR_WAIKAWA_GRAY: (90, 110, 156) + COLOR_WATERLOO_: (123, 124, 148) + COLOR_WEDGEWOOD: (78, 127, 158) + COLOR_ALMOND_FROST: (144, 123, 113) + COLOR_AMERICANO: (135, 117, 110) + COLOR_ANTIQUE_FUCHSIA: (145, 92, 131) + COLOR_ARROWTOWN: (148, 135, 113) + COLOR_BANDICOOT: (133, 132, 112) + COLOR_BARLEY_CORN: (166, 139, 91) + COLOR_BAZAAR: (152, 119, 123) + COLOR_BEAVER: (159, 129, 112) + COLOR_BLAST_OFF_BRONZE: (165, 113, 100) + COLOR_BRASS: (181, 166, 66) + COLOR_BROWN_SUGAR: (175, 110, 77) + COLOR_BURNISHED_BROWN: (161, 122, 116) + COLOR_CADILLAC: (176, 76, 106) + COLOR_CEMENT: (141, 118, 98) + COLOR_CHAMOISEE: (160, 120, 90) + COLOR_CHINA_ROSE: (168, 81, 110) + COLOR_CINEREOUS: (152, 129, 123) + COLOR_CLAY_CREEK: (138, 131, 96) + COLOR_CONCORD: (124, 123, 122) + COLOR_COPPER_PENNY: (173, 111, 105) + COLOR_COPPER_ROSE: (153, 102, 102) + COLOR_CORAL_TREE: (168, 107, 107) + COLOR_DARK_CHESTNUT: (152, 105, 96) + COLOR_DARK_TAN: (145, 129, 81) + COLOR_DEEP_PUCE: (169, 92, 104) + COLOR_DEER: (186, 135, 89) + COLOR_DIRT: (155, 118, 83) + COLOR_DOMINO: (142, 119, 94) + COLOR_DRIFTWOOD: (175, 135, 81) + COLOR_EMPRESS: (129, 115, 119) + COLOR_FRIAR_GRAY: (128, 126, 121) + COLOR_GIANTS_CLUB: (176, 92, 82) + COLOR_GRANITE_GREEN: (141, 137, 116) + COLOR_GURKHA: (154, 149, 119) + COLOR_HEMP: (144, 120, 116) + COLOR_HURRICANE: (135, 124, 123) + COLOR_HUSK: (183, 164, 88) + COLOR_IRRESISTIBLE: (179, 68, 108) + COLOR_LEATHER: (150, 112, 89) + COLOR_LIGHT_TAUPE: (179, 139, 109) + COLOR_LIMED_OAK: (172, 138, 86) + COLOR_LIVER_CHESTNUT: (152, 116, 86) + COLOR_MAKARA: (137, 125, 109) + COLOR_MATRIX: (176, 93, 84) + COLOR_MONSOON: (138, 131, 137) + COLOR_MOUNTBATTEN_PINK: (153, 122, 141) + COLOR_MUDDY_WATERS: (183, 142, 92) + COLOR_MUESLI: (170, 139, 91) + COLOR_MYSTIC_MAROON: (173, 67, 121) + COLOR_NATURAL_GRAY: (139, 134, 128) + COLOR_OLIVE_HAZE: (139, 132, 112) + COLOR_OPIUM: (142, 111, 112) + COLOR_PALE_BROWN: (152, 118, 84) + COLOR_PALE_OYSTER: (152, 141, 119) + COLOR_PHARLAP: (163, 128, 123) + COLOR_RAZZMIC_BERRY: (141, 78, 133) + COLOR_ROCKET_METALLIC: (138, 127, 128) + COLOR_ROSE_DUST: (158, 94, 111) + COLOR_ROSE_GOLD: (183, 110, 121) + COLOR_SANDAL: (170, 141, 111) + COLOR_SANDRIFT: (171, 145, 122) + COLOR_SANTA_FE: (177, 109, 82) + COLOR_SCHOONER: (139, 132, 126) + COLOR_SHADOW: (138, 121, 93) + COLOR_SPICY_PINK: (129, 110, 113) + COLOR_SQUIRREL: (143, 129, 118) + COLOR_STONEWALL: (146, 133, 115) + COLOR_STRIKEMASTER: (149, 99, 135) + COLOR_SUVA_GRAY: (136, 131, 135) + COLOR_TAPA: (123, 120, 116) + COLOR_TAPESTRY: (176, 94, 129) + COLOR_TAUPE_GRAY: (139, 133, 137) + COLOR_TEAK: (177, 148, 97) + COLOR_TOAST: (154, 110, 97) + COLOR_TURKISH_ROSE: (181, 114, 129) + COLOR_TUSCAN_TAN: (166, 123, 91) + COLOR_TWINE: (194, 149, 93) + COLOR_VENUS: (146, 133, 144) + COLOR_AMULET: (123, 159, 128) + COLOR_ANDROID_GREEN: (164, 198, 57) + COLOR_AQUA_FOREST: (95, 167, 119) + COLOR_ARTICHOKE: (143, 151, 121) + COLOR_ASPARAGUS: (135, 169, 107) + COLOR_BATTLESHIP_GRAY: (130, 143, 114) + COLOR_BAY_LEAF: (125, 169, 141) + COLOR_BITTER: (134, 137, 116) + COLOR_BLUE_SMOKE: (116, 136, 129) + COLOR_BREAKER_BAY: (93, 161, 159) + COLOR_BUD_GREEN: (123, 182, 97) + COLOR_CAMOUFLAGE_GREEN: (120, 134, 107) + COLOR_CHELSEA_CUCUMBER: (131, 170, 93) + COLOR_DOLLAR_BILL: (133, 187, 101) + COLOR_EMERALD: (80, 200, 120) + COLOR_FERN: (99, 183, 108) + COLOR_FLAX_SMOKE: (123, 130, 101) + COLOR_GREEN_SHEEN: (110, 174, 161) + COLOR_GUNSMOKE: (130, 134, 133) + COLOR_HIGHLAND: (111, 142, 99) + COLOR_LAUREL: (116, 147, 120) + COLOR_MANTIS: (116, 195, 101) + COLOR_MANTLE: (139, 156, 144) + COLOR_MOSS_GREEN: (138, 154, 91) + COLOR_MUMMYS_TOMB: (130, 142, 132) + COLOR_OCEAN_GREEN: (72, 191, 145) + COLOR_OXLEY: (119, 158, 134) + COLOR_PATINA: (99, 154, 143) + COLOR_POLISHED_PINE: (93, 164, 147) + COLOR_RUSSIAN_GREEN: (103, 146, 103) + COLOR_SEA_NYMPH: (120, 163, 156) + COLOR_SHINY_SHAMROCK: (95, 167, 120) + COLOR_SILVER_TREE: (102, 181, 143) + COLOR_SMOKE: (115, 130, 118) + COLOR_STACK: (138, 143, 138) + COLOR_SUSHI: (135, 171, 57) + COLOR_VERDIGRIS: (67, 179, 174) + COLOR_XANADU: (115, 134, 120) + COLOR_YELLOW_GREEN: (154, 205, 50) + COLOR_BOULDER: (122, 122, 122) + COLOR_GRAY: (128, 128, 128) + COLOR_OLD_SILVER: (132, 132, 130) + COLOR_SONIC_SILVER: (117, 117, 117) + COLOR_DESATURATED_CYAN: (102, 153, 153) + COLOR_JUNIPER: (109, 146, 146) + COLOR_SIROCCO: (113, 128, 128) sadness: - COLOR_AZURE: '(0, 127, 255)' - COLOR_BLUE: '(0, 0, 255)' - COLOR_BLUE_RIBBON: '(0, 102, 255)' - COLOR_BLUEBONNET: '(28, 28, 240)' - COLOR_BRANDEIS_BLUE: '(0, 112, 255)' - COLOR_CRAYOLA_BLUE: '(31, 117, 254)' - COLOR_ELECTRIC_INDIGO: '(111, 0, 255)' - COLOR_HAN_PURPLE: '(82, 24, 250)' - COLOR_MEDIUM_BLUE: '(0, 0, 205)' - COLOR_PALATINATE_BLUE: '(39, 59, 226)' - COLOR_RYB_BLUE: '(2, 71, 254)' - COLOR_ULTRAMARINE: '(63, 0, 255)' - COLOR_ULTRAMARINE_BLUE: '(65, 102, 245)' - COLOR_VIOLET: '(127, 0, 255)' + COLOR_AZURE: (0, 127, 255) + COLOR_BLUE: (0, 0, 255) + COLOR_BLUE_RIBBON: (0, 102, 255) + COLOR_BLUEBONNET: (28, 28, 240) + COLOR_BRANDEIS_BLUE: (0, 112, 255) + COLOR_CRAYOLA_BLUE: (31, 117, 254) + COLOR_ELECTRIC_INDIGO: (111, 0, 255) + COLOR_HAN_PURPLE: (82, 24, 250) + COLOR_MEDIUM_BLUE: (0, 0, 205) + COLOR_PALATINATE_BLUE: (39, 59, 226) + COLOR_RYB_BLUE: (2, 71, 254) + COLOR_ULTRAMARINE: (63, 0, 255) + COLOR_ULTRAMARINE_BLUE: (65, 102, 245) + COLOR_VIOLET: (127, 0, 255) disapproval: - COLOR_BLUE_VIOLET: '(138, 43, 226)' - COLOR_CLAIRVOYANT: '(72, 6, 86)' - COLOR_DAISY_BUSH: '(79, 35, 152)' - COLOR_DARK_ORCHID: '(153, 50, 204)' - COLOR_DARK_VIOLET: '(148, 0, 211)' - COLOR_EMINENCE: '(108, 48, 130)' - COLOR_FRENCH_VIOLET: '(136, 6, 206)' - COLOR_GRAPE: '(111, 45, 168)' - COLOR_HELIOTROPE_MAGENTA: '(170, 0, 187)' - COLOR_HONEY_FLOWER: '(79, 28, 112)' - COLOR_INDIGO: '(75, 0, 130)' - COLOR_MUNSELL_PURPLE: '(159, 0, 197)' - COLOR_PURPLE_HEART: '(105, 53, 156)' - COLOR_RYB_VIOLET: '(134, 1, 175)' - COLOR_REBECCA_PURPLE: '(102, 51, 153)' - COLOR_REGALIA: '(82, 45, 128)' - COLOR_RIPE_PLUM: '(65, 0, 86)' - COLOR_SCARLET_GUM: '(67, 21, 96)' - COLOR_SEANCE: '(115, 30, 143)' - COLOR_SPANISH_VIOLET: '(76, 40, 130)' - COLOR_BOYSENBERRY: '(135, 50, 96)' - COLOR_BYZANTIUM: '(112, 41, 99)' - COLOR_CARDINAL_PINK: '(140, 5, 94)' - COLOR_DARK_RASPBERRY: '(135, 38, 87)' - COLOR_DISCO: '(135, 21, 80)' - COLOR_FANDANGO: '(181, 51, 137)' - COLOR_FLIRT: '(162, 0, 109)' - COLOR_FRENCH_PLUM: '(129, 20, 83)' - COLOR_FRESH_EGGPLANT: '(153, 0, 102)' - COLOR_HIBISCUS: '(182, 49, 108)' - COLOR_JAZZBERRY_JAM: '(165, 11, 94)' - COLOR_LIPSTICK: '(171, 5, 99)' - COLOR_LOULOU: '(70, 11, 65)' - COLOR_MARDI_GRAS: '(136, 0, 133)' - COLOR_PALATINATE_PURPLE: '(104, 40, 96)' - COLOR_PANSY_PURPLE: '(120, 24, 74)' - COLOR_PICTORIAL_CARMINE: '(195, 11, 78)' - COLOR_POMPADOUR: '(102, 0, 69)' - COLOR_RICH_MAROON: '(176, 48, 96)' - COLOR_ROSE_BUD_CHERRY: '(128, 11, 71)' - COLOR_ROSE_RED: '(194, 30, 86)' - COLOR_ROUGE: '(162, 59, 108)' - COLOR_ROYAL_HEATH: '(171, 52, 114)' - COLOR_DARK_MAGENTA: '(139, 0, 139)' - COLOR_MIDNIGHT: '(112, 38, 112)' - COLOR_PURPLE: '(128, 0, 128)' - COLOR_VIOLET_EGGPLANT: '(153, 17, 153)' + COLOR_BLUE_VIOLET: (138, 43, 226) + COLOR_CLAIRVOYANT: (72, 6, 86) + COLOR_DAISY_BUSH: (79, 35, 152) + COLOR_DARK_ORCHID: (153, 50, 204) + COLOR_DARK_VIOLET: (148, 0, 211) + COLOR_EMINENCE: (108, 48, 130) + COLOR_FRENCH_VIOLET: (136, 6, 206) + COLOR_GRAPE: (111, 45, 168) + COLOR_HELIOTROPE_MAGENTA: (170, 0, 187) + COLOR_HONEY_FLOWER: (79, 28, 112) + COLOR_INDIGO: (75, 0, 130) + COLOR_MUNSELL_PURPLE: (159, 0, 197) + COLOR_PURPLE_HEART: (105, 53, 156) + COLOR_RYB_VIOLET: (134, 1, 175) + COLOR_REBECCA_PURPLE: (102, 51, 153) + COLOR_REGALIA: (82, 45, 128) + COLOR_RIPE_PLUM: (65, 0, 86) + COLOR_SCARLET_GUM: (67, 21, 96) + COLOR_SEANCE: (115, 30, 143) + COLOR_SPANISH_VIOLET: (76, 40, 130) + COLOR_BOYSENBERRY: (135, 50, 96) + COLOR_BYZANTIUM: (112, 41, 99) + COLOR_CARDINAL_PINK: (140, 5, 94) + COLOR_DARK_RASPBERRY: (135, 38, 87) + COLOR_DISCO: (135, 21, 80) + COLOR_FANDANGO: (181, 51, 137) + COLOR_FLIRT: (162, 0, 109) + COLOR_FRENCH_PLUM: (129, 20, 83) + COLOR_FRESH_EGGPLANT: (153, 0, 102) + COLOR_HIBISCUS: (182, 49, 108) + COLOR_JAZZBERRY_JAM: (165, 11, 94) + COLOR_LIPSTICK: (171, 5, 99) + COLOR_LOULOU: (70, 11, 65) + COLOR_MARDI_GRAS: (136, 0, 133) + COLOR_PALATINATE_PURPLE: (104, 40, 96) + COLOR_PANSY_PURPLE: (120, 24, 74) + COLOR_PICTORIAL_CARMINE: (195, 11, 78) + COLOR_POMPADOUR: (102, 0, 69) + COLOR_RICH_MAROON: (176, 48, 96) + COLOR_ROSE_BUD_CHERRY: (128, 11, 71) + COLOR_ROSE_RED: (194, 30, 86) + COLOR_ROUGE: (162, 59, 108) + COLOR_ROYAL_HEATH: (171, 52, 114) + COLOR_DARK_MAGENTA: (139, 0, 139) + COLOR_MIDNIGHT: (112, 38, 112) + COLOR_PURPLE: (128, 0, 128) + COLOR_VIOLET_EGGPLANT: (153, 17, 153) embarrassment: - COLOR_BOTTICELLI: '(199, 221, 229)' - COLOR_BRIGHT_LILAC: '(216, 145, 239)' - COLOR_BRIGHT_UBE: '(209, 159, 232)' - COLOR_BRILLIANT_LAVENDER: '(244, 187, 255)' - COLOR_GHOST: '(199, 201, 213)' - COLOR_MAUVE: '(224, 176, 255)' - COLOR_PALE_VIOLET: '(204, 153, 255)' - COLOR_PERFUME: '(208, 190, 248)' - COLOR_PRELUDE: '(208, 192, 229)' - COLOR_RICH_BRILLIANT_LAVENDER: '(241, 167, 254)' - COLOR_SOAP: '(206, 200, 239)' - COLOR_TROPICAL_VIOLET: '(205, 164, 222)' - COLOR_AMARANTH_PINK: '(241, 156, 187)' - COLOR_APRICOT: '(251, 206, 177)' - COLOR_ASTRA: '(250, 234, 185)' - COLOR_AZALEA: '(247, 200, 218)' - COLOR_BANANA_MANIA: '(250, 231, 181)' - COLOR_BEAUTY_BUSH: '(238, 193, 190)' - COLOR_BEESWAX: '(254, 242, 199)' - COLOR_BISQUE: '(255, 228, 196)' - COLOR_BLOND: '(250, 240, 190)' - COLOR_BLOSSOM: '(220, 180, 188)' - COLOR_BUBBLE_GUM: '(255, 193, 204)' - COLOR_BUFF: '(240, 220, 130)' - COLOR_BUTTERMILK: '(255, 241, 181)' - COLOR_CALICO: '(224, 192, 149)' - COLOR_CAMEO_PINK: '(239, 187, 204)' - COLOR_CAPE_HONEY: '(254, 229, 172)' - COLOR_CARAMEL: '(255, 221, 175)' - COLOR_CARNATION_PINK: '(255, 166, 201)' - COLOR_CASHMERE: '(230, 190, 165)' - COLOR_CAVERN_PINK: '(227, 190, 190)' - COLOR_CHALKY: '(238, 215, 148)' - COLOR_CHAMOIS: '(237, 220, 177)' - COLOR_CHANTILLY: '(248, 195, 223)' - COLOR_CHARDONNAY: '(255, 205, 140)' - COLOR_CHEROKEE: '(252, 218, 152)' - COLOR_CHERRY_BLOSSOM_PINK: '(255, 183, 197)' - COLOR_COLONIAL_WHITE: '(255, 237, 188)' - COLOR_CORNFLOWER_LILAC: '(255, 176, 172)' - COLOR_CORVETTE: '(250, 211, 162)' - COLOR_COTTON_CANDY: '(255, 188, 217)' - COLOR_CRAYOLA_YELLOW: '(252, 232, 131)' - COLOR_CREAM_BRULEE: '(255, 229, 160)' - COLOR_CUPID: '(251, 190, 218)' - COLOR_DAIRY_CREAM: '(249, 228, 188)' - COLOR_DESERT_SAND: '(237, 201, 175)' - COLOR_DOUBLE_COLONIAL_WHITE: '(238, 227, 173)' - COLOR_DOUBLE_SPANISH_WHITE: '(230, 215, 185)' - COLOR_DROVER: '(253, 247, 173)' - COLOR_DUTCH_WHITE: '(239, 223, 187)' - COLOR_EGG_WHITE: '(255, 239, 193)' - COLOR_FESTIVAL: '(251, 233, 108)' - COLOR_FLAVESCENT: '(247, 233, 142)' - COLOR_FLAX: '(238, 220, 130)' - COLOR_FRANGIPANI: '(255, 222, 179)' - COLOR_GIVRY: '(248, 228, 191)' - COLOR_GOLDEN_GLOW: '(253, 226, 149)' - COLOR_GOLDEN_SAND: '(240, 219, 125)' - COLOR_GRAIN_BROWN: '(228, 213, 183)' - COLOR_GRANDIS: '(255, 211, 140)' - COLOR_HAMPTON: '(229, 216, 175)' - COLOR_HIT_PINK: '(255, 171, 129)' - COLOR_ILLUSION: '(246, 164, 201)' - COLOR_JASMINE: '(248, 222, 126)' - COLOR_JUST_RIGHT: '(236, 205, 185)' - COLOR_KOBI: '(231, 159, 196)' - COLOR_KOURNIKOVA: '(255, 231, 114)' - COLOR_LAVENDER_PINK: '(251, 174, 210)' - COLOR_LAVENDER_ROSE: '(251, 160, 227)' - COLOR_LEMON_MERINGUE: '(246, 234, 190)' - COLOR_LIGHT_APRICOT: '(253, 213, 177)' - COLOR_LIGHT_HOT_PINK: '(255, 179, 222)' - COLOR_LIGHT_KHAKI: '(240, 230, 140)' - COLOR_LIGHT_ORCHID: '(230, 168, 215)' - COLOR_LIGHT_PINK: '(255, 182, 193)' - COLOR_MACARONI_AND_CHEESE: '(255, 185, 123)' - COLOR_MANDYS_PINK: '(242, 195, 178)' - COLOR_MANHATTAN: '(245, 201, 153)' - COLOR_MARIGOLD_YELLOW: '(251, 232, 112)' - COLOR_MARZIPAN: '(248, 219, 157)' - COLOR_MAVERICK: '(216, 194, 213)' - COLOR_MELANIE: '(228, 194, 213)' - COLOR_MELLOW_APRICOT: '(248, 184, 120)' - COLOR_MELON: '(253, 188, 180)' - COLOR_MINT_JULEP: '(241, 238, 193)' - COLOR_MOCCASIN: '(255, 228, 181)' - COLOR_MONA_LISA: '(255, 161, 148)' - COLOR_NADESHIKO_PINK: '(246, 173, 198)' - COLOR_NAVAJO_WHITE: '(255, 222, 173)' - COLOR_NEGRONI: '(255, 226, 197)' - COLOR_NEW_ORLEANS: '(243, 214, 157)' - COLOR_ORANGE_YELLOW: '(248, 213, 104)' - COLOR_ORCHID_PINK: '(242, 189, 205)' - COLOR_PALE_CHESTNUT: '(221, 173, 175)' - COLOR_PALE_GOLD: '(230, 190, 138)' - COLOR_PALE_GOLDENROD: '(238, 232, 170)' - COLOR_PALE_MAGENTA_PINK: '(255, 153, 204)' - COLOR_PALE_SPRING_BUD: '(236, 235, 189)' - COLOR_PANCHO: '(237, 205, 171)' - COLOR_PARIS_DAISY: '(255, 244, 110)' - COLOR_PASTEL_MAGENTA: '(244, 154, 194)' - COLOR_PEACH: '(255, 203, 164)' - COLOR_PEACH_ORANGE: '(255, 204, 153)' - COLOR_PEACH_PUFF: '(255, 218, 185)' - COLOR_PEACH_YELLOW: '(250, 223, 173)' - COLOR_PEARL: '(234, 224, 200)' - COLOR_PICASSO: '(255, 243, 157)' - COLOR_PINK: '(255, 192, 203)' - COLOR_PINK_FLARE: '(225, 192, 200)' - COLOR_PINK_LAVENDER: '(216, 178, 209)' - COLOR_PINK_PEARL: '(231, 172, 207)' - COLOR_PRIMROSE: '(237, 234, 153)' - COLOR_PUTTY: '(231, 205, 140)' - COLOR_RAFFIA: '(234, 218, 184)' - COLOR_ROMANTIC: '(255, 210, 183)' - COLOR_ROSE_BUD: '(251, 178, 163)' - COLOR_ROSE_FOG: '(231, 188, 180)' - COLOR_SAHARA_SAND: '(241, 231, 136)' - COLOR_SALOMIE: '(254, 219, 141)' - COLOR_SANDWISP: '(245, 231, 162)' - COLOR_SANDY_BEACH: '(255, 234, 200)' - COLOR_SAPLING: '(222, 212, 164)' - COLOR_SHILO: '(232, 185, 179)' - COLOR_SIDECAR: '(243, 231, 187)' - COLOR_SISAL: '(211, 203, 186)' - COLOR_SPANISH_PINK: '(247, 191, 190)' - COLOR_STARK_WHITE: '(229, 215, 189)' - COLOR_SUNDOWN: '(255, 177, 179)' - COLOR_SUNSET: '(250, 214, 165)' - COLOR_SWEET_CORN: '(251, 234, 140)' - COLOR_SWEET_PINK: '(253, 159, 162)' - COLOR_SWIRL: '(211, 205, 197)' - COLOR_TACAO: '(237, 179, 129)' - COLOR_TEA_ROSE: '(244, 194, 194)' - COLOR_TEQUILA: '(255, 230, 199)' - COLOR_TOPAZ: '(255, 200, 124)' - COLOR_VANILLA: '(243, 229, 171)' - COLOR_VERY_LIGHT_TANGELO: '(255, 176, 119)' - COLOR_VERY_PALE_ORANGE: '(255, 223, 191)' - COLOR_VIS_VIS: '(255, 239, 161)' - COLOR_WAFER: '(222, 203, 198)' - COLOR_WAX_FLOWER: '(255, 192, 168)' - COLOR_WEWAK: '(241, 155, 171)' - COLOR_WHEAT: '(245, 222, 179)' - COLOR_WILD_RICE: '(236, 224, 144)' - COLOR_WITCH_HAZE: '(255, 252, 153)' - COLOR_YOUR_PINK: '(255, 195, 192)' - COLOR_ZINNWALDITE: '(235, 194, 175)' - COLOR_ZOMBIE: '(228, 214, 155)' - COLOR_AUSTRALIAN_MINT: '(245, 255, 190)' - COLOR_BERYL_GREEN: '(222, 229, 192)' - COLOR_CAPER: '(220, 237, 180)' - COLOR_CHIFFON: '(241, 255, 200)' - COLOR_DOLLY: '(249, 255, 139)' - COLOR_EDGEWATER: '(200, 227, 215)' - COLOR_GOSSIP: '(210, 248, 176)' - COLOR_HONEYSUCKLE: '(237, 252, 132)' - COLOR_KEY_LIME: '(232, 244, 140)' - COLOR_MANZ: '(238, 239, 120)' - COLOR_MILAN: '(250, 255, 164)' - COLOR_MINDARO: '(227, 249, 136)' - COLOR_PALE_PRIM: '(253, 254, 184)' - COLOR_SEA_MIST: '(197, 219, 202)' - COLOR_SHALIMAR: '(251, 255, 186)' - COLOR_TAHUNA_SANDS: '(238, 240, 200)' - COLOR_TANA: '(217, 220, 193)' - COLOR_TEA_GREEN: '(208, 240, 192)' - COLOR_TEXAS: '(248, 249, 156)' - COLOR_TIDAL: '(241, 255, 173)' - COLOR_TUSK: '(238, 243, 195)' - COLOR_PALE_CANARY: '(255, 255, 153)' - COLOR_PASTEL_GRAY: '(207, 207, 196)' - COLOR_PASTEL_YELLOW: '(253, 253, 150)' - COLOR_PORTAFINO: '(255, 255, 180)' - COLOR_SUNNY: '(242, 242, 122)' - COLOR_VERY_PALE_YELLOW: '(255, 255, 191)' - COLOR_PALE_PLUM: '(221, 160, 221)' - COLOR_THISTLE: '(216, 191, 216)' - COLOR_ALABASTER: '(250, 250, 250)' - COLOR_ALBESCENT_WHITE: '(245, 233, 211)' - COLOR_ALICE_BLUE: '(240, 248, 255)' - COLOR_ALMOND: '(239, 222, 205)' - COLOR_ALTO: '(219, 219, 219)' - COLOR_AMOUR: '(249, 234, 243)' - COLOR_ANTI_FLASH_WHITE: '(242, 243, 244)' - COLOR_ANTIQUE_WHITE: '(250, 235, 215)' - COLOR_APRICOT_WHITE: '(255, 254, 236)' - COLOR_AQUA_HAZE: '(237, 245, 245)' - COLOR_AQUA_SPRING: '(234, 249, 245)' - COLOR_AQUA_SQUEEZE: '(232, 245, 242)' - COLOR_ATHENS_GRAY: '(238, 240, 243)' - COLOR_ATHS_SPECIAL: '(236, 235, 206)' - COLOR_AZURE_MIST: '(240, 255, 255)' - COLOR_AZUREISH_WHITE: '(219, 233, 244)' - COLOR_BABY_POWDER: '(254, 254, 250)' - COLOR_BAJA_WHITE: '(255, 248, 209)' - COLOR_BARLEY_WHITE: '(255, 244, 206)' - COLOR_BEIGE: '(245, 245, 220)' - COLOR_BIANCA: '(252, 251, 243)' - COLOR_BIZARRE: '(238, 222, 218)' - COLOR_BLACK_HAZE: '(246, 247, 247)' - COLOR_BLACK_SQUEEZE: '(242, 250, 250)' - COLOR_BLACK_WHITE: '(255, 254, 246)' - COLOR_BLANCHED_ALMOND: '(255, 235, 205)' - COLOR_BLEACH_WHITE: '(254, 243, 216)' - COLOR_BLUE_CHALK: '(241, 233, 255)' - COLOR_BLUE_ROMANCE: '(210, 246, 222)' - COLOR_BON_JOUR: '(229, 224, 225)' - COLOR_BONE: '(227, 218, 201)' - COLOR_BRIDAL_HEATH: '(255, 250, 244)' - COLOR_BRIDESMAID: '(254, 240, 236)' - COLOR_BUBBLES: '(231, 254, 255)' - COLOR_BUTTERY_WHITE: '(255, 252, 234)' - COLOR_CARARRA: '(238, 238, 232)' - COLOR_CARLA: '(243, 255, 216)' - COLOR_CAROUSEL_PINK: '(249, 224, 237)' - COLOR_CATSKILL_WHITE: '(238, 246, 247)' - COLOR_CERAMIC: '(252, 255, 249)' - COLOR_CHABLIS: '(255, 244, 243)' - COLOR_CHAMPAGNE: '(247, 231, 206)' - COLOR_CHARDON: '(255, 243, 241)' - COLOR_CHERUB: '(248, 217, 233)' - COLOR_CHILEAN_HEATH: '(255, 253, 230)' - COLOR_CHINA_IVORY: '(252, 255, 231)' - COLOR_CHROME_WHITE: '(232, 241, 212)' - COLOR_CINDERELLA: '(253, 225, 220)' - COLOR_CITRINE_WHITE: '(250, 247, 214)' - COLOR_CLASSIC_ROSE: '(251, 204, 231)' - COLOR_CLEAR_DAY: '(233, 255, 253)' - COLOR_COCONUT_CREAM: '(248, 247, 220)' - COLOR_CONCH: '(201, 217, 210)' - COLOR_CONCRETE: '(242, 242, 242)' - COLOR_CORN_FIELD: '(248, 250, 205)' - COLOR_CORNSILK: '(255, 248, 220)' - COLOR_COSMIC_LATTE: '(255, 248, 231)' - COLOR_COSMOS: '(255, 216, 217)' - COLOR_CREAM: '(255, 253, 208)' - COLOR_CUMULUS: '(253, 255, 213)' - COLOR_DAWN_PINK: '(243, 233, 229)' - COLOR_DERBY: '(255, 238, 216)' - COLOR_DESERT_STORM: '(248, 248, 247)' - COLOR_DEW: '(234, 255, 254)' - COLOR_DOUBLE_PEARL_LUSTA: '(252, 244, 208)' - COLOR_DUST_STORM: '(229, 204, 201)' - COLOR_EARLY_DAWN: '(255, 249, 230)' - COLOR_EBB: '(233, 227, 227)' - COLOR_ECRU_WHITE: '(245, 243, 229)' - COLOR_EGG_SOUR: '(255, 244, 221)' - COLOR_EGGSHELL: '(240, 234, 214)' - COLOR_FAIR_PINK: '(255, 239, 236)' - COLOR_FANTASY: '(250, 243, 240)' - COLOR_FETA: '(240, 252, 234)' - COLOR_FLORAL_WHITE: '(255, 250, 240)' - COLOR_FOAM: '(216, 252, 250)' - COLOR_FOG: '(215, 208, 255)' - COLOR_FORGET_ME_NOT: '(255, 241, 238)' - COLOR_FROST: '(237, 245, 221)' - COLOR_FROSTED_MINT: '(219, 255, 248)' - COLOR_FROSTEE: '(228, 246, 231)' - COLOR_GAINSBORO: '(220, 220, 220)' - COLOR_GALLERY: '(239, 239, 239)' - COLOR_GEYSER: '(212, 223, 226)' - COLOR_GHOST_WHITE: '(248, 248, 255)' - COLOR_GIN: '(232, 242, 235)' - COLOR_GIN_FIZZ: '(255, 249, 226)' - COLOR_GLITTER: '(230, 232, 250)' - COLOR_GRANNY_APPLE: '(213, 246, 227)' - COLOR_GRAY_NURSE: '(231, 236, 230)' - COLOR_GREEN_WHITE: '(232, 235, 224)' - COLOR_HALF_COLONIAL_WHITE: '(253, 246, 211)' - COLOR_HALF_DUTCH_WHITE: '(254, 247, 222)' - COLOR_HALF_SPANISH_WHITE: '(254, 244, 219)' - COLOR_HALF_AND_HALF: '(255, 254, 225)' - COLOR_HARP: '(230, 242, 234)' - COLOR_HAWKES_BLUE: '(212, 226, 252)' - COLOR_HINT_OF_GREEN: '(230, 255, 233)' - COLOR_HINT_OF_RED: '(251, 249, 249)' - COLOR_HINT_OF_YELLOW: '(250, 253, 228)' - COLOR_HONEYDEW: '(240, 255, 240)' - COLOR_IRON: '(212, 215, 217)' - COLOR_ISABELLINE: '(244, 240, 236)' - COLOR_ISLAND_SPICE: '(255, 252, 238)' - COLOR_IVORY: '(255, 255, 240)' - COLOR_JANNA: '(244, 235, 211)' - COLOR_KARRY: '(255, 234, 212)' - COLOR_KIDNAPPER: '(225, 234, 212)' - COLOR_LANGUID_LAVENDER: '(214, 202, 221)' - COLOR_LAVENDER_BLUSH: '(255, 240, 245)' - COLOR_LAVENDER_MIST: '(230, 230, 250)' - COLOR_LEMON_CHIFFON: '(255, 250, 205)' - COLOR_LIGHT_CYAN: '(224, 255, 255)' - COLOR_LIGHT_GOLDENROD_YELLOW: '(250, 250, 210)' - COLOR_LIGHT_GRAY: '(211, 211, 211)' - COLOR_LIGHT_YELLOW: '(255, 255, 224)' - COLOR_LILY_WHITE: '(231, 248, 255)' - COLOR_LINEN: '(250, 240, 230)' - COLOR_LINK_WATER: '(217, 228, 245)' - COLOR_LOAFER: '(238, 244, 222)' - COLOR_LOLA: '(223, 207, 219)' - COLOR_LUMBER: '(255, 228, 205)' - COLOR_MABEL: '(217, 247, 255)' - COLOR_MAGNOLIA: '(248, 244, 255)' - COLOR_MERCURY: '(229, 229, 229)' - COLOR_MERINO: '(246, 240, 230)' - COLOR_MILK_PUNCH: '(255, 246, 212)' - COLOR_MIMOSA: '(248, 253, 211)' - COLOR_MINT_CREAM: '(245, 255, 250)' - COLOR_MISCHKA: '(209, 210, 221)' - COLOR_MISTY_ROSE: '(255, 228, 225)' - COLOR_MOON_GLOW: '(252, 254, 218)' - COLOR_MOON_MIST: '(220, 221, 204)' - COLOR_MOON_RAKER: '(214, 206, 246)' - COLOR_NARVIK: '(237, 249, 241)' - COLOR_NEBULA: '(203, 219, 214)' - COLOR_NYANZA: '(233, 255, 219)' - COLOR_OASIS: '(254, 239, 206)' - COLOR_OFF_GREEN: '(230, 248, 243)' - COLOR_OFF_YELLOW: '(254, 249, 227)' - COLOR_OLD_LACE: '(253, 245, 230)' - COLOR_ORANGE_WHITE: '(254, 252, 237)' - COLOR_ORCHID_WHITE: '(255, 253, 243)' - COLOR_ORINOCO: '(243, 251, 212)' - COLOR_OTTOMAN: '(233, 248, 237)' - COLOR_OYSTER_BAY: '(218, 250, 255)' - COLOR_OYSTER_PINK: '(233, 206, 205)' - COLOR_PALE_LAVENDER: '(220, 208, 255)' - COLOR_PALE_PINK: '(250, 218, 221)' - COLOR_PALE_ROSE: '(255, 225, 242)' - COLOR_PAMPAS: '(244, 242, 238)' - COLOR_PANACHE: '(234, 246, 238)' - COLOR_PAPAYA_WHIP: '(255, 239, 213)' - COLOR_PARCHMENT: '(241, 233, 210)' - COLOR_PARIS_WHITE: '(202, 220, 212)' - COLOR_PATTENS_BLUE: '(222, 245, 255)' - COLOR_PEACH_CREAM: '(255, 240, 219)' - COLOR_PEACH_SCHNAPPS: '(255, 220, 214)' - COLOR_PEARL_BUSH: '(232, 224, 213)' - COLOR_PEARL_LUSTA: '(252, 244, 220)' - COLOR_PEPPERMINT: '(227, 245, 225)' - COLOR_PERIGLACIAL_BLUE: '(225, 230, 214)' - COLOR_PIG_PINK: '(253, 215, 228)' - COLOR_PIGGY_PINK: '(253, 221, 230)' - COLOR_PINK_LACE: '(255, 221, 244)' - COLOR_PINK_LADY: '(255, 241, 216)' - COLOR_PIPI: '(254, 244, 204)' - COLOR_PIPPIN: '(255, 225, 223)' - COLOR_PLATINUM: '(229, 228, 226)' - COLOR_POLAR: '(229, 249, 246)' - COLOR_PORCELAIN: '(239, 242, 243)' - COLOR_POT_POURRI: '(245, 231, 226)' - COLOR_PRIM: '(240, 226, 236)' - COLOR_PROVINCIAL_PINK: '(254, 245, 241)' - COLOR_QUARTER_PEARL_LUSTA: '(255, 253, 244)' - COLOR_QUARTER_SPANISH_WHITE: '(247, 242, 225)' - COLOR_QUEEN_PINK: '(232, 204, 215)' - COLOR_QUILL_GRAY: '(214, 214, 209)' - COLOR_REMY: '(254, 235, 243)' - COLOR_RICE_CAKE: '(255, 254, 240)' - COLOR_RICE_FLOWER: '(238, 255, 226)' - COLOR_ROMANCE: '(255, 254, 253)' - COLOR_ROSE_WHITE: '(255, 246, 245)' - COLOR_RUM_SWIZZLE: '(249, 248, 228)' - COLOR_SALTPAN: '(241, 247, 242)' - COLOR_SATIN_LINEN: '(230, 228, 212)' - COLOR_SAUVIGNON: '(255, 245, 243)' - COLOR_SAZERAC: '(255, 244, 224)' - COLOR_SCOTCH_MIST: '(255, 251, 220)' - COLOR_SEASHELL: '(255, 245, 238)' - COLOR_SELAGO: '(240, 238, 253)' - COLOR_SERENADE: '(255, 244, 232)' - COLOR_SHAMPOO: '(255, 207, 241)' - COLOR_SKEPTIC: '(202, 230, 218)' - COLOR_SNOW: '(255, 250, 250)' - COLOR_SNOW_DRIFT: '(247, 250, 247)' - COLOR_SNOW_FLURRY: '(228, 255, 209)' - COLOR_SNOWY_MINT: '(214, 255, 219)' - COLOR_SNUFF: '(226, 216, 237)' - COLOR_SOAPSTONE: '(255, 251, 249)' - COLOR_SOFT_PEACH: '(245, 237, 239)' - COLOR_SOLITAIRE: '(254, 248, 226)' - COLOR_SOLITUDE: '(234, 246, 255)' - COLOR_SPRING_SUN: '(246, 255, 220)' - COLOR_SPRING_WOOD: '(248, 246, 241)' - COLOR_SUGAR_CANE: '(249, 255, 246)' - COLOR_SURF_CREST: '(207, 229, 210)' - COLOR_SWANS_DOWN: '(220, 240, 234)' - COLOR_SWISS_COFFEE: '(221, 214, 213)' - COLOR_TARA: '(225, 246, 232)' - COLOR_TASMAN: '(207, 220, 207)' - COLOR_TIMBERWOLF: '(219, 215, 210)' - COLOR_TITAN_WHITE: '(240, 238, 255)' - COLOR_TRANQUIL: '(230, 255, 255)' - COLOR_TRAVERTINE: '(255, 253, 232)' - COLOR_TUFT_BUSH: '(255, 221, 205)' - COLOR_TUTU: '(255, 241, 249)' - COLOR_TWILIGHT: '(228, 207, 222)' - COLOR_TWILIGHT_BLUE: '(238, 253, 255)' - COLOR_UNBLEACHED_SILK: '(255, 221, 202)' - COLOR_UNDERAGE_PINK: '(249, 230, 244)' - COLOR_VARDEN: '(255, 246, 223)' - COLOR_VISTA_WHITE: '(252, 248, 247)' - COLOR_WATUSI: '(255, 221, 207)' - COLOR_WE_PEEP: '(247, 219, 230)' - COLOR_WESTAR: '(220, 217, 210)' - COLOR_WHEATFIELD: '(243, 237, 207)' - COLOR_WHISPER: '(247, 245, 250)' - COLOR_WHITE: '(255, 255, 255)' - COLOR_WHITE_ICE: '(221, 249, 241)' - COLOR_WHITE_LILAC: '(248, 247, 252)' - COLOR_WHITE_LINEN: '(248, 240, 232)' - COLOR_WHITE_POINTER: '(254, 248, 255)' - COLOR_WHITE_ROCK: '(234, 232, 212)' - COLOR_WHITE_SMOKE: '(245, 245, 245)' - COLOR_WILD_SAND: '(244, 244, 244)' - COLOR_WILLOW_BROOK: '(223, 236, 218)' - COLOR_WISP_PINK: '(254, 244, 248)' - COLOR_ZANAH: '(218, 236, 214)' - COLOR_ZIRCON: '(244, 248, 255)' - COLOR_ZUMTHOR: '(237, 246, 255)' + COLOR_BOTTICELLI: (199, 221, 229) + COLOR_BRIGHT_LILAC: (216, 145, 239) + COLOR_BRIGHT_UBE: (209, 159, 232) + COLOR_BRILLIANT_LAVENDER: (244, 187, 255) + COLOR_GHOST: (199, 201, 213) + COLOR_MAUVE: (224, 176, 255) + COLOR_PALE_VIOLET: (204, 153, 255) + COLOR_PERFUME: (208, 190, 248) + COLOR_PRELUDE: (208, 192, 229) + COLOR_RICH_BRILLIANT_LAVENDER: (241, 167, 254) + COLOR_SOAP: (206, 200, 239) + COLOR_TROPICAL_VIOLET: (205, 164, 222) + COLOR_AMARANTH_PINK: (241, 156, 187) + COLOR_APRICOT: (251, 206, 177) + COLOR_ASTRA: (250, 234, 185) + COLOR_AZALEA: (247, 200, 218) + COLOR_BANANA_MANIA: (250, 231, 181) + COLOR_BEAUTY_BUSH: (238, 193, 190) + COLOR_BEESWAX: (254, 242, 199) + COLOR_BISQUE: (255, 228, 196) + COLOR_BLOND: (250, 240, 190) + COLOR_BLOSSOM: (220, 180, 188) + COLOR_BUBBLE_GUM: (255, 193, 204) + COLOR_BUFF: (240, 220, 130) + COLOR_BUTTERMILK: (255, 241, 181) + COLOR_CALICO: (224, 192, 149) + COLOR_CAMEO_PINK: (239, 187, 204) + COLOR_CAPE_HONEY: (254, 229, 172) + COLOR_CARAMEL: (255, 221, 175) + COLOR_CARNATION_PINK: (255, 166, 201) + COLOR_CASHMERE: (230, 190, 165) + COLOR_CAVERN_PINK: (227, 190, 190) + COLOR_CHALKY: (238, 215, 148) + COLOR_CHAMOIS: (237, 220, 177) + COLOR_CHANTILLY: (248, 195, 223) + COLOR_CHARDONNAY: (255, 205, 140) + COLOR_CHEROKEE: (252, 218, 152) + COLOR_CHERRY_BLOSSOM_PINK: (255, 183, 197) + COLOR_COLONIAL_WHITE: (255, 237, 188) + COLOR_CORNFLOWER_LILAC: (255, 176, 172) + COLOR_CORVETTE: (250, 211, 162) + COLOR_COTTON_CANDY: (255, 188, 217) + COLOR_CRAYOLA_YELLOW: (252, 232, 131) + COLOR_CREAM_BRULEE: (255, 229, 160) + COLOR_CUPID: (251, 190, 218) + COLOR_DAIRY_CREAM: (249, 228, 188) + COLOR_DESERT_SAND: (237, 201, 175) + COLOR_DOUBLE_COLONIAL_WHITE: (238, 227, 173) + COLOR_DOUBLE_SPANISH_WHITE: (230, 215, 185) + COLOR_DROVER: (253, 247, 173) + COLOR_DUTCH_WHITE: (239, 223, 187) + COLOR_EGG_WHITE: (255, 239, 193) + COLOR_FESTIVAL: (251, 233, 108) + COLOR_FLAVESCENT: (247, 233, 142) + COLOR_FLAX: (238, 220, 130) + COLOR_FRANGIPANI: (255, 222, 179) + COLOR_GIVRY: (248, 228, 191) + COLOR_GOLDEN_GLOW: (253, 226, 149) + COLOR_GOLDEN_SAND: (240, 219, 125) + COLOR_GRAIN_BROWN: (228, 213, 183) + COLOR_GRANDIS: (255, 211, 140) + COLOR_HAMPTON: (229, 216, 175) + COLOR_HIT_PINK: (255, 171, 129) + COLOR_ILLUSION: (246, 164, 201) + COLOR_JASMINE: (248, 222, 126) + COLOR_JUST_RIGHT: (236, 205, 185) + COLOR_KOBI: (231, 159, 196) + COLOR_KOURNIKOVA: (255, 231, 114) + COLOR_LAVENDER_PINK: (251, 174, 210) + COLOR_LAVENDER_ROSE: (251, 160, 227) + COLOR_LEMON_MERINGUE: (246, 234, 190) + COLOR_LIGHT_APRICOT: (253, 213, 177) + COLOR_LIGHT_HOT_PINK: (255, 179, 222) + COLOR_LIGHT_KHAKI: (240, 230, 140) + COLOR_LIGHT_ORCHID: (230, 168, 215) + COLOR_LIGHT_PINK: (255, 182, 193) + COLOR_MACARONI_AND_CHEESE: (255, 185, 123) + COLOR_MANDYS_PINK: (242, 195, 178) + COLOR_MANHATTAN: (245, 201, 153) + COLOR_MARIGOLD_YELLOW: (251, 232, 112) + COLOR_MARZIPAN: (248, 219, 157) + COLOR_MAVERICK: (216, 194, 213) + COLOR_MELANIE: (228, 194, 213) + COLOR_MELLOW_APRICOT: (248, 184, 120) + COLOR_MELON: (253, 188, 180) + COLOR_MINT_JULEP: (241, 238, 193) + COLOR_MOCCASIN: (255, 228, 181) + COLOR_MONA_LISA: (255, 161, 148) + COLOR_NADESHIKO_PINK: (246, 173, 198) + COLOR_NAVAJO_WHITE: (255, 222, 173) + COLOR_NEGRONI: (255, 226, 197) + COLOR_NEW_ORLEANS: (243, 214, 157) + COLOR_ORANGE_YELLOW: (248, 213, 104) + COLOR_ORCHID_PINK: (242, 189, 205) + COLOR_PALE_CHESTNUT: (221, 173, 175) + COLOR_PALE_GOLD: (230, 190, 138) + COLOR_PALE_GOLDENROD: (238, 232, 170) + COLOR_PALE_MAGENTA_PINK: (255, 153, 204) + COLOR_PALE_SPRING_BUD: (236, 235, 189) + COLOR_PANCHO: (237, 205, 171) + COLOR_PARIS_DAISY: (255, 244, 110) + COLOR_PASTEL_MAGENTA: (244, 154, 194) + COLOR_PEACH: (255, 203, 164) + COLOR_PEACH_ORANGE: (255, 204, 153) + COLOR_PEACH_PUFF: (255, 218, 185) + COLOR_PEACH_YELLOW: (250, 223, 173) + COLOR_PEARL: (234, 224, 200) + COLOR_PICASSO: (255, 243, 157) + COLOR_PINK: (255, 192, 203) + COLOR_PINK_FLARE: (225, 192, 200) + COLOR_PINK_LAVENDER: (216, 178, 209) + COLOR_PINK_PEARL: (231, 172, 207) + COLOR_PRIMROSE: (237, 234, 153) + COLOR_PUTTY: (231, 205, 140) + COLOR_RAFFIA: (234, 218, 184) + COLOR_ROMANTIC: (255, 210, 183) + COLOR_ROSE_BUD: (251, 178, 163) + COLOR_ROSE_FOG: (231, 188, 180) + COLOR_SAHARA_SAND: (241, 231, 136) + COLOR_SALOMIE: (254, 219, 141) + COLOR_SANDWISP: (245, 231, 162) + COLOR_SANDY_BEACH: (255, 234, 200) + COLOR_SAPLING: (222, 212, 164) + COLOR_SHILO: (232, 185, 179) + COLOR_SIDECAR: (243, 231, 187) + COLOR_SISAL: (211, 203, 186) + COLOR_SPANISH_PINK: (247, 191, 190) + COLOR_STARK_WHITE: (229, 215, 189) + COLOR_SUNDOWN: (255, 177, 179) + COLOR_SUNSET: (250, 214, 165) + COLOR_SWEET_CORN: (251, 234, 140) + COLOR_SWEET_PINK: (253, 159, 162) + COLOR_SWIRL: (211, 205, 197) + COLOR_TACAO: (237, 179, 129) + COLOR_TEA_ROSE: (244, 194, 194) + COLOR_TEQUILA: (255, 230, 199) + COLOR_TOPAZ: (255, 200, 124) + COLOR_VANILLA: (243, 229, 171) + COLOR_VERY_LIGHT_TANGELO: (255, 176, 119) + COLOR_VERY_PALE_ORANGE: (255, 223, 191) + COLOR_VIS_VIS: (255, 239, 161) + COLOR_WAFER: (222, 203, 198) + COLOR_WAX_FLOWER: (255, 192, 168) + COLOR_WEWAK: (241, 155, 171) + COLOR_WHEAT: (245, 222, 179) + COLOR_WILD_RICE: (236, 224, 144) + COLOR_WITCH_HAZE: (255, 252, 153) + COLOR_YOUR_PINK: (255, 195, 192) + COLOR_ZINNWALDITE: (235, 194, 175) + COLOR_ZOMBIE: (228, 214, 155) + COLOR_AUSTRALIAN_MINT: (245, 255, 190) + COLOR_BERYL_GREEN: (222, 229, 192) + COLOR_CAPER: (220, 237, 180) + COLOR_CHIFFON: (241, 255, 200) + COLOR_DOLLY: (249, 255, 139) + COLOR_EDGEWATER: (200, 227, 215) + COLOR_GOSSIP: (210, 248, 176) + COLOR_HONEYSUCKLE: (237, 252, 132) + COLOR_KEY_LIME: (232, 244, 140) + COLOR_MANZ: (238, 239, 120) + COLOR_MILAN: (250, 255, 164) + COLOR_MINDARO: (227, 249, 136) + COLOR_PALE_PRIM: (253, 254, 184) + COLOR_SEA_MIST: (197, 219, 202) + COLOR_SHALIMAR: (251, 255, 186) + COLOR_TAHUNA_SANDS: (238, 240, 200) + COLOR_TANA: (217, 220, 193) + COLOR_TEA_GREEN: (208, 240, 192) + COLOR_TEXAS: (248, 249, 156) + COLOR_TIDAL: (241, 255, 173) + COLOR_TUSK: (238, 243, 195) + COLOR_PALE_CANARY: (255, 255, 153) + COLOR_PASTEL_GRAY: (207, 207, 196) + COLOR_PASTEL_YELLOW: (253, 253, 150) + COLOR_PORTAFINO: (255, 255, 180) + COLOR_SUNNY: (242, 242, 122) + COLOR_VERY_PALE_YELLOW: (255, 255, 191) + COLOR_PALE_PLUM: (221, 160, 221) + COLOR_THISTLE: (216, 191, 216) + COLOR_ALABASTER: (250, 250, 250) + COLOR_ALBESCENT_WHITE: (245, 233, 211) + COLOR_ALICE_BLUE: (240, 248, 255) + COLOR_ALMOND: (239, 222, 205) + COLOR_ALTO: (219, 219, 219) + COLOR_AMOUR: (249, 234, 243) + COLOR_ANTI_FLASH_WHITE: (242, 243, 244) + COLOR_ANTIQUE_WHITE: (250, 235, 215) + COLOR_APRICOT_WHITE: (255, 254, 236) + COLOR_AQUA_HAZE: (237, 245, 245) + COLOR_AQUA_SPRING: (234, 249, 245) + COLOR_AQUA_SQUEEZE: (232, 245, 242) + COLOR_ATHENS_GRAY: (238, 240, 243) + COLOR_ATHS_SPECIAL: (236, 235, 206) + COLOR_AZURE_MIST: (240, 255, 255) + COLOR_AZUREISH_WHITE: (219, 233, 244) + COLOR_BABY_POWDER: (254, 254, 250) + COLOR_BAJA_WHITE: (255, 248, 209) + COLOR_BARLEY_WHITE: (255, 244, 206) + COLOR_BEIGE: (245, 245, 220) + COLOR_BIANCA: (252, 251, 243) + COLOR_BIZARRE: (238, 222, 218) + COLOR_BLACK_HAZE: (246, 247, 247) + COLOR_BLACK_SQUEEZE: (242, 250, 250) + COLOR_BLACK_WHITE: (255, 254, 246) + COLOR_BLANCHED_ALMOND: (255, 235, 205) + COLOR_BLEACH_WHITE: (254, 243, 216) + COLOR_BLUE_CHALK: (241, 233, 255) + COLOR_BLUE_ROMANCE: (210, 246, 222) + COLOR_BON_JOUR: (229, 224, 225) + COLOR_BONE: (227, 218, 201) + COLOR_BRIDAL_HEATH: (255, 250, 244) + COLOR_BRIDESMAID: (254, 240, 236) + COLOR_BUBBLES: (231, 254, 255) + COLOR_BUTTERY_WHITE: (255, 252, 234) + COLOR_CARARRA: (238, 238, 232) + COLOR_CARLA: (243, 255, 216) + COLOR_CAROUSEL_PINK: (249, 224, 237) + COLOR_CATSKILL_WHITE: (238, 246, 247) + COLOR_CERAMIC: (252, 255, 249) + COLOR_CHABLIS: (255, 244, 243) + COLOR_CHAMPAGNE: (247, 231, 206) + COLOR_CHARDON: (255, 243, 241) + COLOR_CHERUB: (248, 217, 233) + COLOR_CHILEAN_HEATH: (255, 253, 230) + COLOR_CHINA_IVORY: (252, 255, 231) + COLOR_CHROME_WHITE: (232, 241, 212) + COLOR_CINDERELLA: (253, 225, 220) + COLOR_CITRINE_WHITE: (250, 247, 214) + COLOR_CLASSIC_ROSE: (251, 204, 231) + COLOR_CLEAR_DAY: (233, 255, 253) + COLOR_COCONUT_CREAM: (248, 247, 220) + COLOR_CONCH: (201, 217, 210) + COLOR_CONCRETE: (242, 242, 242) + COLOR_CORN_FIELD: (248, 250, 205) + COLOR_CORNSILK: (255, 248, 220) + COLOR_COSMIC_LATTE: (255, 248, 231) + COLOR_COSMOS: (255, 216, 217) + COLOR_CREAM: (255, 253, 208) + COLOR_CUMULUS: (253, 255, 213) + COLOR_DAWN_PINK: (243, 233, 229) + COLOR_DERBY: (255, 238, 216) + COLOR_DESERT_STORM: (248, 248, 247) + COLOR_DEW: (234, 255, 254) + COLOR_DOUBLE_PEARL_LUSTA: (252, 244, 208) + COLOR_DUST_STORM: (229, 204, 201) + COLOR_EARLY_DAWN: (255, 249, 230) + COLOR_EBB: (233, 227, 227) + COLOR_ECRU_WHITE: (245, 243, 229) + COLOR_EGG_SOUR: (255, 244, 221) + COLOR_EGGSHELL: (240, 234, 214) + COLOR_FAIR_PINK: (255, 239, 236) + COLOR_FANTASY: (250, 243, 240) + COLOR_FETA: (240, 252, 234) + COLOR_FLORAL_WHITE: (255, 250, 240) + COLOR_FOAM: (216, 252, 250) + COLOR_FOG: (215, 208, 255) + COLOR_FORGET_ME_NOT: (255, 241, 238) + COLOR_FROST: (237, 245, 221) + COLOR_FROSTED_MINT: (219, 255, 248) + COLOR_FROSTEE: (228, 246, 231) + COLOR_GAINSBORO: (220, 220, 220) + COLOR_GALLERY: (239, 239, 239) + COLOR_GEYSER: (212, 223, 226) + COLOR_GHOST_WHITE: (248, 248, 255) + COLOR_GIN: (232, 242, 235) + COLOR_GIN_FIZZ: (255, 249, 226) + COLOR_GLITTER: (230, 232, 250) + COLOR_GRANNY_APPLE: (213, 246, 227) + COLOR_GRAY_NURSE: (231, 236, 230) + COLOR_GREEN_WHITE: (232, 235, 224) + COLOR_HALF_COLONIAL_WHITE: (253, 246, 211) + COLOR_HALF_DUTCH_WHITE: (254, 247, 222) + COLOR_HALF_SPANISH_WHITE: (254, 244, 219) + COLOR_HALF_AND_HALF: (255, 254, 225) + COLOR_HARP: (230, 242, 234) + COLOR_HAWKES_BLUE: (212, 226, 252) + COLOR_HINT_OF_GREEN: (230, 255, 233) + COLOR_HINT_OF_RED: (251, 249, 249) + COLOR_HINT_OF_YELLOW: (250, 253, 228) + COLOR_HONEYDEW: (240, 255, 240) + COLOR_IRON: (212, 215, 217) + COLOR_ISABELLINE: (244, 240, 236) + COLOR_ISLAND_SPICE: (255, 252, 238) + COLOR_IVORY: (255, 255, 240) + COLOR_JANNA: (244, 235, 211) + COLOR_KARRY: (255, 234, 212) + COLOR_KIDNAPPER: (225, 234, 212) + COLOR_LANGUID_LAVENDER: (214, 202, 221) + COLOR_LAVENDER_BLUSH: (255, 240, 245) + COLOR_LAVENDER_MIST: (230, 230, 250) + COLOR_LEMON_CHIFFON: (255, 250, 205) + COLOR_LIGHT_CYAN: (224, 255, 255) + COLOR_LIGHT_GOLDENROD_YELLOW: (250, 250, 210) + COLOR_LIGHT_GRAY: (211, 211, 211) + COLOR_LIGHT_YELLOW: (255, 255, 224) + COLOR_LILY_WHITE: (231, 248, 255) + COLOR_LINEN: (250, 240, 230) + COLOR_LINK_WATER: (217, 228, 245) + COLOR_LOAFER: (238, 244, 222) + COLOR_LOLA: (223, 207, 219) + COLOR_LUMBER: (255, 228, 205) + COLOR_MABEL: (217, 247, 255) + COLOR_MAGNOLIA: (248, 244, 255) + COLOR_MERCURY: (229, 229, 229) + COLOR_MERINO: (246, 240, 230) + COLOR_MILK_PUNCH: (255, 246, 212) + COLOR_MIMOSA: (248, 253, 211) + COLOR_MINT_CREAM: (245, 255, 250) + COLOR_MISCHKA: (209, 210, 221) + COLOR_MISTY_ROSE: (255, 228, 225) + COLOR_MOON_GLOW: (252, 254, 218) + COLOR_MOON_MIST: (220, 221, 204) + COLOR_MOON_RAKER: (214, 206, 246) + COLOR_NARVIK: (237, 249, 241) + COLOR_NEBULA: (203, 219, 214) + COLOR_NYANZA: (233, 255, 219) + COLOR_OASIS: (254, 239, 206) + COLOR_OFF_GREEN: (230, 248, 243) + COLOR_OFF_YELLOW: (254, 249, 227) + COLOR_OLD_LACE: (253, 245, 230) + COLOR_ORANGE_WHITE: (254, 252, 237) + COLOR_ORCHID_WHITE: (255, 253, 243) + COLOR_ORINOCO: (243, 251, 212) + COLOR_OTTOMAN: (233, 248, 237) + COLOR_OYSTER_BAY: (218, 250, 255) + COLOR_OYSTER_PINK: (233, 206, 205) + COLOR_PALE_LAVENDER: (220, 208, 255) + COLOR_PALE_PINK: (250, 218, 221) + COLOR_PALE_ROSE: (255, 225, 242) + COLOR_PAMPAS: (244, 242, 238) + COLOR_PANACHE: (234, 246, 238) + COLOR_PAPAYA_WHIP: (255, 239, 213) + COLOR_PARCHMENT: (241, 233, 210) + COLOR_PARIS_WHITE: (202, 220, 212) + COLOR_PATTENS_BLUE: (222, 245, 255) + COLOR_PEACH_CREAM: (255, 240, 219) + COLOR_PEACH_SCHNAPPS: (255, 220, 214) + COLOR_PEARL_BUSH: (232, 224, 213) + COLOR_PEARL_LUSTA: (252, 244, 220) + COLOR_PEPPERMINT: (227, 245, 225) + COLOR_PERIGLACIAL_BLUE: (225, 230, 214) + COLOR_PIG_PINK: (253, 215, 228) + COLOR_PIGGY_PINK: (253, 221, 230) + COLOR_PINK_LACE: (255, 221, 244) + COLOR_PINK_LADY: (255, 241, 216) + COLOR_PIPI: (254, 244, 204) + COLOR_PIPPIN: (255, 225, 223) + COLOR_PLATINUM: (229, 228, 226) + COLOR_POLAR: (229, 249, 246) + COLOR_PORCELAIN: (239, 242, 243) + COLOR_POT_POURRI: (245, 231, 226) + COLOR_PRIM: (240, 226, 236) + COLOR_PROVINCIAL_PINK: (254, 245, 241) + COLOR_QUARTER_PEARL_LUSTA: (255, 253, 244) + COLOR_QUARTER_SPANISH_WHITE: (247, 242, 225) + COLOR_QUEEN_PINK: (232, 204, 215) + COLOR_QUILL_GRAY: (214, 214, 209) + COLOR_REMY: (254, 235, 243) + COLOR_RICE_CAKE: (255, 254, 240) + COLOR_RICE_FLOWER: (238, 255, 226) + COLOR_ROMANCE: (255, 254, 253) + COLOR_ROSE_WHITE: (255, 246, 245) + COLOR_RUM_SWIZZLE: (249, 248, 228) + COLOR_SALTPAN: (241, 247, 242) + COLOR_SATIN_LINEN: (230, 228, 212) + COLOR_SAUVIGNON: (255, 245, 243) + COLOR_SAZERAC: (255, 244, 224) + COLOR_SCOTCH_MIST: (255, 251, 220) + COLOR_SEASHELL: (255, 245, 238) + COLOR_SELAGO: (240, 238, 253) + COLOR_SERENADE: (255, 244, 232) + COLOR_SHAMPOO: (255, 207, 241) + COLOR_SKEPTIC: (202, 230, 218) + COLOR_SNOW: (255, 250, 250) + COLOR_SNOW_DRIFT: (247, 250, 247) + COLOR_SNOW_FLURRY: (228, 255, 209) + COLOR_SNOWY_MINT: (214, 255, 219) + COLOR_SNUFF: (226, 216, 237) + COLOR_SOAPSTONE: (255, 251, 249) + COLOR_SOFT_PEACH: (245, 237, 239) + COLOR_SOLITAIRE: (254, 248, 226) + COLOR_SOLITUDE: (234, 246, 255) + COLOR_SPRING_SUN: (246, 255, 220) + COLOR_SPRING_WOOD: (248, 246, 241) + COLOR_SUGAR_CANE: (249, 255, 246) + COLOR_SURF_CREST: (207, 229, 210) + COLOR_SWANS_DOWN: (220, 240, 234) + COLOR_SWISS_COFFEE: (221, 214, 213) + COLOR_TARA: (225, 246, 232) + COLOR_TASMAN: (207, 220, 207) + COLOR_TIMBERWOLF: (219, 215, 210) + COLOR_TITAN_WHITE: (240, 238, 255) + COLOR_TRANQUIL: (230, 255, 255) + COLOR_TRAVERTINE: (255, 253, 232) + COLOR_TUFT_BUSH: (255, 221, 205) + COLOR_TUTU: (255, 241, 249) + COLOR_TWILIGHT: (228, 207, 222) + COLOR_TWILIGHT_BLUE: (238, 253, 255) + COLOR_UNBLEACHED_SILK: (255, 221, 202) + COLOR_UNDERAGE_PINK: (249, 230, 244) + COLOR_VARDEN: (255, 246, 223) + COLOR_VISTA_WHITE: (252, 248, 247) + COLOR_WATUSI: (255, 221, 207) + COLOR_WE_PEEP: (247, 219, 230) + COLOR_WESTAR: (220, 217, 210) + COLOR_WHEATFIELD: (243, 237, 207) + COLOR_WHISPER: (247, 245, 250) + COLOR_WHITE: (255, 255, 255) + COLOR_WHITE_ICE: (221, 249, 241) + COLOR_WHITE_LILAC: (248, 247, 252) + COLOR_WHITE_LINEN: (248, 240, 232) + COLOR_WHITE_POINTER: (254, 248, 255) + COLOR_WHITE_ROCK: (234, 232, 212) + COLOR_WHITE_SMOKE: (245, 245, 245) + COLOR_WILD_SAND: (244, 244, 244) + COLOR_WILLOW_BROOK: (223, 236, 218) + COLOR_WISP_PINK: (254, 244, 248) + COLOR_ZANAH: (218, 236, 214) + COLOR_ZIRCON: (244, 248, 255) + COLOR_ZUMTHOR: (237, 246, 255) confusion: - COLOR_ELECTRIC_PURPLE: '(191, 0, 255)' - COLOR_ELECTRIC_VIOLET: '(139, 0, 255)' - COLOR_PSYCHEDELIC_PURPLE: '(223, 0, 255)' - COLOR_VERONICA: '(160, 32, 240)' - COLOR_VIVID_MULBERRY: '(184, 12, 227)' - COLOR_VIVID_ORCHID: '(204, 0, 255)' - COLOR_VIVID_VIOLET: '(159, 0, 255)' - COLOR_HOT_MAGENTA: '(255, 29, 206)' - COLOR_METAL_PINK: '(255, 0, 253)' - COLOR_DEEP_MAGENTA: '(204, 0, 204)' - COLOR_FUCHSIA: '(255, 0, 255)' + COLOR_ELECTRIC_PURPLE: (191, 0, 255) + COLOR_ELECTRIC_VIOLET: (139, 0, 255) + COLOR_PSYCHEDELIC_PURPLE: (223, 0, 255) + COLOR_VERONICA: (160, 32, 240) + COLOR_VIVID_MULBERRY: (184, 12, 227) + COLOR_VIVID_ORCHID: (204, 0, 255) + COLOR_VIVID_VIOLET: (159, 0, 255) + COLOR_HOT_MAGENTA: (255, 29, 206) + COLOR_METAL_PINK: (255, 0, 253) + COLOR_DEEP_MAGENTA: (204, 0, 204) + COLOR_FUCHSIA: (255, 0, 255) surprise: - COLOR_HELIOTROPE: '(223, 115, 255)' - COLOR_MEDIUM_ORCHID: '(186, 85, 211)' - COLOR_PINK_FLAMINGO: '(252, 116, 253)' - COLOR_RICH_LILAC: '(182, 102, 210)' - COLOR_BAKER_MILLER_PINK: '(255, 145, 175)' - COLOR_BLUSH: '(222, 93, 131)' - COLOR_BRILLIANT_ROSE: '(255, 85, 163)' - COLOR_BRINK_PINK: '(251, 96, 127)' - COLOR_CARISSMA: '(234, 136, 168)' - COLOR_CHARM: '(212, 116, 148)' - COLOR_CHARM_PINK: '(230, 143, 172)' - COLOR_CINNAMON_SATIN: '(205, 96, 126)' - COLOR_CYCLAMEN: '(245, 111, 161)' - COLOR_DARK_PINK: '(231, 84, 128)' - COLOR_DEEP_BLUSH: '(228, 118, 152)' - COLOR_FANDANGO_PINK: '(222, 82, 133)' - COLOR_FLAMINGO_PINK: '(252, 142, 172)' - COLOR_FRENCH_PINK: '(253, 108, 158)' - COLOR_FRENCH_ROSE: '(246, 74, 138)' - COLOR_FROLY: '(245, 117, 132)' - COLOR_GERALDINE: '(251, 137, 137)' - COLOR_HOPBUSH: '(208, 109, 161)' - COLOR_HOT_PINK: '(255, 105, 180)' - COLOR_LIGHT_CORAL: '(240, 128, 128)' - COLOR_LIGHT_CRIMSON: '(245, 105, 145)' - COLOR_LIGHT_DEEP_PINK: '(255, 92, 205)' - COLOR_LIGHT_FUCHSIA_PINK: '(249, 132, 239)' - COLOR_LIGHT_SALMON_PINK: '(255, 153, 153)' - COLOR_MAUVELOUS: '(239, 152, 170)' - COLOR_MULBERRY: '(197, 75, 140)' - COLOR_MYSTIC: '(214, 82, 130)' - COLOR_NEW_YORK_PINK: '(215, 131, 127)' - COLOR_ORCHID: '(218, 112, 214)' - COLOR_PALE_MAGENTA: '(249, 132, 229)' - COLOR_PALE_RED_VIOLET: '(219, 112, 147)' - COLOR_PANTONE_PINK: '(215, 72, 148)' - COLOR_PERSIAN_PINK: '(247, 127, 190)' - COLOR_PINK_SHERBET: '(247, 143, 167)' - COLOR_PRINCESS_PERFUME: '(255, 133, 207)' - COLOR_PURPLE_PIZZAZZ: '(254, 78, 218)' - COLOR_RASPBERRY_PINK: '(226, 80, 152)' - COLOR_RAZZLE_DAZZLE_ROSE: '(255, 51, 204)' - COLOR_ROSE_BONBON: '(249, 66, 158)' - COLOR_ROSE_PINK: '(255, 102, 204)' - COLOR_RUDDY_PINK: '(225, 142, 150)' - COLOR_SALMON_PINK: '(255, 145, 164)' - COLOR_SEA_PINK: '(237, 152, 158)' - COLOR_SHIMMERING_BLUSH: '(217, 134, 149)' - COLOR_SHOCKING: '(226, 146, 192)' - COLOR_SKY_MAGENTA: '(207, 113, 175)' - COLOR_STRAWBERRY: '(252, 90, 141)' - COLOR_SUPER_PINK: '(207, 107, 169)' - COLOR_THULIAN_PINK: '(222, 111, 161)' - COLOR_TICKLE_ME_PINK: '(252, 137, 172)' - COLOR_TULIP: '(255, 135, 141)' - COLOR_VANILLA_ICE: '(243, 143, 169)' - COLOR_VIOLET_RED: '(247, 83, 148)' - COLOR_VIVID_TANGERINE: '(255, 160, 137)' - COLOR_WILD_ORCHID: '(212, 112, 162)' - COLOR_WILD_STRAWBERRY: '(255, 67, 164)' - COLOR_WILD_WATERMELON: '(252, 108, 133)' - COLOR_DEEP_FUCHSIA: '(193, 84, 193)' - COLOR_FRENCH_MAUVE: '(212, 115, 212)' - COLOR_FUCHSIA_PINK: '(255, 119, 255)' - COLOR_LAVENDER_MAGENTA: '(238, 130, 238)' - COLOR_STEEL_PINK: '(204, 51, 204)' - COLOR_ULTRA_PINK: '(255, 111, 255)' + COLOR_HELIOTROPE: (223, 115, 255) + COLOR_MEDIUM_ORCHID: (186, 85, 211) + COLOR_PINK_FLAMINGO: (252, 116, 253) + COLOR_RICH_LILAC: (182, 102, 210) + COLOR_BAKER_MILLER_PINK: (255, 145, 175) + COLOR_BLUSH: (222, 93, 131) + COLOR_BRILLIANT_ROSE: (255, 85, 163) + COLOR_BRINK_PINK: (251, 96, 127) + COLOR_CARISSMA: (234, 136, 168) + COLOR_CHARM: (212, 116, 148) + COLOR_CHARM_PINK: (230, 143, 172) + COLOR_CINNAMON_SATIN: (205, 96, 126) + COLOR_CYCLAMEN: (245, 111, 161) + COLOR_DARK_PINK: (231, 84, 128) + COLOR_DEEP_BLUSH: (228, 118, 152) + COLOR_FANDANGO_PINK: (222, 82, 133) + COLOR_FLAMINGO_PINK: (252, 142, 172) + COLOR_FRENCH_PINK: (253, 108, 158) + COLOR_FRENCH_ROSE: (246, 74, 138) + COLOR_FROLY: (245, 117, 132) + COLOR_GERALDINE: (251, 137, 137) + COLOR_HOPBUSH: (208, 109, 161) + COLOR_HOT_PINK: (255, 105, 180) + COLOR_LIGHT_CORAL: (240, 128, 128) + COLOR_LIGHT_CRIMSON: (245, 105, 145) + COLOR_LIGHT_DEEP_PINK: (255, 92, 205) + COLOR_LIGHT_FUCHSIA_PINK: (249, 132, 239) + COLOR_LIGHT_SALMON_PINK: (255, 153, 153) + COLOR_MAUVELOUS: (239, 152, 170) + COLOR_MULBERRY: (197, 75, 140) + COLOR_MYSTIC: (214, 82, 130) + COLOR_NEW_YORK_PINK: (215, 131, 127) + COLOR_ORCHID: (218, 112, 214) + COLOR_PALE_MAGENTA: (249, 132, 229) + COLOR_PALE_RED_VIOLET: (219, 112, 147) + COLOR_PANTONE_PINK: (215, 72, 148) + COLOR_PERSIAN_PINK: (247, 127, 190) + COLOR_PINK_SHERBET: (247, 143, 167) + COLOR_PRINCESS_PERFUME: (255, 133, 207) + COLOR_PURPLE_PIZZAZZ: (254, 78, 218) + COLOR_RASPBERRY_PINK: (226, 80, 152) + COLOR_RAZZLE_DAZZLE_ROSE: (255, 51, 204) + COLOR_ROSE_BONBON: (249, 66, 158) + COLOR_ROSE_PINK: (255, 102, 204) + COLOR_RUDDY_PINK: (225, 142, 150) + COLOR_SALMON_PINK: (255, 145, 164) + COLOR_SEA_PINK: (237, 152, 158) + COLOR_SHIMMERING_BLUSH: (217, 134, 149) + COLOR_SHOCKING: (226, 146, 192) + COLOR_SKY_MAGENTA: (207, 113, 175) + COLOR_STRAWBERRY: (252, 90, 141) + COLOR_SUPER_PINK: (207, 107, 169) + COLOR_THULIAN_PINK: (222, 111, 161) + COLOR_TICKLE_ME_PINK: (252, 137, 172) + COLOR_TULIP: (255, 135, 141) + COLOR_VANILLA_ICE: (243, 143, 169) + COLOR_VIOLET_RED: (247, 83, 148) + COLOR_VIVID_TANGERINE: (255, 160, 137) + COLOR_WILD_ORCHID: (212, 112, 162) + COLOR_WILD_STRAWBERRY: (255, 67, 164) + COLOR_WILD_WATERMELON: (252, 108, 133) + COLOR_DEEP_FUCHSIA: (193, 84, 193) + COLOR_FRENCH_MAUVE: (212, 115, 212) + COLOR_FUCHSIA_PINK: (255, 119, 255) + COLOR_LAVENDER_MAGENTA: (238, 130, 238) + COLOR_STEEL_PINK: (204, 51, 204) + COLOR_ULTRA_PINK: (255, 111, 255) nervousness: - COLOR_ALABAMA_CRIMSON: '(175, 0, 42)' - COLOR_AUBERGINE: '(59, 9, 16)' - COLOR_BARN_RED: '(124, 10, 2)' - COLOR_BAROSSA: '(68, 1, 45)' - COLOR_BLACK_BEAN: '(61, 12, 2)' - COLOR_BLACK_ROSE: '(103, 3, 45)' - COLOR_BLACKBERRY: '(77, 1, 53)' - COLOR_BORDEAUX: '(92, 1, 32)' - COLOR_BRACKEN: '(74, 42, 4)' - COLOR_BRIGHT_RED: '(177, 0, 0)' - COLOR_BROWN_BRAMBLE: '(89, 40, 4)' - COLOR_BROWN_DERBY: '(73, 38, 21)' - COLOR_BROWN_POD: '(64, 24, 1)' - COLOR_BROWN_TUMBLEWEED: '(55, 41, 14)' - COLOR_BULGARIAN_ROSE: '(72, 6, 7)' - COLOR_BURGUNDY: '(128, 0, 32)' - COLOR_BURNT_MAROON: '(66, 3, 3)' - COLOR_CAB_SAV: '(77, 10, 24)' - COLOR_CARMINE: '(150, 0, 24)' - COLOR_CASTRO: '(82, 0, 31)' - COLOR_CEDAR: '(62, 28, 20)' - COLOR_CEDAR_WOOD_FINISH: '(113, 26, 0)' - COLOR_CHERRYWOOD: '(101, 26, 20)' - COLOR_CIOCCOLATO: '(85, 40, 12)' - COLOR_CLARET: '(127, 23, 52)' - COLOR_CLINKER: '(55, 29, 9)' - COLOR_COCOA_BEAN: '(72, 28, 28)' - COLOR_COLA: '(63, 37, 0)' - COLOR_CRATER_BROWN: '(70, 36, 37)' - COLOR_CRIMSON_GLORY: '(190, 0, 50)' - COLOR_CRIMSON_RED: '(153, 0, 0)' - COLOR_DARK_BURGUNDY: '(119, 15, 5)' - COLOR_DARK_CANDY_APPLE_RED: '(164, 0, 0)' - COLOR_DARK_EBONY: '(60, 32, 5)' - COLOR_DARK_RED: '(139, 0, 0)' - COLOR_DARK_SCARLET: '(86, 3, 25)' - COLOR_DARK_SIENNA: '(60, 20, 20)' - COLOR_DEEP_MAROON: '(130, 0, 0)' - COLOR_DEEP_OAK: '(65, 32, 16)' - COLOR_DEEP_RED: '(133, 1, 1)' - COLOR_ENGINEERING_INTERNATIONAL_ORANGE: '(186, 22, 12)' - COLOR_FALU_RED: '(128, 24, 24)' - COLOR_FRENCH_PUCE: '(78, 22, 9)' - COLOR_GUARDSMAN_RED: '(186, 1, 1)' - COLOR_HEATH: '(84, 16, 18)' - COLOR_INDIAN_TAN: '(77, 30, 1)' - COLOR_JAPANESE_MAPLE: '(120, 1, 9)' - COLOR_JON: '(59, 31, 31)' - COLOR_KENYAN_COPPER: '(124, 28, 5)' - COLOR_LONESTAR: '(109, 1, 1)' - COLOR_MAROON: '(128, 0, 0)' - COLOR_MAROON_OAK: '(82, 12, 23)' - COLOR_MERLOT: '(131, 25, 35)' - COLOR_MILANO_RED: '(184, 17, 4)' - COLOR_MOCCACCINO: '(110, 29, 20)' - COLOR_MONARCH: '(139, 7, 35)' - COLOR_MORDANT_RED: '(174, 12, 0)' - COLOR_MOROCCO_BROWN: '(68, 29, 0)' - COLOR_MULBERRY_WOOD: '(92, 5, 54)' - COLOR_PACO: '(65, 31, 16)' - COLOR_PAPRIKA: '(141, 2, 38)' - COLOR_PERSIAN_PLUM: '(112, 28, 28)' - COLOR_PINK_RASPBERRY: '(152, 0, 54)' - COLOR_POHUTUKAWA: '(143, 2, 28)' - COLOR_REBEL: '(60, 18, 6)' - COLOR_RED_BERRY: '(142, 0, 0)' - COLOR_RED_DEVIL: '(134, 1, 17)' - COLOR_RED_OXIDE: '(110, 9, 2)' - COLOR_ROSEWOOD: '(101, 0, 11)' - COLOR_RUBY_RED: '(155, 17, 30)' - COLOR_RUFOUS: '(168, 28, 7)' - COLOR_RUSTIC_RED: '(72, 4, 4)' - COLOR_SAMBUCA: '(58, 32, 16)' - COLOR_SANGRIA: '(146, 0, 10)' - COLOR_SCARLETT: '(149, 0, 21)' - COLOR_SEAL_BROWN: '(89, 38, 11)' - COLOR_SHIRAZ: '(178, 9, 49)' - COLOR_SIREN: '(122, 1, 58)' - COLOR_SPARTAN_CRIMSON: '(158, 19, 22)' - COLOR_TAMARILLO: '(153, 22, 19)' - COLOR_TAMARIND: '(52, 21, 21)' - COLOR_TEMPTRESS: '(59, 0, 11)' - COLOR_TOLEDO: '(58, 0, 32)' - COLOR_TOTEM_POLE: '(153, 27, 7)' - COLOR_TREEHOUSE: '(59, 40, 32)' - COLOR_TYRIAN_PURPLE: '(102, 2, 60)' - COLOR_UP_MAROON: '(123, 17, 19)' - COLOR_VAN_CLEEF: '(73, 23, 12)' - COLOR_WINE_BERRY: '(89, 29, 53)' - COLOR_ACADIA: '(27, 20, 4)' - COLOR_ASPHALT: '(19, 10, 6)' - COLOR_CANNON_BLACK: '(37, 23, 6)' - COLOR_COFFEE_BEAN: '(42, 20, 14)' - COLOR_CREOLE: '(30, 15, 4)' - COLOR_CROWSHEAD: '(28, 18, 8)' - COLOR_DIESEL: '(19, 0, 0)' - COLOR_ECLIPSE: '(49, 28, 23)' - COLOR_EL_PASO: '(30, 23, 8)' - COLOR_ETERNITY: '(33, 26, 14)' - COLOR_GONDOLA: '(38, 20, 20)' - COLOR_GRAPHITE: '(37, 22, 7)' - COLOR_JACARANDA: '(46, 3, 41)' - COLOR_JACKO_BEAN: '(46, 25, 5)' - COLOR_KARAKA: '(30, 22, 9)' - COLOR_KILAMANJARO: '(36, 12, 2)' - COLOR_LICORICE: '(26, 17, 16)' - COLOR_MELANZANE: '(48, 5, 41)' - COLOR_MIKADO: '(45, 37, 16)' - COLOR_NERO: '(20, 6, 0)' - COLOR_NIGHT_RIDER: '(31, 18, 15)' - COLOR_OIL: '(40, 30, 21)' - COLOR_ONION: '(47, 39, 14)' - COLOR_SEPIA_BLACK: '(43, 2, 2)' - COLOR_SMOKY_BLACK: '(16, 12, 8)' - COLOR_WOOD_BARK: '(38, 17, 5)' - COLOR_WOODRUSH: '(48, 42, 15)' - COLOR_ZEUS: '(41, 35, 25)' - COLOR_ZINNWALDITE_BROWN: '(44, 22, 8)' + COLOR_ALABAMA_CRIMSON: (175, 0, 42) + COLOR_AUBERGINE: (59, 9, 16) + COLOR_BARN_RED: (124, 10, 2) + COLOR_BAROSSA: (68, 1, 45) + COLOR_BLACK_BEAN: (61, 12, 2) + COLOR_BLACK_ROSE: (103, 3, 45) + COLOR_BLACKBERRY: (77, 1, 53) + COLOR_BORDEAUX: (92, 1, 32) + COLOR_BRACKEN: (74, 42, 4) + COLOR_BRIGHT_RED: (177, 0, 0) + COLOR_BROWN_BRAMBLE: (89, 40, 4) + COLOR_BROWN_DERBY: (73, 38, 21) + COLOR_BROWN_POD: (64, 24, 1) + COLOR_BROWN_TUMBLEWEED: (55, 41, 14) + COLOR_BULGARIAN_ROSE: (72, 6, 7) + COLOR_BURGUNDY: (128, 0, 32) + COLOR_BURNT_MAROON: (66, 3, 3) + COLOR_CAB_SAV: (77, 10, 24) + COLOR_CARMINE: (150, 0, 24) + COLOR_CASTRO: (82, 0, 31) + COLOR_CEDAR: (62, 28, 20) + COLOR_CEDAR_WOOD_FINISH: (113, 26, 0) + COLOR_CHERRYWOOD: (101, 26, 20) + COLOR_CIOCCOLATO: (85, 40, 12) + COLOR_CLARET: (127, 23, 52) + COLOR_CLINKER: (55, 29, 9) + COLOR_COCOA_BEAN: (72, 28, 28) + COLOR_COLA: (63, 37, 0) + COLOR_CRATER_BROWN: (70, 36, 37) + COLOR_CRIMSON_GLORY: (190, 0, 50) + COLOR_CRIMSON_RED: (153, 0, 0) + COLOR_DARK_BURGUNDY: (119, 15, 5) + COLOR_DARK_CANDY_APPLE_RED: (164, 0, 0) + COLOR_DARK_EBONY: (60, 32, 5) + COLOR_DARK_RED: (139, 0, 0) + COLOR_DARK_SCARLET: (86, 3, 25) + COLOR_DARK_SIENNA: (60, 20, 20) + COLOR_DEEP_MAROON: (130, 0, 0) + COLOR_DEEP_OAK: (65, 32, 16) + COLOR_DEEP_RED: (133, 1, 1) + COLOR_ENGINEERING_INTERNATIONAL_ORANGE: (186, 22, 12) + COLOR_FALU_RED: (128, 24, 24) + COLOR_FRENCH_PUCE: (78, 22, 9) + COLOR_GUARDSMAN_RED: (186, 1, 1) + COLOR_HEATH: (84, 16, 18) + COLOR_INDIAN_TAN: (77, 30, 1) + COLOR_JAPANESE_MAPLE: (120, 1, 9) + COLOR_JON: (59, 31, 31) + COLOR_KENYAN_COPPER: (124, 28, 5) + COLOR_LONESTAR: (109, 1, 1) + COLOR_MAROON: (128, 0, 0) + COLOR_MAROON_OAK: (82, 12, 23) + COLOR_MERLOT: (131, 25, 35) + COLOR_MILANO_RED: (184, 17, 4) + COLOR_MOCCACCINO: (110, 29, 20) + COLOR_MONARCH: (139, 7, 35) + COLOR_MORDANT_RED: (174, 12, 0) + COLOR_MOROCCO_BROWN: (68, 29, 0) + COLOR_MULBERRY_WOOD: (92, 5, 54) + COLOR_PACO: (65, 31, 16) + COLOR_PAPRIKA: (141, 2, 38) + COLOR_PERSIAN_PLUM: (112, 28, 28) + COLOR_PINK_RASPBERRY: (152, 0, 54) + COLOR_POHUTUKAWA: (143, 2, 28) + COLOR_REBEL: (60, 18, 6) + COLOR_RED_BERRY: (142, 0, 0) + COLOR_RED_DEVIL: (134, 1, 17) + COLOR_RED_OXIDE: (110, 9, 2) + COLOR_ROSEWOOD: (101, 0, 11) + COLOR_RUBY_RED: (155, 17, 30) + COLOR_RUFOUS: (168, 28, 7) + COLOR_RUSTIC_RED: (72, 4, 4) + COLOR_SAMBUCA: (58, 32, 16) + COLOR_SANGRIA: (146, 0, 10) + COLOR_SCARLETT: (149, 0, 21) + COLOR_SEAL_BROWN: (89, 38, 11) + COLOR_SHIRAZ: (178, 9, 49) + COLOR_SIREN: (122, 1, 58) + COLOR_SPARTAN_CRIMSON: (158, 19, 22) + COLOR_TAMARILLO: (153, 22, 19) + COLOR_TAMARIND: (52, 21, 21) + COLOR_TEMPTRESS: (59, 0, 11) + COLOR_TOLEDO: (58, 0, 32) + COLOR_TOTEM_POLE: (153, 27, 7) + COLOR_TREEHOUSE: (59, 40, 32) + COLOR_TYRIAN_PURPLE: (102, 2, 60) + COLOR_UP_MAROON: (123, 17, 19) + COLOR_VAN_CLEEF: (73, 23, 12) + COLOR_WINE_BERRY: (89, 29, 53) + COLOR_ACADIA: (27, 20, 4) + COLOR_ASPHALT: (19, 10, 6) + COLOR_CANNON_BLACK: (37, 23, 6) + COLOR_COFFEE_BEAN: (42, 20, 14) + COLOR_CREOLE: (30, 15, 4) + COLOR_CROWSHEAD: (28, 18, 8) + COLOR_DIESEL: (19, 0, 0) + COLOR_ECLIPSE: (49, 28, 23) + COLOR_EL_PASO: (30, 23, 8) + COLOR_ETERNITY: (33, 26, 14) + COLOR_GONDOLA: (38, 20, 20) + COLOR_GRAPHITE: (37, 22, 7) + COLOR_JACARANDA: (46, 3, 41) + COLOR_JACKO_BEAN: (46, 25, 5) + COLOR_KARAKA: (30, 22, 9) + COLOR_KILAMANJARO: (36, 12, 2) + COLOR_LICORICE: (26, 17, 16) + COLOR_MELANZANE: (48, 5, 41) + COLOR_MIKADO: (45, 37, 16) + COLOR_NERO: (20, 6, 0) + COLOR_NIGHT_RIDER: (31, 18, 15) + COLOR_OIL: (40, 30, 21) + COLOR_ONION: (47, 39, 14) + COLOR_SEPIA_BLACK: (43, 2, 2) + COLOR_SMOKY_BLACK: (16, 12, 8) + COLOR_WOOD_BARK: (38, 17, 5) + COLOR_WOODRUSH: (48, 42, 15) + COLOR_ZEUS: (41, 35, 25) + COLOR_ZINNWALDITE_BROWN: (44, 22, 8) desire: - COLOR_ALIZARIN_CRIMSON: '(227, 38, 54)' - COLOR_BAMBOO: '(218, 99, 4)' - COLOR_BLAZE_ORANGE: '(255, 103, 0)' - COLOR_BURNT_ORANGE: '(204, 85, 0)' - COLOR_COQUELICOT: '(255, 56, 0)' - COLOR_FERRARI_RED: '(255, 40, 0)' - COLOR_GIANTS_ORANGE: '(254, 90, 29)' - COLOR_GRENADIER: '(213, 70, 0)' - COLOR_INTERNATIONAL_ORANGE: '(255, 79, 0)' - COLOR_LIGHT_BRILLIANT_RED: '(254, 46, 46)' - COLOR_ORANGE_RED: '(255, 69, 0)' - COLOR_ORIOLES_ORANGE: '(251, 79, 20)' - COLOR_PANTONE_ORANGE: '(255, 88, 0)' - COLOR_PERMANENT_GERANIUM_LAKE: '(225, 44, 44)' - COLOR_PERSIMMON: '(236, 88, 0)' - COLOR_POMEGRANATE: '(243, 71, 35)' - COLOR_RYB_RED: '(254, 39, 18)' - COLOR_RED_STAGE: '(208, 95, 4)' - COLOR_SCARLET: '(255, 36, 0)' - COLOR_SINOPIA: '(203, 65, 11)' - COLOR_SPANISH_ORANGE: '(232, 97, 0)' - COLOR_TANGELO: '(249, 77, 0)' - COLOR_TENNE: '(205, 87, 0)' - COLOR_TRINIDAD: '(230, 78, 3)' - COLOR_VERMILION: '(217, 56, 30)' - COLOR_VIVID_ORANGE: '(255, 95, 0)' - COLOR_WILLPOWER_ORANGE: '(253, 88, 0)' + COLOR_ALIZARIN_CRIMSON: (227, 38, 54) + COLOR_BAMBOO: (218, 99, 4) + COLOR_BLAZE_ORANGE: (255, 103, 0) + COLOR_BURNT_ORANGE: (204, 85, 0) + COLOR_COQUELICOT: (255, 56, 0) + COLOR_FERRARI_RED: (255, 40, 0) + COLOR_GIANTS_ORANGE: (254, 90, 29) + COLOR_GRENADIER: (213, 70, 0) + COLOR_INTERNATIONAL_ORANGE: (255, 79, 0) + COLOR_LIGHT_BRILLIANT_RED: (254, 46, 46) + COLOR_ORANGE_RED: (255, 69, 0) + COLOR_ORIOLES_ORANGE: (251, 79, 20) + COLOR_PANTONE_ORANGE: (255, 88, 0) + COLOR_PERMANENT_GERANIUM_LAKE: (225, 44, 44) + COLOR_PERSIMMON: (236, 88, 0) + COLOR_POMEGRANATE: (243, 71, 35) + COLOR_RYB_RED: (254, 39, 18) + COLOR_RED_STAGE: (208, 95, 4) + COLOR_SCARLET: (255, 36, 0) + COLOR_SINOPIA: (203, 65, 11) + COLOR_SPANISH_ORANGE: (232, 97, 0) + COLOR_TANGELO: (249, 77, 0) + COLOR_TENNE: (205, 87, 0) + COLOR_TRINIDAD: (230, 78, 3) + COLOR_VERMILION: (217, 56, 30) + COLOR_VIVID_ORANGE: (255, 95, 0) + COLOR_WILLPOWER_ORANGE: (253, 88, 0) remorse: - COLOR_ALLOY_ORANGE: '(196, 98, 16)' - COLOR_ALPINE: '(175, 143, 44)' - COLOR_AMARANTH_PURPLE: '(171, 39, 79)' - COLOR_ANTIQUE_BRONZE: '(102, 93, 30)' - COLOR_ANTIQUE_RUBY: '(132, 27, 45)' - COLOR_APPLE_BLOSSOM: '(175, 77, 67)' - COLOR_AUBURN: '(165, 42, 42)' - COLOR_BIG_DIP_ORUBY: '(156, 37, 66)' - COLOR_BIRCH: '(55, 48, 33)' - COLOR_BISTRE: '(61, 43, 31)' - COLOR_BLACK_MARLIN: '(62, 44, 28)' - COLOR_BOLE: '(121, 68, 59)' - COLOR_BOURBON: '(186, 111, 30)' - COLOR_BRIGHT_MAROON: '(195, 33, 72)' - COLOR_BRONZE_OLIVE: '(78, 66, 12)' - COLOR_BRONZE_YELLOW: '(115, 112, 0)' - COLOR_BRONZETONE: '(77, 64, 15)' - COLOR_BROWN: '(150, 75, 0)' - COLOR_BROWN_RUST: '(175, 89, 62)' - COLOR_BUCCANEER: '(98, 47, 48)' - COLOR_BULL_SHOT: '(134, 77, 30)' - COLOR_BURNT_UMBER: '(138, 51, 36)' - COLOR_BUTTERED_RUM: '(161, 117, 13)' - COLOR_CAFE_NOIR: '(75, 54, 33)' - COLOR_CAFE_ROYALE: '(111, 68, 12)' - COLOR_CAMOUFLAGE: '(60, 57, 16)' - COLOR_CAPE_PALLISER: '(162, 102, 69)' - COLOR_CAPUT_MORTUUM: '(89, 39, 32)' - COLOR_CARDINAL: '(196, 30, 58)' - COLOR_CARNABY_TAN: '(92, 46, 1)' - COLOR_CARNELIAN: '(179, 27, 27)' - COLOR_CATAWBA: '(112, 54, 66)' - COLOR_CHELSEA_GEM: '(158, 83, 2)' - COLOR_CHESTNUT: '(149, 69, 53)' - COLOR_CHINESE_RED: '(170, 56, 30)' - COLOR_CHOCOLATE: '(123, 63, 0)' - COLOR_COCONUT: '(150, 90, 62)' - COLOR_COFFEE: '(111, 78, 55)' - COLOR_COGNAC: '(159, 56, 29)' - COLOR_CONGO_BROWN: '(89, 55, 55)' - COLOR_COPPER: '(184, 115, 51)' - COLOR_COPPER_CANYON: '(126, 58, 21)' - COLOR_COPPER_RUST: '(148, 71, 71)' - COLOR_CORDOVAN: '(137, 63, 69)' - COLOR_CORK: '(64, 41, 29)' - COLOR_CORN_HARVEST: '(139, 107, 11)' - COLOR_COSTA_DEL_SOL: '(97, 93, 48)' - COLOR_COWBOY: '(77, 40, 45)' - COLOR_CRAIL: '(185, 81, 64)' - COLOR_CROWN_OF_THORNS: '(119, 31, 31)' - COLOR_CUMIN: '(146, 67, 33)' - COLOR_DALLAS: '(110, 75, 38)' - COLOR_DARK_BROWN: '(101, 67, 33)' - COLOR_DARK_PASTEL_RED: '(194, 59, 34)' - COLOR_DARK_YELLOW: '(155, 135, 12)' - COLOR_DEEP_BRONZE: '(74, 48, 4)' - COLOR_DEEP_CARMINE: '(169, 32, 62)' - COLOR_DEEP_CHESTNUT: '(185, 78, 72)' - COLOR_DESERT: '(174, 96, 32)' - COLOR_DOGS: '(184, 109, 41)' - COLOR_DONKEY_BROWN: '(102, 76, 40)' - COLOR_EL_SALVA: '(143, 62, 51)' - COLOR_ENGLISH_RED: '(171, 75, 82)' - COLOR_ENGLISH_WALNUT: '(62, 43, 35)' - COLOR_ESPRESSO: '(97, 39, 24)' - COLOR_FIELD_DRAB: '(108, 84, 30)' - COLOR_FIERY_ORANGE: '(179, 82, 19)' - COLOR_FIRE: '(170, 66, 3)' - COLOR_FIREBRICK: '(178, 34, 34)' - COLOR_FRENCH_WINE: '(172, 30, 68)' - COLOR_GAMBOGE_ORANGE: '(153, 102, 0)' - COLOR_GINGER: '(176, 101, 0)' - COLOR_GOLDEN_BROWN: '(153, 101, 21)' - COLOR_GOLDEN_GATE_BRIDGE: '(192, 54, 44)' - COLOR_GRIZZLY: '(136, 88, 24)' - COLOR_HACIENDA: '(152, 129, 27)' - COLOR_HAIRY_HEATH: '(107, 42, 20)' - COLOR_HAWAIIAN_TAN: '(157, 86, 22)' - COLOR_HIMALAYA: '(106, 93, 27)' - COLOR_HORSES: '(84, 61, 55)' - COLOR_HORSES_NECK: '(96, 73, 19)' - COLOR_HOT_TODDY: '(179, 128, 7)' - COLOR_INDOCHINE: '(194, 107, 3)' - COLOR_IRISH_COFFEE: '(95, 61, 38)' - COLOR_IROKO: '(67, 49, 32)' - COLOR_IRONSTONE: '(134, 72, 60)' - COLOR_JAMBALAYA: '(91, 48, 19)' - COLOR_JAPANESE_CARMINE: '(157, 41, 51)' - COLOR_JUDGE_GRAY: '(84, 67, 51)' - COLOR_KOBICHA: '(107, 68, 35)' - COLOR_KORMA: '(143, 75, 14)' - COLOR_KUMERA: '(136, 98, 33)' - COLOR_LIGHT_BROWN: '(181, 101, 29)' - COLOR_LISBON_BROWN: '(66, 57, 33)' - COLOR_LIVID_BROWN: '(77, 40, 46)' - COLOR_LOTUS: '(134, 60, 60)' - COLOR_LUXOR_GOLD: '(167, 136, 44)' - COLOR_MADRAS: '(63, 48, 2)' - COLOR_MAHOGANY: '(192, 64, 0)' - COLOR_MAI_TAI: '(176, 102, 8)' - COLOR_MANDALAY: '(173, 120, 27)' - COLOR_METALLIC_BRONZE: '(73, 55, 27)' - COLOR_METALLIC_COPPER: '(113, 41, 29)' - COLOR_METALLIC_SUNBURST: '(156, 124, 56)' - COLOR_MEXICAN_RED: '(167, 37, 37)' - COLOR_MILLBROOK: '(89, 68, 51)' - COLOR_MOCHA: '(120, 45, 25)' - COLOR_MOJO: '(192, 71, 55)' - COLOR_MONDO: '(74, 60, 48)' - COLOR_MULE_FAWN: '(140, 71, 47)' - COLOR_NIGHT_SHADZ: '(170, 55, 90)' - COLOR_NUTMEG: '(129, 66, 44)' - COLOR_NUTMEG_WOOD_FINISH: '(104, 54, 0)' - COLOR_OLD_BRICK: '(144, 30, 30)' - COLOR_OLD_BURGUNDY: '(67, 48, 46)' - COLOR_OLD_COPPER: '(114, 74, 47)' - COLOR_OLIVE_DRAB_SEVEN: '(60, 52, 31)' - COLOR_OLIVETONE: '(113, 110, 16)' - COLOR_ORANGE_ROUGHY: '(196, 87, 25)' - COLOR_OREGON: '(155, 71, 3)' - COLOR_ORGAN: '(108, 46, 31)' - COLOR_PAARL: '(166, 85, 41)' - COLOR_PALE_CARMINE: '(175, 64, 53)' - COLOR_PEANUT: '(120, 47, 22)' - COLOR_PERU_TAN: '(127, 58, 2)' - COLOR_PESTO: '(124, 118, 49)' - COLOR_PICKLED_BEAN: '(110, 72, 38)' - COLOR_POTTERS_CLAY: '(140, 87, 56)' - COLOR_PRAIRIE_SAND: '(154, 56, 32)' - COLOR_PUEBLO: '(125, 44, 20)' - COLOR_PULLMAN_BROWN: '(100, 65, 23)' - COLOR_PULLMAN_GREEN: '(59, 51, 28)' - COLOR_PUMPKIN_SKIN: '(177, 97, 11)' - COLOR_PUNGA: '(77, 61, 20)' - COLOR_QUINCY: '(98, 63, 45)' - COLOR_RED_BEECH: '(123, 56, 1)' - COLOR_RED_ROBIN: '(128, 52, 31)' - COLOR_REEF_GOLD: '(159, 130, 28)' - COLOR_RENO_SAND: '(168, 101, 21)' - COLOR_RICH_GOLD: '(168, 83, 7)' - COLOR_ROAST_COFFEE: '(112, 66, 65)' - COLOR_ROCK: '(77, 56, 51)' - COLOR_ROCK_SPRAY: '(186, 69, 12)' - COLOR_ROOF_TERRACOTTA: '(166, 47, 32)' - COLOR_ROPE: '(142, 77, 30)' - COLOR_ROSE_VALE: '(171, 78, 82)' - COLOR_ROSE_OF_SHARON: '(191, 85, 0)' - COLOR_RUDDY_BROWN: '(187, 101, 40)' - COLOR_RUSSET: '(128, 70, 27)' - COLOR_RUST: '(183, 65, 14)' - COLOR_RUSTY_NAIL: '(134, 86, 10)' - COLOR_SADDLE: '(76, 48, 36)' - COLOR_SADDLE_BROWN: '(139, 69, 19)' - COLOR_SAND_DUNE: '(150, 113, 23)' - COLOR_SANGUINE_BROWN: '(141, 61, 56)' - COLOR_SEPIA: '(112, 66, 20)' - COLOR_SEPIA_SKIN: '(158, 91, 64)' - COLOR_SHINGLE_FAWN: '(107, 78, 49)' - COLOR_SIENNA: '(136, 45, 23)' - COLOR_SMOKEY_TOPAZ: '(131, 42, 13)' - COLOR_SMOKY_TOPAZ: '(147, 61, 65)' - COLOR_SOLID_PINK: '(137, 56, 67)' - COLOR_SPANISH_BISTRE: '(128, 117, 50)' - COLOR_SPICE: '(106, 68, 46)' - COLOR_SPICY_MUSTARD: '(116, 100, 13)' - COLOR_STILETTO: '(156, 51, 54)' - COLOR_SWEET_BROWN: '(168, 55, 49)' - COLOR_TABASCO: '(160, 39, 18)' - COLOR_TALL_POPPY: '(179, 45, 41)' - COLOR_TAUPE: '(72, 60, 50)' - COLOR_TAWNY_PORT: '(105, 37, 69)' - COLOR_THATCH_GREEN: '(64, 61, 25)' - COLOR_THUNDER: '(51, 41, 47)' - COLOR_THUNDERBIRD: '(192, 43, 24)' - COLOR_TIA_MARIA: '(193, 68, 14)' - COLOR_TOSCA: '(141, 63, 63)' - COLOR_UNIVERSITY_OF_CALIFORNIA_GOLD: '(183, 135, 39)' - COLOR_UPSDELL_RED: '(174, 32, 41)' - COLOR_VAN_DYKE_BROWN: '(102, 66, 40)' - COLOR_VESUVIUS: '(177, 74, 11)' - COLOR_VIVID_AUBURN: '(146, 39, 36)' - COLOR_VIVID_BURGUNDY: '(159, 29, 53)' - COLOR_WALNUT: '(119, 63, 26)' - COLOR_WELL_READ: '(180, 51, 50)' - COLOR_WEST_COAST: '(98, 81, 25)' - COLOR_WINDSOR_TAN: '(167, 85, 2)' - COLOR_WINE: '(114, 47, 55)' - COLOR_WOODY_BROWN: '(72, 49, 49)' - COLOR_YUKON_GOLD: '(123, 102, 8)' - COLOR_APPLE_GREEN: '(141, 182, 0)' - COLOR_ARMY_GREEN: '(75, 83, 32)' - COLOR_AVOCADO: '(86, 130, 3)' - COLOR_CHRISTI: '(103, 167, 18)' - COLOR_CRETE: '(115, 120, 41)' - COLOR_DARK_MOSS_GREEN: '(74, 93, 35)' - COLOR_FERN_FROND: '(101, 114, 32)' - COLOR_LIMEADE: '(111, 157, 2)' - COLOR_PACIFIKA: '(119, 129, 32)' - COLOR_RANGITOTO: '(46, 50, 34)' - COLOR_SARATOGA: '(85, 91, 16)' - COLOR_TRENDY_GREEN: '(124, 136, 26)' - COLOR_VERDUN_GREEN: '(73, 84, 0)' - COLOR_WAIOURU: '(54, 60, 13)' - COLOR_WASABI: '(120, 138, 37)' - COLOR_WOODLAND: '(77, 83, 40)' - COLOR_OLIVE: '(128, 128, 0)' + COLOR_ALLOY_ORANGE: (196, 98, 16) + COLOR_ALPINE: (175, 143, 44) + COLOR_AMARANTH_PURPLE: (171, 39, 79) + COLOR_ANTIQUE_BRONZE: (102, 93, 30) + COLOR_ANTIQUE_RUBY: (132, 27, 45) + COLOR_APPLE_BLOSSOM: (175, 77, 67) + COLOR_AUBURN: (165, 42, 42) + COLOR_BIG_DIP_ORUBY: (156, 37, 66) + COLOR_BIRCH: (55, 48, 33) + COLOR_BISTRE: (61, 43, 31) + COLOR_BLACK_MARLIN: (62, 44, 28) + COLOR_BOLE: (121, 68, 59) + COLOR_BOURBON: (186, 111, 30) + COLOR_BRIGHT_MAROON: (195, 33, 72) + COLOR_BRONZE_OLIVE: (78, 66, 12) + COLOR_BRONZE_YELLOW: (115, 112, 0) + COLOR_BRONZETONE: (77, 64, 15) + COLOR_BROWN: (150, 75, 0) + COLOR_BROWN_RUST: (175, 89, 62) + COLOR_BUCCANEER: (98, 47, 48) + COLOR_BULL_SHOT: (134, 77, 30) + COLOR_BURNT_UMBER: (138, 51, 36) + COLOR_BUTTERED_RUM: (161, 117, 13) + COLOR_CAFE_NOIR: (75, 54, 33) + COLOR_CAFE_ROYALE: (111, 68, 12) + COLOR_CAMOUFLAGE: (60, 57, 16) + COLOR_CAPE_PALLISER: (162, 102, 69) + COLOR_CAPUT_MORTUUM: (89, 39, 32) + COLOR_CARDINAL: (196, 30, 58) + COLOR_CARNABY_TAN: (92, 46, 1) + COLOR_CARNELIAN: (179, 27, 27) + COLOR_CATAWBA: (112, 54, 66) + COLOR_CHELSEA_GEM: (158, 83, 2) + COLOR_CHESTNUT: (149, 69, 53) + COLOR_CHINESE_RED: (170, 56, 30) + COLOR_CHOCOLATE: (123, 63, 0) + COLOR_COCONUT: (150, 90, 62) + COLOR_COFFEE: (111, 78, 55) + COLOR_COGNAC: (159, 56, 29) + COLOR_CONGO_BROWN: (89, 55, 55) + COLOR_COPPER: (184, 115, 51) + COLOR_COPPER_CANYON: (126, 58, 21) + COLOR_COPPER_RUST: (148, 71, 71) + COLOR_CORDOVAN: (137, 63, 69) + COLOR_CORK: (64, 41, 29) + COLOR_CORN_HARVEST: (139, 107, 11) + COLOR_COSTA_DEL_SOL: (97, 93, 48) + COLOR_COWBOY: (77, 40, 45) + COLOR_CRAIL: (185, 81, 64) + COLOR_CROWN_OF_THORNS: (119, 31, 31) + COLOR_CUMIN: (146, 67, 33) + COLOR_DALLAS: (110, 75, 38) + COLOR_DARK_BROWN: (101, 67, 33) + COLOR_DARK_PASTEL_RED: (194, 59, 34) + COLOR_DARK_YELLOW: (155, 135, 12) + COLOR_DEEP_BRONZE: (74, 48, 4) + COLOR_DEEP_CARMINE: (169, 32, 62) + COLOR_DEEP_CHESTNUT: (185, 78, 72) + COLOR_DESERT: (174, 96, 32) + COLOR_DOGS: (184, 109, 41) + COLOR_DONKEY_BROWN: (102, 76, 40) + COLOR_EL_SALVA: (143, 62, 51) + COLOR_ENGLISH_RED: (171, 75, 82) + COLOR_ENGLISH_WALNUT: (62, 43, 35) + COLOR_ESPRESSO: (97, 39, 24) + COLOR_FIELD_DRAB: (108, 84, 30) + COLOR_FIERY_ORANGE: (179, 82, 19) + COLOR_FIRE: (170, 66, 3) + COLOR_FIREBRICK: (178, 34, 34) + COLOR_FRENCH_WINE: (172, 30, 68) + COLOR_GAMBOGE_ORANGE: (153, 102, 0) + COLOR_GINGER: (176, 101, 0) + COLOR_GOLDEN_BROWN: (153, 101, 21) + COLOR_GOLDEN_GATE_BRIDGE: (192, 54, 44) + COLOR_GRIZZLY: (136, 88, 24) + COLOR_HACIENDA: (152, 129, 27) + COLOR_HAIRY_HEATH: (107, 42, 20) + COLOR_HAWAIIAN_TAN: (157, 86, 22) + COLOR_HIMALAYA: (106, 93, 27) + COLOR_HORSES: (84, 61, 55) + COLOR_HORSES_NECK: (96, 73, 19) + COLOR_HOT_TODDY: (179, 128, 7) + COLOR_INDOCHINE: (194, 107, 3) + COLOR_IRISH_COFFEE: (95, 61, 38) + COLOR_IROKO: (67, 49, 32) + COLOR_IRONSTONE: (134, 72, 60) + COLOR_JAMBALAYA: (91, 48, 19) + COLOR_JAPANESE_CARMINE: (157, 41, 51) + COLOR_JUDGE_GRAY: (84, 67, 51) + COLOR_KOBICHA: (107, 68, 35) + COLOR_KORMA: (143, 75, 14) + COLOR_KUMERA: (136, 98, 33) + COLOR_LIGHT_BROWN: (181, 101, 29) + COLOR_LISBON_BROWN: (66, 57, 33) + COLOR_LIVID_BROWN: (77, 40, 46) + COLOR_LOTUS: (134, 60, 60) + COLOR_LUXOR_GOLD: (167, 136, 44) + COLOR_MADRAS: (63, 48, 2) + COLOR_MAHOGANY: (192, 64, 0) + COLOR_MAI_TAI: (176, 102, 8) + COLOR_MANDALAY: (173, 120, 27) + COLOR_METALLIC_BRONZE: (73, 55, 27) + COLOR_METALLIC_COPPER: (113, 41, 29) + COLOR_METALLIC_SUNBURST: (156, 124, 56) + COLOR_MEXICAN_RED: (167, 37, 37) + COLOR_MILLBROOK: (89, 68, 51) + COLOR_MOCHA: (120, 45, 25) + COLOR_MOJO: (192, 71, 55) + COLOR_MONDO: (74, 60, 48) + COLOR_MULE_FAWN: (140, 71, 47) + COLOR_NIGHT_SHADZ: (170, 55, 90) + COLOR_NUTMEG: (129, 66, 44) + COLOR_NUTMEG_WOOD_FINISH: (104, 54, 0) + COLOR_OLD_BRICK: (144, 30, 30) + COLOR_OLD_BURGUNDY: (67, 48, 46) + COLOR_OLD_COPPER: (114, 74, 47) + COLOR_OLIVE_DRAB_SEVEN: (60, 52, 31) + COLOR_OLIVETONE: (113, 110, 16) + COLOR_ORANGE_ROUGHY: (196, 87, 25) + COLOR_OREGON: (155, 71, 3) + COLOR_ORGAN: (108, 46, 31) + COLOR_PAARL: (166, 85, 41) + COLOR_PALE_CARMINE: (175, 64, 53) + COLOR_PEANUT: (120, 47, 22) + COLOR_PERU_TAN: (127, 58, 2) + COLOR_PESTO: (124, 118, 49) + COLOR_PICKLED_BEAN: (110, 72, 38) + COLOR_POTTERS_CLAY: (140, 87, 56) + COLOR_PRAIRIE_SAND: (154, 56, 32) + COLOR_PUEBLO: (125, 44, 20) + COLOR_PULLMAN_BROWN: (100, 65, 23) + COLOR_PULLMAN_GREEN: (59, 51, 28) + COLOR_PUMPKIN_SKIN: (177, 97, 11) + COLOR_PUNGA: (77, 61, 20) + COLOR_QUINCY: (98, 63, 45) + COLOR_RED_BEECH: (123, 56, 1) + COLOR_RED_ROBIN: (128, 52, 31) + COLOR_REEF_GOLD: (159, 130, 28) + COLOR_RENO_SAND: (168, 101, 21) + COLOR_RICH_GOLD: (168, 83, 7) + COLOR_ROAST_COFFEE: (112, 66, 65) + COLOR_ROCK: (77, 56, 51) + COLOR_ROCK_SPRAY: (186, 69, 12) + COLOR_ROOF_TERRACOTTA: (166, 47, 32) + COLOR_ROPE: (142, 77, 30) + COLOR_ROSE_VALE: (171, 78, 82) + COLOR_ROSE_OF_SHARON: (191, 85, 0) + COLOR_RUDDY_BROWN: (187, 101, 40) + COLOR_RUSSET: (128, 70, 27) + COLOR_RUST: (183, 65, 14) + COLOR_RUSTY_NAIL: (134, 86, 10) + COLOR_SADDLE: (76, 48, 36) + COLOR_SADDLE_BROWN: (139, 69, 19) + COLOR_SAND_DUNE: (150, 113, 23) + COLOR_SANGUINE_BROWN: (141, 61, 56) + COLOR_SEPIA: (112, 66, 20) + COLOR_SEPIA_SKIN: (158, 91, 64) + COLOR_SHINGLE_FAWN: (107, 78, 49) + COLOR_SIENNA: (136, 45, 23) + COLOR_SMOKEY_TOPAZ: (131, 42, 13) + COLOR_SMOKY_TOPAZ: (147, 61, 65) + COLOR_SOLID_PINK: (137, 56, 67) + COLOR_SPANISH_BISTRE: (128, 117, 50) + COLOR_SPICE: (106, 68, 46) + COLOR_SPICY_MUSTARD: (116, 100, 13) + COLOR_STILETTO: (156, 51, 54) + COLOR_SWEET_BROWN: (168, 55, 49) + COLOR_TABASCO: (160, 39, 18) + COLOR_TALL_POPPY: (179, 45, 41) + COLOR_TAUPE: (72, 60, 50) + COLOR_TAWNY_PORT: (105, 37, 69) + COLOR_THATCH_GREEN: (64, 61, 25) + COLOR_THUNDER: (51, 41, 47) + COLOR_THUNDERBIRD: (192, 43, 24) + COLOR_TIA_MARIA: (193, 68, 14) + COLOR_TOSCA: (141, 63, 63) + COLOR_UNIVERSITY_OF_CALIFORNIA_GOLD: (183, 135, 39) + COLOR_UPSDELL_RED: (174, 32, 41) + COLOR_VAN_DYKE_BROWN: (102, 66, 40) + COLOR_VESUVIUS: (177, 74, 11) + COLOR_VIVID_AUBURN: (146, 39, 36) + COLOR_VIVID_BURGUNDY: (159, 29, 53) + COLOR_WALNUT: (119, 63, 26) + COLOR_WELL_READ: (180, 51, 50) + COLOR_WEST_COAST: (98, 81, 25) + COLOR_WINDSOR_TAN: (167, 85, 2) + COLOR_WINE: (114, 47, 55) + COLOR_WOODY_BROWN: (72, 49, 49) + COLOR_YUKON_GOLD: (123, 102, 8) + COLOR_APPLE_GREEN: (141, 182, 0) + COLOR_ARMY_GREEN: (75, 83, 32) + COLOR_AVOCADO: (86, 130, 3) + COLOR_CHRISTI: (103, 167, 18) + COLOR_CRETE: (115, 120, 41) + COLOR_DARK_MOSS_GREEN: (74, 93, 35) + COLOR_FERN_FROND: (101, 114, 32) + COLOR_LIMEADE: (111, 157, 2) + COLOR_PACIFIKA: (119, 129, 32) + COLOR_RANGITOTO: (46, 50, 34) + COLOR_SARATOGA: (85, 91, 16) + COLOR_TRENDY_GREEN: (124, 136, 26) + COLOR_VERDUN_GREEN: (73, 84, 0) + COLOR_WAIOURU: (54, 60, 13) + COLOR_WASABI: (120, 138, 37) + COLOR_WOODLAND: (77, 83, 40) + COLOR_OLIVE: (128, 128, 0) annoyance: - COLOR_AMARANTH: '(229, 43, 80)' - COLOR_AZTEC_GOLD: '(195, 153, 83)' - COLOR_BIG_FOOT_FEET: '(232, 142, 90)' - COLOR_BITTERSWEET: '(254, 111, 94)' - COLOR_BITTERSWEET_SHIMMER: '(191, 79, 81)' - COLOR_BRICK_RED: '(203, 65, 84)' - COLOR_BRONZE: '(205, 127, 50)' - COLOR_BURNING_ORANGE: '(255, 112, 52)' - COLOR_BURNT_SIENNA: '(233, 116, 81)' - COLOR_CG_RED: '(224, 60, 49)' - COLOR_CABARET: '(217, 73, 114)' - COLOR_CADMIUM_ORANGE: '(237, 135, 45)' - COLOR_CARMINE_PINK: '(235, 76, 66)' - COLOR_CARNATION: '(249, 90, 97)' - COLOR_CEDAR_CHEST: '(201, 90, 73)' - COLOR_CINNABAR: '(227, 66, 52)' - COLOR_COCOA_BROWN: '(210, 105, 30)' - COLOR_CONGO_PINK: '(248, 131, 121)' - COLOR_CONTESSA: '(198, 114, 107)' - COLOR_COPPER_RED: '(203, 109, 81)' - COLOR_CORAL: '(255, 127, 80)' - COLOR_CORAL_RED: '(255, 64, 64)' - COLOR_CRANBERRY: '(219, 80, 121)' - COLOR_CRAYOLA_ORANGE: '(255, 117, 56)' - COLOR_CRAYOLA_RED: '(238, 32, 77)' - COLOR_CRUSTA: '(253, 123, 51)' - COLOR_DARK_CORAL: '(205, 91, 69)' - COLOR_DARK_SALMON: '(233, 150, 122)' - COLOR_DARK_TERRA_COTTA: '(204, 78, 92)' - COLOR_DEEP_CARMINE_PINK: '(239, 48, 56)' - COLOR_DEEP_CARROT_ORANGE: '(233, 105, 44)' - COLOR_DESIRE: '(234, 60, 83)' - COLOR_DI_SERRIA: '(219, 153, 94)' - COLOR_DINGY_DUNGEON: '(197, 49, 81)' - COLOR_EARTH_YELLOW: '(225, 169, 95)' - COLOR_ENGLISH_VERMILLION: '(204, 71, 75)' - COLOR_FIERY_ROSE: '(255, 84, 112)' - COLOR_FLAME: '(226, 88, 34)' - COLOR_FLAME_PEA: '(218, 91, 56)' - COLOR_FLAMINGO: '(242, 85, 42)' - COLOR_FLUSH_MAHOGANY: '(202, 52, 53)' - COLOR_FRENCH_RASPBERRY: '(199, 44, 72)' - COLOR_FUZZY_WUZZY: '(204, 102, 102)' - COLOR_FUZZY_WUZZY_BROWN: '(196, 86, 85)' - COLOR_IMPERIAL_RED: '(237, 41, 57)' - COLOR_INDIAN_RED: '(205, 92, 92)' - COLOR_INDIAN_YELLOW: '(227, 168, 87)' - COLOR_JAFFA: '(239, 134, 63)' - COLOR_JAPONICA: '(216, 124, 99)' - COLOR_JASPER: '(215, 59, 62)' - COLOR_JELLY_BEAN: '(218, 97, 78)' - COLOR_KOROMIKO: '(255, 189, 95)' - COLOR_LIGHT_CARMINE_PINK: '(230, 103, 113)' - COLOR_LIGHT_SALMON: '(255, 160, 122)' - COLOR_MAGIC_POTION: '(255, 68, 102)' - COLOR_MANDARIN: '(243, 122, 72)' - COLOR_MANDY: '(226, 84, 101)' - COLOR_MANGO_TANGO: '(255, 130, 67)' - COLOR_MEDIUM_VERMILION: '(217, 96, 59)' - COLOR_NEON_CARROT: '(255, 163, 67)' - COLOR_NEON_FUCHSIA: '(254, 65, 100)' - COLOR_OGRE_ODOR: '(253, 82, 64)' - COLOR_ORANGE_SODA: '(250, 91, 61)' - COLOR_OUTRAGEOUS_ORANGE: '(255, 110, 74)' - COLOR_PALE_COPPER: '(218, 138, 103)' - COLOR_PARADISE_PINK: '(230, 62, 98)' - COLOR_PASTEL_RED: '(255, 105, 97)' - COLOR_PERSIAN_ORANGE: '(217, 144, 88)' - COLOR_PERSIAN_RED: '(204, 51, 51)' - COLOR_PERU: '(205, 133, 63)' - COLOR_PINK_ORANGE: '(255, 153, 102)' - COLOR_PIPER: '(201, 99, 35)' - COLOR_POPSTAR: '(190, 79, 98)' - COLOR_PORSCHE: '(234, 174, 105)' - COLOR_PORTLAND_ORANGE: '(255, 90, 54)' - COLOR_PUNCH: '(220, 67, 51)' - COLOR_RADICAL_RED: '(255, 53, 94)' - COLOR_RAJAH: '(251, 171, 96)' - COLOR_RAW_SIENNA: '(214, 138, 89)' - COLOR_RED_DAMASK: '(218, 106, 65)' - COLOR_RED_ORANGE: '(255, 83, 73)' - COLOR_RED_SALSA: '(253, 58, 74)' - COLOR_ROMAN: '(222, 99, 96)' - COLOR_ROTI: '(198, 168, 75)' - COLOR_RUBER: '(206, 70, 118)' - COLOR_RUSTY_RED: '(218, 44, 67)' - COLOR_SALMON: '(250, 128, 114)' - COLOR_SANDY_BROWN: '(244, 164, 96)' - COLOR_SIZZLING_RED: '(255, 56, 85)' - COLOR_SMASHED_PUMPKIN: '(255, 109, 58)' - COLOR_SUNBURNT_CYCLOPS: '(255, 64, 76)' - COLOR_SUNGLO: '(225, 104, 101)' - COLOR_SUNRAY: '(227, 171, 87)' - COLOR_SUNSET_ORANGE: '(253, 94, 83)' - COLOR_TAN_HIDE: '(250, 157, 90)' - COLOR_TANGO_PINK: '(228, 113, 122)' - COLOR_TART_ORANGE: '(251, 77, 70)' - COLOR_TERRA_COTTA: '(226, 114, 91)' - COLOR_TEXAS_ROSE: '(255, 181, 85)' - COLOR_TIGERS_EYE: '(224, 141, 60)' - COLOR_TOMATO: '(255, 99, 71)' - COLOR_TUSSOCK: '(197, 153, 75)' - COLOR_VALENCIA: '(216, 68, 55)' - COLOR_VIVID_RED_TANGELO: '(223, 97, 36)' - COLOR_VIVID_TANGELO: '(240, 116, 39)' - COLOR_VIVID_VERMILION: '(229, 96, 36)' + COLOR_AMARANTH: (229, 43, 80) + COLOR_AZTEC_GOLD: (195, 153, 83) + COLOR_BIG_FOOT_FEET: (232, 142, 90) + COLOR_BITTERSWEET: (254, 111, 94) + COLOR_BITTERSWEET_SHIMMER: (191, 79, 81) + COLOR_BRICK_RED: (203, 65, 84) + COLOR_BRONZE: (205, 127, 50) + COLOR_BURNING_ORANGE: (255, 112, 52) + COLOR_BURNT_SIENNA: (233, 116, 81) + COLOR_CG_RED: (224, 60, 49) + COLOR_CABARET: (217, 73, 114) + COLOR_CADMIUM_ORANGE: (237, 135, 45) + COLOR_CARMINE_PINK: (235, 76, 66) + COLOR_CARNATION: (249, 90, 97) + COLOR_CEDAR_CHEST: (201, 90, 73) + COLOR_CINNABAR: (227, 66, 52) + COLOR_COCOA_BROWN: (210, 105, 30) + COLOR_CONGO_PINK: (248, 131, 121) + COLOR_CONTESSA: (198, 114, 107) + COLOR_COPPER_RED: (203, 109, 81) + COLOR_CORAL: (255, 127, 80) + COLOR_CORAL_RED: (255, 64, 64) + COLOR_CRANBERRY: (219, 80, 121) + COLOR_CRAYOLA_ORANGE: (255, 117, 56) + COLOR_CRAYOLA_RED: (238, 32, 77) + COLOR_CRUSTA: (253, 123, 51) + COLOR_DARK_CORAL: (205, 91, 69) + COLOR_DARK_SALMON: (233, 150, 122) + COLOR_DARK_TERRA_COTTA: (204, 78, 92) + COLOR_DEEP_CARMINE_PINK: (239, 48, 56) + COLOR_DEEP_CARROT_ORANGE: (233, 105, 44) + COLOR_DESIRE: (234, 60, 83) + COLOR_DI_SERRIA: (219, 153, 94) + COLOR_DINGY_DUNGEON: (197, 49, 81) + COLOR_EARTH_YELLOW: (225, 169, 95) + COLOR_ENGLISH_VERMILLION: (204, 71, 75) + COLOR_FIERY_ROSE: (255, 84, 112) + COLOR_FLAME: (226, 88, 34) + COLOR_FLAME_PEA: (218, 91, 56) + COLOR_FLAMINGO: (242, 85, 42) + COLOR_FLUSH_MAHOGANY: (202, 52, 53) + COLOR_FRENCH_RASPBERRY: (199, 44, 72) + COLOR_FUZZY_WUZZY: (204, 102, 102) + COLOR_FUZZY_WUZZY_BROWN: (196, 86, 85) + COLOR_IMPERIAL_RED: (237, 41, 57) + COLOR_INDIAN_RED: (205, 92, 92) + COLOR_INDIAN_YELLOW: (227, 168, 87) + COLOR_JAFFA: (239, 134, 63) + COLOR_JAPONICA: (216, 124, 99) + COLOR_JASPER: (215, 59, 62) + COLOR_JELLY_BEAN: (218, 97, 78) + COLOR_KOROMIKO: (255, 189, 95) + COLOR_LIGHT_CARMINE_PINK: (230, 103, 113) + COLOR_LIGHT_SALMON: (255, 160, 122) + COLOR_MAGIC_POTION: (255, 68, 102) + COLOR_MANDARIN: (243, 122, 72) + COLOR_MANDY: (226, 84, 101) + COLOR_MANGO_TANGO: (255, 130, 67) + COLOR_MEDIUM_VERMILION: (217, 96, 59) + COLOR_NEON_CARROT: (255, 163, 67) + COLOR_NEON_FUCHSIA: (254, 65, 100) + COLOR_OGRE_ODOR: (253, 82, 64) + COLOR_ORANGE_SODA: (250, 91, 61) + COLOR_OUTRAGEOUS_ORANGE: (255, 110, 74) + COLOR_PALE_COPPER: (218, 138, 103) + COLOR_PARADISE_PINK: (230, 62, 98) + COLOR_PASTEL_RED: (255, 105, 97) + COLOR_PERSIAN_ORANGE: (217, 144, 88) + COLOR_PERSIAN_RED: (204, 51, 51) + COLOR_PERU: (205, 133, 63) + COLOR_PINK_ORANGE: (255, 153, 102) + COLOR_PIPER: (201, 99, 35) + COLOR_POPSTAR: (190, 79, 98) + COLOR_PORSCHE: (234, 174, 105) + COLOR_PORTLAND_ORANGE: (255, 90, 54) + COLOR_PUNCH: (220, 67, 51) + COLOR_RADICAL_RED: (255, 53, 94) + COLOR_RAJAH: (251, 171, 96) + COLOR_RAW_SIENNA: (214, 138, 89) + COLOR_RED_DAMASK: (218, 106, 65) + COLOR_RED_ORANGE: (255, 83, 73) + COLOR_RED_SALSA: (253, 58, 74) + COLOR_ROMAN: (222, 99, 96) + COLOR_ROTI: (198, 168, 75) + COLOR_RUBER: (206, 70, 118) + COLOR_RUSTY_RED: (218, 44, 67) + COLOR_SALMON: (250, 128, 114) + COLOR_SANDY_BROWN: (244, 164, 96) + COLOR_SIZZLING_RED: (255, 56, 85) + COLOR_SMASHED_PUMPKIN: (255, 109, 58) + COLOR_SUNBURNT_CYCLOPS: (255, 64, 76) + COLOR_SUNGLO: (225, 104, 101) + COLOR_SUNRAY: (227, 171, 87) + COLOR_SUNSET_ORANGE: (253, 94, 83) + COLOR_TAN_HIDE: (250, 157, 90) + COLOR_TANGO_PINK: (228, 113, 122) + COLOR_TART_ORANGE: (251, 77, 70) + COLOR_TERRA_COTTA: (226, 114, 91) + COLOR_TEXAS_ROSE: (255, 181, 85) + COLOR_TIGERS_EYE: (224, 141, 60) + COLOR_TOMATO: (255, 99, 71) + COLOR_TUSSOCK: (197, 153, 75) + COLOR_VALENCIA: (216, 68, 55) + COLOR_VIVID_RED_TANGELO: (223, 97, 36) + COLOR_VIVID_TANGELO: (240, 116, 39) + COLOR_VIVID_VERMILION: (229, 96, 36) love: - COLOR_AMARANTH_RED: '(211, 33, 45)' - COLOR_AMERICAN_ROSE: '(255, 3, 62)' - COLOR_BOSTON_UNIVERSITY_RED: '(204, 0, 0)' - COLOR_CADMIUM_RED: '(227, 0, 34)' - COLOR_CANDY_APPLE_RED: '(255, 8, 0)' - COLOR_CARMINE_RED: '(255, 0, 56)' - COLOR_CRIMSON: '(220, 20, 60)' - COLOR_ELECTRIC_CRIMSON: '(255, 0, 63)' - COLOR_FIRE_ENGINE_RED: '(206, 32, 41)' - COLOR_HARVARD_CRIMSON: '(201, 0, 22)' - COLOR_KU_CRIMSON: '(232, 0, 13)' - COLOR_LAVA: '(207, 16, 32)' - COLOR_LUST: '(230, 32, 32)' - COLOR_MEDIUM_CANDY_APPLE_RED: '(226, 6, 44)' - COLOR_MONZA: '(199, 3, 30)' - COLOR_MUNSELL_RED: '(242, 0, 60)' - COLOR_NCS_RED: '(196, 2, 51)' - COLOR_PIGMENT_RED: '(237, 28, 36)' - COLOR_RED: '(255, 0, 0)' - COLOR_RED_RIBBON: '(237, 10, 63)' - COLOR_RICH_CARMINE: '(215, 0, 64)' - COLOR_ROSSO_CORSA: '(212, 0, 0)' - COLOR_RUDDY: '(255, 0, 40)' - COLOR_SPANISH_CARMINE: '(209, 0, 71)' - COLOR_SPANISH_RED: '(230, 0, 38)' - COLOR_TRACTOR_RED: '(253, 14, 53)' - COLOR_UTAH_CRIMSON: '(211, 0, 63)' - COLOR_VENETIAN_RED: '(200, 8, 21)' - COLOR_VIVID_CRIMSON: '(204, 0, 51)' - COLOR_VIVID_RED: '(247, 13, 26)' + COLOR_AMARANTH_RED: (211, 33, 45) + COLOR_AMERICAN_ROSE: (255, 3, 62) + COLOR_BOSTON_UNIVERSITY_RED: (204, 0, 0) + COLOR_CADMIUM_RED: (227, 0, 34) + COLOR_CANDY_APPLE_RED: (255, 8, 0) + COLOR_CARMINE_RED: (255, 0, 56) + COLOR_CRIMSON: (220, 20, 60) + COLOR_ELECTRIC_CRIMSON: (255, 0, 63) + COLOR_FIRE_ENGINE_RED: (206, 32, 41) + COLOR_HARVARD_CRIMSON: (201, 0, 22) + COLOR_KU_CRIMSON: (232, 0, 13) + COLOR_LAVA: (207, 16, 32) + COLOR_LUST: (230, 32, 32) + COLOR_MEDIUM_CANDY_APPLE_RED: (226, 6, 44) + COLOR_MONZA: (199, 3, 30) + COLOR_MUNSELL_RED: (242, 0, 60) + COLOR_NCS_RED: (196, 2, 51) + COLOR_PIGMENT_RED: (237, 28, 36) + COLOR_RED: (255, 0, 0) + COLOR_RED_RIBBON: (237, 10, 63) + COLOR_RICH_CARMINE: (215, 0, 64) + COLOR_ROSSO_CORSA: (212, 0, 0) + COLOR_RUDDY: (255, 0, 40) + COLOR_SPANISH_CARMINE: (209, 0, 71) + COLOR_SPANISH_RED: (230, 0, 38) + COLOR_TRACTOR_RED: (253, 14, 53) + COLOR_UTAH_CRIMSON: (211, 0, 63) + COLOR_VENETIAN_RED: (200, 8, 21) + COLOR_VIVID_CRIMSON: (204, 0, 51) + COLOR_VIVID_RED: (247, 13, 26) admiration: - COLOR_AMBER: '(255, 191, 0)' - COLOR_BANANA_YELLOW: '(255, 225, 53)' - COLOR_BARBERRY: '(222, 215, 23)' - COLOR_BIRD_FLOWER: '(212, 205, 22)' - COLOR_BRIGHT_SUN: '(254, 211, 60)' - COLOR_CANDLELIGHT: '(252, 217, 23)' - COLOR_CITRINE: '(228, 208, 10)' - COLOR_CONFETTI: '(233, 215, 90)' - COLOR_CORN: '(231, 191, 5)' - COLOR_CREAM_CAN: '(245, 200, 92)' - COLOR_CYBER_YELLOW: '(255, 211, 0)' - COLOR_DANDELION: '(240, 225, 48)' - COLOR_DEEP_LEMON: '(245, 199, 26)' - COLOR_ENERGY_YELLOW: '(248, 221, 92)' - COLOR_GARGOYLE_GAS: '(255, 223, 70)' - COLOR_GOLDEN: '(255, 215, 0)' - COLOR_GOLDEN_DREAM: '(240, 213, 45)' - COLOR_GOLDEN_POPPY: '(252, 194, 0)' - COLOR_GOLDEN_TAINOI: '(255, 204, 92)' - COLOR_GOLDEN_YELLOW: '(255, 223, 0)' - COLOR_JONQUIL: '(244, 202, 22)' - COLOR_LIGHTNING_YELLOW: '(252, 192, 30)' - COLOR_MIKADO_YELLOW: '(255, 196, 12)' - COLOR_MINION_YELLOW: '(245, 224, 80)' - COLOR_MUNSELL_YELLOW: '(239, 204, 0)' - COLOR_MUSTARD: '(255, 219, 88)' - COLOR_NAPLES_YELLOW: '(250, 218, 94)' - COLOR_PANTONE_YELLOW: '(254, 223, 0)' - COLOR_PERIDOT: '(230, 226, 0)' - COLOR_PORTICA: '(249, 230, 99)' - COLOR_RIPE_LEMON: '(244, 216, 28)' - COLOR_RONCHI: '(236, 197, 78)' - COLOR_SAFETY_YELLOW: '(238, 210, 2)' - COLOR_SAFFRON: '(244, 196, 48)' - COLOR_SAFFRON_MANGO: '(249, 191, 88)' - COLOR_SANDSTORM: '(236, 213, 64)' - COLOR_SCHOOL_BUS_YELLOW: '(255, 216, 0)' - COLOR_SIZZLING_SUNRISE: '(255, 219, 0)' - COLOR_SUNFLOWER: '(228, 212, 34)' - COLOR_SUNGLOW: '(255, 204, 51)' - COLOR_SUPERNOVA: '(255, 201, 1)' - COLOR_TANGERINE_YELLOW: '(255, 204, 0)' - COLOR_TITANIUM_YELLOW: '(238, 230, 0)' - COLOR_TURBO: '(250, 230, 0)' - COLOR_VIVID_YELLOW: '(255, 227, 2)' - COLOR_WATTLE: '(220, 215, 71)' - COLOR_ACID_GREEN: '(176, 191, 26)' - COLOR_ALIEN_ARMPIT: '(132, 222, 2)' - COLOR_ATLANTIS: '(151, 205, 45)' - COLOR_BAHIA: '(165, 203, 12)' - COLOR_BITTER_LEMON: '(202, 224, 13)' - COLOR_CITRUS: '(161, 197, 10)' - COLOR_FUEGO: '(190, 222, 13)' - COLOR_INCH_WORM: '(176, 227, 19)' - COLOR_KEY_LIME_PIE: '(191, 201, 33)' - COLOR_LA_RIOJA: '(179, 193, 16)' - COLOR_LAS_PALMAS: '(198, 230, 16)' - COLOR_LIMERICK: '(157, 194, 9)' - COLOR_PEAR: '(209, 226, 49)' - COLOR_RIO_GRANDE: '(187, 208, 9)' - COLOR_SHEEN_GREEN: '(143, 212, 0)' - COLOR_VIVID_LIME_GREEN: '(166, 214, 8)' + COLOR_AMBER: (255, 191, 0) + COLOR_BANANA_YELLOW: (255, 225, 53) + COLOR_BARBERRY: (222, 215, 23) + COLOR_BIRD_FLOWER: (212, 205, 22) + COLOR_BRIGHT_SUN: (254, 211, 60) + COLOR_CANDLELIGHT: (252, 217, 23) + COLOR_CITRINE: (228, 208, 10) + COLOR_CONFETTI: (233, 215, 90) + COLOR_CORN: (231, 191, 5) + COLOR_CREAM_CAN: (245, 200, 92) + COLOR_CYBER_YELLOW: (255, 211, 0) + COLOR_DANDELION: (240, 225, 48) + COLOR_DEEP_LEMON: (245, 199, 26) + COLOR_ENERGY_YELLOW: (248, 221, 92) + COLOR_GARGOYLE_GAS: (255, 223, 70) + COLOR_GOLDEN: (255, 215, 0) + COLOR_GOLDEN_DREAM: (240, 213, 45) + COLOR_GOLDEN_POPPY: (252, 194, 0) + COLOR_GOLDEN_TAINOI: (255, 204, 92) + COLOR_GOLDEN_YELLOW: (255, 223, 0) + COLOR_JONQUIL: (244, 202, 22) + COLOR_LIGHTNING_YELLOW: (252, 192, 30) + COLOR_MIKADO_YELLOW: (255, 196, 12) + COLOR_MINION_YELLOW: (245, 224, 80) + COLOR_MUNSELL_YELLOW: (239, 204, 0) + COLOR_MUSTARD: (255, 219, 88) + COLOR_NAPLES_YELLOW: (250, 218, 94) + COLOR_PANTONE_YELLOW: (254, 223, 0) + COLOR_PERIDOT: (230, 226, 0) + COLOR_PORTICA: (249, 230, 99) + COLOR_RIPE_LEMON: (244, 216, 28) + COLOR_RONCHI: (236, 197, 78) + COLOR_SAFETY_YELLOW: (238, 210, 2) + COLOR_SAFFRON: (244, 196, 48) + COLOR_SAFFRON_MANGO: (249, 191, 88) + COLOR_SANDSTORM: (236, 213, 64) + COLOR_SCHOOL_BUS_YELLOW: (255, 216, 0) + COLOR_SIZZLING_SUNRISE: (255, 219, 0) + COLOR_SUNFLOWER: (228, 212, 34) + COLOR_SUNGLOW: (255, 204, 51) + COLOR_SUPERNOVA: (255, 201, 1) + COLOR_TANGERINE_YELLOW: (255, 204, 0) + COLOR_TITANIUM_YELLOW: (238, 230, 0) + COLOR_TURBO: (250, 230, 0) + COLOR_VIVID_YELLOW: (255, 227, 2) + COLOR_WATTLE: (220, 215, 71) + COLOR_ACID_GREEN: (176, 191, 26) + COLOR_ALIEN_ARMPIT: (132, 222, 2) + COLOR_ATLANTIS: (151, 205, 45) + COLOR_BAHIA: (165, 203, 12) + COLOR_BITTER_LEMON: (202, 224, 13) + COLOR_CITRUS: (161, 197, 10) + COLOR_FUEGO: (190, 222, 13) + COLOR_INCH_WORM: (176, 227, 19) + COLOR_KEY_LIME_PIE: (191, 201, 33) + COLOR_LA_RIOJA: (179, 193, 16) + COLOR_LAS_PALMAS: (198, 230, 16) + COLOR_LIMERICK: (157, 194, 9) + COLOR_PEAR: (209, 226, 49) + COLOR_RIO_GRANDE: (187, 208, 9) + COLOR_SHEEN_GREEN: (143, 212, 0) + COLOR_VIVID_LIME_GREEN: (166, 214, 8) realization: - COLOR_ANZAC: '(224, 182, 70)' - COLOR_BRIGHT_YELLOW: '(255, 170, 29)' - COLOR_BUDDHA_GOLD: '(193, 160, 4)' - COLOR_BUTTERCUP: '(243, 173, 22)' - COLOR_CALIFORNIA: '(254, 157, 4)' - COLOR_CASABLANCA: '(248, 184, 83)' - COLOR_CHROME_YELLOW: '(255, 167, 0)' - COLOR_DARK_TANGERINE: '(255, 168, 18)' - COLOR_DEEP_SAFFRON: '(255, 153, 51)' - COLOR_EARLS_GREEN: '(201, 185, 59)' - COLOR_FIRE_BUSH: '(232, 153, 40)' - COLOR_FUEL_YELLOW: '(236, 169, 39)' - COLOR_GALLIANO: '(220, 178, 12)' - COLOR_GAMBOGE: '(228, 155, 15)' - COLOR_GOLD_TIPS: '(222, 186, 19)' - COLOR_GOLDENROD: '(218, 165, 32)' - COLOR_HOKEY_POKEY: '(200, 165, 40)' - COLOR_LEMON_CURRY: '(204, 160, 29)' - COLOR_LEMON_GINGER: '(172, 158, 34)' - COLOR_LUCKY: '(175, 159, 28)' - COLOR_MARIGOLD: '(234, 162, 33)' - COLOR_MEAT_BROWN: '(229, 183, 59)' - COLOR_METALLIC_GOLD: '(212, 175, 55)' - COLOR_MY_SIN: '(255, 179, 31)' - COLOR_NUGGET: '(197, 153, 34)' - COLOR_OLD_GOLD: '(207, 181, 59)' - COLOR_ORANGE_PEEL: '(255, 159, 0)' - COLOR_PASTEL_ORANGE: '(255, 179, 71)' - COLOR_RYB_ORANGE: '(251, 153, 2)' - COLOR_SAHARA: '(183, 162, 20)' - COLOR_SATIN_SHEEN_GOLD: '(203, 161, 53)' - COLOR_SEA_BUCKTHORN: '(251, 161, 41)' - COLOR_SELECTIVE_YELLOW: '(255, 186, 0)' - COLOR_SUN: '(251, 172, 19)' - COLOR_SUNSHADE: '(255, 158, 44)' - COLOR_TREE_POPPY: '(252, 156, 29)' - COLOR_TULIP_TREE: '(234, 179, 59)' - COLOR_TURMERIC: '(202, 187, 72)' - COLOR_UCLA_GOLD: '(255, 179, 0)' - COLOR_UROBILIN: '(225, 173, 33)' - COLOR_VIVID_AMBER: '(204, 153, 0)' - COLOR_VIVID_GAMBOGE: '(255, 153, 0)' - COLOR_VIVID_ORANGE_PEEL: '(255, 160, 0)' - COLOR_WEB_ORANGE: '(255, 165, 0)' - COLOR_YELLOW_ORANGE: '(255, 174, 66)' - COLOR_YELLOW_SEA: '(254, 169, 4)' - COLOR_CITRON: '(159, 169, 31)' + COLOR_ANZAC: (224, 182, 70) + COLOR_BRIGHT_YELLOW: (255, 170, 29) + COLOR_BUDDHA_GOLD: (193, 160, 4) + COLOR_BUTTERCUP: (243, 173, 22) + COLOR_CALIFORNIA: (254, 157, 4) + COLOR_CASABLANCA: (248, 184, 83) + COLOR_CHROME_YELLOW: (255, 167, 0) + COLOR_DARK_TANGERINE: (255, 168, 18) + COLOR_DEEP_SAFFRON: (255, 153, 51) + COLOR_EARLS_GREEN: (201, 185, 59) + COLOR_FIRE_BUSH: (232, 153, 40) + COLOR_FUEL_YELLOW: (236, 169, 39) + COLOR_GALLIANO: (220, 178, 12) + COLOR_GAMBOGE: (228, 155, 15) + COLOR_GOLD_TIPS: (222, 186, 19) + COLOR_GOLDENROD: (218, 165, 32) + COLOR_HOKEY_POKEY: (200, 165, 40) + COLOR_LEMON_CURRY: (204, 160, 29) + COLOR_LEMON_GINGER: (172, 158, 34) + COLOR_LUCKY: (175, 159, 28) + COLOR_MARIGOLD: (234, 162, 33) + COLOR_MEAT_BROWN: (229, 183, 59) + COLOR_METALLIC_GOLD: (212, 175, 55) + COLOR_MY_SIN: (255, 179, 31) + COLOR_NUGGET: (197, 153, 34) + COLOR_OLD_GOLD: (207, 181, 59) + COLOR_ORANGE_PEEL: (255, 159, 0) + COLOR_PASTEL_ORANGE: (255, 179, 71) + COLOR_RYB_ORANGE: (251, 153, 2) + COLOR_SAHARA: (183, 162, 20) + COLOR_SATIN_SHEEN_GOLD: (203, 161, 53) + COLOR_SEA_BUCKTHORN: (251, 161, 41) + COLOR_SELECTIVE_YELLOW: (255, 186, 0) + COLOR_SUN: (251, 172, 19) + COLOR_SUNSHADE: (255, 158, 44) + COLOR_TREE_POPPY: (252, 156, 29) + COLOR_TULIP_TREE: (234, 179, 59) + COLOR_TURMERIC: (202, 187, 72) + COLOR_UCLA_GOLD: (255, 179, 0) + COLOR_UROBILIN: (225, 173, 33) + COLOR_VIVID_AMBER: (204, 153, 0) + COLOR_VIVID_GAMBOGE: (255, 153, 0) + COLOR_VIVID_ORANGE_PEEL: (255, 160, 0) + COLOR_WEB_ORANGE: (255, 165, 0) + COLOR_YELLOW_ORANGE: (255, 174, 66) + COLOR_YELLOW_SEA: (254, 169, 4) + COLOR_CITRON: (159, 169, 31) optimism: - COLOR_AUREOLIN: '(253, 238, 0)' - COLOR_BROOM: '(255, 236, 19)' - COLOR_CADMIUM_YELLOW: '(255, 246, 0)' - COLOR_CANARY_YELLOW: '(255, 239, 0)' - COLOR_GORSE: '(255, 241, 79)' - COLOR_ICTERINE: '(252, 247, 94)' - COLOR_LEMON: '(255, 247, 0)' - COLOR_LEMON_YELLOW: '(255, 244, 79)' - COLOR_MAIZE: '(251, 236, 93)' - COLOR_YELLOW_ROSE: '(255, 240, 0)' - COLOR_ARCTIC_LIME: '(208, 255, 20)' - COLOR_CANARY: '(243, 251, 98)' - COLOR_CHARTREUSE: '(223, 255, 0)' - COLOR_ELECTRIC_LIME: '(204, 255, 0)' - COLOR_FRENCH_LIME: '(158, 253, 56)' - COLOR_GOLDEN_FIZZ: '(245, 251, 61)' - COLOR_GREEN_LIZARD: '(167, 244, 50)' - COLOR_GREEN_YELLOW: '(173, 255, 47)' - COLOR_LEMON_GLACIER: '(253, 255, 0)' - COLOR_LEMON_LIME: '(227, 255, 0)' - COLOR_LIME: '(191, 255, 0)' - COLOR_SPRING_BUD: '(167, 252, 0)' - COLOR_SPRING_FROST: '(135, 255, 42)' - COLOR_STARSHIP: '(236, 242, 69)' - COLOR_VOLT: '(206, 255, 0)' - COLOR_DAFFODIL: '(255, 255, 49)' - COLOR_ELECTRIC_YELLOW: '(255, 255, 51)' - COLOR_MAXIMUM_YELLOW: '(250, 250, 55)' - COLOR_RYB_YELLOW: '(254, 254, 51)' - COLOR_UNMELLOW_YELLOW: '(255, 255, 102)' - COLOR_YELLOW: '(255, 255, 0)' + COLOR_AUREOLIN: (253, 238, 0) + COLOR_BROOM: (255, 236, 19) + COLOR_CADMIUM_YELLOW: (255, 246, 0) + COLOR_CANARY_YELLOW: (255, 239, 0) + COLOR_GORSE: (255, 241, 79) + COLOR_ICTERINE: (252, 247, 94) + COLOR_LEMON: (255, 247, 0) + COLOR_LEMON_YELLOW: (255, 244, 79) + COLOR_MAIZE: (251, 236, 93) + COLOR_YELLOW_ROSE: (255, 240, 0) + COLOR_ARCTIC_LIME: (208, 255, 20) + COLOR_CANARY: (243, 251, 98) + COLOR_CHARTREUSE: (223, 255, 0) + COLOR_ELECTRIC_LIME: (204, 255, 0) + COLOR_FRENCH_LIME: (158, 253, 56) + COLOR_GOLDEN_FIZZ: (245, 251, 61) + COLOR_GREEN_LIZARD: (167, 244, 50) + COLOR_GREEN_YELLOW: (173, 255, 47) + COLOR_LEMON_GLACIER: (253, 255, 0) + COLOR_LEMON_LIME: (227, 255, 0) + COLOR_LIME: (191, 255, 0) + COLOR_SPRING_BUD: (167, 252, 0) + COLOR_SPRING_FROST: (135, 255, 42) + COLOR_STARSHIP: (236, 242, 69) + COLOR_VOLT: (206, 255, 0) + COLOR_DAFFODIL: (255, 255, 49) + COLOR_ELECTRIC_YELLOW: (255, 255, 51) + COLOR_MAXIMUM_YELLOW: (250, 250, 55) + COLOR_RYB_YELLOW: (254, 254, 51) + COLOR_UNMELLOW_YELLOW: (255, 255, 102) + COLOR_YELLOW: (255, 255, 0) excitement: - COLOR_BARBIE_PINK: '(224, 33, 138)' - COLOR_BYZANTINE: '(189, 51, 164)' - COLOR_CERISE: '(222, 49, 99)' - COLOR_CERISE_PINK: '(236, 59, 131)' - COLOR_DEBIAN_RED: '(215, 10, 83)' - COLOR_DEEP_CERISE: '(218, 50, 135)' - COLOR_DEEP_PINK: '(255, 20, 147)' - COLOR_DOGWOOD_ROSE: '(215, 24, 104)' - COLOR_FASHION_FUCHSIA: '(244, 0, 161)' - COLOR_FOLLY: '(255, 0, 79)' - COLOR_FRENCH_FUCHSIA: '(253, 63, 146)' - COLOR_FROSTBITE: '(233, 54, 167)' - COLOR_FUCHSIA_PURPLE: '(204, 57, 123)' - COLOR_FUCHSIA_ROSE: '(199, 67, 117)' - COLOR_MAGENTA: '(202, 31, 123)' - COLOR_MAGENTA_PINK: '(204, 51, 139)' - COLOR_MEDIUM_RED_VIOLET: '(187, 51, 133)' - COLOR_MEXICAN_PINK: '(228, 0, 124)' - COLOR_PANTONE_MAGENTA: '(208, 65, 126)' - COLOR_PERSIAN_ROSE: '(254, 40, 162)' - COLOR_PROCESS_MAGENTA: '(255, 0, 144)' - COLOR_RASPBERRY: '(227, 11, 93)' - COLOR_RAZZMATAZZ: '(227, 37, 107)' - COLOR_RED_PURPLE: '(228, 0, 120)' - COLOR_RED_VIOLET: '(199, 21, 133)' - COLOR_ROSE: '(255, 0, 127)' - COLOR_ROYAL_FUCHSIA: '(202, 44, 146)' - COLOR_RUBINE_RED: '(209, 0, 86)' - COLOR_RUBY: '(224, 17, 95)' - COLOR_SASQUATCH_SOCKS: '(255, 70, 129)' - COLOR_SHOCKING_PINK: '(252, 15, 192)' - COLOR_SMITTEN: '(200, 65, 134)' - COLOR_SPANISH_CRIMSON: '(229, 26, 76)' - COLOR_TELEMAGENTA: '(207, 52, 118)' - COLOR_UA_RED: '(217, 0, 76)' - COLOR_VIVID_CERISE: '(218, 29, 129)' - COLOR_VIVID_RASPBERRY: '(255, 0, 108)' - COLOR_WINTER_SKY: '(255, 0, 124)' + COLOR_BARBIE_PINK: (224, 33, 138) + COLOR_BYZANTINE: (189, 51, 164) + COLOR_CERISE: (222, 49, 99) + COLOR_CERISE_PINK: (236, 59, 131) + COLOR_DEBIAN_RED: (215, 10, 83) + COLOR_DEEP_CERISE: (218, 50, 135) + COLOR_DEEP_PINK: (255, 20, 147) + COLOR_DOGWOOD_ROSE: (215, 24, 104) + COLOR_FASHION_FUCHSIA: (244, 0, 161) + COLOR_FOLLY: (255, 0, 79) + COLOR_FRENCH_FUCHSIA: (253, 63, 146) + COLOR_FROSTBITE: (233, 54, 167) + COLOR_FUCHSIA_PURPLE: (204, 57, 123) + COLOR_FUCHSIA_ROSE: (199, 67, 117) + COLOR_MAGENTA: (202, 31, 123) + COLOR_MAGENTA_PINK: (204, 51, 139) + COLOR_MEDIUM_RED_VIOLET: (187, 51, 133) + COLOR_MEXICAN_PINK: (228, 0, 124) + COLOR_PANTONE_MAGENTA: (208, 65, 126) + COLOR_PERSIAN_ROSE: (254, 40, 162) + COLOR_PROCESS_MAGENTA: (255, 0, 144) + COLOR_RASPBERRY: (227, 11, 93) + COLOR_RAZZMATAZZ: (227, 37, 107) + COLOR_RED_PURPLE: (228, 0, 120) + COLOR_RED_VIOLET: (199, 21, 133) + COLOR_ROSE: (255, 0, 127) + COLOR_ROYAL_FUCHSIA: (202, 44, 146) + COLOR_RUBINE_RED: (209, 0, 86) + COLOR_RUBY: (224, 17, 95) + COLOR_SASQUATCH_SOCKS: (255, 70, 129) + COLOR_SHOCKING_PINK: (252, 15, 192) + COLOR_SMITTEN: (200, 65, 134) + COLOR_SPANISH_CRIMSON: (229, 26, 76) + COLOR_TELEMAGENTA: (207, 52, 118) + COLOR_UA_RED: (217, 0, 76) + COLOR_VIVID_CERISE: (218, 29, 129) + COLOR_VIVID_RASPBERRY: (255, 0, 108) + COLOR_WINTER_SKY: (255, 0, 124) amusement: - COLOR_BRANDY_PUNCH: '(205, 132, 41)' - COLOR_CARROT_ORANGE: '(237, 145, 33)' - COLOR_CHILEAN_FIRE: '(247, 119, 3)' - COLOR_CHRISTINE: '(231, 115, 10)' - COLOR_CLEMENTINE: '(233, 110, 0)' - COLOR_DARK_GOLDENROD: '(184, 134, 11)' - COLOR_DARK_ORANGE: '(255, 140, 0)' - COLOR_DIXIE: '(226, 148, 24)' - COLOR_ECSTASY: '(250, 120, 20)' - COLOR_FLAMENCO: '(255, 125, 7)' - COLOR_FULVOUS: '(228, 132, 0)' - COLOR_GEEBUNG: '(209, 143, 27)' - COLOR_GOLD_DROP: '(241, 130, 0)' - COLOR_GOLDEN_BELL: '(226, 137, 19)' - COLOR_HARVEST_GOLD: '(218, 145, 0)' - COLOR_HEAT_WAVE: '(255, 122, 0)' - COLOR_METEOR: '(208, 125, 18)' - COLOR_OCHRE: '(204, 119, 34)' - COLOR_ORANGE: '(255, 127, 0)' - COLOR_PIRATE_GOLD: '(186, 127, 3)' - COLOR_PIZAZZ: '(255, 144, 0)' - COLOR_PIZZA: '(201, 148, 21)' - COLOR_PRINCETON_ORANGE: '(245, 128, 37)' - COLOR_PUMPKIN: '(255, 117, 24)' - COLOR_SAE_ECE_AMBER: '(255, 126, 0)' - COLOR_SAFETY_ORANGE: '(255, 120, 0)' - COLOR_SORBUS: '(253, 124, 7)' - COLOR_TAHITI_GOLD: '(233, 124, 7)' - COLOR_TANGERINE: '(242, 133, 0)' - COLOR_TANGO: '(237, 122, 28)' - COLOR_UNIVERSITY_OF_TENNESSEE_ORANGE: '(247, 127, 0)' - COLOR_WEST_SIDE: '(255, 145, 15)' - COLOR_ZEST: '(229, 132, 27)' + COLOR_BRANDY_PUNCH: (205, 132, 41) + COLOR_CARROT_ORANGE: (237, 145, 33) + COLOR_CHILEAN_FIRE: (247, 119, 3) + COLOR_CHRISTINE: (231, 115, 10) + COLOR_CLEMENTINE: (233, 110, 0) + COLOR_DARK_GOLDENROD: (184, 134, 11) + COLOR_DARK_ORANGE: (255, 140, 0) + COLOR_DIXIE: (226, 148, 24) + COLOR_ECSTASY: (250, 120, 20) + COLOR_FLAMENCO: (255, 125, 7) + COLOR_FULVOUS: (228, 132, 0) + COLOR_GEEBUNG: (209, 143, 27) + COLOR_GOLD_DROP: (241, 130, 0) + COLOR_GOLDEN_BELL: (226, 137, 19) + COLOR_HARVEST_GOLD: (218, 145, 0) + COLOR_HEAT_WAVE: (255, 122, 0) + COLOR_METEOR: (208, 125, 18) + COLOR_OCHRE: (204, 119, 34) + COLOR_ORANGE: (255, 127, 0) + COLOR_PIRATE_GOLD: (186, 127, 3) + COLOR_PIZAZZ: (255, 144, 0) + COLOR_PIZZA: (201, 148, 21) + COLOR_PRINCETON_ORANGE: (245, 128, 37) + COLOR_PUMPKIN: (255, 117, 24) + COLOR_SAE_ECE_AMBER: (255, 126, 0) + COLOR_SAFETY_ORANGE: (255, 120, 0) + COLOR_SORBUS: (253, 124, 7) + COLOR_TAHITI_GOLD: (233, 124, 7) + COLOR_TANGERINE: (242, 133, 0) + COLOR_TANGO: (237, 122, 28) + COLOR_UNIVERSITY_OF_TENNESSEE_ORANGE: (247, 127, 0) + COLOR_WEST_SIDE: (255, 145, 15) + COLOR_ZEST: (229, 132, 27) gratitude: - COLOR_AO: '(0, 128, 0)' - COLOR_BILBAO: '(50, 124, 20)' - COLOR_BLACK_LEATHER_JACKET: '(37, 53, 41)' - COLOR_BRITISH_RACING_GREEN: '(0, 66, 37)' - COLOR_BRUNSWICK_GREEN: '(27, 77, 62)' - COLOR_CADMIUM_GREEN: '(0, 107, 60)' - COLOR_CAL_POLY_GREEN: '(30, 77, 43)' - COLOR_CAMARONE: '(0, 88, 26)' - COLOR_CARDIN_GREEN: '(1, 54, 28)' - COLOR_CASTLETON_GREEN: '(0, 86, 59)' - COLOR_CELTIC: '(22, 50, 34)' - COLOR_CLOVER: '(56, 73, 16)' - COLOR_COUNTY_GREEN: '(1, 55, 26)' - COLOR_CRUSOE: '(0, 72, 22)' - COLOR_DARK_FERN: '(10, 72, 13)' - COLOR_DARK_GREEN: '(1, 50, 32)' - COLOR_DARTMOUTH_GREEN: '(0, 112, 60)' - COLOR_DEEP_GREEN: '(5, 102, 8)' - COLOR_DEEP_TEAL: '(0, 53, 50)' - COLOR_DELL: '(57, 100, 19)' - COLOR_EVERGLADE: '(28, 64, 46)' - COLOR_FOREST_GREEN: '(34, 139, 34)' - COLOR_FUN_GREEN: '(1, 109, 57)' - COLOR_GABLE_GREEN: '(22, 53, 49)' - COLOR_GREEN_HOUSE: '(36, 80, 15)' - COLOR_GREEN_LEAF: '(67, 106, 13)' - COLOR_HEAVY_METAL: '(43, 50, 40)' - COLOR_INDIA_GREEN: '(19, 136, 8)' - COLOR_ISLAMIC_GREEN: '(0, 144, 0)' - COLOR_JAPANESE_LAUREL: '(10, 105, 6)' - COLOR_JEWEL: '(18, 107, 64)' - COLOR_KAITOKE_GREEN: '(0, 70, 32)' - COLOR_KELLY_GREEN: '(76, 187, 23)' - COLOR_LA_PALMA: '(54, 135, 22)' - COLOR_LA_SALLE_GREEN: '(8, 120, 48)' - COLOR_LINCOLN_GREEN: '(25, 89, 5)' - COLOR_MSU_GREEN: '(24, 69, 59)' - COLOR_MALLARD: '(35, 52, 24)' - COLOR_MEDIUM_JUNGLE_GREEN: '(28, 53, 45)' - COLOR_MUGHAL_GREEN: '(48, 96, 48)' - COLOR_NAPIER_GREEN: '(42, 128, 0)' - COLOR_NORTH_TEXAS_GREEN: '(5, 144, 51)' - COLOR_PAKISTAN_GREEN: '(0, 102, 0)' - COLOR_PALM_LEAF: '(25, 51, 14)' - COLOR_PARSLEY: '(19, 79, 25)' - COLOR_PHTHALO_GREEN: '(18, 53, 36)' - COLOR_SACRAMENTO_STATE_GREEN: '(4, 57, 39)' - COLOR_SAN_FELIX: '(11, 98, 7)' - COLOR_SHERWOOD_GREEN: '(2, 64, 44)' - COLOR_SLIMY_GREEN: '(41, 150, 23)' - COLOR_TE_PAPA_GREEN: '(30, 67, 60)' - COLOR_TIMBER_GREEN: '(22, 50, 44)' - COLOR_TURTLE_GREEN: '(42, 56, 11)' - COLOR_UP_FOREST_GREEN: '(1, 68, 33)' - COLOR_VIDA_LOCA: '(84, 144, 25)' - COLOR_ZUCCINI: '(4, 64, 34)' - COLOR_AZTEC: '(13, 28, 25)' - COLOR_BLACK_FOREST: '(11, 19, 4)' - COLOR_BURNHAM: '(0, 46, 32)' - COLOR_BUSH: '(13, 46, 28)' - COLOR_DARK_JUNGLE_GREEN: '(26, 36, 33)' - COLOR_DEEP_FIR: '(0, 41, 0)' - COLOR_DEEP_FOREST_GREEN: '(24, 45, 9)' - COLOR_ENGLISH_HOLLY: '(2, 45, 21)' - COLOR_GORDONS_GREEN: '(11, 17, 7)' - COLOR_GREEN_KELP: '(37, 49, 28)' - COLOR_GREEN_WATERLOO: '(16, 20, 5)' - COLOR_HOLLY: '(1, 29, 19)' - COLOR_LOG_CABIN: '(36, 42, 29)' - COLOR_MARSHLAND: '(11, 15, 8)' - COLOR_MIDNIGHT_MOSS: '(4, 16, 4)' - COLOR_PALM_GREEN: '(9, 35, 15)' - COLOR_PINE_TREE: '(23, 31, 4)' - COLOR_RACING_GREEN: '(12, 25, 17)' - COLOR_RANGOON_GREEN: '(28, 30, 19)' - COLOR_SEAWEED: '(27, 47, 17)' + COLOR_AO: (0, 128, 0) + COLOR_BILBAO: (50, 124, 20) + COLOR_BLACK_LEATHER_JACKET: (37, 53, 41) + COLOR_BRITISH_RACING_GREEN: (0, 66, 37) + COLOR_BRUNSWICK_GREEN: (27, 77, 62) + COLOR_CADMIUM_GREEN: (0, 107, 60) + COLOR_CAL_POLY_GREEN: (30, 77, 43) + COLOR_CAMARONE: (0, 88, 26) + COLOR_CARDIN_GREEN: (1, 54, 28) + COLOR_CASTLETON_GREEN: (0, 86, 59) + COLOR_CELTIC: (22, 50, 34) + COLOR_CLOVER: (56, 73, 16) + COLOR_COUNTY_GREEN: (1, 55, 26) + COLOR_CRUSOE: (0, 72, 22) + COLOR_DARK_FERN: (10, 72, 13) + COLOR_DARK_GREEN: (1, 50, 32) + COLOR_DARTMOUTH_GREEN: (0, 112, 60) + COLOR_DEEP_GREEN: (5, 102, 8) + COLOR_DEEP_TEAL: (0, 53, 50) + COLOR_DELL: (57, 100, 19) + COLOR_EVERGLADE: (28, 64, 46) + COLOR_FOREST_GREEN: (34, 139, 34) + COLOR_FUN_GREEN: (1, 109, 57) + COLOR_GABLE_GREEN: (22, 53, 49) + COLOR_GREEN_HOUSE: (36, 80, 15) + COLOR_GREEN_LEAF: (67, 106, 13) + COLOR_HEAVY_METAL: (43, 50, 40) + COLOR_INDIA_GREEN: (19, 136, 8) + COLOR_ISLAMIC_GREEN: (0, 144, 0) + COLOR_JAPANESE_LAUREL: (10, 105, 6) + COLOR_JEWEL: (18, 107, 64) + COLOR_KAITOKE_GREEN: (0, 70, 32) + COLOR_KELLY_GREEN: (76, 187, 23) + COLOR_LA_PALMA: (54, 135, 22) + COLOR_LA_SALLE_GREEN: (8, 120, 48) + COLOR_LINCOLN_GREEN: (25, 89, 5) + COLOR_MSU_GREEN: (24, 69, 59) + COLOR_MALLARD: (35, 52, 24) + COLOR_MEDIUM_JUNGLE_GREEN: (28, 53, 45) + COLOR_MUGHAL_GREEN: (48, 96, 48) + COLOR_NAPIER_GREEN: (42, 128, 0) + COLOR_NORTH_TEXAS_GREEN: (5, 144, 51) + COLOR_PAKISTAN_GREEN: (0, 102, 0) + COLOR_PALM_LEAF: (25, 51, 14) + COLOR_PARSLEY: (19, 79, 25) + COLOR_PHTHALO_GREEN: (18, 53, 36) + COLOR_SACRAMENTO_STATE_GREEN: (4, 57, 39) + COLOR_SAN_FELIX: (11, 98, 7) + COLOR_SHERWOOD_GREEN: (2, 64, 44) + COLOR_SLIMY_GREEN: (41, 150, 23) + COLOR_TE_PAPA_GREEN: (30, 67, 60) + COLOR_TIMBER_GREEN: (22, 50, 44) + COLOR_TURTLE_GREEN: (42, 56, 11) + COLOR_UP_FOREST_GREEN: (1, 68, 33) + COLOR_VIDA_LOCA: (84, 144, 25) + COLOR_ZUCCINI: (4, 64, 34) + COLOR_AZTEC: (13, 28, 25) + COLOR_BLACK_FOREST: (11, 19, 4) + COLOR_BURNHAM: (0, 46, 32) + COLOR_BUSH: (13, 46, 28) + COLOR_DARK_JUNGLE_GREEN: (26, 36, 33) + COLOR_DEEP_FIR: (0, 41, 0) + COLOR_DEEP_FOREST_GREEN: (24, 45, 9) + COLOR_ENGLISH_HOLLY: (2, 45, 21) + COLOR_GORDONS_GREEN: (11, 17, 7) + COLOR_GREEN_KELP: (37, 49, 28) + COLOR_GREEN_WATERLOO: (16, 20, 5) + COLOR_HOLLY: (1, 29, 19) + COLOR_LOG_CABIN: (36, 42, 29) + COLOR_MARSHLAND: (11, 15, 8) + COLOR_MIDNIGHT_MOSS: (4, 16, 4) + COLOR_PALM_GREEN: (9, 35, 15) + COLOR_PINE_TREE: (23, 31, 4) + COLOR_RACING_GREEN: (12, 25, 17) + COLOR_RANGOON_GREEN: (28, 30, 19) + COLOR_SEAWEED: (27, 47, 17) relief: - COLOR_BRIGHT_GREEN: '(102, 255, 0)' - COLOR_CHLOROPHYLL_GREEN: '(74, 255, 0)' - COLOR_DARK_PASTEL_GREEN: '(3, 192, 60)' - COLOR_GREEN: '(0, 255, 0)' - COLOR_HARLEQUIN: '(63, 255, 0)' - COLOR_HARLEQUIN_GREEN: '(70, 203, 24)' - COLOR_LAWN_GREEN: '(124, 252, 0)' - COLOR_LIME_GREEN: '(50, 205, 50)' - COLOR_MALACHITE: '(11, 218, 81)' - COLOR_NEON_GREEN: '(57, 255, 20)' - COLOR_SPRING_GREEN: '(0, 255, 127)' - COLOR_VIVID_MALACHITE: '(0, 204, 51)' - COLOR_WEB_CHARTREUSE: '(127, 255, 0)' \ No newline at end of file + COLOR_BRIGHT_GREEN: (102, 255, 0) + COLOR_CHLOROPHYLL_GREEN: (74, 255, 0) + COLOR_DARK_PASTEL_GREEN: (3, 192, 60) + COLOR_GREEN: (0, 255, 0) + COLOR_HARLEQUIN: (63, 255, 0) + COLOR_HARLEQUIN_GREEN: (70, 203, 24) + COLOR_LAWN_GREEN: (124, 252, 0) + COLOR_LIME_GREEN: (50, 205, 50) + COLOR_MALACHITE: (11, 218, 81) + COLOR_NEON_GREEN: (57, 255, 20) + COLOR_SPRING_GREEN: (0, 255, 127) + COLOR_VIVID_MALACHITE: (0, 204, 51) + COLOR_WEB_CHARTREUSE: (127, 255, 0) + class: EmotionAnalysis diff --git a/modules/neopixel/neopx/config.yml b/modules/neopixel/neopx/config.yml index 84dcbc3c..2fa78195 100644 --- a/modules/neopixel/neopx/config.yml +++ b/modules/neopixel/neopx/config.yml @@ -1,43 +1,40 @@ neopixel: - enabled: false - path: 'modules.neopixel.neopx.neopx.NeoPx' config: - pin: 12 # Only used for GPIO. GPIO2 and 3 are use for i2c, GPIO10 is used for SPI, GPIO12 is used for GPIO - protocol: 'I2C' # choose between GPIO, I2C and SPI + pin: 12 + protocol: I2C count: 12 - positions: { - 'status1': 0, - 'status2': 1, - 'status3': 2, - 'status4': 3, - 'status5': 4, - 'right' : 5, - 'top_right': 6, - 'top_left' : 7, - 'left' : 8, - 'bottom_left' : 9, - 'bottom_right' : 10, - 'middle': 11, - } - brightness: [ - 0.2, - 0.2, - 0.2, - 0.2, - 0.2, - 1, - 1, - 1, - 1, - 1, - 1, - 1 - ] + positions: + status1: 0 + status2: 1 + status3: 2 + status4: 3 + status5: 4 + right: 5 + top_right: 6 + top_left: 7 + left: 8 + bottom_left: 9 + bottom_right: 10 + middle: 11 + brightness: + - 0.2 + - 0.2 + - 0.2 + - 0.2 + - 0.2 + - 1 + - 1 + - 1 + - 1 + - 1 + - 1 + - 1 dependencies: python: - - colour - - adafruit-circuitpython-seesaw # i2c SUPPORT - - adafruit-blinka # SPI SUPPORT - - adafruit-circuitpython-neopixel-spi # SPI SUPPORT + - colour + - adafruit-circuitpython-seesaw + - adafruit-blinka + - adafruit-circuitpython-neopixel-spi emotion_analysis: enable: false + class: NeoPx diff --git a/modules/network/arduinoserial/config.yml b/modules/network/arduinoserial/config.yml index c1543a5e..2a2a3e27 100644 --- a/modules/network/arduinoserial/config.yml +++ b/modules/network/arduinoserial/config.yml @@ -1,6 +1,5 @@ serial: - enabled: false - path: "modules.network.arduinoserial.arduinoserial.ArduinoSerial" config: - port: '/dev/ttyAMA0' - baudrate: '115200' \ No newline at end of file + port: /dev/ttyAMA0 + baudrate: '115200' + class: ArduinoSerial diff --git a/modules/network/discordbot/README.md b/modules/network/discordbot/README.md new file mode 100644 index 00000000..9a703fbf --- /dev/null +++ b/modules/network/discordbot/README.md @@ -0,0 +1,202 @@ +# DiscordBot Module Documentation + +## Overview + +The `DiscordBot` module hosts a Discord bot that monitors for @mentions and +private messages, answers questions using the `ChatGPT` module, and returns +responses directly to Discord. + +The bot responds to: + +- **@mentions** in any server channel or thread. +- **Private messages (DMs)** — no @mention required. + +When triggered the bot will: + +1. Collect the recent thread history as context (if the message is already + inside a thread). +2. Build a query from the thread context and the user's question. +3. Call `ai_instance.text_completion(query, persona=)` where + the system prompt contains the configured `prompt` text **plus the fetched + text content** of every `knowledge_sources` URL (fetched once at startup). +4. Append the configured `footer` to the AI response (if set). +5. Post the response back to Discord: + - **DM**: reply directly in the DM channel. + - **Existing thread**: reply inside the thread. + - **Regular channel**: create a new thread from the original message. + +## Configuration + +Enable the module by editing `config/discordbot.yml`: + +```yaml +discordbot: + enabled: true # Set to true to activate + path: modules.network.discordbot.DiscordBot + config: + prompt: > + You are a helpful assistant for the modular-biped open-source robotics + project. Answer questions clearly and concisely, using the provided + knowledge sources where relevant. If you are unsure of an answer, say so + and direct the user to the appropriate knowledge source. + # Maximum characters to extract from each knowledge source URL. + max_chars_per_source: 5000 + # Footer appended to every AI response (Markdown supported). + footer: | + *I am a bot and currently in beta.* + *Support the project: https://buymeacoffee.com/makerforgetech* + knowledge_sources: + - https://github.com/makerforgetech/modular-biped/wiki + - https://github.com/makerforgetech/modular-biped/discussions + - https://github.com/makerforgetech/modular-biped + dependencies: + python: + - discord.py + additional: + - https://discord.com/developers/applications +``` + +### Configuration options + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `prompt` | `str` | Built-in default | System prompt passed to the AI as the `persona` for every question. | +| `knowledge_sources` | `list[str]` | `[]` | URLs whose full text content is fetched at startup and embedded in the system prompt. | +| `max_chars_per_source` | `int` | `5000` | Maximum characters extracted from each knowledge-source URL. Increase for more context (higher token usage). | +| `footer` | `str` | `""` | Text appended to every AI response before posting. Markdown is supported. Leave empty to omit. | + +## Dependencies + +After enabling the module, install the required Python package: + +```bash +pip install discord.py +``` + +Or run `./install.sh` if the project installer is configured to read module +dependencies. + +## Environment variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `DISCORD_BOT_TOKEN` | **Yes** | Discord bot token obtained from the Developer Portal. | + +**Never commit your bot token to source control.** + +## Setup + +### 1. Create a Discord application and bot + +1. Open the [Discord Developer Portal](https://discord.com/developers/applications). +2. Click **New Application** and give it a name. +3. Navigate to **Bot** → **Add Bot**. +4. Under **Token**, click **Reset Token** and copy the value. +5. Set the token as an environment variable: + + ```bash + export DISCORD_BOT_TOKEN="your-token-here" + ``` + +### 2. Configure bot permissions (intents) + +In the Developer Portal under **Bot**: + +* Enable **MESSAGE CONTENT INTENT** (required so the bot can read message text). + +### 3. Invite the bot to your server + +1. Navigate to **OAuth2 → URL Generator**. +2. Select the `bot` scope. +3. Under **Bot Permissions** select at minimum: + - **Read Messages / View Channels** + - **Send Messages** + - **Create Public Threads** + - **Send Messages in Threads** +4. Copy the generated URL and open it in a browser to invite the bot. + +### 4. Wire up the ChatGPT module + +The bot requires a `ChatGPT` module instance to be injected into +`DiscordBot.ai_instance` before it can answer questions. Set this in +`main.py` after both modules are loaded: + +```python +discord_module.ai_instance = chatgpt_module +``` + +## Pubsub topics + +### Topics the module subscribes to + +| Topic | Arguments | Description | +|-------|-----------|-------------| +| `discord/send` | `channel_id` (int), `message` (str) | Allows other modules to post a message to a specific channel. | +| `system/exit` | – | Gracefully disconnects the bot when the system shuts down. | + +### Topics the module publishes to + +| Topic | Arguments | Description | +|-------|-----------|-------------| +| `log/*` | `message` (str) | Standard structured logging (via `BaseModule.log`). | + +## Usage examples + +### Asking the bot a question in a server channel + +Simply @mention the bot followed by your question in any channel it can see: + +``` +@YourBot What is the purpose of the modular-biped project? +``` + +The bot will create a thread (or reply in the existing one) with an +AI-generated answer informed by the fetched knowledge-source content. + +### Sending a private message + +Message the bot directly — no @mention is required in a DM conversation. + +### Sending a message from another module + +```python +self.publish('discord/send', channel_id=123456789012345678, message='Hello from the robot!') +``` + +## Integration with the ChatGPT module + +The `DiscordBot` module calls the `ChatGPT` module directly via +`ai_instance.text_completion()`: + +``` +@mention / DM → DiscordBot._on_message + → ai_instance.text_completion(query, persona=system_prompt) + → Discord thread / DM reply (with optional footer) +``` + +The system prompt (`persona`) contains the configured `prompt` text **plus +the full text content** of each `knowledge_sources` URL (fetched once at +startup using `urllib.request`). HTML markup, navigation, scripts, and +styles are stripped so only meaningful article text reaches the model. + +Both modules must be **enabled** in their respective YAML config files for +end-to-end functionality. + +## Security + +* The bot token is read exclusively from the `DISCORD_BOT_TOKEN` environment + variable (or the optional `token` config key as a fallback). It is never + logged or published over pubsub. +* Avoid storing the token in the config YAML file if the file is committed to + source control. +* Knowledge-source URLs are fetched once at startup over HTTPS. Ensure URLs + point to trusted, public resources. + +## Conclusion + +The `DiscordBot` module provides a Discord interface to the modular-biped AI +stack. By configuring a system prompt, a list of knowledge-source URLs (whose +content is automatically fetched and embedded at startup), and an optional +response footer, operators can deploy a bot that answers project-specific +questions in Discord channels and DMs with answers grounded in real project +documentation. diff --git a/modules/network/discordbot/__init__.py b/modules/network/discordbot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/discordbot/config.yml b/modules/network/discordbot/config.yml new file mode 100644 index 00000000..f3723137 --- /dev/null +++ b/modules/network/discordbot/config.yml @@ -0,0 +1,27 @@ +discordbot: + class: DiscordBot + config: + prompt: > + You are a helpful assistant for the modular-biped open-source robotics + project. Answer questions clearly and concisely, using the provided + knowledge sources where relevant. Response MUST BE under 1500 characters. If you are unsure of an answer, say so + and direct the user to the appropriate knowledge source. Reference the knowledge sources by name (e.g. "Knowledge source: Electronics") when citing information and include links where possible. Do not make up answers if you are unsure; instead, say "I don't know" or "I'm not sure" and suggest where the user might find the answer in the knowledge sources. + # Maximum characters to extract from each knowledge source URL. + # Increase for more context (higher token usage) or decrease to save tokens. + max_chars_per_source: 20000 + # Footer appended to every AI response (Markdown supported). + footer: | + *I am a bot and currently in beta.* + *If you want to help support the project, buy me a coffee: https://buymeacoffee.com/makerforgetech* + knowledge_sources: + - https://github.com/makerforgetech/modular-biped/wiki + - https://github.com/makerforgetech/modular-biped/wiki/Files + - https://github.com/makerforgetech/modular-biped/wiki/Electronics + - https://github.com/makerforgetech/modular-biped/wiki/Raspberry-Pi + - https://github.com/makerforgetech/modular-biped/wiki/Arduino + - https://github.com/makerforgetech/modular-biped/wiki/Software + dependencies: + python: + - discord.py + additional: + - https://discord.com/developers/applications diff --git a/modules/network/discordbot/discordbot.py b/modules/network/discordbot/discordbot.py new file mode 100644 index 00000000..2a965775 --- /dev/null +++ b/modules/network/discordbot/discordbot.py @@ -0,0 +1,393 @@ +import asyncio +import os +import threading +import urllib.request +from html.parser import HTMLParser + +import discord +from modules.base_module import BaseModule + +# Discord thread names are limited to 100 characters. We reserve 3 for the +# trailing ellipsis ("...") so the effective content limit is 97 characters. +_MAX_THREAD_NAME_CONTENT = 97 + +# Default character limit applied to each fetched knowledge-source URL. +_DEFAULT_MAX_CHARS_PER_SOURCE = 5000 + + +class _TextExtractor(HTMLParser): + """Minimal HTML-to-text converter for extracting knowledge-source content.""" + + # Tags whose content adds no meaningful text (scripts, navigation, etc.) + _SKIP_TAGS = {"script", "style", "nav", "header", "footer", "aside"} + + def __init__(self): + super().__init__() + self._chunks = [] + self._skip_depth = 0 + + def handle_starttag(self, tag, attrs): + if tag in self._SKIP_TAGS: + self._skip_depth += 1 + + def handle_endtag(self, tag): + if tag in self._SKIP_TAGS and self._skip_depth > 0: + self._skip_depth -= 1 + + def handle_data(self, data): + if self._skip_depth == 0: + text = data.strip() + if text: + self._chunks.append(text) + + def get_text(self): + return " ".join(self._chunks) + + +class DiscordBot(BaseModule): + """ + Discord Bot module. + + Hosts a Discord bot that monitors for mentions and answers questions + using the ChatGPT module directly (via ``ai_instance``) with + GitHub-hosted knowledge sources specified in the config YAML. + + At startup the text content of each ``knowledge_sources`` URL is fetched + and embedded directly into the system prompt so that ChatGPT can draw on + the actual information rather than just seeing bare hyperlinks. Each + source is truncated to ``max_chars_per_source`` characters (default + 5,000) to keep prompt size manageable. + + The bot responds to: + - Private messages (DMs): every message in a DM channel is processed + without requiring the bot to be @mentioned. + - Server channels / threads: the bot must be explicitly @mentioned. + + Any time the bot is triggered it will: + 1. Collect the thread context (if the post is already inside a thread). + 2. Forward the question together with the system prompt (which now + contains the fetched knowledge) to the AI via + ``ai_instance.completion()``. + 3. Post the reply back: + - In a DM: directly in the DM channel. + - In a thread: directly in that thread. + - In a regular channel: create a new thread from the original post. + + Requires the environment variable ``DISCORD_BOT_TOKEN``. + The ``ai_instance`` attribute must be set to a ``ChatGPT`` instance + (e.g. via ``main.py``) before the bot will be able to answer questions. + + Subscribes to: + - ``discord/send`` – allows other modules to send a message to a + specific channel (kwarg: channel_id, message) + - ``system/exit`` – gracefully shuts down the bot + + Publishes to: + - ``log/*`` – standard logging (via BaseModule.log) + + Example config (config/discordbot.yml): + + discordbot: + enabled: true + path: modules.network.discordbot.DiscordBot + config: + prompt: "You are a helpful assistant for the modular-biped project..." + max_chars_per_source: 5000 + footer: "*I am a bot and currently in beta.*" + knowledge_sources: + - https://github.com/makerforgetech/modular-biped/wiki + - https://github.com/makerforgetech/modular-biped/discussions + """ + + def __init__(self, **kwargs): + """ + Initialise the Discord bot. + + :kwarg prompt: System prompt sent to the AI as the ``persona`` argument + for every request. + :kwarg knowledge_sources: List of URLs whose content will be fetched + and embedded in the system prompt. + :kwarg max_chars_per_source: Maximum characters to include from each + fetched URL (default 5,000). + :kwarg footer: Text appended to every AI response before posting to + Discord. Supports Markdown. + :kwarg token: Bot token (falls back to DISCORD_BOT_TOKEN env var). + """ + self.token = os.getenv("DISCORD_BOT_TOKEN", kwargs.get("token", None)) + if not self.token: + raise RuntimeError( + "DiscordBot: No DISCORD_BOT_TOKEN provided. " + "Set it as an environment variable or pass it as a keyword argument." + ) + + self.ai_instance = None # Will be set via set_chatgpt() + + self.prompt = kwargs.get( + "prompt", + "You are a helpful assistant. Answer questions clearly and concisely.", + ) + self.knowledge_sources = kwargs.get("knowledge_sources", []) + self.max_chars_per_source = kwargs.get( + "max_chars_per_source", _DEFAULT_MAX_CHARS_PER_SOURCE + ) + self.footer = kwargs.get("footer", "") + + # Build the system prompt, embedding fetched content from each URL so + # ChatGPT can draw on the actual text rather than bare hyperlinks. + if self.knowledge_sources: + parts = [ + self.prompt, + "\nKnowledge base (use this content to answer questions accurately):", + ] + for url in self.knowledge_sources: + content = self._fetch_knowledge_source_content( + url, self.max_chars_per_source + ) + if content: + parts.append(f"\n### Source: {url}\n{content}") + else: + parts.append(f"\n### Source: {url}\n(Content could not be loaded)") + self.system_prompt = "\n".join(parts) + else: + self.system_prompt = self.prompt + + # Serialise AI requests so that concurrent Discord mentions do not + # interleave their pubsub publish/subscribe calls. + self._ai_lock = threading.Lock() + self._current_response = None + + # Set up Discord gateway intents. + intents = discord.Intents.default() + intents.message_content = True + self._bot = discord.Client(intents=intents) + + # Register event handlers via decorators. + @self._bot.event + async def on_ready(): + self.log(f"Discord bot logged in as {self._bot.user}.") + self.log(f"GPT instance set: {self.ai_instance is not None}") + + @self._bot.event + async def on_message(message): + await self._on_message(message) + + # Run the Discord client in a dedicated background thread so it does + # not block the main system loop. + self._loop = asyncio.new_event_loop() + self._bot_thread = threading.Thread( + target=self._run_bot, + name="DiscordBotLoop", + daemon=True, + ) + self._bot_thread.start() + + # ------------------------------------------------------------------ + # BaseModule interface + # ------------------------------------------------------------------ + + def setup_messaging(self): + """Subscribe to pubsub topics after the messaging service is set.""" + # self.subscribe("ai/response", self.handle_ai_response) + self.subscribe("discord/send", self.handle_send) + self.subscribe("system/exit", self._shutdown_wrapper) + + # ------------------------------------------------------------------ + # Bot lifecycle + # ------------------------------------------------------------------ + + def _run_bot(self): + """Entry point for the background thread running the Discord event loop.""" + asyncio.set_event_loop(self._loop) + self._loop.run_until_complete(self._bot.start(self.token)) + + def _shutdown_wrapper(self, *args, **kwargs): + """Schedule a clean shutdown of the Discord client.""" + self.log("Shutting down Discord bot...", level="info") + asyncio.run_coroutine_threadsafe(self._bot.close(), self._loop) + + # ------------------------------------------------------------------ + # Incoming Discord messages + # ------------------------------------------------------------------ + + async def _on_message(self, message): + """ + Called by discord.py for every message the bot can see. + + Responds to: + - Any message in a private DM channel (no @mention required). + - Messages in servers/threads where the bot is explicitly @mentioned. + Does not respond to its own messages. + """ + # print(f"Received message: {message.content} (mentions: {[m.display_name for m in message.mentions]})") + + if message.author == self._bot.user: + return + + # Private messages: respond to everything without requiring a mention. + is_dm = isinstance(message.channel, discord.DMChannel) + + if not is_dm and self._bot.user not in message.mentions: + return + + self.log(f"Processing message from {message.author}: {message.content}", level="info") + + # Strip @mentions from content (only relevant for channel/thread messages). + content = message.content + if not is_dm: + for mention in message.mentions: + content = content.replace(f"<@{mention.id}>", "") + content = content.replace(f"<@!{mention.id}>", "") + content = content.strip() + + # If the message is already inside a thread, collect recent context. + if isinstance(message.channel, discord.Thread): + thread_lines = [] + async for msg in message.channel.history(limit=20, oldest_first=True): + if msg.id != message.id: + thread_lines.append( + f"{msg.author.display_name}: {msg.content}" + ) + if thread_lines: + context = "\n".join(thread_lines) + full_text = ( + f"Thread context:\n{context}\n\n" + f"Question: {content}" + ) + else: + full_text = f"Question: {content}" + else: + full_text = f"Question: {content}" + + # Offload the synchronous AI call to a thread-pool executor so the + # Discord event loop is not blocked while waiting for the AI. + + # print(f"Full text sent to AI:\n{full_text}") + + loop = asyncio.get_running_loop() + response = await loop.run_in_executor( + None, self._get_ai_response, full_text + ) + + if response: + if self.footer: + response += f"\n\n{self.footer}" + # print (f"AI response:\n{response}") + await self._send_response(message, response) + + # log the timestamp and full content of the message for debugging + self.log(f"Received message at {message.created_at} from {message.author}: {message.content}\n\nFull text sent to AI:\n\n{full_text}\n\nResponse:\n\n{response}\n\nPrompt:{self.system_prompt}", level="file") + + # ------------------------------------------------------------------ + # AI integration (pubsub) + # ------------------------------------------------------------------ + + def _get_ai_response(self, text, prompt=None): + """ + Call the ChatGPT module's completion() method directly. + """ + if prompt is None: + prompt = self.system_prompt + if not self.ai_instance: + self.log("ChatGPT instance not set in DiscordBot.", level="error") + return "[Error: ChatGPT module not available]" + try: + return self.ai_instance.text_completion(text, persona=prompt) + except Exception as e: + self.log(f"Error calling ChatGPT completion: {e}", level="error") + return f"[Error: {e}]" + + @staticmethod + def _fetch_knowledge_source_content(url, max_chars=_DEFAULT_MAX_CHARS_PER_SOURCE): + """ + Fetch *url* and return its text content, stripped of HTML markup. + + The result is truncated to *max_chars* characters. Returns ``None`` + on any network or parsing error so callers can fall back gracefully. + """ + try: + req = urllib.request.Request( + url, + headers={"User-Agent": "Mozilla/5.0 (compatible; DiscordBot/1.0)"}, + ) + with urllib.request.urlopen(req, timeout=10) as resp: + raw = resp.read().decode("utf-8", errors="replace") + extractor = _TextExtractor() + extractor.feed(raw) + # Collapse repeated whitespace for a cleaner prompt. + text = " ".join(extractor.get_text().split()) + if len(text) > max_chars: + text = text[:max_chars] + "..." + return text or None + except Exception: + return None + + def handle_ai_response(self, response): + """ + Receive an AI-generated response via the ``ai/response`` pubsub topic. + + :param response: The text generated by the AI module. + """ + self._current_response = response + + # ------------------------------------------------------------------ + # Sending responses back to Discord + # ------------------------------------------------------------------ + + async def _send_response(self, message, response): + """ + Send *response* back to Discord. + + - DM channels and existing threads: reply directly in the channel. + - Regular server channels: create a new thread from the message so + the conversation stays organised. + """ + if len(response) > 2000: + # split into chunks of 2000 characters and send as multiple messages to avoid Discord limits + for i in range(0, len(response), 2000): + chunk = response[i:i+2000] + await self._send_response(message, chunk) + return + try: + if isinstance(message.channel, (discord.Thread, discord.DMChannel)): + await message.channel.send(response) + else: + thread_name = (message.content[:_MAX_THREAD_NAME_CONTENT] + "...") if len(message.content) > _MAX_THREAD_NAME_CONTENT else message.content + thread_name = thread_name.strip() or "Discussion" + try: + thread = await message.create_thread(name=thread_name) + await thread.send(response) + except discord.HTTPException as e: + # Fallback: reply inline if thread creation fails. + self.log(f"Failed to create thread or send response in thread: {e}", level="error") + try: + await message.reply(response) + except Exception as e2: + self.log(f"Failed to reply inline after thread failure: {e2}", level="error") + except Exception as e: + self.log(f"Failed to send response: {e}", level="error") + + def handle_send(self, channel_id=None, message=None): + """ + Send a message to a Discord channel from another module via pubsub. + + Publish ``discord/send`` with kwargs ``channel_id`` (int) and + ``message`` (str) to use this handler. + """ + if channel_id is None or message is None: + self.log( + "handle_send: channel_id and message are required", + level="warning", + ) + return + asyncio.run_coroutine_threadsafe( + self._send_to_channel(int(channel_id), message), + self._loop, + ) + + async def _send_to_channel(self, channel_id, message): + """Send *message* to the Discord channel identified by *channel_id*.""" + channel = self._bot.get_channel(channel_id) + if channel: + await channel.send(message) + else: + self.log(f"Channel {channel_id} not found", level="warning") diff --git a/modules/network/discordbot/tests/__init__.py b/modules/network/discordbot/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/network/discordbot/tests/test_discordbot.py b/modules/network/discordbot/tests/test_discordbot.py new file mode 100644 index 00000000..f879fc8f --- /dev/null +++ b/modules/network/discordbot/tests/test_discordbot.py @@ -0,0 +1,644 @@ +import asyncio +import io +import sys +import types +import unittest +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +# --------------------------------------------------------------------------- +# Stub out the 'discord' package so tests run without installing it. +# --------------------------------------------------------------------------- +discord_stub = types.ModuleType("discord") + +# Minimal flag-container used for Intents.default() +class _Intents: + message_content = False + + @staticmethod + def default(): + return _Intents() + +# Minimal discord.Thread sentinel +class _Thread: + pass + +# Minimal discord.DMChannel sentinel +class _DMChannel: + pass + +# Minimal discord.HTTPException +class _HTTPException(Exception): + pass + +discord_stub.Intents = _Intents +discord_stub.Thread = _Thread +discord_stub.DMChannel = _DMChannel +discord_stub.HTTPException = _HTTPException + +# discord.Client used inside DiscordBot.__init__ +class _Client: + def __init__(self, intents=None): + self.user = Mock() + self.user.id = 999 + self._events = {} + + def event(self, coro): + """Decorator that records event coroutines.""" + self._events[coro.__name__] = coro + return coro + + def get_channel(self, channel_id): + return None + + async def start(self, token): + pass + + async def close(self): + pass + +discord_stub.Client = _Client + +sys.modules["discord"] = discord_stub + +# --------------------------------------------------------------------------- +# Now import the module under test (with discord already stubbed). +# --------------------------------------------------------------------------- +from modules.network.discordbot.discordbot import DiscordBot, _TextExtractor # noqa: E402 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _mock_urlopen(html_content=b""): + """Return a context-manager mock for urllib.request.urlopen.""" + mock_resp = MagicMock() + mock_resp.__enter__ = Mock(return_value=mock_resp) + mock_resp.__exit__ = Mock(return_value=False) + mock_resp.read.return_value = html_content + return Mock(return_value=mock_resp) + + +def _make_bot(prompt="Test prompt", knowledge_sources=None, token="fake-token", + urlopen_mock=None, footer=""): + """Return a DiscordBot instance with background thread and network suppressed.""" + if urlopen_mock is None: + urlopen_mock = _mock_urlopen(b"") + with patch("threading.Thread") as mock_thread, \ + patch.dict("os.environ", {"DISCORD_BOT_TOKEN": token}), \ + patch("modules.network.discordbot.discordbot.urllib.request.urlopen", urlopen_mock): + mock_thread.return_value = Mock() + bot = DiscordBot( + prompt=prompt, + knowledge_sources=knowledge_sources or [], + footer=footer, + ) + bot._loop = asyncio.new_event_loop() + # Provide a messaging service so log() calls don't raise. + bot._messaging_service = Mock() + return bot + + +def _make_ai_mock(return_value="AI response"): + """Return a mock ChatGPT instance whose text_completion() returns *return_value*.""" + mock_ai = Mock() + mock_ai.text_completion.return_value = return_value + return mock_ai + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +class TestTextExtractor(unittest.TestCase): + """Tests for the HTML-to-text helper.""" + + def test_plain_text_passthrough(self): + ex = _TextExtractor() + ex.feed("

Hello world

") + self.assertEqual(ex.get_text(), "Hello world") + + def test_strips_script_content(self): + ex = _TextExtractor() + ex.feed("

Visible

") + self.assertNotIn("secret", ex.get_text()) + self.assertIn("Visible", ex.get_text()) + + def test_strips_style_content(self): + ex = _TextExtractor() + ex.feed("

Content

") + self.assertNotIn("color", ex.get_text()) + self.assertIn("Content", ex.get_text()) + + def test_strips_nav_content(self): + ex = _TextExtractor() + ex.feed("
Article
") + self.assertNotIn("Menu", ex.get_text()) + self.assertIn("Article", ex.get_text()) + + def test_nested_skip_tags(self): + """Nested skip tags should not expose intermediate content.""" + ex = _TextExtractor() + ex.feed("

after

") + self.assertNotIn("inner", ex.get_text()) + self.assertIn("after", ex.get_text()) + + +class TestDiscordBotInit(unittest.TestCase): + """Tests for __init__ and configuration.""" + + def test_raises_without_token(self): + with patch.dict("os.environ", {}, clear=True), \ + patch("threading.Thread"): + # Remove DISCORD_BOT_TOKEN if it happens to be set in the env. + env = {k: v for k, v in __import__("os").environ.items() + if k != "DISCORD_BOT_TOKEN"} + with patch.dict("os.environ", env, clear=True), \ + patch("threading.Thread"): + with self.assertRaises(RuntimeError): + DiscordBot() + + def test_token_from_env(self): + bot = _make_bot(token="env-token") + self.assertEqual(bot.token, "env-token") + + def test_system_prompt_no_sources(self): + bot = _make_bot(prompt="Hello", knowledge_sources=[]) + self.assertEqual(bot.system_prompt, "Hello") + + def test_system_prompt_with_sources_contains_url(self): + """The URL must appear in the system prompt (as a ### Source header).""" + sources = ["https://example.com/wiki", "https://example.com/repo"] + bot = _make_bot(prompt="Help me", knowledge_sources=sources) + self.assertIn("https://example.com/wiki", bot.system_prompt) + self.assertIn("https://example.com/repo", bot.system_prompt) + self.assertIn("Help me", bot.system_prompt) + + def test_footer_defaults_to_empty(self): + bot = _make_bot() + self.assertEqual(bot.footer, "") + + def test_footer_from_kwarg(self): + bot = _make_bot(footer="*Beta bot*") + self.assertEqual(bot.footer, "*Beta bot*") + + def test_background_thread_started(self): + with patch("threading.Thread") as mock_thread_cls, \ + patch.dict("os.environ", {"DISCORD_BOT_TOKEN": "tok"}), \ + patch("modules.network.discordbot.discordbot.urllib.request.urlopen", + _mock_urlopen(b"")): + mock_thread = Mock() + mock_thread_cls.return_value = mock_thread + DiscordBot(prompt="p") + mock_thread.start.assert_called_once() + + +class TestDiscordBotKnowledgeSources(unittest.TestCase): + """Tests for knowledge-source URL fetching and prompt embedding.""" + + def test_fetched_content_embedded_in_system_prompt(self): + """Text content from a knowledge-source URL must appear in the prompt.""" + html = b"

Wiki article about modular-biped.

" + bot = _make_bot( + prompt="Base prompt", + knowledge_sources=["https://example.com/wiki"], + urlopen_mock=_mock_urlopen(html), + ) + self.assertIn("Wiki article about modular-biped.", bot.system_prompt) + self.assertIn("https://example.com/wiki", bot.system_prompt) + + def test_unavailable_source_shows_fallback_message(self): + """When a URL cannot be fetched the prompt should note that gracefully.""" + failing_urlopen = Mock(side_effect=Exception("network error")) + bot = _make_bot( + prompt="Base prompt", + knowledge_sources=["https://example.com/wiki"], + urlopen_mock=failing_urlopen, + ) + self.assertIn("https://example.com/wiki", bot.system_prompt) + self.assertIn("could not be loaded", bot.system_prompt) + + def test_content_truncated_to_max_chars(self): + """Fetched content must be truncated to max_chars_per_source.""" + long_text = ("word " * 2000).encode() # ~10,000 chars + html = b"

" + long_text + b"

" + with patch("threading.Thread"), \ + patch.dict("os.environ", {"DISCORD_BOT_TOKEN": "tok"}), \ + patch("modules.network.discordbot.discordbot.urllib.request.urlopen", + _mock_urlopen(html)): + bot = DiscordBot( + prompt="p", + knowledge_sources=["https://example.com"], + max_chars_per_source=200, + ) + # The embedded content section should not exceed 200 chars (plus "..."). + source_section = bot.system_prompt.split("### Source:")[-1] + self.assertLessEqual(len(source_section.strip()), 300) + self.assertTrue(source_section.strip().endswith("...")) + + def test_navigation_stripped_from_fetched_content(self): + """