mostly getting device and rooms correctly

dev
Anton Franzluebbers 2023-07-07 22:45:30 -04:00
parent c6670adec0
commit 2555c3082f
12 changed files with 387 additions and 96 deletions

View File

@ -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",

View File

@ -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"
}
}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="icon" href="%sveltekit.assets%/favicons/favicon.ico" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { currentUser, pb } from '../velconnect';
import { currentUser, pb } from '$lib/js/velconnect';
let email: string;
let password: string;

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { currentDevice, currentUser, pb, type Device, pairedDevices } from '../velconnect';
import { currentDevice, currentUser, pb, type DeviceData, pairedDevices } from '$lib/js/velconnect';
let pairingCode: string;
let errorMessage: string | null;
@ -9,7 +9,7 @@
try {
let device = (await pb
.collection('Device')
.getFirstListItem(`pairing_code="${pairingCode}"`)) as Device;
.getFirstListItem(`pairing_code="${pairingCode}"`)) as DeviceData;
// add it to the local data
currentDevice.set(device.id);

View File

@ -0,0 +1,30 @@
import { DateTime } from 'luxon';
import humanizeDuration from 'humanize-duration';
export function prettyDate(date: string | Date | DateTime, includeYear = false) {
if (date == null) return '';
let d: DateTime;
if (date instanceof Date) {
d = DateTime.fromJSDate(date);
} else if (date instanceof DateTime) {
d = date;
} else {
date = date.replace(' ', 'T');
d = DateTime.fromISO(date);
}
// return DateTime.fromISO(date).toFormat("yyyy-LL-dd hh:mm a ZZZZ");
const fromNow = DateTime.utc().minus(d.toMillis()).toMillis();
const yearReplace = includeYear ? '' : ', 2023';
if (fromNow > 0) {
return `${d.toLocaleString(DateTime.DATETIME_MED)} (${humanizeDuration(fromNow, {
round: true,
largest: 1
})} ago)`.replaceAll(yearReplace, '');
} else {
return `${d.toLocaleString(DateTime.DATETIME_MED)} (in ${humanizeDuration(fromNow, {
round: true,
largest: 1
})})`.replaceAll(yearReplace, '');
}
}

View File

@ -18,7 +18,7 @@ export const currentDevice = writable('');
interface HasData extends Record {
data: { [key: string]: string };
}
export interface Device extends Record {
export interface DeviceData extends Record {
current_room: string;
current_app: string;
data: { [key: string]: string };

View File

@ -2,23 +2,26 @@
import { onDestroy, onMount } from 'svelte';
import {
currentDevice,
type Device,
type DeviceData,
pairedDevices,
pb,
type RoomData,
currentUser
} from '../lib/velconnect';
} from '$lib/js/velconnect';
import Login from '$lib/components/Login.svelte';
import Pair from '$lib/components/Pair.svelte';
import { prettyDate } from '$lib/js/util';
if ($currentDevice == '' && $pairedDevices.length > 0) {
currentDevice.set($pairedDevices[0]);
}
let unsubscribeDeviceData: () => void;
let unsubscribeRoomData: () => void;
let unsubscribeCurrentDevice: () => void;
let unsubscribeCurrentUser: () => void;
let deviceData: Device | null;
let deviceData: DeviceData | null;
let roomData: RoomData | null;
let sending = false;
@ -38,21 +41,50 @@
unsubscribeDeviceData?.();
if (val != '') {
deviceData = await pb.collection('Device').getOne($currentDevice);
unsubscribeDeviceData = await pb.collection('Device').subscribe(val, (data) => {
deviceData = data.record as Device;
if (deviceData != null) getRoomData(deviceData);
unsubscribeDeviceData = await pb.collection('Device').subscribe(val, async (data) => {
deviceData = data.record as DeviceData;
getRoomData(deviceData);
});
} else {
deviceData = null;
roomData = null;
}
});
unsubscribeCurrentUser = currentUser.subscribe((user) => {
pairedDevices.set(user?.devices ?? []);
});
});
onDestroy(() => {
unsubscribeCurrentDevice?.();
unsubscribeDeviceData?.();
unsubscribeRoomData?.();
unsubscribeCurrentUser?.();
});
async function getRoomData(deviceData: DeviceData) {
// get room data
unsubscribeRoomData?.();
// create or just fetch room by name
roomData = await fetch(
`${pb.baseUrl}/data_block/${deviceData.current_app}_${deviceData.current_room}`,
{
method: 'POST'
}
).then((r) => r.json());
console.log(roomData);
if (roomData) {
unsubscribeDeviceData = await pb.collection('DataBlock').subscribe(roomData.id, (data) => {
roomData = data.record as RoomData;
unsubscribeRoomData?.();
});
} else {
console.error('Failed to get or create room');
}
}
let abortController = new AbortController();
function delayedSend() {
console.log('fn: delayedSend()');
@ -106,6 +138,10 @@
}
</script>
<svelte:head>
<title>VEL-Connect</title>
</svelte:head>
<h1>VEL-Connect</h1>
<img src="/img/velconnect_logo_1.png" alt="logo" width="70px" height="28px" />
<p>
@ -119,20 +155,22 @@
<div>
<h3>Devices:</h3>
{#each $pairedDevices as d}
<div>
<button
on:click={() => {
currentDevice.set(d);
}}>{d}</button
>
<button
on:click={() => {
removeDevice(d);
}}>x</button
>
</div>
{/each}
<div class="device-list">
{#each $pairedDevices as d}
<div>
<button
on:click={() => {
currentDevice.set(d);
}}>{d}</button
>
<button
on:click={() => {
removeDevice(d);
}}>x</button
>
</div>
{/each}
</div>
{#if $pairedDevices.length == 0}
<p>No devices paired. Enter a pairing code above.</p>
{/if}
@ -158,11 +196,11 @@
</device-field>
<device-field>
<h6>First Seen</h6>
<p>{deviceData.created}</p>
<p>{prettyDate(deviceData.created)}</p>
</device-field>
<device-field>
<h6>Last Seen</h6>
<p>{deviceData.updated}</p>
<p>{prettyDate(deviceData.updated)}</p>
</device-field>
</div>
@ -218,8 +256,11 @@
</device-field>
</div>
<h6>Raw JSON:</h6>
<h3>Raw JSON:</h3>
<h6>Device Data</h6>
<pre><code>{JSON.stringify(deviceData, null, 2)}</code></pre>
<h6>Room Data</h6>
<pre><code>{JSON.stringify(roomData, null, 2)}</code></pre>
{/if}
<style lang="scss">
@ -232,4 +273,19 @@
margin: 0;
}
}
.device-list {
display: flex;
flex-direction: column;
width: fit-content;
gap: 0.5em;
& > div {
display: flex;
gap: 0.2em;
& > button:first-child {
flex-grow: 1;
}
}
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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<string, string> data;
/// <summary>
@ -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<State> OnInitialState;
public static Action<string, string> 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<DeviceField, string>
{
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<DeviceField, string>
{
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<State>(json);
state = JsonConvert.DeserializeObject<State>(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;
}
/// <summary>
/// Sets data on the device keys themselves
/// These are fixed fields defined for every application
/// </summary>
public static void SetDeviceField(State.Device device)
public static void SetDeviceField(Dictionary<DeviceField, string> 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
/// </summary>
public static void SetDeviceData(Dictionary<string, string> 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<string, string> 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<string, object> device = new Dictionary<string, object>
{
{ "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);
}
}
}

View File

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

View File

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