working on serial connection to ui

This commit is contained in:
Richard Unger
2024-03-01 01:16:55 +01:00
parent e92e697cfa
commit 462ff5fa65
9 changed files with 262 additions and 61 deletions

View File

@@ -4,6 +4,7 @@ import DevicePreview from '@/components/device/DevicePreview.vue'
import ConfigPane from '@/components/config/ConfigPane.vue'
import Navbar from '@/components/navbar/Navbar.vue'
import { useStore } from '@/store'
import { useMessageHandlers } from '@/device'
const { electron } = window
const store = useStore()
@@ -21,7 +22,18 @@ electron?.onMenu((key) => {
}
})
store.fetchProfiles()
store.fetchProfiles() // TODO remove me!
// handle device events
const handlers = useMessageHandlers(store)
window.nanodevices.on_event('device-attached', (evt, deviceid, data) => store.device_attached(deviceid))
window.nanodevices.on_event('device-detached', (evt, deviceid, data) => store.device_detached(deviceid))
window.nanodevices.on_event('device-error', (evt, deviceid, data) => { /* TODO handle connection errors */ })
window.nanodevices.on_event('connected', (evt, deviceid, data) => store.device_connected(deviceid))
window.nanodevices.on_event('disconnected', (evt, deviceid, data) => store.device_disconnected(deviceid))
window.nanodevices.on_event('update', (evt, deviceid, data) => { handlers.handle_message(data) })
// get list of the currently attached devices
window.nanodevices.list_devices().then((devs)=>store.init_devices(devs))
</script>
<template>

View File

