diff --git a/example_dashboard/package-lock.json b/example_dashboard/package-lock.json index 3326ff8..8dc4b44 100644 --- a/example_dashboard/package-lock.json +++ b/example_dashboard/package-lock.json @@ -8,6 +8,8 @@ "name": "example-dashboard", "version": "0.0.1", "dependencies": { + "humanize-duration": "^3.28.0", + "luxon": "^3.3.0", "pocketbase": "^0.15.2" }, "devDependencies": { @@ -1802,6 +1804,11 @@ "node": ">=8" } }, + "node_modules/humanize-duration": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.28.0.tgz", + "integrity": "sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A==" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2034,6 +2041,14 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", + "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", @@ -4383,6 +4398,11 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "humanize-duration": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.28.0.tgz", + "integrity": "sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A==" + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -4563,6 +4583,11 @@ "yallist": "^4.0.0" } }, + "luxon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", + "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==" + }, "magic-string": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", diff --git a/example_dashboard/package.json b/example_dashboard/package.json index 2786ad6..213b694 100644 --- a/example_dashboard/package.json +++ b/example_dashboard/package.json @@ -21,16 +21,18 @@ "eslint-plugin-svelte": "^2.30.0", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", + "sass": "^1.63.6", "svelte": "^4.0.0", "svelte-check": "^3.4.3", "svelte-preprocess": "^5.0.4", "tslib": "^2.4.1", "typescript": "^5.0.0", - "vite": "^4.3.6", - "sass": "^1.63.6" + "vite": "^4.3.6" }, "type": "module", "dependencies": { + "humanize-duration": "^3.28.0", + "luxon": "^3.3.0", "pocketbase": "^0.15.2" } } diff --git a/example_dashboard/src/app.html b/example_dashboard/src/app.html index effe0d0..c2c3773 100644 --- a/example_dashboard/src/app.html +++ b/example_dashboard/src/app.html @@ -2,7 +2,7 @@ - + %sveltekit.head% diff --git a/example_dashboard/src/lib/components/Login.svelte b/example_dashboard/src/lib/components/Login.svelte index 62226fa..446db14 100644 --- a/example_dashboard/src/lib/components/Login.svelte +++ b/example_dashboard/src/lib/components/Login.svelte @@ -1,5 +1,5 @@ + + VEL-Connect + +

VEL-Connect

logo

@@ -119,20 +155,22 @@

Devices:

- {#each $pairedDevices as d} -
- - -
- {/each} +
+ {#each $pairedDevices as d} +
+ + +
+ {/each} +
{#if $pairedDevices.length == 0}

No devices paired. Enter a pairing code above.

{/if} @@ -158,11 +196,11 @@
First Seen
-

{deviceData.created}

+

{prettyDate(deviceData.created)}

Last Seen
-

{deviceData.updated}

+

{prettyDate(deviceData.updated)}

@@ -218,8 +256,11 @@ -
Raw JSON:
+

Raw JSON:

+
Device Data
{JSON.stringify(deviceData, null, 2)}
+
Room Data
+
{JSON.stringify(roomData, null, 2)}
{/if} diff --git a/example_dashboard/static/favicon.png b/example_dashboard/static/favicon.png deleted file mode 100644 index 825b9e6..0000000 Binary files a/example_dashboard/static/favicon.png and /dev/null differ diff --git a/unity_package/Runtime/VELConnectManager.cs b/unity_package/Runtime/VELConnectManager.cs index 4dcb235..f7d2bf7 100644 --- a/unity_package/Runtime/VELConnectManager.cs +++ b/unity_package/Runtime/VELConnectManager.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Reflection; +using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; @@ -35,17 +36,17 @@ namespace VELConnect public class Device { - public readonly string id; - public readonly DateTime created; - public readonly DateTime updated; - public string device_id; - public string os_info; - public string friendly_name; - public string modified_by; - public string current_app; - public string current_room; - public string pairing_code; - public DateTime last_online; + [CanBeNull] public readonly string id; + [CanBeNull] public string created = null; + [CanBeNull] public string updated = null; + [CanBeNull] public string device_id; + [CanBeNull] public string os_info; + [CanBeNull] public string friendly_name; + [CanBeNull] public string modified_by; + [CanBeNull] public string current_app; + [CanBeNull] public string current_room; + [CanBeNull] public string pairing_code; + [CanBeNull] public string last_online; public Dictionary data; /// @@ -88,9 +89,9 @@ namespace VELConnect public class UserCount { - public readonly string id; - public readonly DateTime created; - public readonly DateTime updated; + [CanBeNull] public readonly string id; + public readonly DateTime? created; + public readonly DateTime? updated; public string device_id; public string app_id; public string room_id; @@ -100,7 +101,20 @@ namespace VELConnect public string platform; } + public enum DeviceField + { + device_id, + os_info, + friendly_name, + modified_by, + current_app, + current_room, + pairing_code, + last_online + } + public State lastState; + public State state; public static Action OnInitialState; public static Action OnDeviceFieldChanged; @@ -136,7 +150,7 @@ namespace VELConnect get { Hash128 hash = new Hash128(); - hash.Append(DeviceId); + hash.Append(deviceId); // change once a day hash.Append(DateTime.UtcNow.DayOfYear); // between 1000 and 9999 inclusive (any 4 digit number) @@ -144,52 +158,52 @@ namespace VELConnect } } - private static string DeviceId - { - get - { -#if UNITY_EDITOR - // allows running multiple builds on the same computer - // return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath); - return SystemInfo.deviceUniqueIdentifier + "_EDITOR"; -#else - return SystemInfo.deviceUniqueIdentifier; -#endif - } - } + private static string deviceId; private void Awake() { if (_instance != null) Debug.LogError("VELConnectManager instance already exists", this); _instance = this; + + // Compute device id + MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); + StringBuilder sb = new StringBuilder(SystemInfo.deviceUniqueIdentifier); + sb.Append(Application.productName); +#if UNITY_EDITOR + // allows running multiple builds on the same computer + // return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath); + sb.Append(Application.dataPath); + sb.Append("EDITOR"); +#endif + string id = Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()))); + deviceId = id[..15]; } // Start is called before the first frame update private void Start() { - SetDeviceField(new State.Device + SetDeviceField(new Dictionary { - os_info = SystemInfo.operatingSystem, - friendly_name = SystemInfo.deviceName, - current_app = Application.productName, - pairing_code = PairingCode, + { DeviceField.os_info, SystemInfo.operatingSystem }, + { DeviceField.friendly_name, SystemInfo.deviceName }, + { DeviceField.current_app, Application.productName }, + { DeviceField.pairing_code, PairingCode }, }); - UpdateUserCount(); + // UpdateUserCount(); StartCoroutine(SlowLoop()); VelNetManager.OnJoinedRoom += room => { - SetDeviceField(new State.Device + SetDeviceField(new Dictionary { - current_app = Application.productName, - current_room = room, + { DeviceField.current_app, Application.productName }, + { DeviceField.current_room, room }, }); }; } - private void UpdateUserCount(bool leaving = false) { if (!VelNetManager.InRoom) return; @@ -198,7 +212,7 @@ namespace VELConnect { UserCount postData = new UserCount { - device_id = DeviceId, + device_id = deviceId, app_id = Application.productName, room_id = VelNetManager.Room ?? "", total_users = rooms.rooms.Sum(r => r.numUsers) - (leaving ? 1 : 0), @@ -219,9 +233,9 @@ namespace VELConnect { try { - GetRequestCallback(velConnectUrl + "/state/device/" + DeviceId, json => + GetRequestCallback(velConnectUrl + "/state/device/" + deviceId, json => { - State state = JsonConvert.DeserializeObject(json); + state = JsonConvert.DeserializeObject(json); if (state == null) return; bool isInitialState = false; @@ -380,6 +394,10 @@ namespace VELConnect } lastState = state; + if (lastState?.device?.pairing_code == null) + { + Debug.LogError("Pairing code nulllll"); + } }); } catch (Exception e) @@ -510,33 +528,46 @@ namespace VELConnect return _instance != null ? _instance.lastState?.room?.TryGetData(key) : null; } - /// /// Sets data on the device keys themselves /// These are fixed fields defined for every application /// - public static void SetDeviceField(State.Device device) + public static void SetDeviceField(Dictionary device) { - device.last_online = DateTime.UtcNow; + device[DeviceField.last_online] = DateTime.UtcNow.ToLongDateString(); - // update our local state, so we don't get change events on our own updates - if (_instance.lastState?.device != null) + if (_instance.state?.device != null) { - FieldInfo[] fields = device.GetType().GetFields(); - // loop through all the fields in the device - foreach (FieldInfo fieldInfo in fields) + foreach (DeviceField key in device.Keys.ToArray()) { - fieldInfo.SetValue(_instance.lastState.device, fieldInfo.GetValue(device)); + FieldInfo field = _instance.state.device.GetType().GetField(key.ToString()); + if ((string)field.GetValue(_instance.state.device) != device[key]) + { + if (_instance.lastState?.device != null) + { + // update our local state, so we don't get change events on our own updates + field.SetValue(_instance.lastState.device, device[key]); + } + } + else + { + // don't send this field, since it's the same + device.Remove(key); + } + } + + // last_online field always changes + if (device.Keys.Count <= 1) + { + // nothing changed, don't send + return; } } PostRequestCallback( - _instance.velConnectUrl + "/device/" + DeviceId, - JsonConvert.SerializeObject(device, Formatting.None, new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }) + _instance.velConnectUrl + "/device/" + deviceId, + JsonConvert.SerializeObject(device) ); } @@ -545,23 +576,44 @@ namespace VELConnect /// public static void SetDeviceData(Dictionary data) { - State.Device device = new State.Device + if (_instance.state?.device != null) { - last_online = DateTime.UtcNow, - data = data, - }; - - // update our local state, so we don't get change events on our own updates - if (_instance.lastState?.device != null) - { - foreach (KeyValuePair kvp in data) + foreach (string key in data.Keys.ToList()) { - _instance.lastState.device.data[kvp.Key] = kvp.Value; + // if the value is unchanged from the current state, remove it so we don't double-update + if (_instance.state.device.data.TryGetValue(key, out string val) && val == data[key]) + { + data.Remove(key); + } + else + { + // update our local state, so we don't get change events on our own updates + if (_instance.lastState?.device?.data != null) + { + _instance.lastState.device.data[key] = data[key]; + } + } } + + // nothing was changed + if (data.Keys.Count == 0) + { + return; + } + + // if we have no data, just set the whole thing + if (_instance.lastState?.device != null) _instance.lastState.device.data ??= data; } + + Dictionary device = new Dictionary + { + { "last_online", DateTime.UtcNow.ToLongDateString() }, + { "data", data }, + }; + PostRequestCallback( - _instance.velConnectUrl + "/device/" + DeviceId, + _instance.velConnectUrl + "/device/" + deviceId, JsonConvert.SerializeObject(device, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore @@ -589,6 +641,24 @@ namespace VELConnect data = data }; + // remove keys that already match our current state + if (_instance.state?.room != null) + { + foreach (string key in data.Keys.ToArray()) + { + if (_instance.state.room.data[key] == data[key]) + { + data.Remove(key); + } + } + } + + // if we have no changed values + if (data.Keys.Count == 0) + { + return; + } + // update our local state, so we don't get change events on our own updates if (_instance.lastState?.room != null) { @@ -727,7 +797,7 @@ namespace VELConnect private void OnApplicationFocus(bool focus) { - UpdateUserCount(!focus); + // UpdateUserCount(!focus); } } } \ No newline at end of file diff --git a/velconnect/migrations/1688782403_updated_Device.go b/velconnect/migrations/1688782403_updated_Device.go new file mode 100644 index 0000000..1e793db --- /dev/null +++ b/velconnect/migrations/1688782403_updated_Device.go @@ -0,0 +1,54 @@ +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("fupstz47c55s69f") + if err != nil { + return err + } + + // add + new_current_room_id := &schema.SchemaField{} + json.Unmarshal([]byte(`{ + "system": false, + "id": "wvpaovjo", + "name": "current_room_id", + "type": "relation", + "required": false, + "unique": false, + "options": { + "collectionId": "3qwwkz4wb0lyi78", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": [] + } + }`), new_current_room_id) + collection.Schema.AddField(new_current_room_id) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("fupstz47c55s69f") + if err != nil { + return err + } + + // remove + collection.Schema.RemoveField("wvpaovjo") + + return dao.SaveCollection(collection) + }) +} diff --git a/velconnect/migrations/1688783036_updated_Device.go b/velconnect/migrations/1688783036_updated_Device.go new file mode 100644 index 0000000..42098fc --- /dev/null +++ b/velconnect/migrations/1688783036_updated_Device.go @@ -0,0 +1,54 @@ +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("fupstz47c55s69f") + if err != nil { + return err + } + + // remove + collection.Schema.RemoveField("wvpaovjo") + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("fupstz47c55s69f") + if err != nil { + return err + } + + // add + del_current_room_id := &schema.SchemaField{} + json.Unmarshal([]byte(`{ + "system": false, + "id": "wvpaovjo", + "name": "current_room_id", + "type": "relation", + "required": false, + "unique": false, + "options": { + "collectionId": "3qwwkz4wb0lyi78", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": [] + } + }`), del_current_room_id) + collection.Schema.AddField(del_current_room_id) + + return dao.SaveCollection(collection) + }) +}