Robert Proaps
Published © GPL3+

Mechanical Stat-Clock for Planetside 2

A standalone device that listens to data from the Daybreak Games websocket to track deaths and kills in game, using a mechanical display.

AdvancedWork in progressOver 4 days60
Mechanical Stat-Clock for Planetside 2

Things used in this project

Hardware components

5v Impulse Counter
×2
SPST Momentary Push Button
×3
Through Hole Resistor, 5 kohm
Through Hole Resistor, 5 kohm
Any 5k resistor should work as long as its tht.
×2
1N4007 – High Voltage, High Current Rated Diode
1N4007 – High Voltage, High Current Rated Diode
Most rectifier diodes should work, I used a 1N4001 but had others on hand.
×2
General Purpose Transistor PNP
General Purpose Transistor PNP
The one I used and that is in the schematic is a 2N3906 PNP Transistor
×2
5.5x2.1 mm Barrel Jack
×1
35RAPC4BV4 3.5mm TRS Jack
×1
Node MCU ESP8266 Breakout Board
×1
JLCPCB Customized PCB
JLCPCB Customized PCB
The manufacturing files are attached in the zipped folder.
×1
Aluminum Housing
If you make your own housing note that the PCBs were designed for this one.
×1

Software apps and online services

Census Websocket
You will need to register for an API key, its free and doesn't expire. Instructions are at this website.
Planetside 2
For debugging and other obvious reasons.
KiCad
KiCad
For pcb design and circuit schematics.
Siemens Solid Edge

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
For printing the rear plate. I used a Flashforge Finder
CNC PCB Milling Machine
I did not personally use one, I ordered from JLCPCB.
Soldering iron (generic)
Soldering iron (generic)
For building the circuit board.

Story

Read more

Custom parts and enclosures

PCB 3d Model

This is a 3d model of the circuit board with all components except impulse counters.

Back Plate

This is the rear plate of the stat tracker, it mounts 3 buttons, the 3.5mm TRS, and the barrel jack.

Schematics

Stat Tracker Circuit Schematic

This is the electrical circuit for the project. It is what the PCBs are built on.

PCB File

This is the PCB, from it copies can be made if manufacturing files are exported in KiCad

Circuit Schematic Picture

This is a picture of the circuit schematic.

Assembly Mockup

This is an image of the 3d assembly before finalizing orders and the BOM.

PCB Top View

This is a top view of the PCB

Code

Stat Tracker V2.1 Firmware

Arduino
This is the firmware for the project. It should be flashed to a NodeMCU ESP8266 devboard like the one listed.
#include <ArduinoWebsockets.h>  //Library for handling websocket connection and subscription.
#include "ESP8266WiFi.h"  //library for handling netowrking.
#include <ArduinoJson.h>  //library for reading and manipulating JSON documents

const char* ssid = "YOUR WIFI SSID";
const char* password = "YOUR WIFI PASSWORD";
const char* wss_server_host = "wss://push.planetside2.com/streaming?environment=ps2&service-id=s:YOURAPIKEY"; //Replace with your API key
const uint16_t wss_server_port = 8080;

using namespace websockets;

WebsocketsClient client;
bool newKill = false; //Variable for storing the condition of a new kill occuring
bool newDeath = false; //Variable for storing the condition of a new death occuring

int killPin = 13; //D1 The pin connected to the kill impulse counter
int deathPin = 12; //D2 The pin connected to the death impulse counter

int jogForward = 0; //Pin connected to the jog Forward button
int delayPin = 2; //Pin connected to the delay button
int selectPin = 14;  //Pin for selecting which counter to alter.

int killDelayCount = 0; //Variable for storing how many kills to not count.
int deathDelayCount = 0; //Variable for storing how many kills to not count.

//Event Outputs
//Whenever a respective event occurs the tip or ring or both on the TRS jack will output a 50ms pulse.
int killOut = 9;  //Variable for assigning the kill out event to the tip of the TRS jack.
int DeathOut = 10;  //Variable for assigning the death out event to the ring og the TRS jack.


