# Project export: Electrify

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2024
- Tagline: Portable taser bracelet module to ZAP away while gaming! Plug and play with higher stakes and experience a literal SHOCK!!
- Devpost: https://devpost.com/software/electrify-o3k867
- GitHub: https://github.com/kavandoctor1/electrifyio
- Demo: https://web-production-3d76c.up.railway.app/
- Video: https://www.youtube.com/embed/FD0qUVjdJoI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — kavandoctor1 (32 commits)

## Devpost submission (written by the team)

### Inspiration

Reflecting on our childhood, many of us used to play .io games with friends for fun. But what if we upped the stakes, and created an even more fun and thrilling environment? We came up with Electrify, a portable taser module that adds an exciting "electric shock" element to gaming. In a world where esports and gaming are popular like never before, we realized that adding physical haptic feedback to gaming would create a more immersive atmosphere amongst the players. Specifically, upon the player's loss, the module sends a pulse of electric shock in the form of a bracelet. Our team believes that the shared experiences in gaming are priceless. We added adaptive user-immersive background music and a photo feature where the computer captures the facial expression of the players getting tased and converts it to a forever memorable NFT as a prize to the winner.

### What it does

Each user attaches the taser module bracelet on their wrist. The users play a simple physics-based multiplayer game (similar to bonk.io) through individual computers. If the player dies, the program sends a signal to the taser module hardware via Arduino Bluetooth. The taser module then converts the 9V battery to a high enough power for a tasing effect, and the player will experience an electric shock in the form of a bracelet. We utilized IntegratedSystems IRIS Vector Search to create a user-immersive and emotion-inducing BGM and combined the camera footage of our computers and Crossmint to create NFT snapshots of the users upon winning/losing the game.

### How we built it

We began the project by designing the taser module circuit, scavenging the necessary parts, soldering the PCB board, and assembling the taser module electronics using jumper wires. We coded a program on the Arduino to receive Bluetooth controls from the computer. We used socket.io to interface the game across multiple devices, enabling realistic play across distance barriers. The game was a complex physics simulation with charmingly simple mechanics. Finally, we designed a CAD model to house the electronic components and manually constructed the taser module.

### Challenges we ran into

We can sort the challenges into two categories. For hardware, we had a hard time fabricating the taser module because each iteration of 3D printing and wrong soldering takes hours to fix. For software, it was quite challenging to reduce latency and save game states across hardware and software components.

### Accomplishments we're proud of

We are proud of being able to integrate many different software and hardware components into a large system project. We were especially excited and relieved to see the multiplayer game perform perfectly in conjunction with their taser module.

### What we learned

We learned how to mechanically prototype hardware from scratch, and integrate different software elements such as socket.io, InterSystems, P5.js, Express.js, Node.js, Arduino, and Crossmint into our project.

### What's next

From a hardware perspective, we want to add a way of gathering information from the user through EEG, heart rate sensors, etc. Also, we want to invest in more ways of "tasering" the user through different electric shock modules. With this, we can start adding more advanced haptic feedback to this project. From a software perspective, we want to make Electrify a plug-and-play device for any video game. By utilizing frame data and machine learning, our next goal is to create a general application that interfaces the taser module to any platform.

## README (from the GitHub repository)

# Electrify.io

## About the Game

A remastering of the classing bonk.io, with TASERS

## Detected evidence (automated analysis)

Indexed codebase: 16 recognized source files, 1420 KB.
- C (language) — detected in the code
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- C++ (language) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (19 of 19)

```
.gitignore
arduino.c
IRISvectorsearch.py
package.json
Procfile
public/css/normalize.css
public/css/styles.css
public/css/tabs.css
public/cut.py
public/index.html
public/js/BluetoothTerminal.js
public/js/index.js
public/js/main.js
public/js/music.js
public/js/p5.js
public/js/sketch.js
public/js/video.js
README.md
server.js
```

### Dependencies

- package.json: aws-sdk@^2.1560.0, express@^4.17.1, http@0.0.1-security, socket.io@^3.0.5

### Recent commits (newest first)

- rename song.py to IRISvectorsearch.py
- fixed README
- physics update
- physics update
- diff link
- added lots of stuff
- :)
- songs working
- video
- fix
- lowered
- static
- maybe?
- fingers crossed
- fix
- debug
- async
- protections
- working
- dampening

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "electrify",
  "version": "1.0.0",
  "description": "",
  "main": "game.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.1560.0",
    "express": "^4.17.1",
    "http": "0.0.1-security",
    "socket.io": "^3.0.5"
  }
}

```

### server.js

```javascript
const express = require("express");
const socket = require("socket.io");
const http = require("http");
const { Readable } = require('stream');

// Function to convert a base64 string to a stream
function base64ToStream(base64String) {
  // Convert base64 string to a buffer
  const buffer = Buffer.from(base64String, 'base64');
  
  // Create a readable stream from the buffer
  const stream = new Readable();
  stream.push(buffer);
  stream.push(null); // Signifies the end of the stream (EOF)
  
  return stream;
}


const aws=require('aws-sdk')
const config = {
    aws_local_config: {
        //Provide details for local configuration
    },
    aws_remote_config: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
        region: process.env.AWS_REGION,
    }
}
aws.config.update(config.aws_remote_config);
const docClient = new aws.DynamoDB.DocumentClient()
const apiGateway = new aws.APIGateway();
const s3 = new aws.S3(config.aws_remote_config)


function sendNFT(url){
  const options1 = {
    method: 'POST',
    headers: {
      'X-API-KEY': process.env.NFT,
      'Content-Type': 'application/json'
    },
    body: `{"metadata":{"description":"test","image":"${url}","name":"rajashekar"},"recipient":"email:vrajashekar2022mscs@gmail.com:polygon","reuploadLinkedFiles":false}`
  };
  
  fetch('https://staging.crossmint.com/api/2022-06-09/collections/default-polygon/nfts', options1)
    .then(response => response.json())
    .then(response => console.log(response))
    .catch(err => console.error(err));


}


async function uploadS3(name,stream){
  const uploadParams = {
    ACL: 'public-read',
    Bucket: 'nftjapan',
    Key: name,
    Body: stream,
    ContentType: 'image/png'
  };
  var result = -1;
  // console.log('uploading', uploadParams);
  await s3.upload(uploadParams, (error, data) => {
    if (error) {
        console.error('Error uploading to S3:', error);
    } else {
        console.log('Upload Success for file:', data.Location);
        sendNFT(data.Location);
    }
  });

  return result;
}


const app = express();
const PORT = process.env.PORT || 3000;
const server = http.createServer(app);

// Set static folder
app.use(express.static("public"));

// Socket setup
const io = socket(server);

const makeNFT = async function(base64Data){
  await uploadS3(Math.floor(Math.random()*100000000)+'.png',base64ToStream(base64Data));
}
// Players array
let balls = [];
let lowerwall = 100;
let resetting = false;

class Ball{

  constructor(x,y,vx,vy,deltaVx,deltaVy,Fx,Fy,mass, index,color){
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.deltaVx = deltaVx;
    this.deltaVy = deltaVy;
    this.Fx = Fx;
    this.Fy = Fy;
    this.mass = mass;
    this.index = index;
    this.color = color
  }
}


io.on("connection", (socket) => {
  console.log("Made socket connection", socket.id);

  socket.on("join", (data) => {
    balls.push(data);
    console.log('join');
    console.log(balls);
    io.sockets.emit("join", data);
  });

  socket.on("joined", () => {
    console.log('joined');
    console.log(balls);
    
    socket.emit("joined", balls);
  });

    socket.on("update", (data) => {
      // console.log('update',balls);
      if (! resetting){
      balls[data.index] = data;
      io.sockets.emit("update", data);
      }
  });


  socket.on("restart", () => {
    console.log('restart');
    lowerwall = 100;
    balls = [];
    io.sockets.emit("restart");
  });

  socket.on("bottom", (data) => {
    if(! resetting){
      console.log('bottom');
      lowerwall = data;
      io.sockets.emit("bottom",lowerwall);
    }
  });

  socket.on("reset", async function(data) {
    resetting = true;
    console.log('reset');
    for(var i = 0; i < balls.length; i ++){
      balls[i] = new Ball(100+100*i,250,0,0,0,0,0,0,3,i,balls[i].color);
    }
    
    await io.sockets.emit("reset", [balls,data]);
    resetting = false;
  })



  socket.on("makeNFT", async (base64Data) => {
    // Assuming makeNFT is async and handles the NFT creation
    await makeNFT(base64Data);
});
});

server.listen(PORT, () => console.log(`Server running on port ${PORT}`));

```

### public/js/main.js

```javascript
// UI elements.
const deviceNameLabel = document.getElementById('device-name');
const connectButton = document.getElementById('connect');
const disconnectButton = document.getElementById('disconnect');
const terminalContainer = document.getElementById('terminal');
const sendForm = document.getElementById('send-form');
const inputField = document.getElementById('input');

// Helpers.
const defaultDeviceName = 'Not Connected';
const terminalAutoScrollingLimit = terminalContainer.offsetHeight / 2;
let isTerminalAutoScrolling = true;

const scrollElement = (element) => {
  const scrollTop = element.scrollHeight - element.offsetHeight;

  if (scrollTop > 0) {
    element.scrollTop = scrollTop;
  }

   // element.lastChild.scrollIntoView(false);
	// console.log("dsaf");
};

const logToTerminal = (message, type = '') => {
  terminalContainer.insertAdjacentHTML('beforeend',
      `<div${type && ` class="${type}"`}>${message}</div>`);

  if (isTerminalAutoScrolling) {
    scrollElement(terminalContainer);
  }
};

// Obtain configured instance.
const terminal = new BluetoothTerminal();

// Override `receive` method to log incoming data to the terminal.
terminal.receive = function(data) {
  logToTerminal(data, 'in');
  // document.getElementById("recv_text").innerHTML = data;
};

// Override default log method to output messages to the terminal and console.
terminal._log = function(...messages) {
  // We can't use `super._log()` here.
  messages.forEach((message) => {
    logToTerminal(message);
    console.log(message); // eslint-disable-line no-console
  });
};

// Implement own send function to log outcoming data to the terminal.
const send = (data) => {
  terminal.send(data).
      then(() => logToTerminal(data, 'out')).
      catch((error) => logToTerminal(error));

	// var objDiv = document.getElementById("terminal");
	// objDiv.scrollTop = objDiv.scrollHeight;
};

// Bind event listeners to the UI elements.
connectButton.addEventListener('click', () => {
  terminal.connect().
      then(() => {
        deviceNameLabel.textContent = terminal.getDeviceName() ?
            terminal.getDeviceName() : defaultDeviceName;
      });
});

disconnectButton.addEventListener('click', () => {
  terminal.disconnect();
  deviceNameLabel.textContent = defaultDeviceName;
});

sendForm.addEventListener('submit', (event) => {
  event.preventDefault();

  send(inputField.value);

  inputField.value = '';
  inputField.focus();
});

// Switch terminal auto scrolling if it scrolls out of bottom.
terminalContainer.addEventListener('scroll', () => {
  const scrollTopOffset = terminalContainer.scrollHeight -
      terminalContainer.offsetHeight - terminalAutoScrollingLimit;

  isTerminalAutoScrolling = (scrollTopOffset < terminalContainer.scrollTop);
});

```

### public/js/index.js

```javascript
// Making Connection
const socket = io.connect("http://localhost:3000");
socket.emit("joined");

let players = []; // All players in the game
let currentPlayer; // Player object for individual players

let canvas = document.getElementById("canvas");
canvas.width = document.documentElement.clientHeight * 0.9;
canvas.height = document.documentElement.clientHeight * 0.9;
let ctx = canvas.getContext("2d");

const redPieceImg = "../images/red_piece.png";
const bluePieceImg = "../images/blue_piece.png";
const yellowPieceImg = "../images/yellow_piece.png";
const greenPieceImg = "../images/green_piece.png";

const side = canvas.width / 10;
const offsetX = side / 2;
const offsetY = side / 2 + 20;

const images = [redPieceImg, bluePieceImg, yellowPieceImg, greenPieceImg];

const ladders = [
  [2, 23],
  [4, 68],
  [6, 45],
  [20, 59],
  [30, 96],
  [52, 72],
  [57, 96],
  [71, 92],
];

const snakes = [
  [98, 40],
  [84, 58],
  [87, 49],
  [73, 15],
  [56, 8],
  [50, 5],
  [43, 17],
];

class Player {
  constructor(id, name, pos, img) {
    this.id = id;
    this.name = name;
    this.pos = pos;
    this.img = img;
  }

  draw() {
    let xPos =
      Math.floor(this.pos / 10) % 2 == 0
        ? (this.pos % 10) * side - 15 + offsetX
        : canvas.width - ((this.pos % 10) * side + offsetX + 15);
    let yPos = canvas.height - (Math.floor(this.pos / 10) * side + offsetY);

    let image = new Image();
    image.src = this.img;
    ctx.drawImage(image, xPos, yPos, 30, 40);
  }

  updatePos(num) {
    if (this.pos + num <= 99) {
      this.pos += num;
      this.pos = this.isLadderOrSnake(this.pos + 1) - 1;
    }
  }

  isLadderOrSnake(pos) {
    let newPos = pos;

    for (let i = 0; i < ladders.length; i++) {
      if (ladders[i][0] == pos) {
        newPos = ladders[i][1];
        break;
      }
    }

    for (let i = 0; i < snakes.length; i++) {
      if (snakes[i][0] == pos) {
        newPos = snakes[i][1];
        break;
      }
    }

    return newPos;
  }
}

document.getElementById("start-btn").addEventListener("click", () => {
  const name = document.getElementById("name").value;
  document.getElementById("name").disabled = true;
  document.getElementById("start-btn").hidden = true;
  document.getElementById("roll-button").hidden = false;
  currentPlayer = new Player(players.length, name, 0, images[players.length]);
  document.getElementById(
    "current-player"
  ).innerHTML = `<p>Anyone can roll</p>`;
  socket.emit("join", currentPlayer);
});

document.getElementById("roll-button").addEventListener("click", () => {
  const num = rollDice();
  currentPlayer.updatePos(num);
  socket.emit("rollDice", {
    num: num,
    id: currentPlayer.id,
    pos: currentPlayer.pos,
  });
});

function rollDice() {
  const number = Math.ceil(Math.random() * 6);
  return number;
}

function drawPins() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  players.forEach((player) => {
    player.draw();
  });
}

// Listen for events
socket.on("join", (data) => {
  players.push(new Player(players.length, data.name, data.pos, data.img));
  drawPins();
  document.getElementById(
    "players-table"
  ).innerHTML += `<tr><td>${data.name}</td><td><img src=${data.img} height=50 width=40></td></tr>`;
});

socket.on("joined", (data) => {
  data.forEach((player, index) => {
    players.push(new Player(index, player.name, player.pos, player.img));
    console.log(player);
    document.getElementById(
      "players-table"
    ).innerHTML += `<tr><td>${player.name}</td><td><img src=${player.img}></td></tr>`;
  });
  drawPins();
});

socket.on("rollDice", (data, turn) => {
  players[data.id].updatePos(data.num);
  document.getElementById("dice").src = `./images/dice/dice${data.num}.png`;
  drawPins();

  if (turn != currentPlayer.id) {
    document.getElementById("roll-button").hidden = true;
    document.getElementById(
      "current-player"
    ).innerHTML = `<p>It's ${players[turn].name}'s turn</p>`;
  } else {
    document.getElementById("roll-button").hidden = false;
    document.getElementById(
      "current-player"
    ).innerHTML = `<p>It's your turn</p>`;
  }

  let winner;
  for (let i = 0; i < players.length; i++) {
    if (players[i].pos == 99) {
      winner = players[i];
      break;
    }
  }

  if (winner) {
    document.getElementById(
      "current-player"
    ).innerHTML = `<p>${winner.name} has won!</p>`;
    document.getElementById("roll-button").hidden = true;
    document.getElementById("dice").hidden = true;
    document.getElementById("restart-btn").hidden = false;
  }
});

// Logic to restart the game
  document.getElementById("restart-btn").addEventListener("click", () => {
    socket.emit("restart");
  });

  socket.on("restart", () => {
    window.location.reload();
  });

```

### arduino.c

```c
#include <SoftwareSerial.h>
#include "bluetooth.h"
void setup() {
  Serial.begin(9600);
  pinMode(3, OUTPUT);
  analogWrite(3, 0);
  config_remote();
  remote_set_name("module1");
}
void loop() {
  bool taser = remote_get_command();
  if(taser){
    analogWrite(3, 255);
    Serial.println("TASER");
    delay(100);
  }
  analogWrite(3, 0);
  if(taser){
    delay(3000);
  }
  remote_send("From Arduino: " + String(millis()));
  delay(100);
}





8:07
const int BLUETOOTH_TX = A4; //SDA
const int BLUETOOTH_RX = A5; //SCL
SoftwareSerial bluetooth(BLUETOOTH_TX, BLUETOOTH_RX);
typedef struct Command{
  int joyX;
  int joyY;
  int slider;
  int button;
} Command;
void config_remote(){
  bluetooth.begin(9600);
}
void remote_set_name(String name){
  bluetooth.println(String("AT+NAME")+name);
  delay(100);
  bluetooth.println("AT+RESET");
  while (bluetooth.available()) { bluetooth.read(); }
}
bool remote_get_command(){
  while (bluetooth.available()) {
    char c = bluetooth.read();
    Serial.println(c);
    if (c== 'T') return true;
  }
  return false;
}
void remote_send(String messsage){
  bluetooth.println(messsage);
} 
```

### IRISvectorsearch.py

```python
import pandas as pd
from sentence_transformers import SentenceTransformer
from sqlalchemy import create_engine, text
import os
import time
import pygame

# Initialize pygame mixer

# Folder containing the MP3 files
folder_path = 'Songs'


username = 'SUPERUSER'
password = 'SYS2' # Replace password with password you set
hostname = 'localhost' 
port = '1972' 
namespace = 'USER'
CONNECTION_STRING = f"iris://{username}:{password}@{hostname}:{port}/{namespace}"


engine = create_engine(CONNECTION_STRING)
conn = engine.connect()
df = pd.read_csv('songs_new.csv')
df.head()


df['Category'] = df['Category'].str.replace(',', '')
df.head()


sql = f"""
        CREATE TABLE songs_featuress1 (
    songname VARCHAR(255),
    category VARCHAR(255),
    description VARCHAR(2000),
    descriptionss_vectors VECTOR(DOUBLE, 384)
)
        """
result = conn.execute(text(sql))
# Load a pre-trained sentence transformer model. This model's output vectors are of size 384
model = SentenceTransformer('all-MiniLM-L6-v2') 

# Generate embeddings for all descriptions at once. Batch processing makes it faster
embeddingsss1 = model.encode(df['SongsDescriptions'].tolist(), normalize_embeddings=True)

# Add the embeddings to the DataFrame
df['descriptions_vectors'] = embeddingsss1.tolist()
df.head()


for index, row in df.iterrows():
    sql = text("""
        INSERT INTO songs_featuress1
        (songname, category, description, descriptionss_vectors) 
        VALUES (:name, :category, :description, TO_VECTOR(:descriptionss_vectors))
    """)
    conn.execute(sql, {
        'name': row['SongName'], 
        'category': row['Category'],  
        'description': row['SongsDescriptions'], 
        'descriptionss_vectors': str(row['descriptions_vectors'])
    })
def extract_singer(song_name):
    parts = song_name.split('by')
    if len(parts) > 1:
        return parts[1].split('(')[0].strip()  # Assuming format "Song Name" by Singer (Year)
    return ""



sql = f"""
        CREATE TABLE songs_featuress1 (
    songname VARCHAR(255),
    category VARCHAR(255),
    description VARCHAR(2000),
    descriptionss_vectors VECTOR(DOUBLE, 384)
)
        """
result = conn.execute(text(sql))

model = SentenceTransformer('all-MiniLM-L6-v2') 

# Generate embeddings for all descriptions at once. Batch processing makes it faster
embeddingsss1 = model.encode(df['SongsDescriptions'].tolist(), normalize_embeddings=True)

# Add the embeddings to the DataFrame
df['descriptions_vectors'] = embeddingsss1.tolist()

