From 0020fe888740cfa0931f90c75978c438edf93f54 Mon Sep 17 00:00:00 2001 From: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:28:06 +0200 Subject: [PATCH 1/4] Add deterministic ESP32 movement helpers --- .../plugins/touch/esp32-movement.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 lib/units/ios-device/plugins/touch/esp32-movement.js diff --git a/lib/units/ios-device/plugins/touch/esp32-movement.js b/lib/units/ios-device/plugins/touch/esp32-movement.js new file mode 100644 index 0000000000..3130183ed0 --- /dev/null +++ b/lib/units/ios-device/plugins/touch/esp32-movement.js @@ -0,0 +1,34 @@ +export const SINGLE_STEP_SIZE = 1 +export const BIG_STEP_SIZE = 4 +export const HUGE_STEP_SIZE = 8 +export const POSITION_TOLERANCE = 1 + +export function getStep(diff) { + const distance = Math.abs(diff) + let magnitude = SINGLE_STEP_SIZE + if (distance >= BIG_STEP_SIZE) { + magnitude = BIG_STEP_SIZE + } + if (distance >= HUGE_STEP_SIZE) { + magnitude = HUGE_STEP_SIZE + } + + return Math.sign(diff) * magnitude +} + +export function getDirection(axis, step) { + const directions = { + x: {positive: ['R', 'r', 'k'], negative: ['L', 'l', 'g']}, + y: {positive: ['D', 'd', 'h'], negative: ['U', 'u', 'j']} + } + const magnitude = Math.abs(step) + let index = 0 + if (magnitude === BIG_STEP_SIZE) { + index = 1 + } + if (magnitude === HUGE_STEP_SIZE) { + index = 2 + } + + return (step > 0 ? directions[axis].positive : directions[axis].negative)[index] +} From d847d824e8b3551d8b66fa807e15cafbc192e574 Mon Sep 17 00:00:00 2001 From: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:28:07 +0200 Subject: [PATCH 2/4] Test ESP32 movement command mapping --- .../plugins/touch/esp32-movement.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/unit/units/ios-device/plugins/touch/esp32-movement.test.ts diff --git a/test/unit/units/ios-device/plugins/touch/esp32-movement.test.ts b/test/unit/units/ios-device/plugins/touch/esp32-movement.test.ts new file mode 100644 index 0000000000..efe42fce7f --- /dev/null +++ b/test/unit/units/ios-device/plugins/touch/esp32-movement.test.ts @@ -0,0 +1,29 @@ +import {describe, expect, it} from 'vitest' +import {getDirection, getStep} from '../../../../../../lib/units/ios-device/plugins/touch/esp32-movement.js' + +describe('ESP32 movement commands', () => { + it('selects a firmware-supported step based on the remaining distance', () => { + expect(getStep(100)).toBe(8) + expect(getStep(-8)).toBe(-8) + expect(getStep(7)).toBe(4) + expect(getStep(-2)).toBe(-1) + expect(getStep(0)).toBe(0) + }) + + it.each([ + ['x', 1, 'R'], + ['x', 4, 'r'], + ['x', 8, 'k'], + ['x', -1, 'L'], + ['x', -4, 'l'], + ['x', -8, 'g'], + ['y', 1, 'D'], + ['y', 4, 'd'], + ['y', 8, 'h'], + ['y', -1, 'U'], + ['y', -4, 'u'], + ['y', -8, 'j'], + ] as const)('maps axis %s step %i to firmware command %s', (axis, step, command) => { + expect(getDirection(axis, step)).toBe(command) + }) +}) From b84ee6e47185c5eff42c1aeb26de2bcfbc7e5142 Mon Sep 17 00:00:00 2001 From: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:28:41 +0200 Subject: [PATCH 3/4] Fix ESP32 iOS cursor coordinate mapping --- .../ios-device/plugins/touch/esp32touch.js | 87 +++++++------------ 1 file changed, 30 insertions(+), 57 deletions(-) diff --git a/lib/units/ios-device/plugins/touch/esp32touch.js b/lib/units/ios-device/plugins/touch/esp32touch.js index 98483179e5..1d1e6227b6 100644 --- a/lib/units/ios-device/plugins/touch/esp32touch.js +++ b/lib/units/ios-device/plugins/touch/esp32touch.js @@ -1,8 +1,7 @@ import {SerialPort} from 'serialport' import Logger from '../../../../util/logger.js' -import {promisify} from 'node:util' -import _ from 'lodash' import EventEmitter from 'node:events' +import {getDirection, getStep, SINGLE_STEP_SIZE, BIG_STEP_SIZE, HUGE_STEP_SIZE, POSITION_TOLERANCE} from './esp32-movement.js' const log = Logger.createLogger('esp32touch') @@ -47,9 +46,10 @@ function throttle(func, ms) { */ export class Esp32Touch extends EventEmitter { static MAX_BLE_NAME_LENGTH = 22 // Match the safe limit for advertising - static SINGLE_STEP_SIZE = 13 / 3 // The amount of pixels a single step does. - static BIG_STEP_SIZE = 70 / 3 // The amount of pixels a big step does. - static HUGE_STEP_SIZE = 178 / 3 // The amount of pixels a huge step does + static SINGLE_STEP_SIZE = SINGLE_STEP_SIZE + static BIG_STEP_SIZE = BIG_STEP_SIZE + static HUGE_STEP_SIZE = HUGE_STEP_SIZE + static POSITION_TOLERANCE = POSITION_TOLERANCE static async listPorts() { return (await SerialPort.list()).filter((dev) => dev.manufacturer === 'Espressif') @@ -88,27 +88,27 @@ export class Esp32Touch extends EventEmitter { if(Buffer.isBuffer(data)) { const c = data.toString() console.log(JSON.stringify(c)) - if(c === 'P\r\n') { + if(c === 'P\\r\\n') { this.curposX = null this.curposY = null this.updateState('paired') } - else if (c === 'C\r\n') { + else if (c === 'C\\r\\n') { this.curposX = null this.curposY = null this.updateState('connected') } - else if (c === 'D\r\n') { + else if (c === 'D\\r\\n') { this.curposX = null this.curposY = null this.updateState('disconnected') } - else if (c === '\r\n') { + else if (c === '\\r\\n') { this.curposX = null this.curposY = null this.updateState('ready') } - else if (c === 'L\r\n') { + else if (c === 'L\\r\\n') { this.curposX = null this.curposY = null this.updateState('booting') @@ -163,11 +163,11 @@ export class Esp32Touch extends EventEmitter { log.warn(`BLE name too long (${name.length} chars). Truncating to ${Esp32Touch.MAX_BLE_NAME_LENGTH} chars.`) realName = name.substring(0, Esp32Touch.MAX_BLE_NAME_LENGTH) } - this.bufferCommand(`N${realName}\n`) + this.bufferCommand(`N${realName}\\n`) } reset() { this.bufferCommand('0') - this.curposX = 14 * Esp32Touch.SINGLE_STEP_SIZE + this.curposX = Esp32Touch.SINGLE_STEP_SIZE * 14 this.curposY = 0 } @@ -186,56 +186,29 @@ export class Esp32Touch extends EventEmitter { return } - if(this.targetPosX === null || this.targetPoxY === null) { + if (this.targetPosX === null || this.targetPosY === null) { return } const targetStepsX = Math.round(this.targetPosX * this.width) - const targetStepsY = Math.round(this.targetPoxY * this.height) - const diffX = targetStepsX - this.curposX // Positive -> left, negative - right - const diffY = targetStepsY - this.curposY // Positive -> up, negative - down - /** @type {[string, number, number][]} */ - let commands = [] - if(diffX > Esp32Touch.HUGE_STEP_SIZE * 1.5) { - commands.push(['k', Esp32Touch.HUGE_STEP_SIZE, 0]) - } - if(diffX < -Esp32Touch.HUGE_STEP_SIZE * 1.5) { - commands.push(['g', -Esp32Touch.HUGE_STEP_SIZE, 0]) - } - if(diffY > Esp32Touch.HUGE_STEP_SIZE * 1.5) { - commands.push(['h', 0, Esp32Touch.HUGE_STEP_SIZE]) + const targetStepsY = Math.round(this.targetPosY * this.height) + const diffX = targetStepsX - this.curposX // Positive -> right, negative -> left + const diffY = targetStepsY - this.curposY // Positive -> down, negative -> up + + if (Math.abs(diffX) <= Esp32Touch.POSITION_TOLERANCE && Math.abs(diffY) <= Esp32Touch.POSITION_TOLERANCE) { + return } - if(diffY < -Esp32Touch.HUGE_STEP_SIZE * 1.5) { - commands.push(['j', 0, -Esp32Touch.HUGE_STEP_SIZE]) + + const moveX = Math.abs(diffX) >= Math.abs(diffY) + const step = getStep(moveX ? diffX : diffY) + // Move the axis with the larger remaining error first. Randomly + // choosing an axis makes the estimated position unstable. + let dir, addX, addY + if (moveX) { + [dir, addX, addY] = [getDirection('x', step), step, 0] } - if (commands.length === 0) { - if(diffX > Esp32Touch.BIG_STEP_SIZE * 1.5) { - commands.push(['r', Esp32Touch.BIG_STEP_SIZE, 0]) - } - if(diffX < -Esp32Touch.BIG_STEP_SIZE * 1.5) { - commands.push(['l', -Esp32Touch.BIG_STEP_SIZE, 0]) - } - if(diffY > Esp32Touch.BIG_STEP_SIZE * 1.5) { - commands.push(['d', 0, Esp32Touch.BIG_STEP_SIZE]) - } - if(diffY < -Esp32Touch.BIG_STEP_SIZE * 1.5) { - commands.push(['u', 0, -Esp32Touch.BIG_STEP_SIZE]) - } - if(commands.length === 0) { - if(diffX > Esp32Touch.SINGLE_STEP_SIZE) { - commands.push(['R', Esp32Touch.SINGLE_STEP_SIZE, 0]) - } - if(diffX < -Esp32Touch.SINGLE_STEP_SIZE) { - commands.push(['L', -Esp32Touch.SINGLE_STEP_SIZE, 0]) - } - if(diffY > Esp32Touch.SINGLE_STEP_SIZE) { - commands.push(['D', 0, Esp32Touch.SINGLE_STEP_SIZE]) - } - if(diffY < -Esp32Touch.SINGLE_STEP_SIZE) { - commands.push(['U', 0, -Esp32Touch.SINGLE_STEP_SIZE]) - } - } + else { + [dir, addX, addY] = [getDirection('y', step), 0, step] } - const [dir, addX, addY] = _.sample(commands) ?? ['', 0, 0] if (dir) { this.bufferCommand(dir) this.curposX += addX @@ -253,7 +226,7 @@ export class Esp32Touch extends EventEmitter { move(targetX, targetY) { this.startWorking() this.targetPosX = targetX - this.targetPoxY = targetY + this.targetPosY = targetY } /** From 68302f1ef4cfc56ec2968e82b0d4446f3ffc056b Mon Sep 17 00:00:00 2001 From: Daniil <8039921+DaniilSmirnov@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:29:06 +0200 Subject: [PATCH 4/4] Preserve ESP32 serial line endings --- lib/units/ios-device/plugins/touch/esp32touch.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/units/ios-device/plugins/touch/esp32touch.js b/lib/units/ios-device/plugins/touch/esp32touch.js index 1d1e6227b6..c3afc349f8 100644 --- a/lib/units/ios-device/plugins/touch/esp32touch.js +++ b/lib/units/ios-device/plugins/touch/esp32touch.js @@ -88,27 +88,27 @@ export class Esp32Touch extends EventEmitter { if(Buffer.isBuffer(data)) { const c = data.toString() console.log(JSON.stringify(c)) - if(c === 'P\\r\\n') { + if(c === 'P\r\n') { this.curposX = null this.curposY = null this.updateState('paired') } - else if (c === 'C\\r\\n') { + else if (c === 'C\r\n') { this.curposX = null this.curposY = null this.updateState('connected') } - else if (c === 'D\\r\\n') { + else if (c === 'D\r\n') { this.curposX = null this.curposY = null this.updateState('disconnected') } - else if (c === '\\r\\n') { + else if (c === '\r\n') { this.curposX = null this.curposY = null this.updateState('ready') } - else if (c === 'L\\r\\n') { + else if (c === 'L\r\n') { this.curposX = null this.curposY = null this.updateState('booting') @@ -163,7 +163,7 @@ export class Esp32Touch extends EventEmitter { log.warn(`BLE name too long (${name.length} chars). Truncating to ${Esp32Touch.MAX_BLE_NAME_LENGTH} chars.`) realName = name.substring(0, Esp32Touch.MAX_BLE_NAME_LENGTH) } - this.bufferCommand(`N${realName}\\n`) + this.bufferCommand(`N${realName}\n`) } reset() { this.bufferCommand('0')