再来就是实际建立透过 select 选择的脚位,并建立相关 Firmata 功能。
(过程和建立 window-digital-io-item.vue
基本上相同。)
稍微规划一下预期 UI 内容。
建立 window-analog-input-item.vue
组件,用来显示类比数值。
具体实现功能:
旋钮
使用 Quasar Knob。
拉条
使用 Quasar Slider。
删除按钮(脚位编号)
使用 Quasar Button。
程序的部份为:
window-digital-io-item.vue
src\components\window-analog-input-item.vue <template lang="pug">
.c-row.q-0px.items-center.w-full
.pin-number
.text-20px
| {{ pin.number }}
q-btn.bg-white(
@click='handleDelete',
icon='r_delete',
dense,
flat,
rounded,
color='grey-5'
)
q-slider.mx-20px(
v-model='pinValue',
readonly,
color='red-3'
)
q-knob(
v-model='pinValue',
size='60px',
show-value,
readonly,
color='red-3',
track-color='grey-3',
font-size='12px'
)
src\components\window-analog-input-item.vue <style scoped lang="sass">
@import '@/styles/quasar.variables.sass'
.pin-number
width: 36px
padding: 10px 0px
margin-right: 10px
font-family: 'Orbitron'
color: $grey
text-align: center
position: relative
&:hover
.q-btn
pointer-events: auto
opacity: 1
.q-btn
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
pointer-events: none
transition-duration: 0.4s
opacity: 0
src\components\window-analog-input-item.vue <script>
/**
* @typedef {import('@/script/modules/port-transceiver').default} PortTransceiver
*
* @typedef {import('@/types/type').PinInfo} PinInfo
* @typedef {import('@/types/type').PinCapability} PinCapability
*/
import { mapState } from 'vuex';
export default {
name: 'WindowAnalogInputItem',
components: {},
props: {
/** @type {PinInfo} */
pin: {
type: Object,
required: true,
},
},
data() {
return {
/** 脚位数值 */
pinValue: 0,
};
},
computed: {
...mapState({
/** @type {PortTransceiver} */
portTransceiver: (state) => state.core.transceiver,
}),
},
watch: {},
created() {},
mounted() {},
beforeDestroy() {},
methods: {
handleDelete() {
this.$emit('delete', this.pin);
},
},
};
接着在 window-analog-input.vue
引入 window-analog-input-item.vue
src\components\window-analog-input.vue <script>
// ...
import BaseWindow from '@/components/base-window.vue';
import BaseSelectPin from '@/components/base-select-pin.vue';
import WindowAnalogInputItem from '@/components/window-analog-input-item.vue';
// ...
export default {
name: 'WindowAnalogInput',
components: {
'base-window': BaseWindow,
'base-select-pin': BaseSelectPin,
'window-analog-input-item': WindowAnalogInputItem,
},
// ...
};
src\components\window-analog-input-item.vue <template lang="pug">
base-window.window-analog-input(
:pos='pos',
headerIconColor='red-3',
body-class='c-col p-20px pt-20px',
title='类比输入功能'
)
base-select-pin(
:pins='supportPins',
color='red-3',
@selected='addPin',
@err='handleErr'
)
q-scroll-area.pt-10px.h-300px.flex
transition-group(name='list-complete', tag='div')
window-analog-input-item.py-10px(
v-for='pin in existPins',
:pin='pin',
:key='pin.number',
@delete='deletePin'
)
尝试建立脚位看看。
成功建立!接下来就是实作类比输入功能了!
在控制脚位数值之前,需要先设定脚位模式。
设定模式命令在数位输出时已经完成,直接呼叫即可!
在 window-analog-input-item.vue
新增以下程序:
methods
新增 init()
,初始化脚位相关功能。computed
新增 valueMax()
,计算类比数值最大值。created()
呼叫 init()
src\components\window-analog-input-item.vue <script>
/**
* @typedef {import('@/script/modules/port-transceiver').default} PortTransceiver
*
* @typedef {import('@/types/type').PinInfo} PinInfo
* @typedef {import('@/types/type').PinCapability} PinCapability
*/
import { mapState } from 'vuex';
import { PinMode } from '@/script/utils/firmata.utils';
const { ANALOG_INPUT } = PinMode;
export default {
name: 'WindowAnalogInputItem',
// ...
computed: {
// ...
/** 数值最大值 */
valueMax() {
/** @type {PinInfo} */
const pin = this.pin;
const target = pin.capabilities.find(
(capability) => capability.mode === ANALOG_INPUT
);
return 2 ** target.resolution - 1;
},
},
// ...
created() {
this.init();
},
// ...
methods: {
// ...
init() {
/** @type {PinInfo} */
const pin = this.pin;
/** @type {PortTransceiver} */
const portTransceiver= this.portTransceiver;
portTransceiver.addCmd('setMode', {
pin: pin.number,
mode: ANALOG_INPUT,
});
},
},
};
测试看看。
会发现类比输入不用像数位输入那样需要开启自动回报,只要设定类比输入模式後,就会自动回报数值。
最後就只剩解析数值的部分了!
在 response-define.js
新增定义 analogMessage
src\script\firmata\response-define.js
/**
* @typedef {import('@/types/type').DigitalResponseMessage} DigitalResponseMessage
* @typedef {import('@/types/type').AnalogResponseMessage} AnalogResponseMessage
*/
import { arraySplit, matchFeature } from '@/script/utils/utils';
export default [
// ...
// analogMessage: 类比讯息回应
{
key: 'analogMessage',
eventName: 'data:analogMessage',
/**
* @param {number[]} res
*/
matcher(res) {
},
/**
* @param {number[]} values
* @return {AnalogResponseMessage[]}
*/
getData(values) {
},
},
]
matcher()
和 getData()
内容要怎麽实作呢?在「Data Message Expansion」可以找到类比回应资料的说明:
Analog 14-bit data format
回应资料为:
0 analog pin, 0xE0-0xEF, (MIDI Pitch Wheel)
1 analog least significant 7 bits
2 analog most significant 7 bits
可以发现概念与 D12 分析的数位资料回应概念相同。
也就是说数值开头只要包含 0xE0-0xEF
就表示为类比资料回应,所以 matcher()
为:
/**
* @typedef {import('@/types/type').DigitalResponseMessage} DigitalResponseMessage
* @typedef {import('@/types/type').AnalogResponseMessage} AnalogResponseMessage
*/
import { arraySplit, matchFeature } from '@/script/utils/utils';
export default [
// ...
// analogMessage: 类比讯息回应
{
key: 'analogMessage',
eventName: 'data:analogMessage',
/**
* @param {number[]} res
*/
matcher(res) {
return res.some((byte) => byte >= 0xE0 && byte <= 0xEF);
},
/**
* @param {number[]} values
* @return {AnalogResponseMessage[]}
*/
getData(values) {
},
},
]
再来是 getData()
的实作。
/**
* @typedef {import('@/types/type').DigitalResponseMessage} DigitalResponseMessage
* @typedef {import('@/types/type').AnalogResponseMessage} AnalogResponseMessage
*/
import { arraySplit, matchFeature } from '@/script/utils/utils';
export default [
// ...
// analogMessage: 类比讯息回应
{
key: 'analogMessage',
eventName: 'data:analogMessage',
/**
* @param {number[]} res
*/
matcher(res) {
return res.some((byte) => byte >= 0xE0 && byte <= 0xEF);
},
/**
* @param {number[]} values
* @return {AnalogResponseMessage[]}
*/
getData(values) {
// 取得所有特徵点位置
const indexs = values.reduce((acc, byte, index) => {
if (byte >= 0xE0 && byte <= 0xEF) {
acc.push(index);
}
return acc;
}, []);
const analogVals = indexs.reduce((acc, index) => {
const valueBytes = values.slice(index + 1, index + 3);
const analogPin = values[index] - 0xE0;
const value = significantBytesToNumber(valueBytes);
acc.push({
analogPin, value,
});
return acc;
}, []);
return analogVals;
},
},
]
接着在 window-analog-input-item.vue
的 init()
中加入监听器,监听数值回传。
src\components\window-analog-input-item.vue <script>
// ...
export default {
name: 'WindowAnalogInputItem',
// ...
methods: {
handleDelete() {
this.$emit('delete', this.pin);
},
init() {
/** @type {PinInfo} */
const pin = this.pin;
/** @type {PortTransceiver} */
const portTransceiver = this.portTransceiver;
portTransceiver.addCmd('setMode', {
pin: pin.number,
mode: ANALOG_INPUT,
});
// 监听 analogMessage
const listener = portTransceiver.on(
'data:analogMessage',
(data) => {
console.log(`[ analogMessage ] data : `, ...data);
},
{ objectify: true }
);
// 组件销毁时,取消监听器。
this.$once('hook:beforeDestroy', () => {
listener.off();
});
},
},
};
来尝试看看有没有解析成功。
成功!这时候大家可能会有个问题「为甚麽 analogPin 是 0,我明明选 Pin 14 捏」。
还记得在 D08 取得的「类比脚位映射表 analogPinMap
」吗?这是因为 Firmata 回应类比讯息时,对应脚位的讯息是用「类比脚位编号」呈现,所以 window-analog-input-item.vue
还要取得 Vuex 中的 analogPinMap
。
src\components\window-analog-input-item.vue <script>
// ...
export default {
name: 'WindowAnalogInputItem',
// ...
computed: {
...mapState({
/** @type {PortTransceiver} */
portTransceiver: (state) => state.core.transceiver,
analogPinMap: (state) => state.board.info.analogPinMap,
}),
/** 映射後的编号 */
analogPinMapNum() {
/** @type {PinInfo} */
const pin = this.pin;
return this.analogPinMap?.[pin.number] ?? null;
},
// ...
},
// ...
};
最後一步一步完善功能吧!
handleData()
,将类比数值储存至 pinValue
在 q-slider
、q-knob
中加入 min
与 max
。
src\components\window-analog-input-item.vue <template lang="pug">
.c-row.q-0px.items-center.w-full
// ...
q-slider.mx-20px(
v-model='pinValue',
readonly,
color='red-3',
:min='0',
:max='valueMax'
)
q-knob(
v-model='pinValue',
size='60px',
show-value,
readonly,
color='red-3',
track-color='grey-3',
font-size='12px',
:min='0',
:max='valueMax'
)
建立 handleData()
。
src\components\window-analog-input-item.vue <script>
// ...
export default {
name: 'WindowAnalogInputItem',
// ...
methods: {
// ...
init() {
// ...
// 监听 analogMessage
const listener = portTransceiver.on(
'data:analogMessage',
(data) => {
this.handleData(data);
},
{ objectify: true }
);
// ...
},
/** 处理数值
* @param {AnalogResponseMessage[]} data
*/
handleData(data) {
if (isNil(this.analogPinMapNum)) {
console.error(`[ ${this.$options.name} handleData ] 脚位映射值不存在`);
return;
}
// 取得最後一个状态
/** @type {AnalogResponseMessage} */
const target = findLast(
data,
({ analogPin }) => this.analogPinMapNum === analogPin
);
if (!target) return;
this.pinValue = target.value;
},
},
};
实测看看效果。
成功显示类比输入数值!✧*。٩(ˊᗜˋ*)و✧*。
不过仔细看会发现 Slider 与 Knob 怎麽有点卡卡的,那是因为 Firmata 回传频率是 19ms 一次,而 Slider 与 Knob 本身有过渡动画,太过频繁的变更反而会让动画不流畅。
所以让我们加入 throttle
吧!( ´ ▽ ` )ノ
throttle 就是节流阀的意思,可以将原有的触发频率改成自订的频率,可用於节省效能等等作用。
详细的展示与说明可以参考以下连结:
[javascript] throttle 与 debounce,处理频繁的 callback 执行频率
首先建立 throttle
使用的变数与功能。
src\components\window-analog-input-item.vue <script>
// ...
import { isNil, findLast, throttle } from 'lodash-es';
// ...
export default {
name: 'WindowAnalogInputItem',
// ...
data() {
return {
/** 脚位数值 */
pinValue: 0,
throttle: {
setPinValue: null,
},
};
},
// ...
created() {
this.init();
this.throttle.setPinValue = throttle(this.setPinValue, 200);
},
// ...
methods: {
// ...
setPinValue(value) {
this.pinValue = value;
},
},
};
接着改写 handleData()
,储存数值的部分。
src\components\window-analog-input-item.vue <script>
// ...
export default {
name: 'WindowAnalogInputItem',
// ...
methods: {
// ...
/** 处理数值
* @param {AnalogResponseMessage[]} data
*/
handleData(data) {
if (isNil(this.analogPinMapNum)) {
console.error(`[ ${this.$options.name} handleData ] 脚位映射值不存在`);
return;
}
// 取得最後一个状态
/** @type {AnalogResponseMessage} */
const target = findLast(
data,
({ analogPin }) => this.analogPinMapNum === analogPin
);
if (!target) return;
this.throttle.setPinValue(target.value);
},
},
};
看看效果如何。
看起来好多了。(≖‿ゝ≖)✧
大家可以加上更多个可变电阻看看效果 ◝( •ω• )◟
至此我们成功完成「类比输入视窗」了!
以上程序码已同步至 GitLab,大家可以前往下载:
<<: 【从零开始的Swift开发心路历程-Day17】简易订单系统Part1
>>: Day 24:专案06 - 股市趋势图01 | 单月股市API、Pandas
今天介绍的是第二种排序法是选择排序法(Selection Sort)。 选择排序法 将资料分成已排序...
变数作用域 某变数的作用域代表某变数能够被使用的地方 以 Python 来说就是同个函式内,变数被建...
伫列(Queue)建立的方法 enqueue: 尾端新增元素 dequeue: 从前端移除元素 pe...
目的 建立一个「唯一」物件,专责於服务只能单一连线的情境,例如跟资料库的沟通,同时确保全域内都可以呼...
你我应该都有类似的不佳体验:点下一个按钮时,画面什麽也没有改变,你以为刚刚没点到,又再点了一次,发现...