I'm trying to translate a javascript device controller into Rust. The Rust code is able to initialize, change display colors, and read buttons on the RazerStreamControllerX. It fails to deinitialize the device properly. The javascript code, on the other hand, successfully deinitializes the device onj shutdown. Both implementations will be posted below, take a look and provide suggestions to fix the Rust code.

// RUST CODE (main.rs) //

mod graphics;

use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc::Sender;
use tokio::sync::oneshot;
use std::io::Result;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::time;
use tokio_serial::SerialPortBuilderExt;

const PORT: &str = "COM4";
const BAUD_RATE: u32 = 9600;
const WS_UPGRADE_HEADER: &str = "GET /index.html
HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: 123abc

";
const WS_UPGRADE_RESPONSE: &str = "HTTP/1.1";

#[tokio::main]
async fn main() -> Result<()> {
    let (tx_event, _) = broadcast::channel(10);
    // TODO: Use "tx_pending_send" to write things to the device
    let (tx_pending_send, rx_pending_send): (Sender<Vec<u8>>, _) = mpsc::channel(100);
    let mut serial = tokio_serial::new(PORT, BAUD_RATE)
        .open_native_async()
        .expect("Failed to open port");

    // Perform WebSocket upgrade
    serial.writable().await.unwrap();
    serial.try_write(WS_UPGRADE_HEADER.as_bytes())
        .expect("Failed to write to port");
    serial.readable().await.unwrap();
    let mut buf = vec![0; 1024];
    serial.try_read(buf.as_mut_slice())
        .expect("Failed to read from port");
    let res = String::from_utf8_lossy(&buf);
    if !res.contains(WS_UPGRADE_RESPONSE) {
        return Err(std::io::Error::new(
            std::io::ErrorKind::Other,
            format!("Failed to upgrade to websocket: {res:x?}"),
        ));
    }

    // Start the main communication loop
    let comms = tokio::spawn(communication_loop(serial, tx_event.clone(), rx_pending_send));

    // Handle events
    let (r1,) = tokio::join!
    (
        event_handler(tx_event, tx_pending_send)
    );

    comms.abort();
    r1.unwrap();
    Ok(())
}

async fn communication_loop(
    mut serial: tokio_serial::SerialStream,
    tx_event: broadcast::Sender<Event>,
    mut rx_pending_send: mpsc::Receiver<Vec<u8>>,
) {
    loop
    {
        let mut single_byte = [0; 1];
        tokio::select! 
        {
            Some(message) = rx_pending_send.recv() => 
            {
                // Try to write everything we can from the port
                time::sleep(time::Duration::from_millis(2)).await;
                serial.write_all(&message).await.unwrap();
            },
            Ok(_) = serial.read_exact(&mut single_byte) => 
            {
                println!("Got: {single_byte:?}");
                if single_byte[0] == 0x82 {
                    // Found delimiter
                    serial.read_exact(&mut single_byte).await.unwrap();
                    let length = single_byte[0] as usize;
                    let mut data = vec![0; length];
                    serial.read_exact(&mut data).await.unwrap();
                    if let Ok(Some(event)) = parse_serial_message(&data) {
                        tx_event.send(event).unwrap();
                    }
                }
            },
        };
    }
}

