velconnect-svelte with easy subscribing

dev
Anton Franzluebbers 2023-07-15 16:26:25 -04:00
parent a0ce622c5d
commit ae3b5b3bd0
5 changed files with 158 additions and 52 deletions

View File

@ -96,15 +96,25 @@
<h3>Settings</h3> <h3>Settings</h3>
<label> <label>
User Name Device Name
<input <input
type="text" type="text"
placeholder="Enter username..." placeholder="Enter friendly device name..."
bind:value={$deviceFields.friendly_name} bind:value={$deviceFields.friendly_name}
on:input={delayedSend} on:input={delayedSend}
/> />
</label> </label>
<label>
Nickname
<input
type="text"
placeholder="Enter nickname..."
bind:value={$deviceData.data.nickname}
on:input={delayedSend}
/>
</label>
<label> <label>
Avatar URL Avatar URL
<a href="https://demo.readyplayer.me" target="blank"> <a href="https://demo.readyplayer.me" target="blank">

View File

@ -50,7 +50,7 @@ namespace VELConnect
[CanBeNull] public string pairing_code; [CanBeNull] public string pairing_code;
[CanBeNull] public string last_online; [CanBeNull] public string last_online;
[CanBeNull] public DeviceExpand expand; [CanBeNull] public DeviceExpand expand;
public DataBlock deviceData => expand.data; public DataBlock deviceData => expand?.data;
public class DeviceExpand public class DeviceExpand
{ {
@ -175,7 +175,7 @@ namespace VELConnect
// allows running multiple builds on the same computer // allows running multiple builds on the same computer
// return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath); // return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath);
sb.Append(Application.dataPath); sb.Append(Application.dataPath);
sb.Append("EDITOR"); sb.Append("EDITOR2");
#endif #endif
string id = Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()))); string id = Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString())));
deviceId = id[..15]; deviceId = id[..15];
@ -349,6 +349,32 @@ namespace VELConnect
} }
} }
} }
// on the initial state, also activate callbacks for null values
if (isInitialState)
{
foreach ((string deviceDataKey, List<CallbackListener> callbackList) in deviceDataCallbacks)
{
if (!state.device.deviceData.data.ContainsKey(deviceDataKey))
{
// send the callbacks
callbackList.ForEach(e =>
{
if (e.sendInitialState)
{
try
{
e.callback(null);
}
catch (Exception ex)
{
Debug.LogError(ex);
}
}
});
}
}
}
} }
} }
@ -394,6 +420,32 @@ namespace VELConnect
} }
} }
} }
// on the initial state, also activate callbacks for null values
if (isInitialState)
{
foreach ((string key, List<CallbackListener> callbackList) in roomDataCallbacks)
{
if (!state.room.data.ContainsKey(key))
{
// send the callbacks
callbackList.ForEach(e =>
{
if (e.sendInitialState)
{
try
{
e.callback(null);
}
catch (Exception ex)
{
Debug.LogError(ex);
}
}
});
}
}
}
} }
lastState = state; lastState = state;
@ -453,6 +505,7 @@ namespace VELConnect
/// <summary> /// <summary>
/// Adds a change listener callback to a particular field name within the Device data JSON. /// Adds a change listener callback to a particular field name within the Device data JSON.
/// If the initial state doesn't contain this key, this sends back null
/// </summary> /// </summary>
public static void AddDeviceDataListener(string key, MonoBehaviour keepAliveObject, Action<string> callback, public static void AddDeviceDataListener(string key, MonoBehaviour keepAliveObject, Action<string> callback,
bool sendInitialState = false) bool sendInitialState = false)
@ -469,11 +522,10 @@ namespace VELConnect
sendInitialState = sendInitialState sendInitialState = sendInitialState
}); });
if (sendInitialState) // if we have already received data, and we should send right away
if (sendInitialState && instance.state != null)
{ {
string val = GetDeviceData(key); string val = GetDeviceData(key);
if (val != null)
{
try try
{ {
callback(val); callback(val);
@ -484,10 +536,10 @@ namespace VELConnect
} }
} }
} }
}
/// <summary> /// <summary>
/// Adds a change listener callback to a particular field name within the Room data JSON. /// Adds a change listener callback to a particular field name within the Room data JSON.
/// If the initial state doesn't contain this key, this sends back null
/// </summary> /// </summary>
public static void AddRoomDataListener(string key, MonoBehaviour keepAliveObject, Action<string> callback, public static void AddRoomDataListener(string key, MonoBehaviour keepAliveObject, Action<string> callback,
bool sendInitialState = false) bool sendInitialState = false)
@ -504,11 +556,10 @@ namespace VELConnect
sendInitialState = sendInitialState sendInitialState = sendInitialState
}); });
if (sendInitialState) // if we have already received data, and we should send right away
if (sendInitialState && instance.state != null)
{ {
string val = GetRoomData(key); string val = GetRoomData(key);
if (val != null)
{
try try
{ {
callback(val); callback(val);
@ -519,16 +570,15 @@ namespace VELConnect
} }
} }
} }
public static string GetDeviceData(string key, string defaultValue = null)
{
return instance != null ? instance.lastState?.device?.TryGetData(key) : defaultValue;
} }
public static string GetDeviceData(string key) public static string GetRoomData(string key, string defaultValue = null)
{ {
return instance != null ? instance.lastState?.device?.TryGetData(key) : null; return instance != null ? instance.lastState?.room?.TryGetData(key) : defaultValue;
}
public static string GetRoomData(string key)
{
return instance != null ? instance.lastState?.room?.TryGetData(key) : null;
} }
/// <summary> /// <summary>
@ -769,6 +819,7 @@ namespace VELConnect
byte[] bodyRaw = Encoding.UTF8.GetBytes(postData); byte[] bodyRaw = Encoding.UTF8.GetBytes(postData);
UploadHandlerRaw uploadHandler = new UploadHandlerRaw(bodyRaw); UploadHandlerRaw uploadHandler = new UploadHandlerRaw(bodyRaw);
webRequest.uploadHandler = uploadHandler; webRequest.uploadHandler = uploadHandler;
webRequest.downloadHandler = new DownloadHandlerBuffer();
webRequest.SetRequestHeader("Content-Type", "application/json"); webRequest.SetRequestHeader("Content-Type", "application/json");
if (headers != null) if (headers != null)
{ {

View File

@ -1,6 +1,6 @@
{ {
"name": "@velaboratory/velconnect-svelte", "name": "@velaboratory/velconnect-svelte",
"version": "1.0.1", "version": "1.0.2",
"description": "Use VEL-Connect with a Svelte dashboard", "description": "Use VEL-Connect with a Svelte dashboard",
"main": "src/index.js", "main": "src/index.js",
"files": [ "files": [

View File

@ -3,6 +3,12 @@ import { writable } from "svelte/store";
import type { Record } from "pocketbase"; import type { Record } from "pocketbase";
import { get } from "svelte/store"; import { get } from "svelte/store";
let debugLog = false;
export function setDebugLog(val: boolean) {
debugLog = val;
}
export const pb = new PocketBase(); export const pb = new PocketBase();
export const currentUser = writable(pb.authStore.model); export const currentUser = writable(pb.authStore.model);
@ -27,6 +33,8 @@ export interface Device extends Record {
expand: { data?: DataBlock }; expand: { data?: DataBlock };
} }
export interface DataBlock extends Record { export interface DataBlock extends Record {
block_id: string;
owner: string;
data: { [key: string]: string }; data: { [key: string]: string };
} }
@ -53,30 +61,45 @@ export async function startListening(baseUrl: string) {
const d = (await pb.collection("Device").getOne(get(currentDeviceId), { const d = (await pb.collection("Device").getOne(get(currentDeviceId), {
expand: "data", expand: "data",
})) as Device; })) as Device;
deviceFields.set(d);
deviceData.set(d.expand.data as DataBlock); 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) => { unsubscribeCurrentDevice = currentDeviceId.subscribe(async (val) => {
console.log("current device changed"); log("currentDeviceId subscribe change event");
unsubscribeDeviceFields?.(); unsubscribeDeviceFields?.();
unsubscribeDeviceData?.(); unsubscribeDeviceData?.();
if (val != "") { if (val != "") {
const d = (await pb const d = (await pb
.collection("Device") .collection("Device")
.getOne(get(currentDeviceId), { expand: "data" })) as Device; .getOne(get(currentDeviceId), { expand: "data" })) as Device;
deviceFields.set(d);
deviceData.set(d.expand.data as DataBlock); deviceData.set(d.expand.data as DataBlock);
// we don't need expand anymore, since it doesn't work in subscribe()
unsubscribeDeviceFields = await pb d.expand = {};
.collection("Device")
.subscribe(val, async (data) => {
const d = data.record as Device;
deviceFields.set(d); deviceFields.set(d);
unsubscribeDeviceData = await pb unsubscribeDeviceData = await pb
.collection("DataBlock") .collection("DataBlock")
.subscribe(d.id, async (data) => { .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); deviceData.set(data.record as DataBlock);
}); });
@ -92,6 +115,7 @@ export async function startListening(baseUrl: string) {
}); });
unsubscribeCurrentUser = currentUser.subscribe((user) => { unsubscribeCurrentUser = currentUser.subscribe((user) => {
log(`currentUser changed ${user}`);
pairedDevices.set(user?.["devices"] ?? []); pairedDevices.set(user?.["devices"] ?? []);
currentDeviceId.set(get(pairedDevices)[0] ?? ""); currentDeviceId.set(get(pairedDevices)[0] ?? "");
}); });
@ -110,16 +134,26 @@ async function getRoomData(device: Device) {
unsubscribeRoomData?.(); unsubscribeRoomData?.();
// create or just fetch room by name // create or just fetch room by name
const r = (await pb let r: DataBlock | null = null;
try {
r = (await pb
.collection("DataBlock") .collection("DataBlock")
.getFirstListItem( .getFirstListItem(
`block_id="${device.current_app}_${device.current_room}"` `block_id="${device.current_app}_${device.current_room}"`
)) as DataBlock; )) 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); roomData.set(r);
if (r) { if (r != null) {
unsubscribeRoomData = await pb unsubscribeRoomData = await pb
.collection("DataBlock") .collection("DataBlock")
.subscribe(r.id, (data) => { .subscribe(r.id, (data) => {
log("roomData subscribe change event");
roomData.set(data.record as DataBlock); roomData.set(data.record as DataBlock);
}); });
} else { } else {
@ -148,9 +182,10 @@ export function send() {
console.log("sending..."); console.log("sending...");
sending = true; sending = true;
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
const room = get(roomData);
const device = get(deviceFields); const device = get(deviceFields);
const data = get(deviceData); const data = get(deviceData);
const room = get(roomData);
// TODO send changes only
if (device) { if (device) {
promises.push(pb.collection("Device").update(device.id, device)); promises.push(pb.collection("Device").update(device.id, device));
} }
@ -195,7 +230,9 @@ export async function pair(pairingCode: string) {
// add it to the local data // add it to the local data
currentDeviceId.set(device.id); currentDeviceId.set(device.id);
if (!get(pairedDevices).includes(device.id)) {
pairedDevices.set([...get(pairedDevices), device.id]); pairedDevices.set([...get(pairedDevices), device.id]);
}
// add it to my account if logged in // add it to my account if logged in
const u = get(currentUser); const u = get(currentUser);
@ -242,26 +279,32 @@ export async function pair(pairingCode: string) {
export async function login(username: string, password: string) { export async function login(username: string, password: string) {
try { try {
await pb.collection("Users").authWithPassword(username, password); await pb.collection("Users").authWithPassword(username, password);
return ""; return {};
} catch (err) { } catch (err: any) {
return err as string; return err;
} }
} }
export async function signUp(username: string, password: string) { export async function signUp(username: string, password: string) {
try { try {
const data = { const data = {
email: username, username: username,
password, password,
passwordConfirm: password, passwordConfirm: password,
}; };
await pb.collection("Users").create(data); await pb.collection("Users").create(data);
return await login(username, password); return await login(username, password);
} catch (err) { } catch (err: any) {
return err as string; return err;
} }
} }
export function signOut() { export function signOut() {
pb.authStore.clear(); pb.authStore.clear();
} }
function log(msg: string) {
if (debugLog) {
console.log(msg);
}
}

View File

@ -147,10 +147,12 @@ func main() {
// apply to the db // apply to the db
if err := dao.SaveRecord(deviceRecord); err != nil { if err := dao.SaveRecord(deviceRecord); err != nil {
return err log.Fatalln(err)
return c.String(500, err.Error())
} }
if err := dao.SaveRecord(deviceDataRecord); err != nil { if err := dao.SaveRecord(deviceDataRecord); err != nil {
return err log.Fatalln(err)
return c.String(500, err.Error())
} }
return c.JSON(http.StatusOK, deviceRecord) return c.JSON(http.StatusOK, deviceRecord)
@ -180,11 +182,11 @@ func main() {
} }
apis.EnrichRecord(c, app.Dao(), deviceRecord, "data") apis.EnrichRecord(c, app.Dao(), deviceRecord, "data")
room, roomErr := app.Dao().FindFirstRecordByData("DataBlock", "block_id", deviceRecord.GetString("current_app")+"_"+deviceRecord.GetString("current_room")) room, _ := app.Dao().FindFirstRecordByData("DataBlock", "block_id", deviceRecord.GetString("current_app")+"_"+deviceRecord.GetString("current_room"))
user, _ := app.Dao().FindRecordById("Users", deviceRecord.GetString("owner")) user, _ := app.Dao().FindRecordById("Users", deviceRecord.GetString("owner"))
log.Println(deviceRecord.GetString("current_room")) // log.Println(deviceRecord.GetString("current_room"))
log.Println(roomErr) // log.Println(roomErr)
output := map[string]interface{}{ output := map[string]interface{}{
"device": deviceRecord, "device": deviceRecord,