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

@@ -10,4 +10,32 @@ Haptic configuration tool
## Connecting your Nano ## Connecting your Nano
TODO Plug the nano to the USB port. :-)
ZERO/ONE will automatically detect the Nano_D++ device.
### Device connection states
The connection between the device and the haptic controller software can be in the following states. Entering each state is associated with equivalent events emitted in the ZERO/ONE software:
**Attached**
The device is attached when it is connected to USB.
**Detached**
The device is detached when disconnected from USB.
**Connected**
The device is in connected state when the USB serial port is successfully opened.
**Disconnected**
The device enters disconnected state when the USB serial port is closed.
Note that a **connected** device is also always **attached**, and if the USB plug is pulled on a **connected** device, it becomes both **disconnected** and **detached** simultaneously. If the connection is closed without disconnecting USB, the device becomes **disconnected** but remains **attached**.

View File

@@ -4,6 +4,7 @@ import DevicePreview from '@/components/device/DevicePreview.vue'
import ConfigPane from '@/components/config/ConfigPane.vue' import ConfigPane from '@/components/config/ConfigPane.vue'
import Navbar from '@/components/navbar/Navbar.vue' import Navbar from '@/components/navbar/Navbar.vue'
import { useStore } from '@/store' import { useStore } from '@/store'
import { useMessageHandlers } from '@/device'
const { electron } = window const { electron } = window
const store = useStore() 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> </script>
<template> <template>

View File

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

View File

@@ -26,8 +26,8 @@
<div class="flex gap-2"> <div class="flex gap-2">
<MenubarMenu> <MenubarMenu>
<MenubarTrigger class="app-titlebar-button"> <MenubarTrigger class="app-titlebar-button">
<template v-if="store.multipleDevicesConnected"> <template v-if="store.numAttachedDevices!==1">
Devices<span class="text-zinc-500">&nbsp;(2)</span> Devices<span class="text-zinc-500">&nbsp;({{ ""+store.numAttachedDevices }})</span>
</template> </template>
<template v-else> <template v-else>
Device 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. // Some APIs can only be used after this event occurs.
app.whenReady().then(() => { app.whenReady().then(() => {
ipcMain.handle('nanodevices:list_devices', ()=>nanodevices.list_devices()) 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:disconnect', nanodevices.disconnect)
ipcMain.handle('nanodevices:send', nanodevices.send) ipcMain.handle('nanodevices:send', nanodevices.send)
const mainWindow = createMainWindow() const mainWindow = createMainWindow()

View File

@@ -6,13 +6,13 @@ const { contextBridge, ipcRenderer } = require('electron')
// expose an API to choose available devices // expose an API to choose available devices
contextBridge.exposeInMainWorld('nanodevices', { contextBridge.exposeInMainWorld('nanodevices', {
list_devices() { list_devices() {
ipcRenderer.invoke('nanodevices:list_devices'); return ipcRenderer.invoke('nanodevices:list_devices');
}, },
connect(deviceid) { connect(deviceid) {
ipcRenderer.invoke('nanodevices:connect', deviceid); return ipcRenderer.invoke('nanodevices:connect', deviceid);
}, },
disconnect(deviceid) { disconnect(deviceid) {
ipcRenderer.invoke('nanodevices:disconnect', deviceid); return ipcRenderer.invoke('nanodevices:disconnect', deviceid);
}, },
on_event(eventid_filter, callback) { on_event(eventid_filter, callback) {
console.log("attaching filter for ", eventid_filter); console.log("attaching filter for ", eventid_filter);
@@ -22,6 +22,9 @@ contextBridge.exposeInMainWorld('nanodevices', {
callback(eventid, deviceid, ...data); 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.config.globalProperties.window = window
app.mount('#app') // TODO remove this
window.nanodevices.on_event("*", (eventid, deviceid, ...data) => { window.nanodevices.on_event("*", (eventid, deviceid, ...data) => {
console.log("Event on window ", 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', { export const useStore = defineStore('main', {
state: () => { state: () => {
return { return {
devices: {},
connected: false, // TODO make into getter
connectedId: null,
profileCategories: [], profileCategories: [],
selectedProfileId: null, selectedProfileId: null,
connected: false,
connectedDevices: ['test1', 'test2'], connectedDevices: ['test1', 'test2'],
selectedFeature: 'knob', selectedFeature: 'knob',
selectedKey: 'a', selectedKey: 'a',
@@ -41,6 +43,13 @@ export const useStore = defineStore('main', {
}, },
previewDeviceModel: 'nanoOne', previewDeviceModel: 'nanoOne',
screenOrientation: 90, screenOrientation: 90,
// device state as received from the device
keyState: "abcd",
turns: 0,
angle: 0,
velocity: 0,
last_event: 0
} }
}, getters: { }, getters: {
profiles: (state) => state.profileCategories.flatMap(c => c.profiles), 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, currentConfigComponent: (state) => state.configPages[state.selectedFeature][state.currentConfigPage]?.component || WIP,
currentConfigPages: (state) => state.configPages[state.selectedFeature] || {}, currentConfigPages: (state) => state.configPages[state.selectedFeature] || {},
multipleDevicesConnected: (state) => state.connectedDevices.length > 1, multipleDevicesConnected: (state) => state.connectedDevices.length > 1,
numAttachedDevices: (state) => Object.keys(state.devices).length,
// connected: (state) => state.connectedId !== null,
}, actions: { }, actions: {
selectProfile(id) { selectProfile(id) {
if (!this.profileIds.includes(id)) return false if (!this.profileIds.includes(id)) return false
@@ -166,6 +177,76 @@ export const useStore = defineStore('main', {
cycleScreenOrientation() { cycleScreenOrientation() {
this.screenOrientation = (this.screenOrientation + 90) % 360 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;
},
}, },
}) })