Skip to content

Commit

Permalink
implement ui to edit existing channels
Browse files Browse the repository at this point in the history
  • Loading branch information
liamcottle committed Nov 28, 2024
1 parent b41af44 commit 542deee
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 131 deletions.
153 changes: 36 additions & 117 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"license": "ISC",
"dependencies": {
"@meshtastic/js": "^2.3.7-5",
"@tailwindcss/forms": "^0.5.9",
"@vitejs/plugin-vue": "^5.2.0",
"axios": "^1.7.7",
"click-outside-vue3": "^4.0.1",
Expand Down
158 changes: 158 additions & 0 deletions src/components/pages/settings/NodeChannelSettingsPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<template>
<Page>

<!-- app bar -->
<AppBar title="Channel Settings" :subtitle="subtitle">
<template v-slot:leading>
<NodeIcon v-if="node" :node-id="node.num" class="mr-3"/>
</template>
<template v-slot:trailing>
<SaveButton :is-saving="isSaving" @click="save"/>
</template>
</AppBar>

<!-- data -->
<div class="flex h-full w-full overflow-hidden">
<div class="w-full overflow-y-auto">

<div class="bg-white divide-y">

<!-- channel name -->
<div class="w-full p-2">
<div class="block mb-2 text-sm font-medium text-gray-900">Name</div>
<input v-model="channelName" type="text" placeholder="e.g: Private" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
</div>

<!-- channel role -->
<div class="w-full p-2">
<div class="block mb-2 text-sm font-medium text-gray-900">Role</div>
<select v-model="channelRole" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option :value="Protobuf.Channel.Channel_Role.PRIMARY">Primary</option>
<option :value="Protobuf.Channel.Channel_Role.SECONDARY">Secondary</option>
<option :value="Protobuf.Channel.Channel_Role.DISABLED">Disabled</option>
</select>
</div>

<!-- pre shared key -->
<div class="w-full p-2">
<div class="block mb-2 text-sm font-medium text-gray-900">Pre-Shared Key</div>
<input v-model="channelKey" type="text" placeholder="e.g: AQ==" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
</div>

</div>

</div>
</div>

</Page>
</template>

<script>
import Page from "../Page.vue";
import AppBar from "../../AppBar.vue";
import SaveButton from "../../SaveButton.vue";
import GlobalState from "../../../js/GlobalState.js";
import NodeIcon from "../../nodes/NodeIcon.vue";
import NodeUtils from "../../../js/NodeUtils.js";
import {Protobuf} from "@meshtastic/js";
import PacketUtils from "../../../js/PacketUtils.js";
import NodeAPI from "../../../js/NodeAPI.js";
import DialogUtils from "../../../js/DialogUtils.js";
export default {
name: 'node.settings.channels.edit',
components: {
NodeIcon,
Page,
AppBar,
SaveButton,
},
props: {
nodeId: String | Number,
channelId: String | Number,
},
data() {
return {
channelName: null,
channelRole: null,
channelKey: null,
isSaving: false,
};
},
mounted() {
// load channel info
this.channelName = this.channel?.settings?.name;
this.channelRole = this.channel?.role;
this.channelKey = this.channel?.settings?.psk ? PacketUtils.uInt8ArrayToBase64(this.channel?.settings?.psk) : null;
},
methods: {
getNodeLongName: (nodeId) => NodeUtils.getNodeLongName(nodeId),
async save() {
// do nothing if already saving
if(this.isSaving){
return;
}
// mark as saving
this.isSaving = true;
// create a clone of the channel object
const channel = Protobuf.Channel.Channel.fromJson(this.channel.toJson());
// set local values on channel object
channel.settings.name = this.channelName;
channel.role = this.channelRole;
channel.settings.psk = PacketUtils.base64ToUInt8Array(this.channelKey);
// save channel to node
try {
// save
await NodeAPI.remoteAdminSetChannel(this.nodeId, channel);
alert("Channel saved successfully");
// save was successful, update original object from previous page in ui
this.channel.settings.name = channel.settings.name;
this.channel.role = channel.role;
this.channel.settings.psk = channel.settings.psk;
// fixme: don't really want to do this here?
// update global state if this was our own node
if(this.nodeId.toString() === GlobalState.myNodeId.toString()){
const channelsByIndex = {};
for(const channel of GlobalState.remoteNodeChannels[this.nodeId]){
channelsByIndex[channel.index] = channel;
}
GlobalState.channelsByIndex = channelsByIndex;
}
} catch(e) {
DialogUtils.showErrorAlert(e);
}
// no longer saving
this.isSaving = false;
},
},
computed: {
Protobuf() {
return Protobuf;
},
node() {
return GlobalState.nodesById[this.nodeId];
},
channel() {
return GlobalState.remoteNodeChannels[this.nodeId].find((channel) => {
return channel.index.toString() === this.channelId.toString();
});
},
subtitle() {
return this.node ? this.getNodeLongName(this.node.num) : "Unknown Node";
},
},
}
</script>
30 changes: 18 additions & 12 deletions src/components/pages/settings/NodeChannelsSettingsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,22 @@
<div v-else-if="loraConfig != null && channels != null" class="flex h-full w-full overflow-hidden">

<div class="w-full overflow-y-auto">
<div v-for="channel of channels" class="flex cursor-pointer p-2 bg-white hover:bg-gray-50">
<div class="flex my-auto mr-4 size-12 bg-gray-200 rounded-full text-black">
<span class="mx-auto my-auto">{{ channel.index }}</span>
</div>
<div class="my-auto">
<div>{{ getChannelName(channel) }}</div>
<div class="text-sm text-gray-500">
<span v-if="channel.role === Protobuf.Channel.Channel_Role.PRIMARY">Primary Channel</span>
<span v-else-if="channel.role === Protobuf.Channel.Channel_Role.SECONDARY">Secondary Channel</span>
<span v-else-if="channel.role === Protobuf.Channel.Channel_Role.DISABLED">Disabled Channel</span>
<span v-else>Unknown Channel Role</span>
<RouterLink v-for="channel of channels" :to="{ name: 'node.settings.channels.edit', params: { nodeId: node.num, channelId: channel.index } }">
<div class="flex cursor-pointer p-2 bg-white hover:bg-gray-50">
<div class="flex my-auto mr-4 size-12 bg-gray-200 rounded-full text-black">
<span class="mx-auto my-auto">{{ channel.index }}</span>
</div>
<div class="my-auto">
<div>{{ getChannelName(channel) }}</div>
<div class="text-sm text-gray-500">
<span v-if="channel.role === Protobuf.Channel.Channel_Role.PRIMARY">Primary Channel</span>
<span v-else-if="channel.role === Protobuf.Channel.Channel_Role.SECONDARY">Secondary Channel</span>
<span v-else-if="channel.role === Protobuf.Channel.Channel_Role.DISABLED">Disabled Channel</span>
<span v-else>Unknown Channel Role</span>
</div>
</div>
</div>
</div>
</RouterLink>
</div>

</div>
Expand Down Expand Up @@ -156,6 +158,10 @@ export default {
this.loraConfig = loraConfig;
this.channels = channels;
// update global state
// fixme: this is used to access cached channels from channel settings page
GlobalState.remoteNodeChannels[this.nodeId] = channels;
} catch(e) {
// check if this is a routing error
Expand Down
3 changes: 3 additions & 0 deletions src/js/GlobalState.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const globalState = reactive({
channelsByIndex: {},
nodesById: {},

// cache channels fetched from remote nodes
remoteNodeChannels: {},

});

export default globalState;
19 changes: 19 additions & 0 deletions src/js/NodeAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,25 @@ class NodeAPI {

}

/**
* Sends an admin request to set a channel for the provided node id
* @param nodeId
* @param channel
* @param timeoutMillis
* @returns {Promise<*>}
*/
static async remoteAdminSetChannel(nodeId, channel, timeoutMillis) {

// create admin message packet
const adminMessageRequest = Protobuf.Admin.AdminMessage.fromJson({
setChannel: channel.toJson(),
});

// send packet and wait for response
await this.sendAdminPacketAndWaitForResponse(nodeId, adminMessageRequest, false, timeoutMillis);

}

}

export default NodeAPI;
9 changes: 9 additions & 0 deletions src/js/PacketUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ class PacketUtils {
return btoa(binary);
}

static base64ToUInt8Array(base64) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for(var i = 0; i < binaryString.length; i++){
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}

}

export default PacketUtils;
6 changes: 6 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ const router = createRouter({
props: true,
component: () => import("./components/pages/settings/NodeChannelsSettingsPage.vue"),
},
{
name: "node.settings.channels.edit",
path: '/nodes/:nodeId/settings/channels/:channelId',
props: true,
component: () => import("./components/pages/settings/NodeChannelSettingsPage.vue"),
},
{
name: "node.traceroutes",
path: '/nodes/:nodeId/traceroutes',
Expand Down
6 changes: 4 additions & 2 deletions tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import formsPlugin from '@tailwindcss/forms';

/** @type {import('tailwindcss').Config} */
export default {
content: [
Expand All @@ -10,6 +12,6 @@ export default {
},
},
plugins: [

formsPlugin,
],
}
};

0 comments on commit 542deee

Please sign in to comment.