Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bluetooth #84

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
venv

# testing
/coverage
Expand Down
1 change: 1 addition & 0 deletions app/android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ android {

apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-community-bluetooth-le')
implementation project(':capacitor-preferences')

}
Expand Down
3 changes: 3 additions & 0 deletions app/android/capacitor.settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

include ':capacitor-community-bluetooth-le'
project(':capacitor-community-bluetooth-le').projectDir = new File('../node_modules/@capacitor-community/bluetooth-le/android')

include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
7 changes: 6 additions & 1 deletion app/capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ const config: CapacitorConfig = {
appName: 'adeus',
webDir: 'out',
server: {
androidScheme: 'https',
androidScheme: 'https'
},
plugins: {
CapacitorHttp: {
enabled: true,
},
},
};

Expand Down
10 changes: 8 additions & 2 deletions app/ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,18 @@
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Q892SK2RNM;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = ai.adeus;
PRODUCT_BUNDLE_IDENTIFIER = app.mkrupskis;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand All @@ -365,14 +368,17 @@
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Q892SK2RNM;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = ai.adeus;
PRODUCT_BUNDLE_IDENTIFIER = app.mkrupskis;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
6 changes: 5 additions & 1 deletion app/ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>adeus</string>
<string>adeus</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
Expand All @@ -22,6 +22,10 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>We require Bluetooth access to connect to nearby devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>We require Bluetooth access to connect to nearby devices.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand Down
1 change: 1 addition & 0 deletions app/ios/App/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCommunityBluetoothLe', :path => '../../node_modules/@capacitor-community/bluetooth-le'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
end

Expand Down
8 changes: 7 additions & 1 deletion app/ios/App/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
PODS:
- Capacitor (5.6.0):
- CapacitorCordova
- CapacitorCommunityBluetoothLe (3.1.1):
- Capacitor
- CapacitorCordova (5.6.0)
- CapacitorPreferences (5.0.7):
- Capacitor

DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorCommunityBluetoothLe (from `../../node_modules/@capacitor-community/bluetooth-le`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"

EXTERNAL SOURCES:
Capacitor:
:path: "../../node_modules/@capacitor/ios"
CapacitorCommunityBluetoothLe:
:path: "../../node_modules/@capacitor-community/bluetooth-le"
CapacitorCordova:
:path: "../../node_modules/@capacitor/ios"
CapacitorPreferences:
:path: "../../node_modules/@capacitor/preferences"

SPEC CHECKSUMS:
Capacitor: ebfc16cdb8116d04c101686b080342872da42d43
CapacitorCommunityBluetoothLe: 86ca83c89199336039bad94f45f8363114ae1464
CapacitorCordova: 931b48fcdbc9bc985fc2f16cec9f77c794a27729
CapacitorPreferences: 77ac427e98db83bace772455f8ba447430382c4c

PODFILE CHECKSUM: 769e120bf4dfe4ef1095b83775e36bafeeeb3cdd
PODFILE CHECKSUM: da5221e2db218790239ebdc591ce1525f4e8ad73

COCOAPODS: 1.15.2
17 changes: 17 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"android": "npm run app && npx cap open android"
},
"dependencies": {
"@capacitor-community/bluetooth-le": "^3.1.1",
"@capacitor/android": "^5.6.0",
"@capacitor/core": "^5.6.0",
"@capacitor/ios": "^5.6.0",
Expand Down
13 changes: 12 additions & 1 deletion app/src/components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { useMutation, useQuery } from '@tanstack/react-query';
import { Bluetooth } from 'lucide-react';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import ChatLog, { Message } from './ChatLog';
Expand All @@ -9,6 +11,7 @@ import NewConversationButton from './NewConversationButton';
import PromptForm from './PromptForm';
import SideMenu from './SideMenu';
import { ThemeToggle } from './ThemeToggle';
import { Button } from './ui/button';

export default function Chat({
supabaseClient,
Expand All @@ -17,6 +20,7 @@ export default function Chat({
}) {
const bottomRef = useRef<HTMLDivElement | null>(null);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const router = useRouter();

const [entryData, setEntryData] = useState('');
const [messages, setMessages] = useState<Message[]>([]);
Expand Down Expand Up @@ -154,12 +158,19 @@ export default function Chat({
</div>
<div className="fixed right-4 top-4 flex space-x-4">
<NavMenu>
<LogoutButton supabaseClient={supabaseClient} />
<NewConversationButton
createNewConversation={() => {
newConversation.mutate();
}}
/>
<Button
Copy link
Collaborator

@Jacksonmills Jacksonmills Mar 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets make sure to use next/link Link for these. You can use asChild on Button so it renders just as the <a></a> tag and not <button><a></a></button>

e.g.

<Button
  ...
  + asChild
  - onClick={() => router.push('/')}
>
  + <Link href="/connect">
      <Bluetooth size={20} />
  + </Link>
</Button>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh and then remove the useRouter if we arent needing it anymore

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could definitely clean up the connect page and make it more of a fleshed out page
totally agree. ESP32 is still fairly unusable - high power consumption, requires soldering a mic on it, want to push out nrf PR and then polish the connect page.

size={'icon'}
className="bg-muted/20 text-muted-foreground hover:bg-muted/40 rounded-full"
onClick={() => router.push('/connect')}
>
<Bluetooth size={20} />
</Button>
<LogoutButton supabaseClient={supabaseClient} />
<ThemeToggle />
</NavMenu>
</div>
Expand Down
154 changes: 154 additions & 0 deletions app/src/pages/connect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { useEffect, useState } from 'react';

import LogoutButton from '@/components/LogoutButton';
import { NavMenu } from '@/components/NavMenu';
import { ThemeToggle } from '@/components/ThemeToggle';
import { Button } from '@/components/ui/button';
import { useSupabase, useSupabaseConfig } from '@/utils/useSupabaseConfig';
import { BleClient, ScanResult } from '@capacitor-community/bluetooth-le';
import { CapacitorHttp, HttpResponse } from '@capacitor/core';
import { Files } from 'lucide-react';
import { useRouter } from 'next/router';

// const SERVICE_ID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
// const CHARACTERISTIC_ID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
// const SAMPLE_RATE = 44100;
// const FRAMES_PER_BUFFER = 512;
// const RECORD_SECONDS = 10;

export default function Index() {
const [devices, setDevices] = useState<ScanResult[]>([]);
const { supabaseUrl, supabaseToken } = useSupabaseConfig();

const connect = async (deviceId: string) => {
const device = await BleClient.requestDevice({
services: ['4fafc201-1fb5-459e-8fcc-c5c9c331914b'],
});
await BleClient.connect(device.deviceId);

return device.deviceId;
};

const router = useRouter();
const [loggedIn, setLoggedIn] = useState(false);
const [data, setData] = useState<any>(null);
const [connected, setConnected] = useState(false);

const { user, supabaseClient } = useSupabase();

useEffect(() => {
scan();
}, []);

useEffect(() => {
if (user) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
}, [user]);

async function scan(): Promise<void> {
try {
await BleClient.initialize();

await BleClient.requestLEScan({}, (result: ScanResult) => {
setDevices((prev) => {
return [...prev, result];
});
});

setTimeout(async () => {
await BleClient.stopLEScan();
console.log('stopped scanning');
}, 5000);
} catch (error) {
console.error(error);
}
}

async function sendAudioData(audioData: Uint8Array) {
const data = Buffer.from(audioData).toString('base64');

if (!supabaseUrl) {
throw new Error('Supabase URL is not defined');
}

const options = {
url: supabaseUrl + '/functions/v1/process-audio',
headers: {
'Content-Type': 'application/json',
},
data: { data: data },
};

const response: HttpResponse = await CapacitorHttp.post(options);

if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const result = await response.data;
console.log('Response from the backend:', result);
}

return (
<>
<div className="from-background fixed top-0 flex h-24 w-full items-center justify-between bg-gradient-to-b"></div>
<div className="fixed right-4 top-4 flex space-x-4">
<NavMenu>
<Button
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as other link

size={'icon'}
className="bg-muted/20 text-muted-foreground hover:bg-muted/40 rounded-full"
onClick={() => router.push('/')}
>
<Files size={20} />
</Button>
<ThemeToggle />
<LogoutButton supabaseClient={supabaseClient!} />
Copy link
Collaborator

@Jacksonmills Jacksonmills Mar 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supabaseClient! It's best practice to check for a null supabaseClient at the beginning of your component's render function and handle it by returning null. This prevents the rest of the function from executing with an invalid client, which is more efficient and makes the code easier to read and maintain.

  if (!supabaseClient) {
    return <div>Supabase client not found</div>;
  }

</NavMenu>
</div>
<div className="mt-40">
{devices.map((device, index) => {
if (device.device.name?.includes('ESP32')) {
return (
<Button
onClick={async () => {
const deviceId = await connect(device.device.deviceId);

let bufferSize = 500000;
let buffer = new Uint8Array(bufferSize);

let count = 0;

const result = await BleClient.startNotifications(
deviceId,
'4fafc201-1fb5-459e-8fcc-c5c9c331914b',
'beb5483e-36e1-4688-b7f5-ea07361b26a8',
async (value) => {
for (let i = 0; i < value.byteLength; i++) {
buffer[count] = value.getUint8(i);
count++;
if (count === bufferSize) {
await sendAudioData(buffer);
count = 0;
}
}
}
);

await sendAudioData(buffer);
}}
key={index}
>
<h1>Device: {device.device.name + ''}</h1>
<p>UUID: {device.uuids}</p>
</Button>
);
}
})}
<Button onClick={scan}>Scan Again</Button>
</div>
</>
);
}
1 change: 1 addition & 0 deletions supabase/functions/common/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const corsHeaders = {
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
"Access-Control-Max-Age": "3600",
};

Loading