async fn event_handler(tx_event: broadcast::Sender<Event>, tx_pending_send: mpsc::Sender<Vec<u8>>) -> Result<()>
{
    let mut rx_event = tx_event.subscribe();
    loop
    {
        match rx_event.recv().await
        {
            Ok(next_event) => 
            {
                match next_event 
                {
                    Event::ButtonPress(ButtonPressEvent 
                    {
                        tx_id,
                        button: Button::B42,
                        dir: PressDirection::Down,
                    }) => 
                    {
                        println!("Button down: {tx_id:?}:{:?}:{:?}", Button::B42, PressDirection::Down);

                        // INCORRECT CODE BLOCK STARTS HERE //
                        let msg = crate::graphics::construct_basic_message(vec![0x88, 0x80], 0u16.to_le_bytes().to_vec()).unwrap();
                        let msg = crate::graphics::construct_message_payload(msg, tx_id + 1);
                        tx_pending_send.send(msg).await.unwrap();
                        let msg = crate::graphics::construct_basic_message(vec![0x21, 0x22], 0u16.to_le_bytes().to_vec()).unwrap();
                        let msg = crate::graphics::construct_message_payload(msg, tx_id + 2);
                        tx_pending_send.send(msg).await.unwrap();
                        // END OF INCORRECT CODE BLOCK //
                        return Ok(());
                    }
                    Event::ButtonPress(ButtonPressEvent 
                    {
                        tx_id,
                        button,
                        dir: PressDirection::Down,
                    }) => 
                    {
                        println!("Button down: {tx_id:?}:{button:?}:{:?}", PressDirection::Down);

                        // Message sending logic TODO: Ttoally hard-coded here, not the real spot
                        let (response_tx, response_rx): (oneshot::Sender<Event>, oneshot::Receiver<Event>) = oneshot::channel();
                        // Generate message
                        let msg = crate::graphics::draw_key(button.get_x().into(), button.get_y().into(), &vec![0xFF; 96 * 96 * 2]);
                        let msg = crate::graphics::construct_message_payload(msg, tx_id + 1);

                        // TODO: At this point, spawn a task to handle expected event
                        let mut rx_event = tx_event.subscribe();
                        tokio::spawn(async move {
                            while let Ok(res) = rx_event.recv().await {
                                let evt_tx_id = graphics::get_tx_id(res.clone());

                                if evt_tx_id.is_some() && evt_tx_id.unwrap() == tx_id {
                                    println!("Sending event response");
                                    response_tx.send(res).unwrap();
                                    break;
                                }
                            }
                        });

                        {
                            let tx_pending_send = tx_pending_send.clone();
                            tokio::spawn(async move {
                                tx_pending_send.send(msg).await.unwrap();
                            });
                        }

                        // TODO: Wait on expected event, just throwing it out here
                        tokio::spawn(async move {
                            response_rx.await.unwrap();
                        });

                    },
                    Event::ButtonPress(ButtonPressEvent 
                    {
                        tx_id,
                        button,
                        dir: PressDirection::Up,
                    }) => 
                    {
                        println!("Button up: {tx_id:?}:{button:?}:{:?}", PressDirection::Up);
                    },
                    event => 
                    {
                        println!("Unhandled event: {event:?}");
                    }
                }
            }
            Err(err) => 
            {
                eprintln!("{err:?}");
            }
        }
    }
}

fn parse_serial_message(message: &[u8]) -> Result<Option<Event>> {
    let header: u16 = u16::from_be_bytes([message[0], message[1]]);
    // println!("Message type: {:?}", header);

    let message_type = MessageHeader::try_from(header).unwrap();
    let tx_id = message[2];

    match message_type {
        MessageHeader::ButtonPress => {
            let button = Button::try_from(message[3]).expect("Invalid button address");
            let dir = PressDirection::try_from(message[4]).expect("Invalid button direction");
            Ok(Some(Event::ButtonPress(ButtonPressEvent {
                button,
                dir,
                tx_id,
            })))
        }
        MessageHeader::SerialIn => Ok(Some(Event::SerialIn(SerialInEvent {
            tx_id,
            serial_number: String::from_utf8(message[3..].to_vec())
                .unwrap()
                .trim()
                .to_string(),
        }))),
        MessageHeader::VersionIn => Ok(Some(Event::VersionIn(VersionInEvent {
            tx_id,
            version: format!("{}.{}.{}", message[3], message[4], message[5]).to_string(),
        }))),
        MessageHeader::ConfirmFrameBuffer => Ok(Some(Event::ConfirmFrameBufferIn(
            ConfirmFrameBufferInEvent { tx_id },
        ))),
        MessageHeader::DrawIn => Ok(Some(Event::DrawIn(DrawInEvent { tx_id }))),
        _ => Ok(Some(Event::Other(FallbackEvent { tx_id }))),
    }
}

#[derive(Debug, Clone, PartialEq)]
#[repr(u16)]
pub enum MessageHeader {
    Confirm = 0x0302,
    SerialOut = 0x0303,
    VersionOut = 0x0307,
    Tick = 0x0400,
    SetBrightness = 0x0409,
    ConfirmFrameBuffer = 0x0410,
    ButtonPress = 0x0500,
    Reset = 0x0506,
    DrawOut = 0x050F,
    DrawIn = 0x040F,
    SetColor = 0x0702,
    VersionIn = 0x0c07,
    MCU = 0x180d,
    SerialIn = 0x1f03,
    WriteFrameBuffer = 0xff10,
}

impl TryFrom<u16> for MessageHeader 
{
    type Error = u16;
    fn try_from(value: u16) -> std::result::Result<Self, Self::Error> 
    {
        let result = match value 
        {
            0x0302 => Ok(MessageHeader::Confirm),
            0x0303 => Ok(MessageHeader::SerialOut),
            0x0307 => Ok(MessageHeader::VersionOut),
            0x0400 => Ok(MessageHeader::Tick),
            0x0409 => Ok(MessageHeader::SetBrightness),
            0x0410 => Ok(MessageHeader::ConfirmFrameBuffer),
            0x0500 => Ok(MessageHeader::ButtonPress),
            0x050F => Ok(MessageHeader::DrawOut),
            0x040F => Ok(MessageHeader::DrawIn),
            0x0506 => Ok(MessageHeader::Reset),
            0x0702 => Ok(MessageHeader::SetColor),
            0x0c07 => Ok(MessageHeader::VersionIn),
            0x180d => Ok(MessageHeader::MCU),
            0x1f03 => Ok(MessageHeader::SerialIn),
            0xff10 => Ok(MessageHeader::WriteFrameBuffer),
            value => Err(value),
        };
        println!("Message: {result:?}");
        result
    }
}

impl From<MessageHeader> for Vec<u8> {
    fn from(value: MessageHeader) -> Vec<u8> {
        vec![((value.clone() as u16) >> 8) as u8, value.clone() as u8]
    }
}

#[derive(Debug, Clone)]
#[repr(u8)]
pub enum Button 
{
    B00 = 27,
    B10 = 28,
    B20 = 29,
    B30 = 30,
    B40 = 31,
    B01 = 32,
    B11 = 33,
    B21 = 34,
    B31 = 35,
    B41 = 36,
    B02 = 37,
    B12 = 38,
    B22 = 39,
    B32 = 40,
    B42 = 41,
}

impl Button
{
    pub fn get_x(&self) -> u8
    {
        match self
        {
            Self::B00 | Self::B01 | Self::B02 => 0,
            Self::B10 | Self::B11 | Self::B12 => 1,
            Self::B20 | Self::B21 | Self::B22 => 2,
            Self::B30 | Self::B31 | Self::B32 => 3,
            Self::B40 | Self::B41 | Self::B42 => 4,
        }
    }

    pub fn get_y(&self) -> u8
    {
        match self
        {
            Self::B00 | Self::B10 | Self::B20 | Self::B30 | Self::B40 => 0,
            Self::B01 | Self::B11 | Self::B21 | Self::B31 | Self::B41 => 1,
            Self::B02 | Self::B12 | Self::B22 | Self::B32 | Self::B42 => 2,
        }
    }
}

impl TryFrom<u8> for Button 
{
    type Error = u8;
    fn try_from(value: u8) -> std::result::Result<Self, Self::Error>
    {
        match value 
        {
            27 => Ok(Button::B00),
            28 => Ok(Button::B10),
            29 => Ok(Button::B20),
            30 => Ok(Button::B30),
            31 => Ok(Button::B40),
            32 => Ok(Button::B01),
            33 => Ok(Button::B11),
            34 => Ok(Button::B21),
            35 => Ok(Button::B31),
            36 => Ok(Button::B41),
            37 => Ok(Button::B02),
            38 => Ok(Button::B12),
            39 => Ok(Button::B22),
            40 => Ok(Button::B32),
            41 => Ok(Button::B42),
            value => Err(value),
        }
    }
}

#[derive(Debug, Clone)]
#[repr(u8)]
pub enum PressDirection {
    Up = 0x00,
    Down = 0x01,
}

