电子助教:「这个标题...我闻到了停刊的味道... (́⊙◞౪◟⊙‵)」
这个章节开始我们要建立「数位功能 I/O 视窗」。
简单来说就是 0 与 1,只有开与关两种状态的讯号。问题来了,所以到底要怎麽用电压表示 0、1?电压不是可以连续变化吗?
将连续变化的电压定义为 0 或 1,这个过程我们称之为「逻辑电压准位」。
以 Arduino Uno 为例,若输入电压在 0.5 到 1.5 V 之间,则判断为 0;3 到 5.5 V 之间,则判断为 1。
1.5 到 3 V 这个区间称之为「不确定」区间,意思是如果输入电压在这之间,Arduino Uno 不能保证读取到的状态到底是 0 还是 1。
若有兴趣想了解更深入的说明,可以参考以下连结。
从 protocol 我们可以知道脚位模式等等资讯都是数值代号,无法直觉阅读,所以我们建立一个转换 firmata 资讯的工具,并设计每个模式对应的颜色(使用 Quasar Color Palette)。
src\script\utils\firmata.utils.js
const pinModeDefinition = [
{
code: 0x00,
key: 'digitalInput',
name: 'Digital Input',
color: 'light-blue-3',
},
{
code: 0x01,
key: 'digitalOutput',
name: 'Digital Output',
color: 'cyan-3',
},
{
code: 0x02,
key: 'analogInput',
name: 'Analog Input',
color: 'red-4',
},
{
code: 0x03,
key: 'pwm',
name: 'PWM',
color: 'light-green-4',
},
{
code: 0x04,
key: 'servo',
name: 'Servo',
color: 'blue-5',
},
{
code: 0x05,
key: 'shift',
name: 'Shift',
color: 'purple-3',
},
{
code: 0x06,
key: 'i2c',
name: 'I2C',
color: 'green-4',
},
{
code: 0x07,
key: 'onewire',
name: 'Onewire',
color: 'indigo-4',
},
{
code: 0x08,
key: 'stepper',
name: 'Stepper',
color: 'lime-4',
},
{
code: 0x09,
key: 'encoder',
name: 'Encoder',
color: 'yellow-4',
},
{
code: 0x0A,
key: 'serial',
name: 'Serial',
color: 'amber-5',
},
{
code: 0x0B,
key: 'inputPullup',
name: 'Input Pullup',
color: 'teal-3',
},
{
code: 0x0C,
key: 'spi',
name: 'SPI',
color: 'amber-4',
},
{
code: 0x0D,
key: 'sonar',
name: 'Sonar',
color: 'orange-4',
},
{
code: 0x0E,
key: 'tone',
name: 'Tone',
color: 'deep-orange-4',
},
{
code: 0x0F,
key: 'dht',
name: 'DHT',
color: 'brown-3',
},
];
export const PinMode = {
/** 数位输入 : 0x00 */
DIGITAL_INPUT: 0x00,
/** 数位输出 : 0x01 */
DIGITAL_OUTPUT: 0x01,
/** 类比输入 : 0x02 */
ANALOG_INPUT: 0x02,
/** PWM : 0x03 */
PWM: 0x03,
/** 数位上拉输入 : 0x0B */
INPUT_PULLUP: 0x0B,
}
export default {
getDefineByCode(mode) {
const target = pinModeDefinition.find((item) => item.code === mode);
if (!target) {
return null;
}
return target;
},
};
将 window-example.vue
复制一份後改个名字,建立 src\components\window-digital-io.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 功能'
)
</template>
<style lang="sass">
.window-digital-io
width: 330px
height: 440px
</style>
<script>
import BaseWindow from '@/components/base-window.vue';
export default {
name: 'WindowDigitalIo',
components: {
'base-window': BaseWindow,
},
props: {
pos: {
type: Object,
default() {
return {
x: 0,
y: 0,
};
},
},
},
data() {
return {
id: this.$vnode.key,
};
},
provide() {
return {
id: this.id,
};
},
computed: {},
watch: {},
created() {},
mounted() {},
methods: {},
};
</script>
可以观察到 props
中的 pos
在每个 wnidow 都会使用,所以抽离、独立成 mixin。
src\mixins\mixin-window.js
/**
* 标准 window 共用内容
*/
export default {
props: {
pos: {
type: Object,
default() {
return {
x: 0,
y: 0,
};
},
},
},
}
window-digital-io.vue
加入 mixin-window.js
并移除 props
原本的 pos
。
src\components\window-digital-io.vue <script>
import BaseWindow from '@/components/base-window.vue';
import mixinWindow from '@/mixins/mixin-window';
export default {
name: 'WindowDigitalIo',
components: {
'base-window': BaseWindow,
},
mixins: [mixinWindow],
props: {},
data() {
return {
id: this.$vnode.key,
};
},
provide() {
return {
id: this.id,
};
},
computed: {},
watch: {},
created() {},
mounted() {},
methods: {},
};
回到 app.vue
,将右键选单内的「范例视窗」改为新增「数位 I/O 视窗」,并引入组件。
src\app.vue <template lang="pug">
.screen(@click='handleClick')
// ...
// 右键选单
q-menu(context-menu, content-class='border-radius-s')
q-list.min-w-260px
q-item(@click='addWindow("window-digital-io")', clickable, v-close-popup)
q-item-section
| 新增「数位 I/O 视窗」
src\app.vue <script>
// ...
import WindowDigitalIo from '@/components/window-digital-io.vue';
export default {
name: 'App',
components: {
'dialog-system-setting': DialogSystemSetting,
'window-digital-io': WindowDigitalIo,
},
// ...
};
稍微规划一下 UI 呈现。
首先需要设计「选择 pin 脚的下拉选单」,功能预计如下:
v-model
绑定选择数值,也可以选取後 emit 选择项目。建立 base-select-pin.vue
,使用并魔改 Quasar Select 组件。
src\components\base-select-pin.vue <template lang="pug">
q-select.text-shadow.base-select-pin(
:value='value',
use-input,
:bg-color='color',
:color='color',
:clearable='clearable',
:options='filterOptions',
:placeholder='placeholderText',
:input-debounce='0',
:option-label='calcOptionLabel',
rounded,
outlined,
hide-dropdown-icon,
dense,
input-class='text-center font-black placeholder-black',
popup-content-class='border-radius-m',
@filter='filterFn'
)
template(v-slot:no-option)
// 替换 option 为空时,显示的内容
q-item.py-10px.border-b-1.text-red.text-center
q-item-section(v-if='pins.length === 0')
q-item-label
| 无脚位资料
q-item-section(v-else)
q-item-label
| 无符合关键字的脚位
template(v-slot:option='{ opt }')
// 自定 option 内容
q-item.py-10px.border-b-1(
@click='handleClick(opt)',
dense,
clickable,
v-close-popup,
:key='opt.number'
)
// 显示脚位编号
q-item-section(avatar)
q-item-label.c-row.items-end.font-orbitron.w-50px.text-grey-8
.text-14px.mr-4px.text-grey
| Pin
.text-20px.font-100
| {{ opt.label }}
// 显示脚位模式
q-item-section
q-item-label
q-chip.text-shadow-md.font-700(
v-for='chip in opt.chips',
rounded,
size='md',
:color='chip.color',
text-color='white',
:key='chip.name'
)
| {{ chip.name }}
src\components\base-select-pin.vue <style lang="sass">
@import '@/styles/quasar.variables.sass'
.base-select-pin
position: relative
.q-field__control
&::before
border: none !important
src\components\base-select-pin.vue <script>
/**
* @typedef {import('@/types/type').PinInfo} PinInfo
*/
import firmataUtils from '@/script/utils/firmata.utils';
export default {
name: 'BaseSelectPin',
components: {},
props: {
value: {
type: Object,
default() {
return null;
},
},
/** 候选脚位
* @type {PinInfo[]}
*/
pins: {
type: Array,
default() {
return [];
},
},
placeholder: {
type: String,
default: '选择新增脚位',
},
color: {
type: String,
default: 'blue-grey-4',
},
clearable: {
type: Boolean,
default: false,
},
},
data() {
return {
// 过滤完成的 options
filterOptions: [],
};
},
computed: {
options() {
/** @type {PinInfo[]} */
const pins = this.pins;
const options = pins.map((pin) => {
const chips = pin.capabilities.map((capability) =>
firmataUtils.getDefineByCode(capability.mode)
);
return {
label: pin.number,
chips,
value: pin,
};
});
return options;
},
placeholderText() {
if (!this.value) {
return this.placeholder;
}
return '';
},
},
watch: {},
created() {},
mounted() {},
methods: {
handleClick(option) {
const pin = option.value;
if (!pin) {
return;
}
this.$emit('selected', pin);
// 更新 v-model 绑定数值
this.$emit('input', pin);
},
/** 计算 Label 显示文字
* @param {PinInfo} pin
*/
calcOptionLabel(pin) {
if (!pin) {
return '';
}
return `Pin ${pin.number}`;
},
/**
* Quasar Select 过滤功能
* https://v1.quasar.dev/vue-components/select#filtering-and-autocomplete
*/
filterFn(keyWord, update) {
if (!keyWord) {
update(() => {
this.filterOptions = this.options;
});
return;
}
update(() => {
// 根据关键字过滤
const regex = new RegExp(keyWord, 'i');
this.filterOptions = this.options.filter((option) => {
const pinNum = option.label;
const chips = option.chips;
// 搜寻脚位模式名称
const matchChip = chips.some((chip) => {
return regex.test(chip.name);
});
return regex.test(pinNum) || matchChip;
});
});
},
},
};
在 window-digital-io.vue
引入 base-select-pin.vue
。
接着便是提供脚位清单资料作为 base-select-pin.vue
的 options 显示。
在 window-digital-io.vue
之 computed
增加 supportPins
,提供支援数位功能脚位清单。
src\components\window-digital-io.vue <script>
// ...
import { PinMode } from '@/script/utils/firmata.utils';
const { DIGITAL_INPUT, DIGITAL_OUTPUT, INPUT_PULLUP } = PinMode;
export default {
name: 'WindowDigitalIo',
// ...
computed: {
...mapState({
boardPins: (state) => state.board.info.pins,
}),
// 支援功能的脚位
supportPins() {
/** @type {PinInfo[]} */
const boardPins = this.boardPins;
return boardPins.filter((pin) => {
const hasDigitalFcn = pin.capabilities.some((capability) =>
[DIGITAL_INPUT, DIGITAL_OUTPUT, INPUT_PULLUP].includes(
capability.mode
)
);
return hasDigitalFcn;
});
},
},
// ...
};
将 supportPins
输入 base-select-pin.vue
。
src\components\window-digital-io.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')
尝试看看效果。
接着增加新增脚位的部分:
existPins
变数,储存目前已建立脚位base-select-pin.vue
之 selected
事件,接收被选择的脚位。src\components\window-digital-io.vue <script>
/**
* @typedef {import('@/types/type').PinInfo} PinInfo
*/
// ...
export default {
name: 'WindowDigitalIo',
// ...
data() {
return {
id: this.$vnode.key,
/** @type {PinInfo[]} */
existPins: [],
};
},
// ...
methods: {
/** 新增脚位
* @param {PinInfo} pin
*/
addPin(pin) {
if (!pin) {
return;
}
this.existPins.push(pin);
},
/** 移除脚位
* @param {PinInfo} pin
*/
deletePin(pin) {
if (!pin) {
return;
}
const index = this.existPins.findIndex(
(existPin) => existPin.number === pin.number
);
this.existPins.splice(index, 1);
},
/** 接收错误讯息 */
handleErr(msg) {
this.$q.notify({
type: 'negative',
message: msg,
});
},
},
};
src\components\window-digital-io.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'
)
可以看到成功在 existPins
增加对应的脚位。
但是有一个严重的问题,就是「可以重复新增脚位」,不只单一视窗内有此问题,不同视窗也会发生脚位占用问题,所以必须将已使用的脚位储存至 Vuex,并进行限制。
在 window.store.js
新增脚位占用相关功能:
Window
物件新增 occupiedPins
,纪录目前已占用脚位。mutations
新增 addOccupiedPin
、deleteOccupiedPin
处理占用脚位新增与移除。getters
新增 occupiedPins
列出所有被占用的脚位。src\store\modules\window.store.js
/**
* 管理视窗相关资料
*/
/**
* @typedef {import('vuex').Module} Module
*
* @typedef {import('@/types/type').Window} Window
* @typedef {import('@/types/type').OccupiedPin} OccupiedPin
*/
// ...
/** @type {Module} */
const self = {
// ...
mutations: {
// ...
/** Window 新增占用脚位 */
addOccupiedPin(state, { id, pin }) {
/** @type {Window[]} */
const windows = state.list;
const target = windows.find((window) => window.id === id);
if (!target) {
console.error(`[ window.store addOccupiedPin ] window 不存在,id : `, id);
return;
}
target.occupiedPins.push(pin);
},
/** Window 移除占用脚位 */
deleteOccupiedPin(state, { id, pin }) {
/** @type {Window[]} */
const windows = state.list;
const target = windows.find((window) => window.id === id);
if (!target) {
console.error(`[ window.store deleteOccupiedPin ] window 不存在,id : `, id);
return;
}
const targetPinIndex = target.occupiedPins.findIndex(({ number }) =>
number === pin.number
);
if (targetPinIndex < 0) {
return;
}
target.occupiedPins.splice(targetPinIndex, 1);
},
},
// ...
getters: {
// ...
/** 列出所有被占用的脚位
* @return {OccupiedPin[]}
*/
occupiedPins: (state) => {
/** @type {Window[]} */
const windows = state.list;
// 找出有占用脚位的 window
const occupiedPinWindows = windows.filter(({ occupiedPins }) =>
occupiedPins.length !== 0
);
const occupiedPins = occupiedPinWindows.reduce((acc, window) => {
const { component, id } = window;
window.occupiedPins.forEach((pin) => {
acc.push({
info: pin,
occupier: {
component, id,
},
});
});
return acc;
}, []);
return occupiedPins;
},
},
// ...
};
export default self;
在 window-digital-io.vue
之 addPin()
与 deletePin()
分别呼叫 window.store.js
之 addOccupiedPin()
与 deleteOccupiedPin()
。
src\components\window-digital-io.vue <script>
// ...
export default {
name: 'WindowDigitalIo',
// ...
methods: {
/** 新增脚位
* @param {PinInfo} pin
*/
addPin(pin) {
if (!pin) {
return;
}
this.$store.commit('window/addOccupiedPin', {
id: this.id,
pin,
});
this.existPins.push(pin);
},
/** 移除脚位
* @param {PinInfo} pin
*/
deletePin(pin) {
if (!pin) {
return;
}
this.$store.commit('window/deleteOccupiedPin', {
id: this.id,
pin,
});
const index = this.existPins.findIndex(
(existPin) => existPin.number === pin.number
);
this.existPins.splice(index, 1);
},
// ...
},
};
尝试看看有没有成功提交占用脚位。
接着在 base-select-pin.vue
取得 occupiedPins
,并限制 options 内容。
disable
。src\components\base-select-pin.vue <script>
/**
* @typedef {import('@/types/type').PinInfo} PinInfo
* @typedef {import('@/types/type').OccupiedPin} OccupiedPin
*/
import { mapGetters } from 'vuex';
import firmataUtils from '@/script/utils/firmata.utils';
export default {
name: 'BaseSelectPin',
// ...
computed: {
...mapGetters({
occupiedPins: 'window/occupiedPins',
}),
options() {
/** @type {PinInfo[]} */
const pins = this.pins;
/** @type {OccupiedPin[]} */
const occupiedPins = this.occupiedPins;
const options = pins.map((pin) => {
const chips = pin.capabilities.map((capability) =>
firmataUtils.getDefineByCode(capability.mode)
);
// 若此 pin 出现在 occupiedPins 中,则 disable 为 true
const disable = occupiedPins.some(
(occupiedPin) => occupiedPin.info.number === pin.number
);
return {
label: pin.number,
chips,
value: pin,
disable,
};
});
return options;
},
// ...
},
// ...
methods: {
handleClick(option) {
/** @type {PinInfo} */
const pin = option.value;
if (!pin) {
return;
}
if (option.disable) {
this.$emit('err', `「${pin.number} 号脚位」已被占用`);
return;
}
this.$emit('selected', pin);
// 更新 v-model 绑定数值
this.$emit('input', pin);
},
// ...
},
};
src\components\base-select-pin.vue <template lang="pug">
q-select.text-shadow.base-select-pin(
// ...
)
// ...
template(v-slot:option='{ opt }')
// 自定 option 内容
q-item.py-10px.border-b-1(
// ...
:class='{ "cursor-not-allowed opacity-40": opt.disable }'
)
// ...
试试看效果。
成功阻止重复新增脚位,世界恢复和平!ᕕ( ゚ ∀。)ᕗ
下一步我们要来实际建立 I/O 控制组件,显示、控制真实的数位讯号。
base-select-pin
组件,用於选择脚位。base-select-pin
显示。以上程序码已同步至 GitLab,大家可以前往下载:
<<: Day.16 应用中学习- 资料库操作 ( golang / sql )
>>: Day16-策略不只用在兵法 什麽是策略(Strategy)
第 29 天 ! 剩~两~天~! 昨天已经把整个 redux 的流程给接起来了, 从 store 读...
我很重视客户的意见与收货速度。 当时我是网路拍卖的创办人,与客户约定好了要五天内到货。为了达成目标,...
【前言】 之前在 Day 3 有介绍过 Non-fungible Token(非同质化代币)与相关...
前言 各位早安,书接上回我们已经搞定接下来会用到的套件的安装了,套件是很强大的工具可以帮助我们简化很...
如何在网际网路宣告真实的 IP?这或许是很多人的疑问。 我们之前有说过 BGP 是需要 ASN 来宣...