def evaluate(prompt):
    # description_search = "trying hard and hopeful"
    search_vector = model.encode(prompt, normalize_embeddings=True).tolist()
    sql = text("""
         SELECT TOP 3 * FROM songs_featuress1  
         ORDER BY VECTOR_DOT_PRODUCT(descriptionss_vectors, TO_VECTOR(:search_vector)) DESC
    """)
    results = conn.execute(sql, {'search_vector': str(search_vector)}).fetchall()
    results_df = pd.DataFrame(results, columns=df.columns).iloc[:, :-1] # Remove vector
    pd.set_option('display.max_colwidth', None)  # Easier to read description
     #results_df.head()
    song_names = results_df['SongName'].tolist()
    singer_names = [extract_singer(name) for name in song_names]
    mp3_files = [f for f in os.listdir(folder_path) if f.endswith('.mp3') and any(singer.lower() in f.lower() for singer in singer_names)]
    for file in mp3_files:
        full_path = os.path.join(folder_path, file)
        # pygame.mixer.music.load(full_path)
        print(f"Playing {file}...")
        # pygame.mixer.music.play()
        time.sleep(10)
        # pygame.mixer.music.stop()

```

### public/cut.py

```python
from pydub import AudioSegment
import os

# Directory containing the original .mp3 files
source_dir = 'Songs/'
# Directory where the compressed first minute of each song will be saved
target_dir = 'NewSongs/'
hopeful = ['Adele - Rolling in the Deep (Official Music Video).mp3', 'Adele - Someone Like You (Official Music Video).mp3', 'Coldplay - Fix You (Official Video).mp3', 'Katrina and The Waves - Walking On Sunshine (Official Music Video).mp3', 'Lizzo - Good As Hell (Video).mp3', '[HD] Pearl Jam - Black [Pinkpop 1992].mp3']

happy = ['Katrina and The Waves - Walking On Sunshine (Official Music Video).mp3', 'The Black Eyed Peas - I Gotta Feeling (Official Music Video).mp3',"CAN'T STOP THE FEELING! (from DreamWorks Animation's _TROLLS_) (Official Video).mp3","Mark Ronson - Uptown Funk (Official Video) ft. Bruno Mars.mp3"]

sad = ['Adele - Rolling in the Deep (Official Music Video).mp3', 'Adele - Someone Like You (Official Music Video).mp3', 'Eric Clapton - Tears In Heaven (Official Video).mp3', '[HD] Pearl Jam - Black [Pinkpop 1992].mp3']

celebrate = ['Abba - Dancing Queen (Official Music Video Remastered).mp3', 'Katrina and The Waves - Walking On Sunshine (Official Music Video).mp3', 'Lizzo - Good As Hell (Video).mp3', 'The Black Eyed Peas - I Gotta Feeling (Official Music Video).mp3']


all = hopeful + happy + sad + celebrate
# Create the target directory if it doesn't exist
os.makedirs(target_dir, exist_ok=True)

# Iterate over every file in the source directory
for filename in os.listdir(source_dir):
    onlyname = filename
    if '/' in onlyname:
        onlyname = onlyname.split('/')[-1]
    if filename.endswith('.mp3') and onlyname in all:
        # Construct the full path to the source file
        file_path = os.path.join(source_dir, filename)
        # Load the .mp3 file
        song = AudioSegment.from_mp3(file_path)
        # Get the first 60 seconds (60000 milliseconds) of the song
        first_minute = song[:60000]
        # Optional: Apply compression here if needed. This example just trims the song.
        # Save the compressed first minute to the target directory
        first_minute.export(os.path.join(target_dir, filename), format='mp3')

print("Processing complete.")