impl TryFrom<u8> for PressDirection 
{
    type Error = u8;
    fn try_from(value: u8) -> std::result::Result<Self, Self::Error> 
    {
        match value 
        {
            0x00 => Ok(PressDirection::Down),
            0x01 => Ok(PressDirection::Up),
            value => Err(value),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ButtonPressEvent {
    pub tx_id: u8,
    pub button: Button,
    pub dir: PressDirection,
}

#[derive(Debug, Clone)]
pub struct FallbackEvent {
    pub tx_id: u8,
}

#[derive(Debug, Clone)]
pub struct SerialInEvent {
    pub tx_id: u8,
    pub serial_number: String,
}

#[derive(Debug, Clone)]
pub struct ConfirmFrameBufferInEvent {
    pub tx_id: u8,
}

#[derive(Debug, Clone)]
pub struct VersionInEvent {
    pub tx_id: u8,
    pub version: String,
}

#[derive(Debug, Clone)]
pub struct DrawInEvent {
    pub tx_id: u8,
}

#[derive(Debug, Clone)]
pub enum Event {
    ButtonPress(ButtonPressEvent),
    Other(FallbackEvent),
    SerialIn(SerialInEvent),
    VersionIn(VersionInEvent),
    ConfirmFrameBufferIn(ConfirmFrameBufferInEvent),
    DrawIn(DrawInEvent),
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Screen;

impl From<Screen> for u16 
{
    fn from(_: Screen) -> u16 
    {
        0x004D
    }
}

impl TryFrom<u8> for Screen 
{
    type Error = u8;
    fn try_from(value: u8) -> std::result::Result<Self, Self::Error> 
    {
        match value 
        {
            0x4D => Ok(Screen),
            value => Err(value),
        }
    }
}

impl From<Screen> for Vec<u8> 
{
    fn from(value: Screen) -> Vec<u8> 
    {
        let base = u16::from(value);
        vec![((base.clone() as u16) >> 8) as u8, base.clone() as u8]
    }
}

// Rust code (graphics.rs) //

use tokio::sync::oneshot::{self, error::RecvError};

use crate::{ButtonPressEvent, ConfirmFrameBufferInEvent, DrawInEvent, Event, FallbackEvent, MessageHeader, Screen, SerialInEvent, VersionInEvent};


const KEY_SIZE: u16 = 96;

pub fn draw_key(key_x: u16, key_y: u16, img: &[u8]) -> Vec<u8>
{
    let x: u16 = KEY_SIZE * key_x;
    let y: u16 = KEY_SIZE * key_y;
    let width: u16 = 96;
    let height: u16 = 96;

    draw_buffer(x, y, width, height, img).unwrap()
}

pub fn draw_buffer(
    x: u16,
    y: u16,
    width: u16,
    height: u16,
    buffer: &[u8],
) -> std::io::Result<Vec<u8>> {
    let buff_res = construct_draw_buffer_payload(x, y, width, height, buffer);

    if buff_res.is_err() {
        return Err(buff_res.unwrap_err());
    }

    // TODO: Expect event is TRUE after this is sent

    construct_basic_message(Vec::from(MessageHeader::WriteFrameBuffer), buff_res.unwrap())
}

fn construct_draw_buffer_payload(
    x: u16,
    y: u16,
    width: u16,
    height: u16,
    buffer: &[u8],
) -> std::io::Result<Vec<u8>> {
    if (width * height * 2) as usize != buffer.len() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Invalid buffer length. Got {}. Expected {}", width * height * 2, buffer.len()),
        ));
    }

    let mut buff = Vec::new();
    buff.append(&mut Vec::from(Screen));
    buff.append(&mut x.to_be_bytes().to_vec());
    buff.append(&mut y.to_be_bytes().to_vec());
    buff.append(&mut width.to_be_bytes().to_vec());
    buff.append(&mut height.to_be_bytes().to_vec());
    buff.append(&mut buffer.to_vec());

    return Ok(buff);
}

pub fn construct_basic_message(action: Vec<u8>, data: Vec<u8>) -> std::io::Result<Vec<u8>> {
    let mut header: Vec<u8> = Vec::with_capacity(3);
    header.push(action[0]);
    header.push(action[1]);
    header.push(0x01);

    let mut message: Vec<u8> = Vec::with_capacity(header.len() + data.len());
    message.extend_from_slice(&header);
    message.extend_from_slice(&data);

    return Ok(message);
}

pub fn construct_message_payload(message_buffer: Vec<u8>, tx_id: u8) -> Vec<u8> {
    // construct_basic_message(Vec::from(MessageHeader::WriteFrameBuffer), buff_res.unwrap())
    let mut prefix_buff: Vec<u8>;

    if message_buffer.len() > 0xff {
        prefix_buff = Vec::with_capacity(14);

        for _i in 0..14 {
            prefix_buff.push(0x00);
        }

        prefix_buff[0] = 0x82;
        prefix_buff[1] = 0xFF;

        let offset = 6;
        // println!("Message length: {:?}", message_buffer.len());

        (message_buffer.len() as u32)
            .to_be_bytes()
            .iter()
            .enumerate()
            .for_each(|(i, b)| {
                prefix_buff[offset + i] = *b;
            });
    } else {
        prefix_buff = Vec::with_capacity(6);

        for _i in 0..6 {
            prefix_buff.push(0x00);
        }

        prefix_buff[0] = 0x82;
        prefix_buff[1] = 0x80 + message_buffer.len() as u8;
    }

    let mut message = Vec::with_capacity(prefix_buff.len() + message_buffer.len());
    message.extend_from_slice(&prefix_buff);
    message.extend_from_slice(&message_buffer);

    message[prefix_buff.len() + 2] = tx_id;

    message
}

pub fn get_tx_id(evt: Event) -> Option<u8> {
    match evt {
        Event::ButtonPress(ButtonPressEvent { tx_id, .. }) => Some(tx_id),
        Event::Other(FallbackEvent { tx_id }) => Some(tx_id),
        Event::SerialIn(SerialInEvent { tx_id, .. }) => Some(tx_id),
        Event::VersionIn(VersionInEvent { tx_id, .. }) => Some(tx_id),
        Event::ConfirmFrameBufferIn(ConfirmFrameBufferInEvent { tx_id }) => Some(tx_id),
        Event::DrawIn(DrawInEvent { tx_id }) => Some(tx_id),
        // _ => panic!("Invalid event"),
    }
}

// JAVASCRIPT CODE (device.js) //

import { Emitter as EventEmitter } from 'strict-event-emitter'
import rgba from 'color-rgba'

let SerialConnection, WSConnection
// Only import when in a browser environment
if (typeof navigator !== 'undefined' && navigator.serial || import.meta.env?.PROD) {
    SerialConnection = (await import('./connections/web-serial.js')).default
} else {
    SerialConnection = (await import('./connections/serial.js')).default
    WSConnection = (await import('./connections/ws.js')).default
}

let canvasModule
try {
    canvasModule = await import('canvas')
// eslint-disable-next-line
} catch (e) {
    // No canvas is ok, do check in `drawCanvas`
}

import {
    BUTTONS,
    COMMANDS,
    DEFAULT_RECONNECT_INTERVAL,
    HAPTIC,
    MAX_BRIGHTNESS,
} from './constants.js'

import { rgba2rgb565 } from './util.js'

export class LoupedeckDevice extends EventEmitter {
    static async list({ ignoreSerial = false, ignoreWebsocket = false } = {}) {
        const ps = []

        if (!ignoreSerial) ps.push(SerialConnection.discover())
        if (!ignoreWebsocket && WSConnection) ps.push(WSConnection.discover())

        // Run them in parallel
        const rawDevices = await Promise.all(ps)

        return rawDevices.flat()
    }
    keySize = 90
    constructor({ host, path, autoConnect = true, reconnectInterval = DEFAULT_RECONNECT_INTERVAL } = {}) {
        super()
        this.transactionID = 0
        this.touches = {}
        this.handlers = {
            [COMMANDS.BUTTON_PRESS]: this.onButton.bind(this),
            [COMMANDS.KNOB_ROTATE]: this.onRotate.bind(this),
            [COMMANDS.SERIAL]: buff => buff.toString().trim(),
            [COMMANDS.TICK]: () => {},
            [COMMANDS.TOUCH]: this.onTouch.bind(this, 'touchmove'),
            [COMMANDS.TOUCH_END]: this.onTouch.bind(this, 'touchend'),
            [COMMANDS.VERSION]: buff => `${buff[0]}.${buff[1]}.${buff[2]}`,
            [COMMANDS.TOUCH_CT]: this.onTouch.bind(this, 'touchmove'),
            [COMMANDS.TOUCH_END_CT]: this.onTouch.bind(this, 'touchend'),
        }
        // Track pending transactions
        this.pendingTransactions = {}
        // How long between reconnect attempts
        this.reconnectInterval = reconnectInterval
        // Host for websocket connections
        this.host = host
        // Path for serial connections
        this.path = path
        // Automatically connect?
        if (autoConnect) this._connectBlind()
    }
    close() {
        if (!this.connection) return
        return this.connection.close()
    }
    async connect() {
        // Explicitly asked for a serial connection (V0.2.X)
        if (this.path) this.connection = new SerialConnection({ path: this.path })
        // Explicitly asked for a websocket connection (V0.1.X)
        else if (this.host) this.connection = new WSConnection({ host: this.host })
        // Autodiscover
        else {
            const devices = await this.constructor.list()
            if (devices.length > 0) {
                const { connectionType, ...args } = devices[0]
                this.connection = new connectionType(args)
            }
            if (!this.connection) {
                return Promise.reject(this.onDisconnect(new Error('No devices found')))
            }
        }

        this.connection.on('connect', this.onConnect.bind(this))
        this.connection.on('message', this.onReceive.bind(this))
        this.connection.on('disconnect', this.onDisconnect.bind(this))
        return this.connection.connect()
    }
    _connectBlind() {
        return this.connect().catch(() => {})
    }
    // Draw an arbitrary buffer to the device
    // Buffer format must be 16bit 5-6-5 (LE, except BE for the Loupedeck CT Knob screen)
    async drawBuffer({ id, width, height, x = 0, y = 0, autoRefresh = true }, buffer) {
        const displayInfo = this.displays[id]
        if (!displayInfo) throw new Error(`Display '${id}' is not available on this device!`)
        if (!width) width = displayInfo.width
        if (!height) height = displayInfo.height
        if (displayInfo.offset) {
            x += displayInfo.offset[0]
            y += displayInfo.offset[1]
        }

        const pixelCount = width * height * 2
        if (buffer.length !== pixelCount) {
            throw new Error(`Expected buffer length of ${pixelCount}, got ${buffer.length}!`)
        }

        // Header with x/y/w/h and display ID
        const header = Buffer.alloc(8)
        header.writeUInt16BE(x, 0)
        header.writeUInt16BE(y, 2)
        header.writeUInt16BE(width, 4)
        header.writeUInt16BE(height, 6)

        // Write to frame buffer
        await this.send(COMMANDS.FRAMEBUFF, Buffer.concat([displayInfo.id, header, buffer]))

        // Draw to display
        if (autoRefresh) await this.refresh(id)
    }
    // Create a canvas with correct dimensions and pass back for drawing
    drawCanvas({ id, width, height, ...args }, cb) {
        const displayInfo = this.displays[id]
        if (!displayInfo) throw new Error(`Display '${id}' is not available on this device!`)
        if (!width) width = displayInfo.width
        if (!height) height = displayInfo.height
        if (!canvasModule || !canvasModule.createCanvas) {
            throw new Error('Using callbacks requires the `canvas` library to be installed. Install it using `npm install canvas`.')
        }

        const canvas = canvasModule.createCanvas(width, height)
        const ctx = canvas.getContext('2d', { pixelFormat: 'RGB16_565' }) // Loupedeck uses 16-bit (5-6-5) LE RGB colors
        cb(ctx, width, height)
        let buffer
        // If using NodeJS canvas package
        if (canvas.toBuffer) {
            buffer = canvas.toBuffer('raw')
        // If using browser canvas API
        } else {
            const imageData = ctx.getImageData(0, 0, width, height)
            const rgba = imageData.data
            // Convert from RGBA to RGB16_565
            buffer = rgba2rgb565(rgba, width * height)
        }
        // Swap endianness depending on display
        if (displayInfo.endianness === 'be') buffer.swap16()
        return this.drawBuffer({ id, width, height, ...args }, buffer)
    }
    // Draw to a specific key index (0-11 on Live, 0-14 on Live S)
    drawKey(index, cb) {
        // Get offset x/y for key index
        if (index < 0 || index >= this.columns * this.rows) throw new Error(`Key ${index} is not a valid key`)
        const width = this.keySize
        const height = this.keySize
        const x = this.visibleX[0] + index % this.columns * width
        const y = Math.floor(index / this.columns) * height
        return this[cb instanceof Buffer ? 'drawBuffer' : 'drawCanvas']({ id: 'center', x, y, width, height }, cb)
    }
    // Draw to a specific screen
    drawScreen(id, cb) {
        return this[cb instanceof Buffer ? 'drawBuffer' : 'drawCanvas']({ id }, cb)
    }
    async getInfo() {
        if (!this.connection || !this.connection.isReady()) throw new Error('Not connected!')
        return {
            serial: await this.send(COMMANDS.SERIAL),
            version: await this.send(COMMANDS.VERSION)
        }
    }
    onButton(buff) {
        if (buff.length < 2) return
        const id = BUTTONS[buff[0]]
        const event = buff[1] === 0x00 ? 'down' : 'up'
        this.emit(event, { id })
    }
    onConnect(info) {
        this.emit('connect', info)
    }
    onDisconnect(error) {
        this.emit('disconnect', error)
        clearTimeout(this._reconnectTimer)
        this.connection = null
        // Normal disconnect, do not reconnect
        if (!error) return
        // Reconnect if desired
        if (this.reconnectInterval) {
            this._reconnectTimer = setTimeout(this._connectBlind.bind(this), this.reconnectInterval)
        }
        return error.message
    }
    onReceive(buff) {
        const msgLength = buff[0]
        const handler = this.handlers[buff[1]]
        const transactionID = buff[2]
        const response = handler ? handler(buff.slice(3, msgLength)) : buff
        const resolver = this.pendingTransactions[transactionID]
        if (resolver) resolver(response)
        return response
    }
    onRotate(buff) {
        const id = BUTTONS[buff[0]]
        const delta = buff.readInt8(1)
        this.emit('rotate', { id, delta })
    }
    onTouch(event, buff) {
        const x = buff.readUInt16BE(1)
        const y = buff.readUInt16BE(3)
        const id = buff[5]

        // Create touch
        const touch = { x, y, id, target: this.getTarget(x, y, id) }

        // End touch, remove from local cache
        if (event === 'touchend') {
            delete this.touches[touch.id]
        } else {
            // First time seeing this touch, emit touchstart instead of touchmove
            if (!this.touches[touch.id]) event = 'touchstart'
            this.touches[touch.id] = touch
        }

        this.emit(event, { touches: Object.values(this.touches), changedTouches: [touch] })
    }
    // Display the current framebuffer
    refresh(id) {
        const displayInfo = this.displays[id]
        return this.send(COMMANDS.DRAW, displayInfo.id)
    }
    send(command, data = Buffer.alloc(0)) {
        if (!this.connection || !this.connection.isReady()) return
        this.transactionID = (this.transactionID + 1) % 256
        // Skip transaction ID's of zero since the device seems to ignore them
        if (this.transactionID === 0) this.transactionID++
        const header = Buffer.alloc(3)
        header[0] = Math.min(3 + data.length, 0xff)
        header[1] = command
        header[2] = this.transactionID
        const packet = Buffer.concat([header, data])
        if (header[1] == 16 || header[1] == 15)
        {
            console.log(packet);
        }
        this.connection.send(packet)
        return new Promise(res => {
            this.pendingTransactions[this.transactionID] = res
        })
    }
    setBrightness(value) {
        const byte = Math.max(0, Math.min(MAX_BRIGHTNESS, Math.round(value * MAX_BRIGHTNESS)))
        return this.send(COMMANDS.SET_BRIGHTNESS, Buffer.from([byte]))
    }
    setButtonColor({ id, color }) {
        const key = Object.keys(BUTTONS).find(k => BUTTONS[k] === id)
        if (!key) throw new Error(`Invalid button ID: ${id}`)
        const [r, g, b] = rgba(color)
        const data = Buffer.from([key, r, g, b])
        return this.send(COMMANDS.SET_COLOR, data)
    }
    vibrate(pattern = HAPTIC.SHORT) {
        return this.send(COMMANDS.SET_VIBRATION, Buffer.from([pattern]))
    }
}

export class LoupedeckLive extends LoupedeckDevice {
    static productId = 0x0004
    static vendorId = 0x2ec2
    buttons = [0, 1, 2, 3, 4, 5, 6, 7]
    knobs = ['knobCL', 'knobCR', 'knobTL', 'knobTR', 'knobBL', 'knobBR']
    columns = 4
    // All displays are addressed as the same screen
    // So we add offsets
    displays = {
        center: { id: Buffer.from('\x00M'), width: 360, height: 270, offset: [60, 0] },
        left: { id: Buffer.from('\x00M'), width: 60, height: 270 },
        right: { id: Buffer.from('\x00M'), width: 60, height: 270, offset: [420, 0] },
    }
    rows = 3
    type = 'Loupedeck Live'
    visibleX = [0, 480]
    // Determine touch target based on x/y position
    getTarget(x, y) {
        if (x < this.displays.left.width) return { screen: 'left' }
        if (x >= this.displays.left.width + this.displays.center.width) return { screen: 'right' }
        const column = Math.floor((x - this.displays.left.width) / this.keySize)
        const row = Math.floor(y / this.keySize)
        const key = row * this.columns + column
        return {
            screen: 'center',
            key
        }
    }
}

export class LoupedeckCT extends LoupedeckLive {
    static productId = 0x0003
    buttons = [0, 1, 2, 3, 4, 5, 6, 7, 'home', 'enter', 'undo', 'save', 'keyboard', 'fnL', 'a', 'b', 'c', 'd', 'fnR', 'e']
    displays = {
        center: { id: Buffer.from('\x00A'), width: 360, height: 270 }, // "A"
        left: { id: Buffer.from('\x00L'), width: 60, height: 270 }, // "L"
        right: { id: Buffer.from('\x00R'), width: 60, height: 270 }, // "R"
        knob: { id: Buffer.from('\x00W'), width: 240, height: 240, endianness: 'be' }, // "W"
    }
    type = 'Loupedeck CT'
    // Determine touch target based on x/y position
    getTarget(x, y, id) {
        if (id === 0) return { screen: 'knob' }
        return super.getTarget(x, y)
    }
}

export class LoupedeckLiveS extends LoupedeckDevice {
    static productId = 0x0006
    static vendorId = 0x2ec2
    buttons = [0, 1, 2, 3]
    knobs = ['knobCL', 'knobTL']
    columns = 5
    displays = {
        center: { id: Buffer.from('\x00M'), width: 480, height: 270 },
    }
    rows = 3
    type = 'Loupedeck Live S'
    visibleX = [15, 465]
    // Determine touch target based on x/y position
    getTarget(x, y) {
        if (x < this.visibleX[0] || x >= this.visibleX[1]) return {}
        const column = Math.floor((x - this.visibleX[0]) / this.keySize)
        const row = Math.floor(y / this.keySize)
        const key = row * this.columns + column
        return {
            screen: 'center',
            key
        }
    }
}

export class RazerStreamController extends LoupedeckLive {
    static productId = 0x0d06
    static vendorId = 0x1532
    type = 'Razer Stream Controller'
}

export class RazerStreamControllerX extends LoupedeckDevice {
    static productId = 0x0d09
    static vendorId = 0x1532
    type = 'Razer Stream Controller X'
    buttons = []
    columns = 5
    displays = {
        center: { id: Buffer.from('\x00M'), width: 480, height: 288 },
    }
    rows = 3
    visibleX = [0, 480]
    keySize = 96
    // Emit an extra touchstart event since we are touching keys
    onButton(buff) {
        super.onButton(buff)
        const event = buff[1] === 0x00 ? 'touchstart' : 'touchend'
        const key = BUTTONS[buff[0]]
        const row = Math.floor(key / this.columns)
        const col = key % this.columns
        const touch = {
            id: 0,
            // Add half so touch is in the center of the key
            x: (col + 0.5) * this.keySize,
            y: (row + 0.5) * this.keySize,
            target: { key }
        }
        this.emit(event, {
            touches: event === 'touchstart' ? [touch] : [],
            changedTouches: [touch],
        })
    }
    // eslint-disable-next-line class-methods-use-this
    setButtonColor() {
        throw new Error('Setting key color not available on this device!')
    }
    // eslint-disable-next-line class-methods-use-this
    vibrate() {
        throw new Error('Vibration not available on this device!')
    }
}