From 2e553732dc54375121571ae7254f2ac9b1c6ba6e Mon Sep 17 00:00:00 2001 From: Anton Franzluebbers Date: Thu, 7 Dec 2023 17:11:05 -0500 Subject: [PATCH] v4 hopefully no breaky by pushy --- .github/workflows/deploy_oracle.yml | 2 +- unity_package/Runtime/VELConnectManager.cs | 51 +-- unity_package/package.json | 2 +- velconnect-npm/.gitignore | 12 + velconnect-npm/package-lock.json | 50 +++ velconnect-npm/package.json | 21 ++ velconnect-npm/src/index.ts | 317 ++++++++++++++++++ velconnect-npm/tsconfig.json | 27 ++ velconnect-svelte-npm/package.json | 8 +- .../migrations/1701978583_updated_Users.go | 74 ++++ .../migrations/1701981023_updated_Users.go | 74 ++++ 11 files changed, 608 insertions(+), 30 deletions(-) create mode 100644 velconnect-npm/.gitignore create mode 100644 velconnect-npm/package-lock.json create mode 100644 velconnect-npm/package.json create mode 100644 velconnect-npm/src/index.ts create mode 100644 velconnect-npm/tsconfig.json create mode 100644 velconnect/migrations/1701978583_updated_Users.go create mode 100644 velconnect/migrations/1701981023_updated_Users.go diff --git a/.github/workflows/deploy_oracle.yml b/.github/workflows/deploy_oracle.yml index 9ebbf0c..cc2bdca 100644 --- a/.github/workflows/deploy_oracle.yml +++ b/.github/workflows/deploy_oracle.yml @@ -15,7 +15,7 @@ jobs: echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.SSH_HOST }} > ~/.ssh/known_hosts - name: connect and pull - run: ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd /home/ubuntu/VEL-Connect-v3/velconnect && git pull && docker compose up -d --build && exit" + run: ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd /home/ubuntu/VEL-Connect-v4/velconnect && git pull && docker compose -p velconnect-v4 up -d --build && exit" - name: cleanup run: rm -rf ~/.ssh diff --git a/unity_package/Runtime/VELConnectManager.cs b/unity_package/Runtime/VELConnectManager.cs index 9c75240..64a77e3 100644 --- a/unity_package/Runtime/VELConnectManager.cs +++ b/unity_package/Runtime/VELConnectManager.cs @@ -281,35 +281,38 @@ namespace VELConnect : null; DeviceField fieldName; - if (Enum.TryParse(fieldInfo.Name, out fieldName) && newValue != oldValue) + if (Enum.TryParse(fieldInfo.Name, out fieldName)) { - try + if (newValue != oldValue) { - if (!isInitialState) OnDeviceFieldChanged?.Invoke(fieldName, newValue); - } - catch (Exception e) - { - Debug.LogError(e); - } - - // send specific listeners data - if (deviceFieldCallbacks.ContainsKey(fieldName)) - { - // clear the list of old listeners - deviceFieldCallbacks[fieldName].RemoveAll(e => e.keepAliveObject == null); - - // send the callbacks - foreach (CallbackListener e in deviceFieldCallbacks[fieldName]) + try { - if (!isInitialState || e.sendInitialState) + if (!isInitialState) OnDeviceFieldChanged?.Invoke(fieldName, newValue); + } + catch (Exception e) + { + Debug.LogError(e); + } + + // send specific listeners data + if (deviceFieldCallbacks.ContainsKey(fieldName)) + { + // clear the list of old listeners + deviceFieldCallbacks[fieldName].RemoveAll(e => e.keepAliveObject == null); + + // send the callbacks + foreach (CallbackListener e in deviceFieldCallbacks[fieldName]) { - try + if (!isInitialState || e.sendInitialState) { - e.callback(newValue); - } - catch (Exception ex) - { - Debug.LogError(ex); + try + { + e.callback(newValue); + } + catch (Exception ex) + { + Debug.LogError(ex); + } } } } diff --git a/unity_package/package.json b/unity_package/package.json index 0a9a24a..4cd2c74 100644 --- a/unity_package/package.json +++ b/unity_package/package.json @@ -1,7 +1,7 @@ { "name": "edu.uga.engr.vel.vel-connect", "displayName": "VEL-Connect", - "version": "2.1.3", + "version": "4.0.0", "unity": "2019.1", "description": "Web-based configuration for VR applications", "keywords": [], diff --git a/velconnect-npm/.gitignore b/velconnect-npm/.gitignore new file mode 100644 index 0000000..5e8f25e --- /dev/null +++ b/velconnect-npm/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +dist/ +lib/ \ No newline at end of file diff --git a/velconnect-npm/package-lock.json b/velconnect-npm/package-lock.json new file mode 100644 index 0000000..c7308e9 --- /dev/null +++ b/velconnect-npm/package-lock.json @@ -0,0 +1,50 @@ +{ + "name": "@velaboratory/velconnect", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@velaboratory/velconnect", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "pocketbase": "^0.15.3" + }, + "devDependencies": { + "typescript": "^5.1.6" + } + }, + "node_modules/pocketbase": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.15.3.tgz", + "integrity": "sha512-sjM0XO4wHUlVZs94VhRJi4FeYtbLqvxFbRDJlfjFb/4FkxypbGwxLM4HDAEr8q6jdreuxAM1/n/b5HB1GjQ1Vg==" + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + }, + "dependencies": { + "pocketbase": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.15.3.tgz", + "integrity": "sha512-sjM0XO4wHUlVZs94VhRJi4FeYtbLqvxFbRDJlfjFb/4FkxypbGwxLM4HDAEr8q6jdreuxAM1/n/b5HB1GjQ1Vg==" + }, + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true + } + } +} diff --git a/velconnect-npm/package.json b/velconnect-npm/package.json new file mode 100644 index 0000000..5183fc4 --- /dev/null +++ b/velconnect-npm/package.json @@ -0,0 +1,21 @@ +{ + "name": "@velaboratory/velconnect", + "version": "1.0.0", + "description": "Use VEL-Connect with a dashboard", + "main": "src/index.js", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc --module commonjs" + }, + "author": "VEL", + "license": "MIT", + "dependencies": { + "pocketbase": "^0.19.0" + }, + "devDependencies": { + "typescript": "^5.3.3" + } +} \ No newline at end of file diff --git a/velconnect-npm/src/index.ts b/velconnect-npm/src/index.ts new file mode 100644 index 0000000..577f945 --- /dev/null +++ b/velconnect-npm/src/index.ts @@ -0,0 +1,317 @@ +import PocketBase from "pocketbase"; +import type { Record } from "pocketbase"; + + +export interface Device extends Record { + os_info: string; + friendly_name: string; + current_room: string; + current_app: string; + pairing_code: string; + data: string; + expand: { data?: DataBlock }; +} +export interface DataBlock extends Record { + block_id: string; + owner: string; + data: { [key: string]: string }; +} + +export class VELConnect { + + pb: PocketBase; + debugLog = false; + + constructor() { + this.pb = new PocketBase(); + + this.pb.authStore.onChange((auth) => { + console.log("authStore changed", auth); + currentUser.set(pb.authStore.model); + if (pb.authStore.isValid) { + } + }); + } + + + export const currentUser = writable(pb.authStore.model); + + + + export const pairedDevices = writable([]); + export const currentDeviceId = writable(""); + + +// const device = get(currentDevice); +// if (device == '' && device.length > 0) { +// currentDevice.set(device[0]); +// } + +let unsubscribeDeviceFields: () => void; +let unsubscribeDeviceData: () => void; +let unsubscribeRoomData: () => void; +let unsubscribeCurrentDevice: () => void; +let unsubscribeCurrentUser: () => void; + +export let deviceFields = writable(null); +export let deviceData = writable(null); +export let roomData = writable(null); + +export let sending = false; + +export async function startListening(baseUrl: string) { + pb.baseUrl = baseUrl; + if (get(currentDeviceId) != "") { + const d = (await pb.collection("Device").getOne(get(currentDeviceId), { + expand: "data", + })) as Device; + deviceData.set(d.expand.data as DataBlock); + // we don't need expand anymore, since it doesn't work in subscribe() + d.expand = {}; + deviceFields.set(d); + } + + unsubscribeCurrentDevice = currentDeviceId.subscribe(async (val) => { + log("currentDeviceId subscribe change event"); + unsubscribeDeviceFields?.(); + unsubscribeDeviceData?.(); + if (val != "") { + const d = (await pb + .collection("Device") + .getOne(get(currentDeviceId), { expand: "data" })) as Device; + deviceData.set(d.expand.data as DataBlock); + // we don't need expand anymore, since it doesn't work in subscribe() + d.expand = {}; + deviceFields.set(d); + + unsubscribeDeviceData = await pb + .collection("DataBlock") + .subscribe(d.data, async (data) => { + log("deviceData subscribe change event"); + deviceData.set(data.record as DataBlock); + }); + + unsubscribeDeviceFields = await pb + .collection("Device") + .subscribe(val, async (data) => { + log("deviceFields subscribe change event"); + const d = data.record as Device; + deviceFields.set(d); + + // if the devie changes, the devicedata could change, so we need to resubscribe + unsubscribeDeviceData?.(); + unsubscribeDeviceData = await pb + .collection("DataBlock") + .subscribe(d.data, async (data) => { + log("deviceData subscribe change event"); + deviceData.set(data.record as DataBlock); + }); + + getRoomData(d); + }); + + if (d != null) getRoomData(d); + } else { + deviceFields.set(null); + deviceData.set(null); + roomData.set(null); + } + }); + + unsubscribeCurrentUser = currentUser.subscribe((user) => { + log(`currentUser changed ${user}`); + pairedDevices.set(user?.["devices"] ?? []); + currentDeviceId.set(get(pairedDevices)[0] ?? ""); + }); +} + +export function stopListening() { + unsubscribeCurrentDevice?.(); + unsubscribeDeviceFields?.(); + unsubscribeDeviceData?.(); + unsubscribeRoomData?.(); + unsubscribeCurrentUser?.(); + console.log("Stop listening"); +} + +async function getRoomData(device: Device) { + unsubscribeRoomData?.(); + + // create or just fetch room by name + let r: DataBlock | null = null; + try { + r = (await pb + .collection("DataBlock") + .getFirstListItem( + `block_id="${device.current_app}_${device.current_room}"` + )) as DataBlock; + } catch (e: any) { + r = (await pb.collection("DataBlock").create({ + block_id: `${device.current_app}_${device.current_room}`, + category: "room", + data: {}, + })) as DataBlock; + } + roomData.set(r); + if (r != null) { + unsubscribeRoomData = await pb + .collection("DataBlock") + .subscribe(r.id, (data) => { + log("roomData subscribe change event"); + roomData.set(data.record as DataBlock); + }); + } else { + console.error("Failed to get or create room"); + } +} + +let abortController = new AbortController(); +export function delayedSend() { + console.log("fn: delayedSend()"); + + // abort the previous send + abortController.abort(); + const newAbortController = new AbortController(); + abortController = newAbortController; + setTimeout(() => { + if (!newAbortController.signal.aborted) { + send(); + } else { + console.log("aborted"); + } + }, 1000); +} + +export function send() { + console.log("sending..."); + sending = true; + let promises: Promise[] = []; + const device = get(deviceFields); + const data = get(deviceData); + const room = get(roomData); + // TODO send changes only + if (device) { + promises.push(pb.collection("Device").update(device.id, device)); + } + if (data) { + promises.push(pb.collection("DataBlock").update(data.id, data)); + } + if (room) { + promises.push(pb.collection("DataBlock").update(room.id, room)); + } + Promise.all(promises).then(() => { + sending = false; + }); +} + +export function removeDevice(d: string) { + pairedDevices.set(get(pairedDevices).filter((i) => i != d)); + + if (get(currentDeviceId) == d) { + console.log("Removed current device"); + + // if there are still devices left + if (get(pairedDevices).length > 0) { + currentDeviceId.set(get(pairedDevices)[0] ?? ""); + } else { + currentDeviceId.set(""); + } + } + + const user = get(currentUser); + if (user) { + user["devices"] = user["devices"].filter((i: string) => i != d); + pb.collection("Users").update(user.id, user); + } +} + +async function pair(pairingCode: string) { + try { + // find the device by pairing code + const device = (await pb + .collection("Device") + .getFirstListItem(`pairing_code="${pairingCode}"`)) as Device; + + // add it to the local data + currentDeviceId.set(device.id); + if (!get(pairedDevices).includes(device.id)) { + pairedDevices.set([...get(pairedDevices), device.id]); + } + + // add it to my account if logged in + const u = get(currentUser); + if (u) { + // add the device to the user's devices + u["devices"].push(device.id); + + // add the account data to the device + if ( + u.user_data == null || + u.user_data == undefined || + u.user_data == "" + ) { + // create a new user data block if it doesn't exist on the user already + const userDataBlock = await pb.collection("DataBlock").create({ + category: "device", + data: {}, + owner: u.id, + }); + u.user_data = userDataBlock.id; + } + device["data"] = u.user_data; + device["owner"] = u.id; + device["past_owners"] = [...device["past_owners"], u.id]; + + await pb.collection("Device").update(device.id, device); + await pb.collection("Users").update(u.id, u); + } + + return { error: null }; + } catch (e) { + console.error("Not found: " + e); + if (e == "ClientResponseError 404: The requested resource wasn't found.") { + return { + error: "Device not found with this pairing code.", + }; + } + return { + error: e as string, + }; + } +} + +export async function login(username: string, password: string) { + try { + await pb.collection("Users").authWithPassword(username, password); + return {}; + } catch (err: any) { + return err; + } +} + +export async function signUp(username: string, password: string) { + try { + const data = { + username: username, + password, + passwordConfirm: password, + }; + await pb.collection("Users").create(data); + return await login(username, password); + } catch (err: any) { + return err; + } +} + +export function signOut() { + pb.authStore.clear(); +} + +function log(msg: string) { + if (debugLog) { + console.log(msg); + } +} + + +} \ No newline at end of file diff --git a/velconnect-npm/tsconfig.json b/velconnect-npm/tsconfig.json new file mode 100644 index 0000000..28f1b0d --- /dev/null +++ b/velconnect-npm/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": ["src"], + "compilerOptions": { + "target": "es2018", + "moduleResolution": "node", // don't have to import actual filenames, can import extensionless files + "declaration": true, // generate .d.ts files + "sourceMap": true, // generate source map + "outDir": "dist", // output compiled js, d.ts, and source map to dist folder + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true + } +} diff --git a/velconnect-svelte-npm/package.json b/velconnect-svelte-npm/package.json index 4fd10b3..d94f5ad 100644 --- a/velconnect-svelte-npm/package.json +++ b/velconnect-svelte-npm/package.json @@ -1,6 +1,6 @@ { "name": "@velaboratory/velconnect-svelte", - "version": "1.0.6", + "version": "4.0.0", "description": "Use VEL-Connect with a Svelte dashboard", "main": "src/index.js", "files": [ @@ -13,11 +13,11 @@ "author": "VEL", "license": "MIT", "dependencies": { - "pocketbase": "^0.15.3", - "svelte": "^4.0.5", + "pocketbase": "^0.19.0", + "svelte": "^4.2.8", "@velaboratory/velconnect": "../velconnect-npm" }, "devDependencies": { - "typescript": "^5.1.6" + "typescript": "^5.3.3" } } \ No newline at end of file diff --git a/velconnect/migrations/1701978583_updated_Users.go b/velconnect/migrations/1701978583_updated_Users.go new file mode 100644 index 0000000..92a70e6 --- /dev/null +++ b/velconnect/migrations/1701978583_updated_Users.go @@ -0,0 +1,74 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_") + if err != nil { + return err + } + + // update + edit_user_data := &schema.SchemaField{} + json.Unmarshal([]byte(`{ + "system": false, + "id": "xvw8arlm", + "name": "user_data", + "type": "relation", + "required": false, + "unique": false, + "options": { + "collectionId": "3qwwkz4wb0lyi78", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": null, + "displayFields": [ + "data" + ] + } + }`), edit_user_data) + collection.Schema.AddField(edit_user_data) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_") + if err != nil { + return err + } + + // update + edit_user_data := &schema.SchemaField{} + json.Unmarshal([]byte(`{ + "system": false, + "id": "xvw8arlm", + "name": "user_data", + "type": "relation", + "required": false, + "unique": false, + "options": { + "collectionId": "3qwwkz4wb0lyi78", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": [ + "data" + ] + } + }`), edit_user_data) + collection.Schema.AddField(edit_user_data) + + return dao.SaveCollection(collection) + }) +} diff --git a/velconnect/migrations/1701981023_updated_Users.go b/velconnect/migrations/1701981023_updated_Users.go new file mode 100644 index 0000000..b05bdfe --- /dev/null +++ b/velconnect/migrations/1701981023_updated_Users.go @@ -0,0 +1,74 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_") + if err != nil { + return err + } + + // update + edit_profiles := &schema.SchemaField{} + json.Unmarshal([]byte(`{ + "system": false, + "id": "xvw8arlm", + "name": "profiles", + "type": "relation", + "required": false, + "unique": false, + "options": { + "collectionId": "3qwwkz4wb0lyi78", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": null, + "displayFields": [ + "data" + ] + } + }`), edit_profiles) + collection.Schema.AddField(edit_profiles) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_") + if err != nil { + return err + } + + // update + edit_profiles := &schema.SchemaField{} + json.Unmarshal([]byte(`{ + "system": false, + "id": "xvw8arlm", + "name": "user_data", + "type": "relation", + "required": false, + "unique": false, + "options": { + "collectionId": "3qwwkz4wb0lyi78", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": null, + "displayFields": [ + "data" + ] + } + }`), edit_profiles) + collection.Schema.AddField(edit_profiles) + + return dao.SaveCollection(collection) + }) +}