```

### public/index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="msapplication-TileColor" content="#2196f3">
    <meta name="msapplication-config" content="browserconfig.xml">
    <meta name="theme-color" content="#2196f3">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="msapplication-starturl" content="./">
    <meta name="description"
          content="Progressive Web Application for serial communication with your own Bluetooth Low Energy (Smart) devices">

    <title>Bonk Taser</title>

    <!-- <link rel="manifest" href="manifest.json"> -->
    <!-- <link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
    <link rel="mask-icon" href="icons/safari-pinned-tab.svg" color="#2196f3">
    <link rel="shortcut icon" href="icons/favicon.ico"> -->

    <link href="https://fonts.googleapis.com/css?family=Roboto+Mono" rel="stylesheet">
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link href="css/normalize.css" rel="stylesheet">
    <link href="css/tabs.css" rel="stylesheet">
    <link href="css/styles.css" rel="stylesheet">
    <!-- <script src="js/p5.js"></script>
     -->
     <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js" defer></script>
     <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/addons/p5.sound.min.js"></script> -->
</head>
<body>

    <div id = "app" class="app">

        <div class="toolbar">
            <div id="device-name" class="name">Not Connected</div>

            <div class="buttons">
                <button id="connect" type="button" aria-label="Connect">
                    <i class="material-icons">bluetooth_connected</i>
                </button>

                <button id="disconnect" type="button" aria-label="Disconnect">
                    <i class="material-icons">bluetooth_disabled</i>
                </button>
            </div>
        </div>

<div class="pc-tab">

<input checked="checked" id="tab1" type="radio" name="pct" />
<input                   id="tab2" type="radio" name="pct" />
<!-- <input  id="tab1" type="radio" name="pct" /> -->
<!-- <input checked="checked" id="tab2" type="radio" name="pct" /> -->

  <nav>
    <ul>
      <li class="tab1">
        <label for="tab1">Control</label>
      </li>

    </ul>
  </nav>

   <section>
	   <div class="tab1">
		<!-- <div id="recv_text"> Incoming Message </div>
		<div id="joyDiv" style="videowidth:200px;videoheight:200px;margin-bottom:20px;  margin-left: auto; margin-right: auto;"></div>

		<div class="slidecontainer">
		  <input type="range" min="-100" max="100" value="0" class="slider" id="myRange" style="display: block; margin-left: auto; margin-right: auto;">
		</div> -->
		<div class="buttoncontainder">
		  <button type="button" id="candy_button"  style="display: block; margin-left: auto; margin-right: auto;">Join</button>
      <button type="button" id="restart_button"  style="display: block; margin-left: auto; margin-right: auto;">Restart</button>

		</div> 

		<div id="sent_text">  </div>
	   </div>

	   <div class="tab2 vexpand" style = "display: none;">
		<div class="terminal-tab">
			<div id="terminal_container" style="flex-grow: 1;">
				<div id="terminal" class="terminal"></div>
			</div>


		<form id="send-form" class="send-form">
		    <input id="input" type="text" aria-label="Input" autocomplete="off" placeholder="Type something to send...">
		    <button type="submit" aria-label="Send">
			<i class="material-icons">send</i>
		    </button>
		</form>
         	   </div>
	   </div>


    
   </section>
</div>

</div>

<div id = 'videostuff'> 
<div class="camera">
  <video id="video">Video stream not available.</video>
  <button id="startbutton">Take photo</button>
</div>

<canvas id="canvas"> </canvas>
<div class="output">
  <img id="photo" alt="The screen capture will appear in this box." />
</div>
</div>
<script src="/socket.io/socket.io.js"></script>

<script>
const socket = io('https://web-production-3d76c.up.railway.app');
// const socket = io('localhost:3000')

</script>
<script src="js/music.js"></script>

    <script src="js/BluetoothTerminal.js"></script>
    <script src="js/main.js"></script>
    <!-- <script src="js/joy.js"></script> -->
    <!-- <script src="js/companion.js" data-service-worker="sw.js"></script> -->

    

    <script type="text/javascript">
	    // var joy = new JoyStick('joyDiv', { internalFillColor: "#E97777",
			// 		       internalStrokeColor: "#E97777",
			// 		       externalStrokeColor: "#FF9F9F" ,
      //      					boundsType: "diamond"});

	    var slider = document.getElementById("myRange")
	    var output = document.getElementById("sent_text")
	    var button_count = 0

      var tasechar = 'T';

      function tase(){
        if (terminal.getDeviceName() != ""){
				  terminal.send("T").then(() => {});
			      }

          console.log('TASED');
			    // console.log(message);
			    // output.innerHTML = message;
		    }

        function taserandom(p){
          if(Math.random() < p){
            tase();
          }
          else{
            console.log('won');
          }
        }

    </script>
<script src="js/sketch.js"></script>
<script src="js/video.js"></script>

</body>
</html>

```

### public/css/tabs.css

