Created
June 30, 2023 08:52
-
-
Save geegaz/e62f3d8cf7bd9b650231b882cd3fc2a9 to your computer and use it in GitHub Desktop.
OVUM - Arduino code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define SECRET_SSID "OVUMConnection" | |
#define SECRET_PASS "OVUMConnection" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Based on the Simple UDP example by Tom Igoe | |
*/ | |
#include <SPI.h> | |
#include <WiFiNINA.h> | |
#include "arduino_secrets.h" | |
#include "components.h" | |
#include "protocol.h" | |
#define BUFFER_SIZE 256 | |
#define TICK_RATE 10 | |
unsigned int localPort = 8889; | |
IPAddress broadcastIp(255, 255, 255, 255); | |
WiFiUDP udp; | |
byte buffer[256]; | |
Data data; | |
EyeLightActor eyl; | |
HeartbeatActor hbt; | |
PettingSensor pts; | |
//UprightSensor upr; | |
unsigned long lastMillis = 0; | |
int packetDelay = 0; | |
void setup() { | |
Serial.begin(9600); | |
setupComponents(); | |
data.reset(); | |
// Connection | |
delay(3000); | |
} | |
void loop() { | |
int delta = millis() - lastMillis; | |
lastMillis = millis(); | |
processConnection(delta); | |
} | |
void processConnection(int delta) { | |
switch (WiFi.status()) { | |
case WL_NO_SHIELD: | |
Serial.println("No available WiFi shield or module"); | |
delay(2000); | |
break; | |
case WL_CONNECTED: | |
// Update components | |
// (only when the connection is active) | |
processComponents(delta); | |
/* Should not be needed if it only listens | |
if (packetDelay < (1000 / TICK_RATE)) { | |
packetDelay += delta; | |
break; | |
} | |
packetDelay = 0; | |
*/ | |
// Read/write packets | |
while (udp.parsePacket()) { | |
// Read all packets available | |
int bytes = udp.read(buffer, BUFFER_SIZE - 1); | |
buffer[bytes] = 0; | |
data.parseIn(buffer, bytes); | |
IPAddress remoteIp = udp.remoteIP(); | |
unsigned int remotePort = udp.remotePort(); | |
if (udp.beginPacket(remoteIp, remotePort)) { | |
// Send a new packet | |
int bytes = data.parseOut(buffer, BUFFER_SIZE); | |
buffer[bytes] = 0; | |
udp.write(buffer, bytes); | |
udp.endPacket(); | |
} | |
} | |
break; | |
default: | |
data.reset(); | |
resetComponents(); | |
// Disconnected - try to connect to the given network | |
Serial.print("Attempting to connect to network: "); | |
Serial.println(SECRET_SSID); // print the network name (SSID) | |
WiFi.begin(SECRET_SSID, SECRET_PASS); // try to connect | |
//delay(2000); | |
if (WiFi.status() == WL_CONNECTED) { | |
Serial.print("Connected at IP address "); | |
Serial.println(WiFi.localIP()); | |
udp.begin(localPort); | |
} | |
break; | |
} | |
} | |
void setupComponents() { | |
// Actors | |
eyl.setup(); | |
hbt.setup(); | |
// Sensors | |
pts.setup(); | |
//upr.setup(); | |
} | |
void processComponents(int delta) { | |
// Actors | |
eyl.color[0] = data.eye_color_r; | |
eyl.color[1] = data.eye_color_g; | |
eyl.color[2] = data.eye_color_b; | |
eyl.updateBrightness(data.eye_brightness); | |
eyl.process(delta); | |
hbt.beatSpeed = data.stress_level; | |
hbt.beatIntensity = data.stamina_level; | |
hbt.process(delta); | |
// Sensors | |
pts.process(delta); | |
data.petting = pts.value; | |
//upr.process(delta); | |
//data.upright = upr.value; | |
} | |
void resetComponents() { | |
// Actors | |
eyl.reset(); | |
hbt.reset(); | |
// Sensors don't need to be reset | |
// - this only happens when you lose the connection | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <Adafruit_NeoPixel.h> | |
#define PIEZO_FILTER_SAMPLES 8 | |
struct MinMax { | |
int minValue; | |
int maxValue; | |
MinMax() {} | |
MinMax(int _min, int _max) | |
: minValue(_min), maxValue(_max) {} | |
int interpolate(float time) { | |
return minValue + time * (maxValue - minValue); | |
} | |
int remap(int _min, int _max, int value) { | |
float t = float(value - _min) / float(_max - _min); | |
return interpolate(t); | |
} | |
}; | |
struct HeartbeatActor { | |
// ACTOR | |
// Unity -> Arduino | |
const int MOTOR_PIN_0 = 2; // vibrator Grove connected to digital pin 2 | |
const int MOTOR_PIN_1 = 3; // vibrator Grove connected to digital pin 3 | |
const float LONG_BEAT_RATIO = 0.75; | |
const float SHORT_BEAT_RATIO = 0.25; | |
const float LONG_BEAT_MAX_TIME = 80; | |
const float SHORT_BEAT_MAX_TIME = 40; | |
MinMax cycleTimeRange = MinMax(400, 1600); | |
int cycleTime = 1000; | |
float beatSpeed = 0.0; | |
float beatIntensity = 1.0; | |
bool stopped = false; | |
int pulseStep = 0; | |
unsigned long stepDelay = 0; | |
int stepDelays[4]; | |
void setup() { | |
pinMode(MOTOR_PIN_0, OUTPUT); | |
pinMode(MOTOR_PIN_1, OUTPUT); | |
updateDelays(); | |
} | |
void process(int delta) { | |
if (stepDelay <= 0) { | |
switch (pulseStep) { | |
case 0: // First pulse ON | |
digitalWrite(MOTOR_PIN_0, HIGH); | |
digitalWrite(MOTOR_PIN_1, HIGH); | |
break; | |
case 1: // First pulse OFF | |
digitalWrite(MOTOR_PIN_0, LOW); | |
digitalWrite(MOTOR_PIN_1, LOW); | |
break; | |
case 2: // Second pulse ON | |
digitalWrite(MOTOR_PIN_0, HIGH); | |
digitalWrite(MOTOR_PIN_1, HIGH); | |
break; | |
case 3: // Second pulse OFF | |
digitalWrite(MOTOR_PIN_0, LOW); | |
digitalWrite(MOTOR_PIN_1, LOW); | |
break; | |
} | |
updateDelays(); | |
stepDelay = stepDelays[pulseStep]; | |
pulseStep = (pulseStep + 1) % 4; | |
} else { | |
stepDelay -= min(stepDelay, delta); // Avoid overflow | |
} | |
// DEBUG | |
//Serial.println(delta); | |
//Serial.println(pulseStep); | |
//Serial.println(delta); | |
} | |
void reset() { | |
digitalWrite(MOTOR_PIN_0, LOW); | |
digitalWrite(MOTOR_PIN_1, LOW); | |
pulseStep = 0; | |
stepDelay = 0; | |
beatSpeed = 0.0; | |
beatIntensity = 1.0; | |
stopped = false; | |
} | |
// 0 0.5 1 | |
// |-----:-----|-----:-----| | |
// [long][short] | |
// First beat ON -> 0.8 * 0.25 | |
// First beat OFF -> cycleTime * 0.25 - <First beat ON> | |
// Second beat ON -> cycleTime * (0.25 * SHORT_BEAT_TIME * beatIntensity) | |
// Second beat OFF -> cycleTime * 0.75 - <Second beat ON> | |
void updateDelays() { | |
cycleTime = cycleTimeRange.interpolate(1.0 - beatSpeed); | |
stepDelays[0] = min(cycleTime * (0.25 * LONG_BEAT_RATIO), LONG_BEAT_MAX_TIME) * beatIntensity; | |
stepDelays[1] = cycleTime * 0.25 - stepDelays[0]; | |
stepDelays[2] = min(cycleTime * (0.25 * SHORT_BEAT_RATIO), SHORT_BEAT_MAX_TIME) * beatIntensity; | |
stepDelays[3] = cycleTime * 0.75 - stepDelays[2]; | |
// DEBUG | |
//Serial.println(stepDelays[0]); | |
//Serial.println(stepDelays[1]); | |
//Serial.println(stepDelays[2]); | |
//Serial.println(stepDelays[3]); | |
} | |
}; | |
struct EyeLightActor { | |
// ACTOR | |
// Unity -> Arduino | |
const int STRIP_PIN_1 = 5; | |
const int STRIP_PIN_2 = 6; | |
const int STRIP_LENGTH = 1; | |
Adafruit_NeoPixel strip1 = Adafruit_NeoPixel(STRIP_LENGTH, STRIP_PIN_1, NEO_GRB + NEO_KHZ800); | |
Adafruit_NeoPixel strip2 = Adafruit_NeoPixel(STRIP_LENGTH, STRIP_PIN_2, NEO_GRB + NEO_KHZ800); | |
byte color[3] = {0, 0, 0}; | |
void setup() { | |
strip1.begin(); | |
strip2.begin(); | |
} | |
void process(int delta) { | |
for (size_t i = 0; i < STRIP_LENGTH; i++) { | |
// Eye 1 | |
strip1.setPixelColor(i, color[0], color[1], color[2]); | |
strip1.show(); | |
// Eye 2 | |
strip2.setPixelColor(i, color[0], color[1], color[2]); | |
strip2.show(); | |
} | |
} | |
void reset() { | |
for (size_t i = 0; i < STRIP_LENGTH; i++) { | |
color[0] = 0; | |
color[1] = 0; | |
color[2] = 0; | |
updateBrightness(0.0); | |
} | |
} | |
void updateBrightness(float value = 1.0) { | |
color[0] = color[0] * value; | |
color[1] = color[1] * value; | |
color[2] = color[2] * value; | |
} | |
}; | |
struct PettingSensor { | |
// SENSOR | |
// Arduino -> Unity | |
const int LED_PIN = 4; // led Grove connected to digital pin 4 | |
const int PIEZO_PIN = A0; // Piezo output | |
const int RISE_TIME = 200; // ms to reach max calming | |
const int LOWER_TIME = 800; // ms to reach min calming | |
const float THRESHOLD = 0.5f; // min pettingAmount for value to be true | |
float pettingAmount = 0.0f; // 0 -> 1 | |
bool value = false; | |
// Don't need this anymore | |
// int filterValue = 0; | |
// size_t filterIndex = 0; | |
// int filter[PIEZO_FILTER_SAMPLES]; | |
void setup() { | |
// DEBUG | |
pinMode(LED_PIN, OUTPUT); | |
} | |
void process(int delta) { | |
int piezoAnalog = analogRead(PIEZO_PIN); | |
float piezoValue = piezoAnalog / 1023.0 * 5.0f; | |
// Don't need this anymore | |
// filter[filterIndex] = piezoAnalog; | |
// filterValue = 0; | |
// for(size_t f = 0; f < PIEZO_FILTER_SAMPLES; f++) { | |
// filterValue += filter[f]; | |
// } | |
// filterValue /= PIEZO_FILTER_SAMPLES; | |
// filterIndex = (filterIndex + 1) % PIEZO_FILTER_SAMPLES; | |
float deltaValue = delta; | |
if (piezoValue > 0.1f) | |
pettingAmount += deltaValue / RISE_TIME; | |
else | |
pettingAmount -= deltaValue / LOWER_TIME; | |
pettingAmount = constrain(pettingAmount, 0.0f, 1.0f); | |
value = (pettingAmount > THRESHOLD); | |
// DEBUG | |
digitalWrite(LED_PIN, value); // Light up the debug led | |
//Serial.println(filterValue); // Print the filtered value. | |
} | |
}; | |
struct UprightSensor { | |
// SENSOR | |
// Arduino -> Unity | |
const int BUTTON_PIN = 7; | |
bool value = false; | |
void setup() { | |
pinMode(BUTTON_PIN, INPUT_PULLUP); | |
} | |
void process(int delta) { | |
bool last_value = value; | |
value = digitalRead(BUTTON_PIN); | |
// DEBUG | |
//if (last_value != value) Serial.println(value); | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* OVUM Protocol | |
Received from Unity: | |
'c' -> Eye color, RGBA color sent as 4 bytes | |
's' -> Stress, value between 0 and 1 as 1 byte | |
'l' -> Stamina, value between 0 and 1 as 1 byte | |
'r' -> Reset event, resets the data | |
Sent from Arduino: | |
'f' -> Petting the egg, sent as 1 byte | |
'u' -> Holding the egg upright, sent as 1 byte | |
(bools are exchanged as 127 = true and 255 = false since 0 indicates the end of a packet) | |
*/ | |
struct Data { | |
// Eye color | |
byte eye_color_r = 0; | |
byte eye_color_g = 0; | |
byte eye_color_b = 0; | |
float eye_brightness = 0.0; | |
// Heartbeat | |
float stress_level = 0.0; | |
float stamina_level = 1.0; | |
// Petting | |
bool petting; | |
bool upright; | |
void reset() { | |
eye_color_r = 0; | |
eye_color_g = 0; | |
eye_color_b = 0; | |
eye_brightness = 0; | |
stress_level = 0.0; | |
stamina_level = 1.0; | |
petting = false; | |
upright = false; | |
} | |
/// Writes values in a data buffer | |
/// | |
/// | |
size_t parseOut(byte* data, size_t length) { | |
size_t packet_size = 0; | |
packet_size += 2; // Petting identifier + Petting value -> 2 bytes | |
packet_size += 2; // Upright identifier + Upright value -> 2 bytes | |
if (packet_size <= length) { | |
// Petting | |
data[0] = 'f'; | |
data[1] = boolToByte(petting); | |
// Upright | |
data[2] = 'u'; | |
data[3] = boolToByte(upright); | |
return packet_size; | |
} | |
return 0; | |
} | |
/// Reads values from a data buffer | |
/// | |
/// | |
size_t parseIn(byte* data, size_t length) { | |
size_t i = 0; | |
while (i < length) { | |
char identifier = data[i]; | |
i++; // Identifier -> 1 byte | |
switch (identifier) { | |
case 'c': | |
if ((length - i) < 4) break; // Skip if not enough bytes left to read | |
eye_color_r = data[i]; | |
eye_color_g = data[i+1]; | |
eye_color_b = data[i+2]; | |
eye_brightness = floatFromByte(data[i+3]); | |
i += 4; // RGBA -> 5 bytes | |
break; | |
case 's': | |
if ((length - i) < 1) break; // Skip if not enough bytes left to read | |
stress_level = floatFromByte(data[i]); | |
i += 1; // Speed -> 1 bytes | |
break; | |
case 'l': | |
if ((length - i) < 1) break; // Skip if not enough bytes left to read | |
stamina_level = floatFromByte(data[i]); | |
i += 1; // Speed -> 1 bytes | |
break; | |
case 'r': | |
reset(); | |
break; | |
} | |
} | |
return i; | |
} | |
inline byte boolToByte(bool b) { | |
return b ? 255 : 127; | |
} | |
inline float floatFromByte(byte b) { | |
return (float)b / 255.0f; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment