再来就是实际建立透过 select 选择的脚位,并建立相关 Firmata 功能。
稍微规划一下预期 UI 内容。
建立 window-digital-io-item.vue
组件,用来作为数位功能控制与显示功能。
具体实现功能:
开关
使用 Quasar Toggle。
下拉选单
使用 Quasar Select。
显示数位功能:Digital Input、Digital Output、Input Pullup
删除按钮(脚位编号)
使用 Quasar Button。
程序的部份为:
src\components\window-digital-io-item.vue <template lang="pug">
.c-row.q-0px.w-full
.pin-number
.text-20px
| {{ pin.number }}
q-btn.bg-white(
@click='handleDelete',
icon='r_delete',
dense,
flat,
rounded,
color='grey-5'
)
q-select.w-full(
v-model='selectedCapabilitity',
outlined,
dense,
rounded,
emit-value,
map-options,
:options='capabilitityOptions',
color='teal'
)
q-toggle(
v-model='pinValue',
color='teal-5',
keep-color,
checked-icon='r_bolt'
)
src\components\window-digital-io-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-digital-io-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 firmataUtils, { PinMode } from '@/script/utils/firmata.utils';
export default {
name: 'WindowDigitalIoItem',
components: {},
props: {
/** @type {PinInfo} */
pin: {
type: Object,
required: true,
},
},
data() {
return {
/** 数位功能种类 */
digitalModes: [
PinMode.DIGITAL_INPUT,
PinMode.DIGITAL_OUTPUT,
PinMode.INPUT_PULLUP,
],
/** 选择模式 */
selectedCapabilitity: null,
/** 脚位数值 */
pinValue: false,
};
},
computed: {
...mapState({
/** @type {PortTransceiver} */
portTransceiver: (state) => state.core.transceiver,
}),
/** 此脚位可用的模式 */
usableCapabilities() {
/** @type {PinInfo} */
const pin = this.pin;
const capabilities = pin.capabilities.filter((capabilitity) =>
this.digitalModes.includes(capabilitity.mode)
);
return capabilities;
},
/** 脚位模式 select options */
capabilitityOptions() {
/** @type {PinCapability[]} */
const usableCapabilities = this.usableCapabilities;
return usableCapabilities.map((capabilitity) => {
const { name: label } = firmataUtils.getDefineByCode(capabilitity.mode);
return {
label,
value: capabilitity,
};
});
},
},
watch: {},
created() {},
mounted() {},
beforeDestroy() {},
methods: {
handleDelete() {},
},
};
接着在 window-digital-io.vue
引入 window-digital-io-item.vue
src\components\window-digital-io.vue <script>
/**
* @typedef {import('@/types/type').PinInfo} PinInfo
*/
import { mapState } from 'vuex';
import BaseWindow from '@/components/base-window.vue';
import BaseSelectPin from '@/components/base-select-pin.vue';
import WindowDigitalIoItem from '@/components/window-digital-io-item.vue';
//...
export default {
name: 'WindowDigitalIo',
components: {
'base-window': BaseWindow,
'base-select-pin': BaseSelectPin,
'window-digital-io-item': WindowDigitalIoItem,
},
// ...
};
src\components\window-digital-io-item.vue <template lang="pug">
base-window.window-digital-io(
:pos='pos',
headerIconColor='teal-3',
body-class='c-col p-20px pt-20px',
title='数位 I/O 功能'
)
base-select-pin(
:pins='supportPins',
color='teal-3',
@selected='addPin',
@err='handleErr'
)
q-scroll-area.pt-10px.h-300px.flex
transition-group(name='list-complete', tag='div')
window-digital-io-item.py-10px(
v-for='pin in existPins',
:pin='pin',
:key='pin.number',
@delete='deletePin'
)
q-scroll-area
组件,保留未来滚动事件扩充性(文档:Quasar Scroll Area)。transition-group
让项目建立、移除时有过渡动画。尝试建立脚位看看。
出现了!ヾ(◍'౪`◍)ノ゙
再来就是实作数位功能了!
在控制脚位数值之前,需要先设定脚位模式。
在「Control Messages Expansion」可以找到设定脚位模式的命令为:
0 set digital pin mode (0xF4) (MIDI Undefined)
1 set pin number (0-127)
2 mode (INPUT/OUTPUT/ANALOG/PWM/SERVO/I2C/ONEWIRE/STEPPER/ENCODER/SERIAL/PULLUP, 0/1/2/3/4/6/7/8/9/10/11)
由以上说明可以得知:
0xF4
也就是说,如果要设定 Pin 11(0x0B
)为「数位输出(0x01
)」模式,则命令为 [ 0xF4, 0x0B, 0x01 ]
接着根据刚才的结论,在 cmd-define.js
新增设定脚位模式命令。
src\script\firmata\cmd-define.js
export default [
//...
// setMode: 设定脚位模式
{
key: 'setMode',
getValue({ pin, mode }) {
const cmds = [0xF4, pin, mode];
return cmds;
},
},
]
最後则是设定数位脚位数值,在「Control Messages Expansion」可以找到设定脚位数值的命令为:
0 set digital pin value (0xF5) (MIDI Undefined)
1 set pin number (0-127)
2 value (LOW/HIGH, 0/1)
由以上说明可以得知:
0xF5
也就是说,若要设定 Pin 11(0x0B
)为「数位输出数值」为:
0x01
)」,则命令为 [ 0xF5, 0x0B, 0x01 ]
0x00
)」,则命令为 [ 0xF5, 0x0B, 0x00 ]
接着根据刚才的结论,在 cmd-define.js
新增设定脚位模式命令。
src\script\firmata\cmd-define.js
export default [
//...
// setMode: 设定脚位模式
{
key: 'setMode',
getValue({ pin, mode }) {
const cmds = [0xF4, pin, mode];
return cmds;
},
},
// setDigitalPinValue: 设定数位脚位数值
{
key: 'setDigitalPinValue',
getValue({ pin, value }) {
const level = value ? 0x01 : 0x00;
return [0xF5, pin, level];
},
},
]
在 window-digital-io-item.vue
中侦测选择模式变化,进行脚位模式设定。
src\components\window-digital-io-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 firmataUtils, { PinMode } from '@/script/utils/firmata.utils';
export default {
name: 'WindowDigitalIoItem',
// ...
watch: {
/** 侦测选择的脚位功能 */
selectedCapabilitity() {
this.initMode();
},
},
// ...
methods: {
// ...
/** 初始化指定模式 */
initMode() {
/** @type {PinInfo} */
const pin = this.pin;
/** @type {PortTransceiver} */
const portTransceiver = this.portTransceiver;
/** @type {PinCapability} */
const selectedCapabilitity = this.selectedCapabilitity;
const mode = selectedCapabilitity.mode;
portTransceiver.addCmd('setMode', {
pin: pin.number,
mode,
});
},
},
};
只要使用者选定脚位功能後,就会发送 setMode
命令设定脚位模式。
再来就是侦测开关变化,并将开关数值传送至脚位数值。
src\components\window-digital-io-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 firmataUtils, { PinMode } from '@/script/utils/firmata.utils';
export default {
name: 'WindowDigitalIoItem',
// ...
watch: {
/** 侦测选择的脚位功能 */
selectedCapabilitity() {
this.initMode();
},
pinValue(value) {
/** @type {PinInfo} */
const pin = this.pin;
/** @type {PortTransceiver} */
const portTransceiver = this.portTransceiver;
portTransceiver.addCmd('setDigitalPinValue', {
pin: pin.number,
value,
});
},
},
// ...
};
实测看看有没有发送成功。
使用两个 LED 试试看。
感觉真棒 ✧*。٩(ˊᗜˋ*)و✧*。
上拉输入与数位输入都是数位讯号输入,所以解析回应的地方都一样,差别在脚位模式不同而已。
设定模式命令在数位输出时已经完成,所以只差解析数位资料回应而已!
这时候有一个问题「所以要如何取得脚位数值?」,最直觉的想法是不断发送查询命令、取得数值,不过 Firmata 有个贴心的设计,可以让 MCU 侦测脚位状态变化,自动回传状态。
在「Message Types」找到 report digital port。
可以看到 report digital port 的命令为 0xD0
,根据说明,可以得知:
0xD0 + port
实际上 Uno 操作脚位状态是操作 Port Register 数值,每个 Port 之 Register 为 1 byte,所以可以控制 8 只脚位状态。
不过这里的 port 并非实际上 Register 编号,单纯是依顺序分组。
例如:Pin 0 至 Pin 7 为 Port 0,Pin 8 至 Pin 15 为 Port 1,以此类推。
详细内容可以查看以下连结:
Arduino CC - Port Manipulation
取得例子:
设定 Pin 4 为数位输入。
Pin 4 为 Port 0,所以 [ 0xD0 + 0, 0x01 ]
,得命令为 [ 0xD0, 0x01 ]
设定 Pin 10 为数位输入。
Pin 10 为 Port 1,所以 [ 0xD0 + 1, 0x01 ]
,得命令为 [ 0xD1, 0x01 ]
让我们调整一下刚才 cmd-define.js
中的 setMode
命令,让模式为数位输入时,自动加入「开启自动回报命令」。
src\script\firmata\cmd-define.js
import { PinMode } from '@/script/utils/firmata.utils';
const { DIGITAL_INPUT, INPUT_PULLUP } = PinMode;
export default [
// ..
// setMode: 设定脚位模式
{
key: 'setMode',
getValue({ pin, mode }) {
const cmds = [0xF4, pin, mode];
// Mode 如果为 Digital Input,加入开启自动回报命令
if ([DIGITAL_INPUT, INPUT_PULLUP].includes(mode)) {
const port = 0xD0 + ((pin / 8) | 0);
cmds.push(port, 0x01);
}
return cmds;
},
},
// ...
]
尝试看看发送命令後,有没有未定义的资料回应。
资料进来了!再来就是解析资料了。
在 response-define.js
新增定义 digitalMessage
src\script\firmata\response-define.js
/**
* @typedef {import('@/types/type').DigitalResponseMessage} DigitalResponseMessage
*/
import { arraySplit, matchFeature } from '@/script/utils/utils';
export default [
// ...
// digitalMessage: 数位讯息回应
{
key: 'digitalMessage',
eventName: 'data:digitalMessage',
/**
* @param {number[]} res
*/
matcher(res) {
},
/**
* @param {number[]} values
* @return {DigitalResponseMessage[]}
*/
getData(values) {
},
},
]
matcher()
和 getData()
内容要怎麽实作呢?在「Data Message Expansion」可以找到数位回应资料的说明:
Two byte digital data format, second nibble of byte 0 gives the port number (eg 0x92 is the third port, port 2)
回应资料为:
0 digital data, 0x90-0x9F, (MIDI NoteOn, bud different data format)
1 digital pins 0-6 bitmask
2 digital pin 7 bitmask
也就是说数值开头只要包含 0x90-0x9F
就表示为数位资料回应,所以 matcher()
为:
/**
* @typedef {import('@/types/type').DigitalResponseMessage} DigitalResponseMessage
*/
import { arraySplit, matchFeature } from '@/script/utils/utils';
export default [
// ...
// digitalMessage: 数位讯息回应
{
key: 'digitalMessage',
eventName: 'data:digitalMessage',
/**
* @param {number[]} res
*/
matcher(res) {
const hasCmd = res.some((byte) => byte >= 0x90 && byte <= 0x9F);
return hasCmd;
},
/**
* @param {number[]} values
* @return {DigitalResponseMessage[]}
*/
getData(values) {
},
},
]
getData()
的部份则是如以下需求:
就是 D04 提到的「分屍传输」,将大於 ,1 byte 的数值拆分传输,现在则是要组装回来。
所以先在 utils.js
建立一个专门组装数值的函数。
src\script\utils\utils.js
// ...
/** 将有效 Bytes 转为数值
* @param {number[]} bytes 有效位元矩阵。bytes[0] 为 LSB
* @param {number} [bitsNum] 每 byte 有效位元数
*/
export function significantBytesToNumber(bytes, bitsNum = 7) {
const number = bytes.reduce((acc, byte, index) => {
const mesh = 2 ** bitsNum - 1;
const validBits = byte & mesh;
acc += (validBits << (bitsNum * index))
return acc;
}, 0);
return number;
}
bitsNum
参数预设 7接着完成 getData()
部份。
/**
* @typedef {import('@/types/type').DigitalResponseMessage} DigitalResponseMessage
*/
import { arraySplit, matchFeature, significantBytesToNumber } from '@/script/utils/utils';
export default [
// ...
// digitalMessage: 数位讯息回应
{
key: 'digitalMessage',
eventName: 'data:digitalMessage',
/**
* @param {number[]} res
*/
matcher(res) {
const hasCmd = res.some((byte) => byte >= 0x90 && byte <= 0x9F);
return hasCmd;
},
/**
* @param {number[]} values
* @return {DigitalResponseMessage[]}
*/
getData(values) {
// 取得所有特徵点位置
const indexs = values.reduce((acc, byte, index) => {
if (byte >= 0x90 && byte <= 0x9F) {
acc.push(index);
}
return acc;
}, []);
/** @type {DigitalResponseMessage[]} */
const responses = indexs.reduce((acc, index) => {
const bytes = values.slice(index + 1, index + 3);
const port = values[index] - 0x90;
const value = significantBytesToNumber(bytes);
acc.push({
port, value,
});
return acc;
}, []);
return responses;
},
},
]
最後在 window-digital-io-item.vue
加入 portTransceiver.on('data:digitalMessage')
,接收看看资料。
src\components\window-digital-io-item.vue <script>
// ...
export default {
name: 'WindowDigitalIoItem',
// ...
methods: {
handleDelete() {},
/** 初始化指定模式 */
initMode() {
// ...
portTransceiver.addCmd('setMode', {
pin: pin.number,
mode,
});
portTransceiver.on('data:digitalMessage', (data) => {
console.log(`[ digitalMessage ] data : `, ...data);
});
},
},
};
成功接收资料!
最後来完善功能吧,首先 window-digital-io-item.vue
中的 initMode()
,需要针对不同模式进行对应动作。
listener
储存监听器,并於组件销毁或脚位模式切换时删除监听器。src\components\window-digital-io-item.vue <script>
// ...
const { DIGITAL_INPUT, DIGITAL_OUTPUT, INPUT_PULLUP } = PinMode;
export default {
name: 'WindowDigitalIoItem',
// ...
data() {
return {
// ...
listener: null,
};
},
// ...
beforeDestroy() {
this.listener?.off?.();
},
methods: {
handleDelete() {},
/** 初始化指定模式 */
initMode() {
// ...
/** @type {PinCapability} */
const selectedCapabilitity = this.selectedCapabilitity;
const mode = selectedCapabilitity.mode;
portTransceiver.addCmd('setMode', {
pin: pin.number,
mode,
});
// 数位输入
if ([INPUT_PULLUP, DIGITAL_INPUT].includes(mode)) {
this.listener = portTransceiver.on(
'data:digitalMessage',
(data) => {
console.log(`[ digitalMessage ] data : `, ...data);
},
{
objectify: true,
}
);
return;
}
// 数位输出
if ([DIGITAL_OUTPUT].includes(mode)) {
this.listener?.off?.();
this.pinValue = false;
return;
}
console.error(`[ initMode ] 未定义模式 : `, mode);
},
},
};
接着让解析後的数位资料反映在开关上,有以下需求:
handleData()
,负责处理接收到的资料并储存至 pinValue
。
pinValue
数值。数位资料中的 value 是指整个 Port 的数值,也就是 8 只脚的数值以二进位方式组合,关系如下图:
在 utils.js
建立一个从数值中取得指定 bit 的函数。
src\script\utils\utils.js
// ...
/** 取得数值特定 Bit
* @param {Number} number
* @param {Number} bitIndex bit Index。从最小位元并以 0 开始
*/
export function getBitWithNumber(number, bitIndex) {
const mesh = 1 << bitIndex;
const value = number & mesh;
return !!value;
}
依照需求设计程序。
src\components\window-digital-io-item.vue <script>
// ...
const { DIGITAL_INPUT, DIGITAL_OUTPUT, INPUT_PULLUP } = PinMode;
export default {
name: 'WindowDigitalIoItem',
// ...
computed: {
// ...
/** 此脚位所属的 Port 编号 */
pinPortNum() {
/** @type {PinInfo} */
const pin = this.pin;
const port = (pin.number / 8) | 0;
return port;
},
},
watch: {
// ...
pinValue(value) {
/** @type {PinInfo} */
const pin = this.pin;
/** @type {PortTransceiver} */
const portTransceiver = this.portTransceiver;
// 只有输出模式才要设定数值
/** @type {PinCapability} */
const { mode } = this.selectedCapabilitity;
if (mode !== DIGITAL_OUTPUT) return;
portTransceiver.addCmd('setDigitalPinValue', {
pin: pin.number,
value,
});
},
},
// ...
methods: {
// ...
/** 初始化指定模式 */
initMode() {
// ...
// 数位输入
if ([INPUT_PULLUP, DIGITAL_INPUT].includes(mode)) {
this.listener = portTransceiver.on(
'data:digitalMessage',
(data) => {
this.handleData(data);
},
{
objectify: true,
}
);
return;
}
// ...
},
/** 处理数值
* @param {DigitalResponseMessage[]} data
*/
handleData(data) {
// 取得最後一次数值即可
/** @type {DigitalResponseMessage} */
const target = findLast(data, ({ port }) => {
return this.pinPortNum === port;
});
if (!target) return;
const { value } = target;
/** @type {PinInfo} */
const pin = this.pin;
const bitIndex = pin.number % 8;
this.pinValue = getBitWithNumber(value, bitIndex);
},
},
};
最後增加一点 UX 和其他细节:
src\components\window-digital-io-item.vue <script>
// ...
const { DIGITAL_INPUT, DIGITAL_OUTPUT, INPUT_PULLUP } = PinMode;
export default {
name: 'WindowDigitalIoItem',
// ...
computed: {
handleDelete() {
this.$emit('delete', this.pin);
},
// ...
/** 停用开关 */
lockToggle() {
/** @type {PinCapability} */
const selectedCapabilitity = this.selectedCapabilitity;
if (!selectedCapabilitity) {
return true;
}
// 只有 OUTPUT 模式可以使用
if (selectedCapabilitity.mode === DIGITAL_OUTPUT) {
return false;
}
return true;
},
},
// ..
};
src\components\window-digital-io-item.vue <template lang="pug">
.c-row.q-0px.w-full
// ...
q-toggle(
v-model='pinValue',
color='teal-5',
keep-color,
:disable='lockToggle',
checked-icon='r_bolt'
)
尝试看看效果。
到此成功完成「数位 I/O 视窗」了!
以上程序码已同步至 GitLab,大家可以前往下载:
<<: Day 14 - AI-900 认证心得(2) - 考试
>>: [Day26] 电脑有秘密档案不想被发现吗? 教你用图片伪装秘密档案!
这系列的文章不会讲完全部 KSP 的实作,毕竟我也还正在实作中,不过实作的方向应该是跟前几篇讲的差不...
根据官方文件给的方法上传 https://firebase.google.com/docs/stor...
今天来教教 JavaScript 的基础语法:注解、变数、常数、回圈、条件、函式 以及 运算。 目的...
本篇同步发布於个人Blog: [PoEAA] Domain Logic Pattern - Tabl...
工作上没机会用到Redis 自己就搞一个来玩,以後工作说不定也会用到 Redis是什麽 官网:htt...