Siyu's Double Transducer
Ire's Double Transducer
Niki's Double Transducer.
LCD display shown
Close up of the magnetometer
Driver of the linear actuator shown
The NeoPixel LED and color sensor is soldered onto a proto board, facing each other
Ires Double Transducer Demo - Magnet drives color output which changes the position of the linear actuator.
Siyu's Double Transducer video. The magnet drive a large range of color which effect the motor a lot.
Niki's double transducer video. In this video, I move the magnet in varying distances towards and away from the magnetometer inducing the LED pixel to change colors. The linear actuator retracts and extends based on my movement with the magnet (which drives the emitted color of the LED pixel).
This device takes magnetic force and turns it into physical force. A sensor reads the magnetic strength and displays this as a light color (red to blue). Also a color sensor reads the RGB values of the LED and transfer the data into hue value. An arm with a spring moves based on the light color. The arm can also be directly controlled by the magnetic using a button. A screen shows the current magnetic strength, color, and arm position values.
Trial and error in mapping out range of magnetometer values
Unit testing the linear actuator
Testing the linear actuator (output) with the color sensor and NeoPixel LED (input)
Lighting the NeoPixel LED with the color sensor attached.
First assembly of all three boards of all members
Physically arranging board (with labels) and assembling final components
We found certain principles/practices to be helpful throughout the entire process. Firstly, unit testing. This was a useful debugging tool that served as a sanity check and gave us clarity as to which parts were not working or if the code itself was faulty. A component that we often overlook is the sometimes unreliable nature of the hardware. For example, the color sensor itself was not sensitive enough to pick up the exact RBG output of the Neopixel LED, so we had to account for the difference in RBG values by mapping them to hue values to create a reliable range of values.
Another important idea was simplification. This came up, especially in trying to obtain reliable readings from the extremely sensitive magnetometer and magnet. We spent a significant amount of time considering how to even read the 3 axes values read from the magnetometer. Not only did it read across the 3 axes but the polarity of the magnet meant we would get negative or positive values depending on which way it faced. As a result, we took the simplest approach and only considered the x axis as well as only read positive polarity. This meant we needed our magnet in a specific orientation and direction to get reliable readings. However, this trial and error process allowed us to calibrate our magnet and our system, labelling the direction we wanted with tape and getting fairly reliable readings as the magnet moved farther or closer to the magnetometer
We also had to work around issues a lot of the time. For example, we found that the light sensor being so close to the led seemed to overload the sensor a bit. As a result, we opted to place a piece of paper/duct tape between the sensor and the led. This dampened the brightness of the light, lead to a better spread of the color and had significant improvements in the accuracy of the readings from the color sensor. Additionally, toward the end of the project when we were testing the LCD monitor, we found that one of them was faulty and had to account for the delay in obtaining new hardware.
It also took some time to get the LCD screen to display the correct values. Initially, the input and output didn't match well.We later discovered some inconsistencies in variable naming in the code. The original hue was mixed with color sensor hue. Organizing the variable names properly at the begining of building up the code is essential.
Overall, although things took longer than we expected, we learned a lot from the entire experience.
/*
Project 1: Double Transducer
60-223 Intro to Physical Computing
Niki Wang, Siyu Ou, Ire Alarape
A double transudcer that takes in magnetic force as input, converting it to a color on the hue range, and then reads the hue color to drive a linear actuator that has a spring on the end. This changes the amount of force exerted on the other side.
lightLED code from Robert Zacharias demo
RGB to hue adapted from https://stackoverflow.com/questions/23090019/fastest-formula-to-get-hue-from-rgb
Linear actuator control from https://www.firgelliauto.com/blogs/tutorials/how-do-you-control-a-linear-actuator-with-an-arduino
Pin mapping:
Arduino pin | role | description
-------------------------------------
A0 output LED strip data
SCL input QMC5883L SCL (I2C)
SDA input QMC5883L SDA (I2C)
4 input Mode select button (pull-up)
6 output Linear actuator input 1
5 output Linear actuator input 2
*/
#include <Wire.h>
#include <QMC5883LCompass.h>
#include <PololuLedStrip.h>
#include <Arduino_APDS9960.h>
#include <LiquidCrystal_I2C.h>
// PIN ASSIGNMENTS
const int LEDSTRIPPIN = A0,
INPUT_1 = 6,
INPUT_2 = 5,
BUTTON_PIN = 4;
// LED STRIP SETTINGS
const int NUMLEDS = 1;
// SENSOR RANGES
struct Range {
int min;
int max;
};
const Range MAGNET_RANGE = { -1000, -450 };
const Range HUE_RANGE = { 10, 240 };
const Range ACTUATOR_RANGE = { 0, 100 };
const Range LCD_RANGE = { 0, 99 };
const Range COLOR_SENSOR_RANGE = { 0, 4097 };
const Range RGB_RANGE = { 0, 255 };
// ACTUATOR SETTINGS
const float SPEED_MM_PER_SEC = 50.0;
const float MAX_STROKE_MM = 400.0;
const unsigned long FULL_EXTEND_MS = (MAX_STROKE_MM / SPEED_MM_PER_SEC) * 1000;
// GLOBAL VARIABLES
struct State {
// Magnet readings
int magnet_x, magnet_y, magnet_z;
float magnet_field_input; // magnet input
float magnet_hue_output; // magnet output
// Color sensor readings
int cs_r, cs_g, cs_b;
int cs_hue_input;
// Actuator state
float actuator_target_pos;
float actuator_current_pos;
// LCD display values
int magnet_field_input_lcd;
int magnet_hue_output_lcd;
int cs_hue_input_lcd;
int actuator_target_pos_lcd;
// Button state
int button_state;
} state;
// Global objects
LiquidCrystal_I2C screen(0x27, 16, 2);
QMC5883LCompass compass;
PololuLedStrip<LEDSTRIPPIN> ledStrip;
rgb_color colors[NUMLEDS];
/////////////////////////////////////////////////////////////////////////////////////
// MAIN ROUTINE
/////////////////////////////////////////////////////////////////////////////////////
void setup() {
Serial.begin(9600);
setupMagnetometer();
setupColorSensor();
setupLinearActuator();
setupButton();
setupLCD();
Serial.println("All Systems setup");
}
void loop() {
readInputs();
updateInternalState();
driveOutputs();
reportBack();
delay(100);
}
void readInputs() {
// Read Magnetometer
compass.read();
state.magnet_x = compass.getX();
state.magnet_y = compass.getY();
state.magnet_z = compass.getZ();
// Read Color Sensor
while (!APDS.colorAvailable()) {
delay(5);
}
APDS.readColor(state.cs_r, state.cs_g, state.cs_b);
// Read button
state.button_state = digitalRead(BUTTON_PIN);
}
void updateInternalState() {
// Process magnet input
state.magnet_field_input = constrain(state.magnet_x, MAGNET_RANGE.min, MAGNET_RANGE.max);
if (state.button_state == HIGH) {
// Convert magnet input to hue
state.magnet_hue_output = constrainAndMap(state.magnet_field_input,
MAGNET_RANGE.min, MAGNET_RANGE.max,
HUE_RANGE.min, HUE_RANGE.max);
// Process color sensor data
int mapped_cs_r = map(state.cs_r, COLOR_SENSOR_RANGE.min, COLOR_SENSOR_RANGE.max, RGB_RANGE.min, RGB_RANGE.max);
int mapped_cs_g = map(state.cs_g, COLOR_SENSOR_RANGE.min, COLOR_SENSOR_RANGE.max, RGB_RANGE.min, RGB_RANGE.max);
int mapped_cs_b = map(state.cs_b, COLOR_SENSOR_RANGE.min, COLOR_SENSOR_RANGE.max, RGB_RANGE.min, RGB_RANGE.max);
state.cs_hue_input = rgbToHue(mapped_cs_r, mapped_cs_g, mapped_cs_b);
// Map color to actuator position
state.actuator_target_pos = constrainAndMap(state.cs_hue_input,
HUE_RANGE.min, HUE_RANGE.max,
ACTUATOR_RANGE.min, ACTUATOR_RANGE.max);
// Reverse actuator position so more red = farther out
state.actuator_target_pos = constrainAndMap(state.actuator_target_pos,
ACTUATOR_RANGE.min, ACTUATOR_RANGE.max,
ACTUATOR_RANGE.max, ACTUATOR_RANGE.min);
} else {
Serial.println("Button Pressed");
// Magnetic force direct to actuator control
state.actuator_target_pos = constrainAndMap(state.magnet_field_input,
MAGNET_RANGE.min, MAGNET_RANGE.max,
ACTUATOR_RANGE.min, ACTUATOR_RANGE.max);
state.actuator_target_pos = constrainAndMap(state.actuator_target_pos,
ACTUATOR_RANGE.min, ACTUATOR_RANGE.max,
ACTUATOR_RANGE.max, ACTUATOR_RANGE.min);
// Turn off LED
state.magnet_hue_output = 0;
}
updateLCDValues();
}
void driveOutputs() {
writeLCD();
lightLED(state.magnet_hue_output);
moveToPosition(state.actuator_target_pos);
}
void reportBack() {
Serial.print("Magnetic Force: ");
Serial.print(state.magnet_field_input);
Serial.print(" Magnet Hue: ");
Serial.print(state.magnet_hue_output);
Serial.print(" Color Sensor Hue = ");
Serial.print(state.cs_hue_input);
Serial.print(" Actuator Position ");
Serial.println(state.actuator_target_pos);
}
/////////////////////////////////////////////////////////////////////////////////////
// STATE UPDATE HELPERS
/////////////////////////////////////////////////////////////////////////////////////
void updateLCDValues() {
state.magnet_field_input_lcd = constrainAndMap(state.magnet_field_input,
MAGNET_RANGE.min, MAGNET_RANGE.max,
LCD_RANGE.min, LCD_RANGE.max);
state.magnet_hue_output_lcd = constrainAndMap(state.magnet_hue_output,
HUE_RANGE.min, HUE_RANGE.max,
LCD_RANGE.min, LCD_RANGE.max);
state.cs_hue_input_lcd = constrainAndMap(state.cs_hue_input,
HUE_RANGE.min, HUE_RANGE.max,
LCD_RANGE.min, LCD_RANGE.max);
state.actuator_target_pos_lcd = constrainAndMap(state.actuator_target_pos,
ACTUATOR_RANGE.min, ACTUATOR_RANGE.max,
LCD_RANGE.min, LCD_RANGE.max);
}
/////////////////////////////////////////////////////////////////////////////////////
// OUTPUT HELPERS
/////////////////////////////////////////////////////////////////////////////////////
void writeLCD() {
screen.clear();
screen.home();
screen.print("i:");
screen.print(state.magnet_field_input_lcd);
screen.setCursor(6, 0);
screen.print("m:");
screen.print(state.magnet_hue_output_lcd);
screen.setCursor(8, 1);
screen.print(state.cs_hue_input_lcd);
screen.setCursor(12, 1);
screen.print("o:");
screen.print(state.actuator_target_pos_lcd);
}
/////////////////////////////////////////////////////////////////////////////////////
// SETUP HELPERS
/////////////////////////////////////////////////////////////////////////////////////
void setupMagnetometer() {
Wire.begin();
compass.init();
Serial.println("Magnetometer setup");
}
void setupColorSensor() {
while (!APDS.begin()) {
Serial.println("Error initializing APDS-9960 sensor.");
}
Serial.println("Color Sensor setup");
}
void setupLinearActuator() {
pinMode(INPUT_1, OUTPUT);
pinMode(INPUT_2, OUTPUT);
fullRetract();
Serial.println("Linear Actuator setup");
}
void setupButton() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println("Button setup");
}
void setupLCD() {
screen.init();
screen.backlight();
screen.home();
Serial.println("LCD setup");
}
/////////////////////////////////////////////////////////////////////////////////////
// UTILITY HELPERS
/////////////////////////////////////////////////////////////////////////////////////
// Constrains val to input range (input_min, input_max) then maps it to output range
float constrainAndMap(float val, float input_min, float input_max, float output_min, float output_max) {
val = constrain(val, input_min, input_max);
return map(val, input_min, input_max, output_min, output_max);
}
/////////////////////////////////////////////////////////////////////////////////////
// COLOR/LIGHT HELPERS
/////////////////////////////////////////////////////////////////////////////////////
void lightLED(int hue_val) {
if (hue_val == 0) {
turnOffLED();
return;
}
rgb_color ledColor = hueToRGB(hue_val);
for (int i = 0; i < NUMLEDS; i++) {
colors[i] = ledColor;
}
ledStrip.write(colors, NUMLEDS);
}
void turnOffLED() {
rgb_color offColor = rgb_color(0, 0, 0);
for (int i = 0; i < NUMLEDS; i++) {
colors[i] = offColor;
}
ledStrip.write(colors, NUMLEDS);
}
float rgbToHue(int r, int g, int b) {
float rf = r / 255.0, gf = g / 255.0, bf = b / 255.0;
float maxVal = max(rf, max(gf, bf));
float minVal = min(rf, min(gf, bf));
float hue = 0;
float delta = maxVal - minVal;
if (delta == 0) {
hue = 0;
} else if (maxVal == rf) {
hue = 60 * fmod(((gf - bf) / delta), 6);
} else if (maxVal == gf) {
hue = 60 * (((bf - rf) / delta) + 2);
} else if (maxVal == bf) {
hue = 60 * (((rf - gf) / delta) + 4);
}
if (hue < 0) {
hue += 360;
}
if (hue >= 340 || hue < 160) {
hue = map(hue >= 340 ? (int)hue - 360 : (int)hue, -20, 160, 0, 120);
} else if (hue >= 160 && hue < 220) {
hue = map((int)hue, 160, 220, 120, 240);
} else if (hue >= 220 && hue < 240) {
hue = 240;
} else if (hue >= 260 && hue < 340) {
hue = 0;
}
return hue;
}
rgb_color hueToRGB(float hue) {
float h = hue / 60.0;
float c = 255;
float x = c * (1 - abs(fmod(h, 2) - 1));
float r = 0, g = 0, b = 0;
if (h >= 0 && h < 1) {
r = c;
g = x;
b = 0;
} else if (h >= 1 && h < 2) {
r = x;
g = c;
b = 0;
} else if (h >= 2 && h < 3) {
r = 0;
g = c;
b = x;
} else if (h >= 3 && h < 4) {
r = 0;
g = x;
b = c;
} else {
r = 0;
g = 0;
b = c; // For hue > 240, keep it blue
}
return rgb_color(r, g, b);
}
/////////////////////////////////////////////////////////////////////////////////////
// ACTUATOR HELPERS
/////////////////////////////////////////////////////////////////////////////////////
void moveToPosition(float position_mm) {
float distance = position_mm - state.actuator_current_pos;
if (abs(distance) < 0.5) {
return;
}
unsigned long moveTime = abs(distance) / SPEED_MM_PER_SEC * 1000;
if (distance > 0) {
extendActuator();
} else {
retractActuator();
}
delay(moveTime);
stopActuator();
state.actuator_current_pos = position_mm;
}
void fullRetract() {
retractActuator();
delay(FULL_EXTEND_MS + 500);
stopActuator();
state.actuator_current_pos = 0;
}
void extendActuator() {
digitalWrite(INPUT_1, HIGH);
digitalWrite(INPUT_2, LOW);
}
void retractActuator() {
digitalWrite(INPUT_1, LOW);
digitalWrite(INPUT_2, HIGH);
}
void stopActuator() {
digitalWrite(INPUT_1, LOW);
digitalWrite(INPUT_2, LOW);
}