Categories
Interaction Lab

Ezra’s Final Documentation Blog

Is the fare fair?  

Conception and Design

This project was created to shed light on the racial profiling done in New York City and shed light on the issue to an unfamiliar audience. For the past few semesters, I’ve been interested in transit and its accessibility to community members whether it be its efficiency and safety or the social climate of a station. Since coming to Shanghai, I noticed a very stark difference between the city’s Metro systems, being that the one in Shanghai was cleaner, more efficient, and cheaper, whereas the NYC one was grimy, dangerous, and expensive. Another important thing I noticed was the decreased police presence in the stations. In NYC, people often evade the fare because of the cost, because of this many black and Hispanic people are targeted for this crime at a disproportionate level making up the majority of fare evasion arrests.

Our project goal is to simulate this feeling of bias when paying the fare in NYC. To do this we created a replica turnstile that functions similarly to a real one except that the user pays with their demographics instead of money.  We wanted the interaction to be as similar to what is seen at the NYC subway stations as possible, and also include bias in the design.  The interaction is an initial survey, then the user taps the card, and finally, they go through the turnstile or not go through it based on the survey results. Regardless of the survey result, they can also choose to evade by going through the “Emergency Exit”. If they initially receive a negative result and evade, the sirens will blair and a video will play.

During the initial user testing, we got positive feedback on the design of the physical turnstile. People seemed to think it replicated the real one very well. However, one of the main pieces of feedback was that it wasn’t very clear what the purpose of the project was without my explaining it. To fix this, we added a little debrief at the beginning of the project to explain to the user the purpose and the goal of our design. We also added more direction on what to do after the user is finished with the survey.  Another thing was the fare evasion function, we realized because of the size of the project, we’d have to place it in a very isolated location to avoid the sensor’s going off and the siren blaring.

Fabrication and Production

When I first thought of the idea, I thought we’d only do a small-scale model, not a full-sized one. Professor Gottfried explained how creating a lifesized one would better fulfill our goal so we decided to switch. The professor helped us create a prototype with a base that would be mounted to a table and a wooden board with washers and three poles that rotated like a turnstile. We cut a slot in the base and added a solenoid that would act as a locking mechanism. On the rotating base, we cut holes for locking and for the magnets that would be used to lock the turnstile after it touches a magnet sensor. We used clamps to mount it to the table.

The most difficult part of the project was getting everything components to work with each other. Everything from the card reader, motor, magnet sensor, lights, video, and sound were all connected so they used serial communication to work with each other. This interconnectedness of our project gave us a lot of trouble when it came to coding. The survey sent a 0 or 1 value to the Arduino and when the card has been tapped a sound and light would play depending on that value. If you had enough fare you could pass through the turnstile, the motor would unlock and you could pass. If not you would hear a denied sound and if you chose to evade and pass the PIR sensor the lights would flash and a video with sirens would play.


 

It was difficult to understand which data was being read and sent by processing or Arduino, so as our project progressed it became a bit tricky to make sure everything communicated well with each other. For example, when it came time to add the video and siren effects and lights to our design since the values of the sensors consistently changed, it made the effect very inconsistent. To fix this I had to add boolean statements to stabilize the playing of the movie and lights so they would only play consistently when someone who did not have enough fare evaded.

import processing.serial.*;
import processing.sound.*;
import processing.video.*;

SoundFile farepay;
SoundFile farenotpay;
//SoundFile siren;
Serial serialPort;
int NUM_OF_VALUES_FROM_PROCESSING = 1;  /* CHANGE THIS ACCORDING TO YOUR PROJECT */
/* This array stores values you might want to send to Arduino */
int processing_values[] = new int[NUM_OF_VALUES_FROM_PROCESSING];
int NUM_OF_VALUES_FROM_ARDUINO = 3;  /* CHANGE THIS ACCORDING TO YOUR PROJECT */
/* This array stores values from Arduino */
int arduino_values[] = new int[NUM_OF_VALUES_FROM_ARDUINO];
int Mode = 0;
int Fare = 0;
boolean pMousePressed;
PFont font; ////!
PImage img; ////!
int Total ;
int pArduinoValue0;
Movie movie;///!!!
MyButton chargeButton;
MyButton q1a;
MyButton q1b;
MyButton q1c;
MyButton q1d;
MyButton q2a;
MyButton q2b;
MyButton q2c;
MyButton q2d;
MyButton q3a;
MyButton q3b;
MyButton q3c;
MyButton q3d;
MyButton q4a;
MyButton q4b;
MyButton q4c;
MyButton q4d;
int b_height = 1440;
int b_width = 900;
boolean armSensor = false;

class MyButton {
  PVector Pos = new PVector(0, 0);
  float Width = 0;
  float Height = 0;
  color Colour;
  String Text;
  Boolean Clicked = false;
  int Value = 0;
  MyButton(int x, int y, int w, int h, String t, int r, int g, int b, int v)
  {
    Pos.x = x;
    Pos.y = y;
    Width = w;
    Height = h;
    Text = t;
    Colour = color(r, g, b);
    Value = v;
  }
  void update()
  {
    if (mousePressed == true && pMousePressed == false)
    {
      if (mouseX>=Pos.x && mouseX<=Pos.x+Width && mouseY>=Pos.y && mouseY<= Pos.y+Height)
      {
        Mode = Mode + 1;
        Fare += Value;
      }
    } else
    {
      Clicked = false;
    }
  }
  void render()
  {
    noFill();
    strokeWeight(7);
    stroke(255, 255, 255);
    rect(Pos.x, Pos.y, Width, Height, 40); ///!!!
    fill(Colour);
    textAlign(CENTER, CENTER);
    text(Text, Pos.x+(Width/2), Pos.y+(Height/2));
  }
  boolean isClicked()
  {
    return Clicked;
  }
}


void setup() {
  size(1440, 900);
  init();
}

void init() {
  if(serialPort != null) {
    serialPort.stop();
    println("Closing serial port, hopefully this will reset the Arduino");
    delay(2000);
  }
  if (movie != null) {
    movie.stop();
  }

  background(255);
  img = loadImage("logo.png");
  farepay= new SoundFile(this, "pay.mp3");
  farenotpay= new SoundFile(this, "nopay.mp3");
  //siren= new SoundFile(this, "siren.mp3");
  movie = new Movie(this, "invasion.mp4");
  chargeButton = new MyButton(b_height/2-250, b_width/2-50, 500, 100, "Add Fare", 255, 255, 255, 0);

  serialPort = new Serial(this, "COM3", 9600);
  q1a = new MyButton(b_height/2-250, b_width/4+35, 500, 100, "Black", 255, 255, 255, 1);
  q1b = new MyButton(b_height/2-250, (b_width/4+35)+150, 500, 100, "Hispanic", 255, 255, 255, 2);
  q1c = new MyButton(b_height/2-250, (b_width/4+35)+300, 500, 100, "Asian", 255, 255, 255, 3);
  q1d = new MyButton(b_height/2-250, (b_width/4+35)+450, 500, 100, "White", 255, 255, 255, 4);
  q2a = new MyButton(b_height/2-250, (b_width/4+35), 500, 100, "18-24", 255, 255, 255, 1);
  q2b = new MyButton(b_height/2-250, (b_width/4+35)+150, 500, 100, "25-34", 255, 255, 255, 2);
  q2c = new MyButton(b_height/2-250, (b_width/4+35)+300, 500, 100, "35-44", 255, 255, 255, 3);
  q2d = new MyButton(b_height/2-250, (b_width/4+35)+450, 500, 100, "45+", 255, 255, 255, 4);
  q3a = new MyButton(b_height/2-350, (b_width/4+35), 700, 100, "<= Middle school", 255, 255, 255, 1);
  q3b = new MyButton(b_height/2-350, (b_width/4+35)+150, 700, 100, "High School", 255, 255, 255, 2);
  q3c = new MyButton(b_height/2-350, (b_width/4+35)+300, 700, 100, "University", 255, 255, 255, 3);
  q3d = new MyButton(b_height/2-350, (b_width/4+35)+450, 700, 100, "Graduate School", 255, 255, 255, 4);
  q4a = new MyButton(b_height/2-250, (b_width/4+35), 500, 100, "Unemployed", 255, 255, 255, 1);
  q4b = new MyButton(b_height/2-250, (b_width/4+35)+150, 500, 100, "Student", 255, 255, 255, 2);
  q4c = new MyButton(b_height/2-250, (b_width/4+35)+300, 500, 100, "Part-time", 255, 255, 255, 3);
  q4d = new MyButton(b_height/2-250, (b_width/4+35)+450, 500, 100, "Full-time", 255, 255, 255, 4);
  font = loadFont("SansSerif-48.vlw");

  for (int i=0; i < NUM_OF_VALUES_FROM_PROCESSING; i=i+1) {
    processing_values[i] = 0;
  }
  for (int i=0; i < NUM_OF_VALUES_FROM_ARDUINO; i=i+1) {
    arduino_values[i] = 0;
  }
  Mode = 0;
  Fare = 0;
  pMousePressed = false;
  Total = 0;
  pArduinoValue0 = 0;
  armSensor = false;
}

