From 3acdfe52e91598353af777f15f5d8da6b69ee19e Mon Sep 17 00:00:00 2001 From: heiso Date: Tue, 12 Nov 2024 14:00:11 +0100 Subject: [PATCH] feat: can now configure the timeout of a tap key --- firmware/.settings/language.settings.xml | 4 +- firmware/Core/Inc/main.h | 3 +- firmware/Core/Src/main.c | 3 +- .../routes/_layout.configurator.$row.$col.tsx | 558 ------------------ web-app/app/routes/_layout.configurator.tsx | 18 + web-app/app/useDevice.ts | 42 +- 6 files changed, 57 insertions(+), 571 deletions(-) delete mode 100644 web-app/app/routes/_layout.configurator.$row.$col.tsx diff --git a/firmware/.settings/language.settings.xml b/firmware/.settings/language.settings.xml index af0d254..438adaf 100644 --- a/firmware/.settings/language.settings.xml +++ b/firmware/.settings/language.settings.xml @@ -5,7 +5,7 @@ - + @@ -16,7 +16,7 @@ - + diff --git a/firmware/Core/Inc/main.h b/firmware/Core/Inc/main.h index b94784a..b909646 100644 --- a/firmware/Core/Inc/main.h +++ b/firmware/Core/Inc/main.h @@ -39,12 +39,12 @@ extern "C" { #define DEFAULT_RESET_THRESHOLD 3 #define DEFAULT_RAPID_TRIGGER_OFFSET 40 #define DEFAULT_SCREAMING_VELOCITY_TRIGGER 45 +#define DEFAULT_TAP_TIMEOUT 200 #define IDLE_VALUE_APPROX 1800 #define MAX_DISTANCE_APPROX 500 #define IDLE_VALUE_OFFSET 10 #define MAX_DISTANCE_OFFSET 60 -#define TAP_TIMEOUT 200 #define IDLE_CYCLES_UNTIL_SLEEP 15 #define ADC_CHANNEL_COUNT 5 @@ -136,6 +136,7 @@ struct user_config { uint8_t reset_threshold; uint8_t rapid_trigger_offset; uint8_t screaming_velocity_trigger; + uint16_t tap_timeout; uint16_t keymaps[LAYERS_COUNT][MATRIX_ROWS][MATRIX_COLS]; }; /* USER CODE END ET */ diff --git a/firmware/Core/Src/main.c b/firmware/Core/Src/main.c index 240daa5..e6e4d1a 100644 --- a/firmware/Core/Src/main.c +++ b/firmware/Core/Src/main.c @@ -73,6 +73,7 @@ const struct user_config default_user_config = { .reset_threshold = DEFAULT_RESET_THRESHOLD, .rapid_trigger_offset = DEFAULT_RAPID_TRIGGER_OFFSET, .screaming_velocity_trigger = DEFAULT_SCREAMING_VELOCITY_TRIGGER, + .tap_timeout = DEFAULT_TAP_TIMEOUT, .keymaps = { // clang-format off [_BASE_LAYER] = { @@ -307,7 +308,7 @@ int main(void) { struct key *key = &keys[adc_channel][amux_channel]; uint8_t is_before_reset_offset = key->state.distance_8bits < key->actuation.reset_offset; - uint8_t is_before_timeout = HAL_GetTick() - key->actuation.triggered_at <= TAP_TIMEOUT; + uint8_t is_before_timeout = HAL_GetTick() - key->actuation.triggered_at <= user_config.tap_timeout; // if might be tap, can be tap or triggered if (is_before_reset_offset && is_before_timeout) { diff --git a/web-app/app/routes/_layout.configurator.$row.$col.tsx b/web-app/app/routes/_layout.configurator.$row.$col.tsx deleted file mode 100644 index 7d3baa2..0000000 --- a/web-app/app/routes/_layout.configurator.$row.$col.tsx +++ /dev/null @@ -1,558 +0,0 @@ -// import { CategoryScale, Chart, LineElement, LinearScale, PointElement } from 'chart.js' -// import Annotation from 'chartjs-plugin-annotation' -// import { useCallback, useEffect, useMemo, useState } from 'react' -// import { Line } from 'react-chartjs-2' -// import layout from '../parsed-layout.json' -// import { Port } from '../serial.ts' -// import { Button } from '../ui/button.tsx' -// import { Icon } from '../ui/icon.tsx' -// import { Link } from '../ui/link.tsx' - -// const status = [ -// 'STATUS_MIGHT_BE_TAP', -// 'STATUS_TAP', -// 'STATUS_TRIGGERED', -// 'STATUS_RESET', -// 'STATUS_RAPID_TRIGGER_RESET', -// ] as const -// type Status = (typeof status)[number] -// const visualizationModes = ['NORMAL', 'DISTANCE_HEATMAP', 'IDLE_HEATMAP'] as const -// type VisualizationMode = (typeof visualizationModes)[number] - -// type Calibration = { -// idleValue: number -// maxDistance: number -// } - -// type CalibrationRange = { -// idleValue: { -// min: number -// max: number -// } -// maxDistance: { -// min: number -// max: number -// } -// } - -// type State = { -// value: number -// distance8bits: number -// status: Status -// } - -// type Key = { -// row: number -// col: number - -// calibration: Calibration -// state: State - -// calibrations: (Calibration & { time: Date })[] -// states: (State & { time: Date })[] -// } - -// type KeysByRowCol = Record - -// function parseRawOffset(value: number) { -// return Number(((value * 4) / 255).toFixed(1)) -// } - -// function formatRawOffset(value: number) { -// return (value * 255) / 4 -// } - -// export default function Index() { -// const [port, setPort] = useState(null) -// const [device, setDevice] = useState(null) -// const [duration, setDuration] = useState(0) -// const [keys, setKeys] = useState({}) -// const [keyIndex, setKeyIndex] = useState(null) -// const [mode, setMode] = useState('NORMAL') -// const [triggerOffset, setTriggerOffset] = useState(0) -// const [rapidTriggerOffset, setRapidTriggerOffset] = useState(0) -// const [resetOffset, setResetOffset] = useState(0) - -// const connect = useCallback(async () => { -// const port = await Port.requestPort() -// setPort(port) -// await port.connect() - -// port.onReceive = (data) => { -// let textDecoder = new TextDecoder() -// console.log(data, textDecoder.decode(data)) -// if (data) { -// setTriggerOffset(() => parseRawOffset(Number(data.getUint8(0)))) -// setResetOffset(() => Number(data.getUint8(1))) -// setRapidTriggerOffset(() => parseRawOffset(Number(data.getUint8(2)))) -// } -// // if (data.getInt8() === 13) { -// // currentReceiverLine = null -// // } else { -// // appendLines('receiver_lines', textDecoder.decode(data)) -// // } -// // const parseReport = useCallback((data: DataView) => { -// } - -// port.onReceiveError = (error) => { -// console.error(error) -// } -// // const device = await navigator.usb.requestDevice({ filters: [{ vendorId: 0xcafe }] }) -// // console.log(device) -// // await device.open() -// // await device.selectConfiguration(1) -// // await device.claimInterface(1) -// // await device.transferOut(2, new Uint8Array([50, 40, 30])) -// setDevice(port.device) -// }, []) - -// // useEffect(() => { -// // device?.transferIn -// // return () => { - -// // } -// // }, []) - -// // const parseReport = useCallback((data: DataView) => { -// // const time = new Date() -// // new Array(6).fill(null).forEach((_, index) => { -// // if (isBufferEmpty(data, index * 10, 10)) return null - -// // const offset = index * 10 - -// // const row = data.getUint8(offset) -// // const col = data.getUint8(offset + 1) - -// // const state: State = { -// // value: data.getUint16(offset + 6, true), -// // distance8bits: data.getUint8(offset + 8), -// // status: status[data.getUint8(offset + 9)], -// // } - -// // const calibration: Calibration = { -// // idleValue: data.getUint16(offset + 2, true), -// // maxDistance: data.getUint16(offset + 4, true), -// // } - -// // setKeys((keys) => ({ -// // ...keys, -// // [`${row}-${col}`]: { -// // row, -// // col, -// // calibration, -// // state, -// // calibrations: [ -// // ...(keys[`${row}-${col}`]?.calibrations || []), -// // { ...calibration, time }, -// // ].slice(-MAX_VALUES), -// // states: [...(keys[`${row}-${col}`]?.states || []), { ...state, time }].slice(-MAX_VALUES), -// // }, -// // })) -// // }) -// // setDuration((duration) => data.getUint8(60) * (1 - 0.8) + duration * 0.8) -// // setTriggerOffset(() => parseRawOffset(Number(data.getUint8(61)))) -// // setResetOffset(() => Number(data.getUint8(62))) -// // setRapidTriggerOffset(() => parseRawOffset(Number(data.getUint8(63)))) -// // setRawReports((rawReports) => [dataViewToHexs(data), ...rawReports].slice(0, MAX_VALUES)) -// // }, []) - -// return ( -//
-//
-// {!device && ( -//
-// -//
-// )} -//
-// -//
-//
-//
-// setMode('IDLE_HEATMAP')}> -// Show idle value heatmap -// -// setMode('DISTANCE_HEATMAP')}> -// Show max distance heatmap -// -// setMode('NORMAL')}> -// Show analog values -// -// { -// const json = JSON.stringify(keys, null, 2) -// navigator.clipboard.writeText(json) -// }} -// title="Copy dataset to clipboard" -// > -// -// -//
-// { -// setTriggerOffset(Number(event.target.value)) -// }} -// value={triggerOffset} -// /> -// {triggerOffset} mm -// { -// setRapidTriggerOffset(Number(event.target.value)) -// }} -// value={rapidTriggerOffset} -// /> -// {rapidTriggerOffset} mm -// -// port?.send( -// new Uint8Array([ -// formatRawOffset(triggerOffset), -// resetOffset, -// formatRawOffset(rapidTriggerOffset), -// ]), -// ) -// } -// > -// Save -// -//
-//
-//
-// Avg cycle duration -// {duration.toFixed(0)} ms -//
-//
-//
- -// {device && ( -// <> -//
-//
-// {keyIndex ? ( -// -// ) : ( -//
Click on a key to show some charts
-// )} -//
-//
- -//
-// -//
-// -// )} -//
-// ) -// } - -// const heatmapColorsStyles = [ -// 'bg-slate-300', -// 'bg-slate-400', -// 'bg-slate-500', -// 'bg-slate-600', -// 'bg-slate-700', -// ] -// function getColor(min: number, max: number, value?: number) { -// if (!value) return 'bg-slate-500' - -// const index = Math.floor(((value - min) * (heatmapColorsStyles.length - 1)) / (max - min)) -// return heatmapColorsStyles[index] || 'bg-slate-500' -// } - -// type KeyProps = { -// keyItem?: Key -// mode: VisualizationMode -// calibrationRange: CalibrationRange -// legends: string[] -// onClick: (index: string) => void -// } -// function Key({ mode, calibrationRange, legends, keyItem: key, onClick }: KeyProps) { -// const color = useMemo(() => { -// switch (mode) { -// case 'NORMAL': -// return 'bg-slate-500' - -// case 'IDLE_HEATMAP': -// return getColor( -// calibrationRange.idleValue.min, -// calibrationRange.idleValue.max, -// key?.calibration.idleValue, -// ) - -// case 'DISTANCE_HEATMAP': -// return getColor( -// calibrationRange.maxDistance.min, -// calibrationRange.maxDistance.max, -// key?.calibration.maxDistance, -// ) -// } -// }, [key, calibrationRange, mode]) - -// return ( -//
onClick(`${key?.row}-${key?.col}`)} -// > -// {key && ( -//
-// )} -// {legends.map((legend, index) => ( -// -// {legend} -// -// ))} -// {key && ( -// -// {mode === 'IDLE_HEATMAP' -// ? key.calibration.idleValue.toString() -// : mode === 'DISTANCE_HEATMAP' -// ? key.calibration.maxDistance.toString() -// : key.state.distance8bits.toString()} -// -// )} -//
-// ) -// } - -// type KeyboardProps = { -// keys: KeysByRowCol -// mode: VisualizationMode -// onClick: KeyProps['onClick'] -// } -// function Keyboard({ keys, mode, onClick }: KeyboardProps) { -// const width = useMemo(() => layout.reduce((acc, key) => Math.max(acc, key.x + key.w), 0), []) -// const height = useMemo(() => layout.reduce((acc, key) => Math.max(acc, key.y + key.h), 0), []) -// const [calibrationRange, setCalibrationRange] = useState({ -// idleValue: { min: Infinity, max: 0 }, -// maxDistance: { min: Infinity, max: 0 }, -// }) - -// useEffect(() => { -// const calibrations = Object.values(keys).map((key) => key!.calibration) -// if (calibrations.length) { -// setCalibrationRange({ -// idleValue: { -// min: calibrations.reduce( -// (acc, calibration) => -// calibration.idleValue !== 0 ? Math.min(acc, calibration.idleValue) : acc, -// Infinity, -// ), -// max: calibrations.reduce((acc, calibration) => Math.max(acc, calibration.idleValue), 0), -// }, -// maxDistance: { -// min: calibrations.reduce( -// (acc, calibration) => -// calibration.maxDistance !== 0 ? Math.min(acc, calibration.maxDistance) : acc, -// Infinity, -// ), -// max: calibrations.reduce((acc, calibration) => Math.max(acc, calibration.maxDistance), 0), -// }, -// }) -// } -// }, [keys]) - -// return ( -//
-// {layout.map((keyCoord) => { -// const index = `${keyCoord.row}-${keyCoord.col}` -// const key = keys[index] -// return ( -//
-// -//
-// ) -// })} -//
-// ) -// } - -// const triggered = (ctx: any, value: string) => -// ctx.p1.raw.status !== 'STATUS_RESET' && ctx.p1.raw.status !== 'STATUS_RAPID_TRIGGER_RESET' -// ? value -// : undefined -// const reset = (ctx: any, value: string) => -// ctx.p1.raw.status === 'STATUS_RESET' || ctx.p1.raw.status === 'STATUS_RAPID_TRIGGER_RESET' -// ? value -// : undefined - -// Chart.register(CategoryScale, LinearScale, PointElement, LineElement, Annotation) - -// type GraphProps = { -// keyItem?: Key -// triggerOffset: number -// resetOffset: number -// } -// function Graph({ keyItem, triggerOffset, resetOffset }: GraphProps) { -// const [isStateMode, setIsStateMode] = useState(true) -// if (!keyItem) return null - -// return ( -//
-// {!isStateMode ? ( -// calibration.time), -// datasets: [ -// { -// label: 'Idle', -// data: keyItem.calibrations.map((calibration) => calibration.idleValue), -// backgroundColor: '#be185d', -// borderColor: '#be185d', -// pointRadius: 0, -// }, -// { -// label: 'Value', -// data: keyItem.states.map((state) => state.value), -// backgroundColor: '#f472b6', -// borderColor: '#f472b6', -// pointRadius: 0, -// }, -// { -// label: 'Max Distance', -// data: keyItem.calibrations.map((calibration) => calibration.maxDistance), -// backgroundColor: '#fbcfe8', -// borderColor: '#fbcfe8', -// pointRadius: 0, -// }, -// ], -// }} -// /> -// ) : ( -// triggered(ctx, '#be185d') || reset(ctx, '#f9a8d4'), -// borderColor: (ctx) => triggered(ctx, '#be185d') || reset(ctx, '#f9a8d4'), -// }, -// pointRadius: 0, -// }, -// { -// label: 'Changing point', -// data: keyItem.states, -// parsing: { -// yAxisKey: 'directionChangingPoint', -// xAxisKey: 'time', -// }, -// pointRadius: 3, -// pointBackgroundColor: 'red', -// showLine: false, -// }, -// ], -// }} -// /> -// )} -//
-// setIsStateMode(!isStateMode)}> -// Change mode -// -//
-//
-// ) -// } diff --git a/web-app/app/routes/_layout.configurator.tsx b/web-app/app/routes/_layout.configurator.tsx index dbec803..8c61ac4 100644 --- a/web-app/app/routes/_layout.configurator.tsx +++ b/web-app/app/routes/_layout.configurator.tsx @@ -128,6 +128,23 @@ export default function Index() { /> {Number(userConfig.screamingVelocityTrigger)} + +
+ + { + setUserConfig({ + ...userConfig, + tapTimeout: Number(event.target.value), + }) + }} + value={userConfig.tapTimeout} + /> + ms +
@@ -150,6 +167,7 @@ export default function Index() {
+
{JSON.stringify(userConfig, null, 2)}
)} diff --git a/web-app/app/useDevice.ts b/web-app/app/useDevice.ts index e4d02a0..7f01848 100644 --- a/web-app/app/useDevice.ts +++ b/web-app/app/useDevice.ts @@ -2,6 +2,9 @@ import { useCallback, useMemo, useRef, useState } from 'react' const PRODUCT_NAME = 'Macrolev' const VENDOR_ID = 0xcafe +// const LAYER_COUNT = 2 +// const MATRIX_ROWS = 5 +// const MATRIX_COLS = 15 const vendorRequests = { VENDOR_REQUEST_KEYS: 0xfe, @@ -38,7 +41,23 @@ type UserConfig = { resetThreshold: number rapidTriggerOffset: number screamingVelocityTrigger: number - keymaps: ArrayBuffer + tapTimeout: number + keymaps: number[] +} + +function parseKeymaps(config: DataView, byteOffset: number) { + const test: number[] = [] + let i = byteOffset + while (i < config.byteLength) { + const value = config.getUint16(i, true) + // if (value > 0xff) { + // console.log(value & 0b0111111111111111) + // } + test.push(value) + i = i + 2 + } + + return test } function parseUserConfig(config: DataView): UserConfig { @@ -47,18 +66,23 @@ function parseUserConfig(config: DataView): UserConfig { resetThreshold: config.getUint8(1), rapidTriggerOffset: config.getUint8(2), screamingVelocityTrigger: config.getUint8(3), - keymaps: config.buffer.slice(4), + tapTimeout: config.getUint16(4, true), + keymaps: parseKeymaps(config, 6), } } function formatUserConfig(config: UserConfig): BufferSource { - return new Uint8Array([ - config.triggerOffset, - config.resetThreshold, - config.rapidTriggerOffset, - config.screamingVelocityTrigger, - ...new Uint8Array(config.keymaps), - ]) + const buffer = new DataView(new ArrayBuffer(306)) + buffer.setUint8(0, config.triggerOffset) + buffer.setUint8(1, config.resetThreshold) + buffer.setUint8(2, config.rapidTriggerOffset) + buffer.setUint8(3, config.screamingVelocityTrigger) + buffer.setUint16(4, config.tapTimeout, true) + config.keymaps.forEach((keycode, index) => { + buffer.setUint16(6 + index * 2, keycode, true) + }) + + return buffer } const layerTypes = [