void setup() {
  Serial.begin(115200); //Open a serial port for debuggin'
  delay(500);
  WiFi.begin(ssid, password);   //connect to wifi using ssid and password defined in the global constants section
  while (WiFi.status() != WL_CONNECTED) {   //While waiting for a connection print dots. this will loop forever unless connected.
    Serial.print(".");
    delay(1000);
  }
  pinMode(selectPin, INPUT_PULLUP);
  pinMode(jogForward, INPUT_PULLUP);
  pinMode(delayPin, INPUT_PULLUP);

  pinMode(killPin, OUTPUT);
  pinMode(deathPin, OUTPUT);
  pinMode(killOut, OUTPUT);
  pinMode(DeathOut, OUTPUT);
  
  digitalWrite(killPin, HIGH);
  digitalWrite(deathPin, HIGH);
  digitalWrite(killOut, HIGH);
  digitalWrite(DeathOut, HIGH);

  Serial.println();
  Serial.println("Connected to WiFi!");
  Serial.println("Connecting to Websocket.");

  //Attempt to connected to the Rogue Planet Census Websocket
  bool connected = client.connect(wss_server_host); //this line attempts connection and stores the status in a boolean
  if (connected) {
    Serial.println("Successfully connected to Census!");
    //Here we will subscribe to the events and characters we want to track, 
    //Make sure to replace the long number with your character ID
    client.send("{\"service\":\"event\",\"action\":\"subscribe\",\"characters\":[\"5429221350718803697\"],\"eventNames\":[\"Death\"]}");
    /*this is a very messy string but here is the basics.
    all interior quotes are escaped with a \
    service event action subscribe tells the server we want to subscribe to information
    characters defines which planetside account we want information to pertain to.
    the long number is my player id number which you can find by going to the planetside players website and looking in the url
    event names defines the event we want to subscrbe to.
  */
  }
  else {
    Serial.println("Failed to connect to census websocket.");
  }

  //Create a json document with ArduinoJSON and a variable to store objects.
  DynamicJsonDocument doc(1024); //1024 is what i calculated will store the longest messages from the server
  JsonObject obj = doc.as<JsonObject>();

  //Run this callback whenever a message is recieved.
  client.onMessage([&](WebsocketsMessage message) {
    Serial.print("Message Recieved: ");Serial.println(message.data());
    DynamicJsonDocument doc(1024);
    deserializeJson(doc, message.data());
    JsonObject obj = doc.as<JsonObject>();
    //now that we have a somewhat easy to use json file we can start parsing it for the information we want.
    String eventType = obj[String("type")]; //This creates a string variable called event type that looks in the json document for the object called type.
    String eventName = obj["payload"][String("event_name")]; //These strings follow the same pattern.
    String attackerId = obj["payload"][String("attacker_character_id")];
    String victamId = obj["payload"][String("character_id")];
    /*These two strings are very important to the function of our tracker
    the census websocket does not count kills and deaths for a player seperately, only death events which are interactions
    so to distinguish between the two we will look at the player IDs to see who killed who
    we will also need a carve out for suicides.
    */
    Serial.print("Event Type: ");Serial.println(eventType);
    Serial.print("Event Name: ");Serial.println(eventName);
    Serial.print("Attacker ID:  ");Serial.println(attackerId);
    Serial.print("Victam ID:  ");Serial.println(victamId);
    Serial.print("Kill Delays:  ");Serial.println(killDelayCount);
     Serial.print("Death Delays:  ");Serial.println(deathDelayCount);
    //Now that we have all our information in easy to sue strings we will start processing the information..
    //This statement counts a kill, it checks to make sure I was not the victim and sends a 50ms impulse to the counter.
    
//This statement tracks a kill.
if (eventName == "Death" && victamId != "5429221350718803697" && killDelayCount != 0) {
  digitalWrite(killPin, LOW);
      delay(100);
      digitalWrite(killPin, HIGH);
      delay(50);
  digitalWrite(killOut, LOW);
      delay(50);
      digitalWrite(killOut, HIGH);
      delay(50);
}
//This statement tracks a death but not a suicide.
if (eventName == "Death" && victamId == "5429221350718803697" && attackerId != "5429221350718803697" && deathDelayCount != 0) {
  digitalWrite(deathPin, LOW);
  delay(100);
  digitalWrite(deathPin, HIGH);
  delay(50);
  digitalWrite(DeathOut, LOW);
      delay(50);
      digitalWrite(DeathOut, HIGH);
      delay(50);
}
  });


}

void loop() {
  if (client.available()) { //this statements checks if there is information from the websocket and gets it
    client.poll();
  }
  //this code manages the buttons for incrementing and delaying kills or deaths because we cannot always trust our device to be powered on, or census to work right.
  if(digitalRead(selectPin) == HIGH) { //Select Released
    if (digitalRead(jogForward) == LOW) {
      digitalWrite(deathPin, LOW);
      delay(100);
      digitalWrite(deathPin, HIGH);
      delay(50);
    }

    if (digitalRead(delayPin) == LOW) {
      deathDelayCount ++;
    }

  }

  if(digitalRead(selectPin) == HIGH) { //Select Pressed
    if (digitalRead(jogForward) == LOW) {
      digitalWrite(killPin, LOW);
      delay(100);
      digitalWrite(killPin, HIGH);
      delay(50);
    }

    if (digitalRead(delayPin) == LOW) {
      killDelayCount ++;
    }
  }


}

RequiredLibraries.zip

Arduino
These are the libraries required for the firmware. The folders within the zip file should be put in your arduino IDE's library directory.
No preview (download only).

Credits

Robert Proaps
3 projects • 1 follower
Maker, Hacker, Amateur Radio Operator, College Student, Aspiring Engineer, Drone Pilot.
Contact

Comments

Please log in or sign up to comment.