本系列文已改編成書「Arduino 自造趣:結合 JavaScript x Vue x Phaser 輕鬆打造個人遊戲機」,本書改用 Vue3 與 TypeScript 全面重構且加上更詳細的說明,
在此感謝 iT 邦幫忙、博碩文化與編輯小 p 的協助,歡迎大家前往購書,鱈魚在此感謝大家 (。・∀・)。
若想 DIY 卻不知道零件去哪裡買的讀者,可以參考此連結 。( •̀ ω •́ )✧
再來就是實際建立透過 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,大家可以前往下載: