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
@@ -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)
+ })
+}