void movieEvent(Movie m) {
  m.read();
}

void draw() {
  getSerialData();
  boolean playmov = false;
  String research = "When it comes to fare evasion in NYC, there is racial bias when it comes to who is stopped and fined. According to NYPD data, Black and Hispanic people accounted for nearly 90% of fare-related arrests and nearly 70% of summonses. Moreover, it is reported that up to 20% of New Yorkers struggle to pay the fair which is $2.90 (21 rmb). Our project aims to simulate this bias within the fare system and policing and emulate what people who evade the fare may experience based on their demographics. By doing so, we hope to raise awareness of the inequity that plagues transit policing in NYC.";
  if (Mode == 0) {
    background(255);
    fill(0);
    textFont(font, 70);
    text("Background", b_height/2+200, b_width/10);
    textFont(font, 30);
    textAlign(LEFT);
    text(research, 620, 180, 650, 650);  // Text wraps within text box
    image(img, 0, b_width-750, 600, 600);
    // Draw the circle
    fill(28, 33, 67); // Dark color for the circle
    noStroke();
    float circleX = b_height - 100;
    float circleY = b_width - 100;
    float circleDiameter = 100;
    circle(circleX, circleY, circleDiameter);
    // Set the text properties
    fill(255); // Set the text color to white for contrast
    textSize(16); // Set the text size
    textAlign(CENTER, CENTER); // Center the text horizontally and vertically
    // Draw the text on top of the circle
    font = loadFont("SansSerif-48.vlw");
    textFont(font, 35);
    text("Next", circleX, circleY);
  } else if (Mode == 1) {
    background(28, 33, 67);
    fill(255, 189, 89); ///!!!
    noStroke();
    rect(0, -33, b_height, 220, 40); ///!!!!
    fill(255);
    font = loadFont("SansSerif-48.vlw");
    textFont(font, 70);
    text("Welcome to the NYC Subway", b_height/2, b_width/10);
    chargeButton.update();
    chargeButton.render();
  } else if (Mode == 2) {
    background(28, 33, 67);
    fill(255, 189, 89); ///!!!
    noStroke();
    rect(0, -33, b_height, 220, 40); ///!!!!
    fill(255);
    font = loadFont("SansSerif-48.vlw");
    textFont(font, 70);
    text("What is your race?", b_height/2, b_width/10);
    q1a.update();
    q1a.render();
    q1b.update();
    q1b.render();
    q1c.update();
    q1c.render();
    q1d.update();
    q1d.render();
  } else if (Mode == 3) {
    background(28, 33, 67);
    fill(255, 189, 89); ///!!!
    noStroke();
    rect(0, -33, b_height, 220, 40); ///!!!!
    fill(255);
    font = loadFont("SansSerif-48.vlw");
    textFont(font, 70);
    text("What is your age bracket?", b_height/2, b_width/10);
    q2a.update();
    q2a.render();
    q2b.update();
    q2b.render();
    q2c.update();
    q2c.render();
    q2d.update();
    q2d.render();
  } else if (Mode == 4) {
    background(28, 33, 67);
    fill(255, 189, 89); ///!!!
    noStroke();
    rect(0, -33, b_height, 220, 40); ///!!!!
    fill(255);
    font = loadFont("SansSerif-48.vlw");
    textFont(font, 70);
    text("What is your highest-level of education?", b_height/2, b_width/10);
    q3a.update();
    q3a.render();
    q3b.update();
    q3b.render();
    q3c.update();
    q3c.render();
    q3d.update();
    q3d.render();
  } else if (Mode == 5) {
    background(28, 33, 67);
    fill(255, 189, 89); ///!!!
    noStroke();
    rect(0, -33, b_height, 220, 40); ///!!!!
    fill(255);
    font = loadFont("SansSerif-48.vlw");
    textFont(font, 70);
    text("What is your employment status?", b_height/2, b_width/10);
    q4a.update();
    q4a.render();
    q4b.update();
    q4b.render();
    q4c.update();
    q4c.render();
    q4d.update();
    q4d.render();
  } else if (Mode == 6) {
    background(28, 33, 67);
    fill(255, 189, 89); ///!!!
    text("Card Loaded", b_height/2, b_width/2);
    text("Tap your card on the reader to pay", b_height/2, b_width/2 + 100);
    if (Fare < 8) {
      Total = 0; // Set Total to false if Fare is greater than 8
      //print("arduinoval ", arduino_values[0]);
      if (pArduinoValue0 != 0 && arduino_values[0] == 0) {
        armSensor = true;
        farenotpay.play();
        print(armSensor);
        print("fuck");
        print(arduino_values[1]);
      }
    } else {
      Total = 1; // Set Total to true otherwise
      print("arduinoval", arduino_values[0]);
      if (pArduinoValue0 != 1 && arduino_values[0] == 1) {
        //armSensor = true;
        farepay.play();
      }
    }
    processing_values[0] = Total;
    //print("processing value",processing_values[0]);
    if (armSensor == true && arduino_values[1] == 1) {
      println("Fare evasion in progress");
      //siren.loop();
      //delay(6000);
      //siren.stop();
      playmov = true;
      print(playmov);
      //delay(8000);
      //movie.stop();
    }
    if (playmov == true) {
      movie.loop();
      image(movie, 0, 0, width, height);
    }
    //keyPressed();
  }
  pMousePressed = mousePressed;
  pArduinoValue0 = arduino_values[0];
  sendSerialData();
}

void getSerialData() {
  while (serialPort.available() > 0) {
    String in = serialPort.readStringUntil( 10 ); // 10 = '\n'  Linefeed in ASCII
    if (in != null) {
      print("From Arduino: " + in);
      String[] serialInArray = split(trim(in), ",");
      if (serialInArray.length == NUM_OF_VALUES_FROM_ARDUINO) {
        for (int i=0; i<serialInArray.length; i++) {
          arduino_values[i] = int(serialInArray[i]);
        }
      }
    }
  }
}

void sendSerialData() {
  String data = "";
  for (int i=0; i<processing_values.length; i++) {
    data += processing_values[i];
    // if i is less than the index number of the last element in the values array
    if (i < processing_values.length-1) {
      data += ",";  // add splitter character "," between each values element
    }
    // if it is the last element in the values array
    else {
      data += "\n";  // add the end of data character linefeed "\n"
    }
  }
  // write to Arduino
  serialPort.write(data);
  //print("To Arduino: " + data);  // this prints to the console the values going to arduino
}

void mousePressed() {
  // Only proceed if Mode is 0
  if (Mode == 0) {
    float distance = dist(mouseX, mouseY, b_height - 100, b_width - 100);
    float radius = 100 / 2; // The radius is half of the diameter
    // Check if the mouse click was within the circle
    if (distance < radius) {
      Mode = Mode + 1;
      if (Mode == 6) {
        armSensor = false;
      }
      print(Mode);
    }
  }
}

void keyPressed() {
  if (key == ' ') {
    //Mode = 0;
    //Fare = 0;
    init();
  }
}



Conclusion

 

I feel like people understood the general function of the interaction, which was that the survey was used as a way for payment, and based on this people could pass through. I think the main thing that could be improved was the consequences of fare evasion. To make it more realistic, I think we could’ve made it trigger at different frequencies versus having it start all the time when someone who doesn’t have enough money evades. Additionally, I think we could’ve made the setup a bit cleaner and expanded the questions in the survey. I think overall our project accomplished its designated goal which was to inform users about racial profiling in the subway; however, I didn’t take into consideration the cultural differences of the US and China. Our project is very American and it was very hard for some Chinese people to understand the complexity surrounding its meaning. I’m not sure if there was much I could’ve done to make it more accessible to a foreign audience. 

Disassembly

Appendix

 

#include <FastLED.h>

// Motor A connections

int enA = 5;

int in1 = 6;

int in2 = 7;

bool evade = false;

bool lights = false;

int sensorVal = 4;

//magnet sensor

int mag = 2;

#define NUM_LEDS 60  // How many leds on your strip?

#define DATA_PIN 3

CRGB leds[NUM_LEDS];

#include <SPI.h>

#include <MFRC522.h>

#define NUM_OF_VALUES_FROM_PROCESSING 1 /* CHANGE THIS ACCORDING TO YOUR PROJECT */

/* This array stores values from Processing */

int processing_values[NUM_OF_VALUES_FROM_PROCESSING];

int pay = 2;

#define RST_PIN 9  // Configurable, see typical pin layout above

#define SS_PIN 10  // Configurable, see typical pin layout above

MFRC522 mfrc522(SS_PIN, RST_PIN);  // Create MFRC522 instance

