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
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 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;
},
},
})