```css
.pc-tab > input, .pc-tab section > div {
	 display: none;
}
 #tab1:checked ~ section .tab1, #tab2:checked ~ section .tab2 {
	 display: block;
}
 #tab1:checked ~ nav .tab1, #tab2:checked ~ nav .tab2 {
	 color: red;
}


/* Visual Styles */
 *, *:after, *:before {
	 -webkit-box-sizing: border-box;
	 -moz-box-sizing: border-box;
	 box-sizing: border-box;
}
 .pc-tab {
	 width: 100%;
	 /* max-width: 700px; */
	 margin: 0 auto;
}
 .pc-tab ul {
	 list-style: none;
	 margin: 0;
	 padding: 0;
	   align-items: center;
	   display: flex;
	
	 border: 1px solid rgba(0, 0, 0, 0.12);

}
 .pc-tab ul li label {
	 float: left;
	 padding: 15px 25px;
	 border: 1px solid #ddd;
	 border-bottom: 0;
	 background: #eee;
	 color: #444;
}
 .pc-tab ul li label:hover {
	 background: #ddd;
}
 .pc-tab ul li label:active {
	 background: #fff;
}
 .pc-tab ul li:not(:last-child) label {
	 border-right-width: 0;
}

nav {
	display: block;
	text-align: center;

}
 #tab1:checked ~ nav .tab1 label, #tab2:checked ~ nav .tab2 label {
	 background: white;
	 color: #111;
	 position: relative;
}
 #tab1:checked ~ nav .tab1 label:after, #tab2:checked ~ nav .tab2 label:after {
	 content: '';
	 display: block;
	 position: absolute;
	 height: 2px;
	 width: 100%;
	 background: #fff;
	 left: 0;
	 bottom: -1px;
}


```

### public/js/music.js

```javascript
const hopeful = ['Adele - Rolling in the Deep (Official Music Video).mp3', 'Adele - Someone Like You (Official Music Video).mp3', 'Coldplay - Fix You (Official Video).mp3', 'Katrina and The Waves - Walking On Sunshine (Official Music Video).mp3', 'Lizzo - Good As Hell (Video).mp3', '[HD] Pearl Jam - Black [Pinkpop 1992].mp3']

const happy = ['Katrina and The Waves - Walking On Sunshine (Official Music Video).mp3', 'The Black Eyed Peas - I Gotta Feeling (Official Music Video).mp3',"CAN'T STOP THE FEELING! (from DreamWorks Animation's _TROLLS_) (Official Video).mp3","Mark Ronson - Uptown Funk (Official Video) ft. Bruno Mars.mp3"]

const sad = ['Adele - Rolling in the Deep (Official Music Video).mp3', 'Adele - Someone Like You (Official Music Video).mp3', 'Eric Clapton - Tears In Heaven (Official Video).mp3', '[HD] Pearl Jam - Black [Pinkpop 1992].mp3']

const celebrate = ['Abba - Dancing Queen (Official Music Video Remastered).mp3', 'Katrina and The Waves - Walking On Sunshine (Official Music Video).mp3', 'Lizzo - Good As Hell (Video).mp3', 'The Black Eyed Peas - I Gotta Feeling (Official Music Video).mp3']

const nextsong = {
    'happy': ['happy','happy', 'celebrate', 'sad'],
    'hopeful': ['hopeful','hopeful', 'celebrate', 'happy'],
    'celebrate': ['celebrate','hopeful', 'celebrate', 'sad'],
    'sad': ['happy','hopeful', 'sad', 'sad'],
}

const songs = {
    'happy':happy,
    'hopeful': hopeful,
    'celebrate': celebrate,
    'sad': sad,
}

function randompick(array){
    var index = Math.floor(Math.random() * array.length);
    return array[index];
}
function playMusic(mood){
    var song = randompick(songs[mood]);  
    var audio = new Audio('NewSongs/'+song);
    var value =  Math.random()*20 + 10;
    console.log(value);
    
    audio.currentTime = value;
    audio.play();
    setTimeout(function() {
      audio.pause(); // Stop playing after 10 seconds
       // Optionally, reset audio to start position
        var nextmood = randompick(nextsong[mood]);
        playMusic(nextmood);

    }, 15000); 
  
  
  }
  
```

[5 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]