# Project export: PulseParty

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: PulseParty is the app that turns exercise into a dance party! Our wearable tech tracks your heart rate and steps as you move to your favorite tunes, turning fitness into a social, competitive game.
- Devpost: https://devpost.com/software/pulseparty
- GitHub: https://github.com/KevinL10/treehacks
- Team: 2 GitHub contributor(s) — Bartek Pacia (22 commits), Kevin Liu (18 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired by the desire to transform how people approach fitness. Traditional workouts often feel repetitive and uninspiring, causing many to lose motivation. We saw the inherent joy and social energy of dance and wanted to harness that to create a fitness experience that users genuinely look forward to.

### What it does

PulseParty is a unique combination of wearable technology and a gamified mobile app that turns dance into a thrilling competition. Users track their heart rate and dance intensity, throw dance parties with friends, battle for the top of the leaderboard.

### How we built it

We built PulseParty as a seamless cross-platform experience. Our web frontend leverages the power of NextJS for performant rendering and TailwindCSS for rapid styling. To create a native, responsive mobile experience, we chose Flutter. At the core of PulseParty's real-time functionality lies a FastAPI-powered websocket server, ensuring efficient communication between multiple mobile devices in a session, handling data like heart rate, steps, and calorie calculations. For music hosting we use Spotify Web Playback SDK for their Spotify Player for music handler.

### Challenges we ran into

Our biggest challenge lay in establishing smooth, real-time data streaming from wearable devices into the Flutter app using the TerraAPI. Balancing efficiency, accuracy, and cross-platform compatibility in this real-time data flow required multiple rounds of optimization. Another challenge we ran into is that our flutter codebase was not compatible with socket.io for websocket transfer and receiving so instead we rewrote it with FastAPI.

### Accomplishments we're proud of

During the hackathon, we proudly achieved a functional MVP of PulseParty with both web and mobile app, demonstrating its core concept. We efficiently integrated wearable data, smooth session management, and real-time score updates in a limited timeframe.

### What we learned

This project deepened our understanding of the complexities involved in real-time data handling. Optimizing websocket communication for low latency and efficient data transfer between a server and multiple Flutter clients proved invaluable experience. The limitations of cross-platform wearable device integrations highlighted the need for adaptable protocols and potential fallback mechanisms. Through PulseParty, we gained practical insights into balancing gamification elements with accurate fitness data representations, pushing us to think critically about algorithm design.

### What's next

We're eager to fine-tune our dance intensity algorithm for even greater accuracy and provide meaningful health data analytics to users. We plan to expand PulseParty's social aspect through in-app challenges and explore dance style categories. Future directions include investigating partnerships with health wearables brands and potentially offering tiered subscriptions to unlock premium features.

## README (from the GitHub repository)

# treehacks

## Detected evidence (automated analysis)

Indexed codebase: 41 recognized source files, 58 KB.
- C (language) — detected in the code
- CSS (language) — detected in the code
- Dart (language) — detected in the code
- JavaScript (language) — detected in the code
- Kotlin (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Swift (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (81 of 81)

```
.DS_Store
backend/.gitignore
backend/app.py
backend/client.py
backend/docker-compose.yml
backend/Dockerfile
backend/fly.toml
frontend/.env
frontend/.eslintrc.json
frontend/.gitignore
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/components.json
frontend/components/pages/playing.tsx
frontend/components/pages/standings.tsx
frontend/components/pages/start.tsx
frontend/components/pages/waiting.tsx
frontend/components/ranking-table.tsx
frontend/components/spotify-card.tsx
frontend/components/standing-chart.tsx
frontend/components/ui/button.tsx
frontend/lib/utils.ts
frontend/package.json
frontend/postcss.config.js
frontend/README.md
frontend/tailwind.config.ts
frontend/tsconfig.json
mobile/.gitignore
mobile/.metadata
mobile/.vscode/launch.json
mobile/analysis_options.yaml
mobile/android/.gitignore
mobile/android/app/build.gradle
mobile/android/app/src/debug/AndroidManifest.xml
mobile/android/app/src/main/AndroidManifest.xml
mobile/android/app/src/main/kotlin/tech/pacia/treehacks_app/MainActivity.kt
mobile/android/app/src/main/res/drawable-v21/launch_background.xml
mobile/android/app/src/main/res/drawable/launch_background.xml
mobile/android/app/src/main/res/values-night/styles.xml
mobile/android/app/src/main/res/values/styles.xml
mobile/android/app/src/profile/AndroidManifest.xml
mobile/android/build.gradle
mobile/android/gradle.properties
mobile/android/gradle/wrapper/gradle-wrapper.properties
mobile/android/settings.gradle
mobile/ios/.gitignore
mobile/ios/Flutter/AppFrameworkInfo.plist
mobile/ios/Flutter/Debug.xcconfig
mobile/ios/Flutter/Release.xcconfig
mobile/ios/Podfile
mobile/ios/Podfile.lock
mobile/ios/Runner.xcodeproj/project.pbxproj
mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
mobile/ios/Runner/AppDelegate.swift
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard
mobile/ios/Runner/Base.lproj/Main.storyboard
mobile/ios/Runner/Info.plist
mobile/ios/Runner/Runner-Bridging-Header.h
mobile/ios/Runner/Runner.entitlements
mobile/lib/app_router.dart
mobile/lib/connection_cubit.dart
mobile/lib/join_party_screen.dart
mobile/lib/main.dart
mobile/lib/party_screen.dart
mobile/lib/pin_input.dart
mobile/lib/routes.dart
mobile/pubspec.lock
mobile/pubspec.yaml
mobile/README.md
mobile/test/widget_test.dart
README.md
```

### Dependencies

- frontend/package.json: @radix-ui/react-slot@^1.0.2, @types/node@^20, @types/react@^18, @types/react-dom@^18, autoprefixer@^10.0.1, class-variance-authority@^0.7.0, clsx@^2.1.0, eslint@^8, eslint-config-next@14.1.0, lucide-react@^0.331.0, next@^14.1.0, postcss@^8, react@^18.2.0, react-dom@^18.2.0, react-icons@^5.0.1, recharts@^2.12.0, tailwind-merge@^2.2.1, tailwindcss@^3.3.0, tailwindcss-animate@^1.0.7, typescript@^5

### Recent commits (newest first)

- revoke spotify client app
- last fixes
- updat frontend
- move finish to timeout
- add frontend
- add spotify card
- implement showing "you win" page
- add placement
- random ios changes
- fix bug
- display placement
- fix replacing mutex
- Merge pull request #6 from KevinL10/terra_streaming
- fix full screen
- prepare for deployment
- add backend set song
- ditch terra
- shit
- trying to make realtime streaming work
- Merge pull request #5 from KevinL10/mobile_app_websockets

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

### backend/docker-compose.yml

```yaml
services:
  server:
    build: .
    ports:
      - 8000:8000
    volumes:
      - .:/usr/src/app

```

### backend/Dockerfile

```
# Use an official Python runtime as a parent image
FROM python:3.9-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy the current directory contents into the container at /usr/src/app
COPY . .

# Install any needed packages specified in requirements.txt
RUN pip install fastapi uvicorn[standard]

# Make port 8000 available to the world outside this container
EXPOSE 8000

# Install required packages
RUN apk update update
RUN apk add dumb-init

# Start simple init system
ENTRYPOINT ["dumb-init", "--"]

# Run app.py when the container launches
CMD ["uvicorn", "app:app", "--host", "0.0.0.0"]
```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-slot": "^1.0.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.0",
    "lucide-react": "^0.331.0",
    "next": "^14.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^5.0.1",
    "recharts": "^2.12.0",
    "tailwind-merge": "^2.2.1",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8",
    "eslint-config-next": "14.1.0",
    "postcss": "^8",
    "tailwindcss": "^3.3.0",
    "typescript": "^5"
  }
}

```

### backend/app.py

```python
from fastapi import FastAPI, WebSocket, Request
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
from collections import defaultdict
from uuid import uuid4
from typing import List, Dict   
import asyncio
import time

app = FastAPI()

origins = [
    "*",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# room_id: list of names in the room
# note: names server as unique identifiers; two players must not have the same name
room_users = defaultdict(list)

# room_id: event
room_started_event: Dict[int, List[asyncio.Event]] = defaultdict(list) 

room_song_playing: Dict[int, asyncio.Event] = {} 

# maps from room_id: {name: [(heart_rate, step, time)]}
user_health_data = defaultdict(lambda: defaultdict(list))

def compute_overall_score(measurements):
    avg_heart_rate = sum([m[0] for m in measurements]) / len(measurements)
    return avg_heart_rate * 5


def compute_calories_score(measurements):
    # 200bpm maximum heart rate
    # calories per minute for 100bpm is 6
    # for 150 bpm is 13
    # for 160 bpm is 15

    score = 0
    for i in range(len(measurements) - 1):
        minute_diff = (measurements[i+ 1][2] - measurements[i][2]) / 60
        calories_per_minute = (7 * measurements[i + 1][0] / 50) - 8
        score += calories_per_minute * minute_diff
    
    return score

@app.get("/users")
async def get_room_data(room_id: int = -1):
    return JSONResponse({"data": room_users[room_id]})


@app.get("/")
async def index():
    return JSONResponse({"status": "ok"})


@app.post("/start")
async def start_room(request: Request):
    data = await request.json()
    room_id = data["room_id"]
    for event in room_started_event[room_id]:
        event.set()
    
    room_song_playing[room_id] = asyncio.Event()
    return JSONResponse({"status": "ok", "room_id": room_id})


@app.get("/finish-song")
async def finish_song(room_id: int = -1):
    room_song_playing[room_id].set()
    return JSONResponse({"status": "ok", "room_id": room_id})


@app.get("/top/overall")
async def get_top_overall(room_id: int = -1):
    # (name, score)
    items = [(user, compute_overall_score(measurements)) for user, measurements in user_health_data[room_id].items()]
    ranked_results = [{"name": item[0], "score": item[1]} for item in sorted(items, key=lambda k: -k[1])]
    return JSONResponse({"data": ranked_results})


@app.get("/top/calories")
async def get_top_calories(room_id: int = -1):
    # (name, score)
    items = [(user, compute_calories_score(measurements)) for user, measurements in user_health_data[room_id].items()]
    ranked_results = [{"name": item[0], "score": item[1]} for item in sorted(items, key=lambda k: -k[1])]
    return JSONResponse({"data": ranked_results})


@app.get("/data")
async def get_room_data(room_id: int = -1):
    return JSONResponse({"data": user_health_data[room_id]})


def get_top_room_users(room_id: int):
    items = [(user, compute_overall_score(measurements)) for user, measurements in user_health_data[room_id].items()]
    return [key[0] for key in sorted(items, key=lambda s:-s[1])]

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()

    # join room with room_id and name
    data = await websocket.receive_json()
    assert data["method"] == "join_room"
    
    room_id = data["params"]["room_id"]
    name = data["params"]["name"]
    room_users[room_id].append(name)

    while True:
        await websocket.send_json({"method": "update_status", "params": {"status": "waiting"}})

        # wait for game to be started
        game_started = asyncio.Event()
        room_started_event[room_id].append(game_started)
        await game_started.wait()
        
        await websocket.send_json({"method": "update_status", "params": {"status": "starting"}})
        
        # receive data from watch repeatedly
        # TODO: stop when the song finishes
        while not room_song_playing[room_id].is_set():
        # while True not room_song_playing[room_id].is_set()::
            data = await websocket.receive_json()
            assert data["method"] == "submit_data"

            heartrate = data["params"]["heartrate"]
            step_count = data["params"]["step_count"]
            timestamp = int(time.time())


            user_health_data[room_id][name].append((heartrate, step_count, timestamp))
            top_users = get_top_room_users(room_id)
            await websocket.send_json({"method": "update_placement", "params": {"index": top_users.index(name)+1}})


        print("FOUND WINNER")
        winner_name = get_top_room_users(room_id)[0]
        await websocket.send_json({"method": "update_status", "params": {"status": "finished", "won": name == winner_name}})

        if winner_name == name:
            data = await websocket.receive_json()
            assert data["method"] == "set_song"

            song_name = data["params"]["song"]
            print("SETTING NEW SONG", song_name)

'''

accept() name

send() ok


send() start

while ...
accept() data

send() done




'''

```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Pulse Party",
  description: "",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}  style={{
      }}>{children}</body>

    </html>
  );
}

```

### frontend/app/page.tsx

```typescript
'use client';
import React, {useRef, useState, useEffect} from 'react';
import StartPage from "@/components/pages/start";
import { PageState } from "@/lib/utils";
import Image from "next/image";
import WaitingPage from '@/components/pages/waiting';
import PlayingPage from '@/components/pages/playing';
import StandingsPage from '@/components/pages/standings';


export default function Home() {
  const [state, setState] = useState<PageState>(PageState.START)
  const [roomId, setRoomId] = useState<number>(1)

  if (state === PageState.START) {
    return <StartPage setRoomId={setRoomId} setState={setState} />
  }

  if (state === PageState.WAITING) {
    return <WaitingPage roomId={roomId} setState={setState}/>
  }


  if (state === PageState.PLAYING) {
    return <PlayingPage roomId={roomId} setState={setState}/>
  }

  if (state === PageState.STANDINGS) {
    return <StandingsPage roomId={roomId} setState={setState} />
  }

  return <div>missing page</div>
}

```

### mobile/analysis_options.yaml

```yaml
include: package:leancode_lint/analysis_options.yaml

```

### frontend/postcss.config.js

```javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

```

### mobile/pubspec.yaml

```yaml
name: treehacks_app
description: A new Flutter project.
publish_to: none
version: 1.0.0+1
environment:
  sdk: '>=3.3.0 <4.0.0'
  flutter: '3.19.0'

dependencies:
  bloc: ^8.1.3
  bloc_presentation: ^1.0.0
  cupertino_icons: ^1.0.6
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.4
  go_router: ^13.2.0
  pin_code_fields: ^8.0.1
  # terra_flutter_bridge: ^0.6.3
  # terra_flutter_rt: ^0.1.1
  web_socket_channel: ^2.4.4

dev_dependencies:
  flutter_test:
    sdk: flutter
  leancode_lint: ^9.0.0

flutter:
  uses-material-design: true

```

### backend/client.py

```python
import websockets
import asyncio
import json
from random import randint
import time

async def main():
    name = input("name: ")
    room = int(input("room_id: "))

    # uri = "wss://terrahacks.fly.dev/ws"
    uri = "ws://localhost:8000/ws"
    async with websockets.connect(uri) as websocket:
        await websocket.send(json.dumps({"method": "join_room", "params": {"room_id": room, "name": name}}))
        
        # check waiting response
        data = await websocket.recv()
        data = json.loads(data)
        assert data["method"] == "update_status"
        assert data["params"]["status"] == "waiting"
        print("Joined room")

        # wait until room starts 
        data = await websocket.recv()
        data = json.loads(data)
        assert data["method"] == "update_status"
        assert data["params"]["status"] == "starting"
        print("Started room")




        while True:
            mock_data = {"heartrate": randint(60, 180), "step_count": randint(0, 200)}
            await websocket.send(json.dumps({"method": "submit_data", "params": mock_data}))

            time.sleep(1)

'''
start room: `curl -X POST -H "Content-Type: application/json" -d '{"room_id": 123456}' localhost:8000/start`

get data: `curl http://localhost:8000/data?room_id=123456`

'''


if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(main())
    # asyncio.run(send_message)
```

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