void setup() {

  Serial.begin(9600);  // Initialize serial communications with the PC

  SPI.begin();         // Init SPI bus

  mfrc522.PCD_Init();  // Init MFRC522 card

  pinMode(enA, OUTPUT);

  pinMode(in1, OUTPUT);

  digitalWrite(in1, HIGH);

  pinMode(in2, OUTPUT);

  digitalWrite(in2, LOW);

  //neopixel

  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);

  FastLED.setBrightness(40);

  //magnet sensor

  pinMode(mag, INPUT);

  fill_solid(leds, NUM_LEDS, CRGB(0, 0, 0));

  FastLED.show();

}

void loop() {

  getSerialData();

  // Turn the first LED on, then pause

  int fareevade = digitalRead(sensorVal);

  int magstate = digitalRead(mag);

  int fare = processing_values[0];

  if (mfrc522.PICC_IsNewCardPresent() && fare == 1) {

    pay = 1;

    //Serial.print(fare);

    //Serial.print(",");  // put comma between sensor values

    Serial.print(pay);

    Serial.print(",");

    Serial.print(fareevade);

    Serial.print(",");

    Serial.print(magstate);

    Serial.println();

    //Serial.print(evade);

    Serial.println();  

    delay(20);

    for (int i = 0; i < NUM_LEDS; i++) {

      leds[i] = CRGB(0, 255, 0);  // Set LED color to red

      FastLED.show();

    }

    digitalWrite(enA, HIGH);

    delay(2000);

    delay(1000);

    for (int i = 0; i < NUM_LEDS; i++) {

      leds[i] = CRGB(0, 0, 0);  

      FastLED.show();

    }

   

    digitalWrite(enA, HIGH);

    //delay(2000);

    if(magstate == 1){

    digitalWrite(enA, LOW);}

   

  }

  else if (mfrc522.PICC_IsNewCardPresent() && fare == 0) {

    pay = 0;

    evade = true;

    Serial.print(pay);

    Serial.print(",");

    Serial.print(fareevade);

    Serial.print(",");

    Serial.print(magstate);

    Serial.println();

   // Serial.print(evade);

    Serial.println();  

    delay(20);

    for (int i = 0; i < NUM_LEDS; i++) {

      leds[i] = CRGB(255, 0, 0);  // Set LED color to red

      FastLED.show();

    }

    delay(5000);

    for (int i = 0; i < NUM_LEDS; i++) {

      leds[i] = CRGB(0, 0, 0);  // Set LED color to red

      FastLED.show();

    }

  }

if (fareevade == 1 && evade == true) {

   Serial.print(pay);

    Serial.print(",");

    Serial.print(fareevade);

    Serial.print(",");

    Serial.print(magstate);

    Serial.println();

  lights = true;

}

  if(lights == true){

   

    fill_solid(leds, NUM_LEDS, CRGB(255, 0, 0));

    FastLED.show();

    delay(100); // One second delay

    // Turn all LEDs blue

    fill_solid(leds, NUM_LEDS, CRGB(0, 0, 255));

    FastLED.show();

    delay(100); // One second

  }

else if (mfrc522.PICC_IsNewCardPresent() == false) {

  pay = 2;




    Serial.print(pay);

    Serial.print(",");

    Serial.print(fareevade);

    Serial.print(",");

    Serial.print(magstate);

    //Serial.print(evade);

    Serial.println();  

    delay(20);

   if(magstate == 1){

    digitalWrite(enA, LOW);}

    if (fare == 2 && fareevade == 1){

        fill_solid(leds, NUM_LEDS, CRGB(0, 0, 0));

        FastLED.show();

  }




}







}

void getSerialData() {

  static int tempValue = 0;  // the "static" makes the local variable retain its value between calls of this function

  static int tempSign = 1;

  static int valueIndex = 0;

  while (Serial.available()) {

    char c = Serial.read();

    if (c >= '0' && c <= '9') {

      // received a digit:

      // multiply the current value by 10, and add the character (converted to a number) as the last digit

      tempValue = tempValue * 10 + (c - '0');

    } else if (c == '-') {

      // received a minus sign:

      // make a note to multiply the final value by -1

      tempSign = -1;

    } else if (c == ',' || c == '\n') {

      // received a comma, or the newline character at the end of the line:

      // update the processing_values array with the temporary value

      if (valueIndex < NUM_OF_VALUES_FROM_PROCESSING) {  // should always be the case, but double-check

        processing_values[valueIndex] = tempValue * tempSign;

      }

      // get ready for the new data by resetting the temporary value and sign

      tempValue = 0;

      tempSign = 1;

      if (c == ',') {

        // move to dealing with the next entry in the processing_values array

        valueIndex = valueIndex + 1;

      } else {

        // except when we reach the end of the line

        // go back to the first entry in this case

        valueIndex = 0;

      }

    }

  }




}