@@ -8,8 +8,11 @@ const NANO_BAUD_RATE = 115200;
class NanoDevices extends EventEmitter {
all_nano_devices = {};
connected_nano_devices = {};
constructor() {
super();
this.all_nano_devices = {};
this.connected_nano_devices = {};
}
_list() {
let p = new Promise((resolve, reject) => {
@@ -49,7 +52,12 @@ class NanoDevices extends EventEmitter {
let lines = connected_port.data.split('\n');
if (lines.length > 1) {
for (let i = 0; i < lines.length - 1; i++) {
this.emit('nanodevices:update', connected_port.serialNumber, lines[i]);
if (lines[i].length > 0) {
if (lines[i].startsWith('{')) // if its a json object
this.emit('nanodevices:update', connected_port.serialNumber, lines[i]);
else
console.log("Device: "+lines[i]); // otherwise just log it
}
}
connected_port.data = lines[lines.length - 1];
}
@@ -80,58 +88,63 @@ class NanoDevices extends EventEmitter {
async connect(deviceid) {
let p = new Promise();
let nano_device = this.all_nano_devices[deviceid];
if (nano_device === undefined) {
p.reject('Device not attached');
}
else {
let port = new SerialPort(nano_device.path, { baudRate: NANO_BAUD_RATE, autoOpen: false });
port.on('error', (err) => {
// forward error to FE
this.emit('nanodevices:error', nano_device.serialNumber, err);
});
port.on('close', (err) => {
if (err && err.disconnected) {
// forward close to FE
this.emit('nanodevices:disconnected', nano_device.serialNumber);
}
delete this.connected_nano_devices[nano_device.serialNumber];
});
port.on('open', () => {
p.resolve(nano_device.serialNumber);
this.connected_nano_devices[nano_device.serialNumber] = { port: port, data: '' };
this.emit('nanodevices:connected', nano_device.serialNumber);
});
port.on('data', (data) => {
let connected_port = this.connected_nano_devices[nano_device.serialNumber];
this._handle_data(connected_port, data);
});
port.open((err)=>{
if (err) {
p.reject(err);
}
});
}
const nanodevices = this;
let p = new Promise((resolve, reject) => {
let nano_device = nanodevices.all_nano_devices[deviceid];
if (nano_device === undefined) {
reject('Device not attached');
}
else {
console.log('nano_device', nano_device);
let port = new SerialPort({ path: nano_device.path, baudRate: NANO_BAUD_RATE, autoOpen: false });
port.on('error', (err) => {
// forward error to FE
nanodevices.emit('nanodevices:error', nano_device.serialNumber, err);
});
port.on('close', (err) => {
if (err && err.disconnected) {
// forward close to FE
nanodevices.emit('nanodevices:disconnected', nano_device.serialNumber);
}
delete nanodevices.connected_nano_devices[nano_device.serialNumber];
});
port.on('open', () => {
resolve(nano_device.serialNumber);
nanodevices.connected_nano_devices[nano_device.serialNumber] = { port: port, data: '' };
nanodevices.emit('nanodevices:connected', nano_device.serialNumber);
});
port.on('data', (data) => {
let connected_port = nanodevices.connected_nano_devices[nano_device.serialNumber];
nanodevices._handle_data(connected_port, data);
});
port.open((err)=>{
if (err) {
reject(err);
}
});
}
});
return p;
};
disconnect(deviceid) {
let p = new Promise();
let nano_device = this.all_nano_devices[deviceid];
if (nano_device === undefined) {
p.reject('Device not attached');
}
else {
if (this.connected_nano_devices[nano_device.serialNumber] === undefined) {
p.reject('Device not connected');
const nanodevices = this;
let p = new Promise((resolve, reject) => {
let nano_device = nanodevices.all_nano_devices[deviceid];
if (nano_device === undefined) {
reject('Device not attached');
}
else {
nano_device.close();
p.resolve(nano_device.serialNumber);
if (nanodevices.connected_nano_devices[nano_device.serialNumber] === undefined) {
reject('Device not connected');
}
else {
nano_device.close();
resolve(nano_device.serialNumber);
}
}
}
});
return p;
};

View File

@@ -26,8 +26,8 @@
<div class="flex gap-2">
<MenubarMenu>
<MenubarTrigger class="app-titlebar-button">
<template v-if="store.multipleDevicesConnected">
Devices<span class="text-zinc-500">&nbsp;(2)</span>
<template v-if="store.numAttachedDevices!==1">
Devices<span class="text-zinc-500">&nbsp;({{ ""+store.numAttachedDevices }})</span>
</template>
<template v-else>
Device

64
src/device.js Normal file
View File

@@ -0,0 +1,64 @@
export const useMessageHandlers = function(store) {
return {
handle_message: (jsonstr) => {
let message = JSON.parse(jsonstr);
if (message.hasOwnProperty('event')) {
this.handle_event_message(message);
}
if (message.hasOwnProperty('p')) {
this.handle_profile_message(message);
}
if (message.hasOwnProperty('profiles')) {
this.handle_profiles_message(message);
}
if (message.hasOwnProperty('settings')) {
this.handle_settings_message(message);
}
if (message.hasOwnProperty('error')) {
this.handle_error_message(message);
}
if (message.hasOwnProperty('debug')) {
console.log("Device: DEBUG: ", message.debug);
}
if (message.hasOwnProperty('idle')) { // TODO remove
console.log("Device present, idle since: ", message.idle);
}
},
handle_event_message: (event) => {
if (event.ks){
store.update_keystates(event.ks);
}
if (event.hasOwnProperty('a')) {
store.update_knob_position(event.t, event.a, event.v);
}
},
handle_profile_message: (profile) => {
// TODO update profile
},
handle_profiles_message: (profiles) => {
// TODO update profiles
},
handle_settings_message: (settings) => {
if (settings.hasOwnProperty('deviceName')) {
store.update_device_name(settings.deviceName);
}
// TODO update other settings - maybe this should be in a separate store? Or in the main store, but move these ifs to a action method in the store?
},
handle_error_message: (error) => {
console.error("Device: ERROR: ", error.error);
// TODO show/handle error in UI?
}
};
};

View File

@@ -129,7 +129,7 @@ const createLoadingWindow = (mainWindow) => {
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
ipcMain.handle('nanodevices:list_devices', ()=>nanodevices.list_devices())
ipcMain.handle('nanodevices:connect', nanodevices.connect)
ipcMain.handle('nanodevices:connect', (event, deviceid)=>nanodevices.connect(deviceid))
ipcMain.handle('nanodevices:disconnect', nanodevices.disconnect)
ipcMain.handle('nanodevices:send', nanodevices.send)
const mainWindow = createMainWindow()

View File

@@ -6,13 +6,13 @@ const { contextBridge, ipcRenderer } = require('electron')
// expose an API to choose available devices
contextBridge.exposeInMainWorld('nanodevices', {
list_devices() {
ipcRenderer.invoke('nanodevices:list_devices');
return ipcRenderer.invoke('nanodevices:list_devices');
},
connect(deviceid) {
ipcRenderer.invoke('nanodevices:connect', deviceid);
return ipcRenderer.invoke('nanodevices:connect', deviceid);
},
disconnect(deviceid) {
ipcRenderer.invoke('nanodevices:disconnect', deviceid);
return ipcRenderer.invoke('nanodevices:disconnect', deviceid);
},
on_event(eventid_filter, callback) {
console.log("attaching filter for ", eventid_filter);
@@ -21,7 +21,10 @@ contextBridge.exposeInMainWorld('nanodevices', {
if (eventid_filter=="*" || eventid_filter==eventid) {
callback(eventid, deviceid, ...data);
}
});
});
},
send(deviceid, jsonstr) {
return ipcRenderer.invoke('nanodevices:send', deviceid, jsonstr);
}
});

View File

@@ -50,10 +50,10 @@ app.use(i18n)
app.config.globalProperties.window = window
app.mount('#app')
// TODO remove this
window.nanodevices.on_event("*", (eventid, deviceid, ...data) => {
console.log("Event on window ", eventid, deviceid, data)
});
window.nanodevices.list_devices();
app.mount('#app')

View File

@@ -15,9 +15,11 @@ const ajv = new Ajv()
export const useStore = defineStore('main', {
state: () => {
return {
devices: {},
connected: false, // TODO make into getter
connectedId: null,
profileCategories: [],
selectedProfileId: null,
connected: false,
connectedDevices: ['test1', 'test2'],
selectedFeature: 'knob',
selectedKey: 'a',
@@ -41,6 +43,13 @@ export const useStore = defineStore('main', {
},
previewDeviceModel: 'nanoOne',
screenOrientation: 90,
// device state as received from the device
keyState: "abcd",
turns: 0,
angle: 0,
velocity: 0,
last_event: 0
}
}, getters: {
profiles: (state) => state.profileCategories.flatMap(c => c.profiles),
@@ -50,6 +59,8 @@ export const useStore = defineStore('main', {
currentConfigComponent: (state) => state.configPages[state.selectedFeature][state.currentConfigPage]?.component || WIP,
currentConfigPages: (state) => state.configPages[state.selectedFeature] || {},
multipleDevicesConnected: (state) => state.connectedDevices.length > 1,
numAttachedDevices: (state) => Object.keys(state.devices).length,
// connected: (state) => state.connectedId !== null,
}, actions: {
selectProfile(id) {
if (!this.profileIds.includes(id)) return false
@@ -166,6 +177,76 @@ export const useStore = defineStore('main', {
cycleScreenOrientation() {
this.screenOrientation = (this.screenOrientation + 90) % 360
},
// devices, device attachment, connection, and disconnection
init_devices(ids) {
console.log("Initializing devices: ", ids);
for (let id of ids)
this.update_devices(id, true);
if (Object.keys(this.devices).length == 1) {
// TODO auto-connect to the device
let deviceid = Object.keys(this.devices)[0];
console.log("Auto-connecting to device ", deviceid);
window.nanodevices.connect(deviceid);
}
},
update_devices(deviceid, attached) {
if (attached) {
if (!this.devices.hasOwnProperty(deviceid))
this.devices[deviceid] = { serialNumber: deviceid, connected: false };
}
else {
if (this.devices.hasOwnProperty(deviceid))
delete this.devices[deviceid]; // TODO maybe mark as detached instead of deleting? then we can remember its name, etc...
}
},
device_attached(deviceid) {
this.update_devices(deviceid, true);
if (Object.keys(this.devices).length == 1) {
// TODO auto-connect to the device
console.log("Auto-connecting to device ", deviceid);
window.nanodevices.connect(deviceid);
}
},
device_detached(deviceid) {
if (this.devices[deviceid].connected) {
// detached event arrived before disconnected event?
this.devices[deviceid].connected = false;
this.connected = false;
}
this.update_devices(deviceid, false);
},
device_connected(deviceid) {
this.devices[deviceid].connected = true;
this.connected = true;
this.connectedId = deviceid;
// TODO load profiles from device
},
device_disconnected(deviceid) {
this.devices[deviceid].connected = false;
this.connected = false;
this.connectedId = null;
// TODO switch UI to disconnected state
},
// device events
update_knob_position(turns, angle, velocity) {
this.turns = turns;
this.angle = angle;
this.velocity = velocity;
this.last_event = Date.now();
},
update_keystate(keystate) {
this.keyState = keystate;
this.last_event = Date.now();
},
// settings changes
update_device_name(name) {
this.devices[this.connectedId].name = name;
},
},
})