电子助教:「所以标题提到的那些东西好吃吗?ლ(´∀`ლ)」
鳕鱼:「不,都不能吃 (́◉◞౪◟◉‵)」
电子助教:(乾骗我... ლ ( ´•̥̥̥ ω •̥̥̥` ლ ) )
在进入建立视窗组件之前,我们必须先取得必要 MCU 资讯,所以这个章节让我们继续取得更多 Firmata 资料吧。
接下来预计还要取得以下资讯:
打开 Firmata Protocol,在「Capability Query」章节可以找到脚位与功能相关说明。
The capability query provides a list of all modes supported by each pin. Each mode is described by 2 bytes where the first byte is the pin mode (such as digital input, digital output, PWM) and the second byte is the resolution (or sometimes the type of pin such as RX or TX for a UART pin). A value of 0x7F is used as a separator to mark the end each pin's list of modes. The number of pins supported is inferred by the message length.
查询命令为:
0 START_SYSEX (0xF0)
1 CAPABILITY_QUERY (0x6B)
2 END_SYSEX (0xF7)
回应资料为:
0 START_SYSEX (0xF0)
1 CAPABILITY_RESPONSE (0x6C)
2 1st supported mode of pin 0
3 1st mode's resolution of pin 0
4 2nd supported mode of pin 0
5 2nd mode's resolution of pin 0
... additional modes/resolutions, followed by `0x7F`,
to mark the end of the pin's modes. Subsequently, each pin
follows with its modes/resolutions and `0x7F`,
until all pins are defined.
N END_SYSEX (0xF7)
从以上说明可以得知:
[ 0xF0, 0x6B, 0xF7 ]
0x6C
之後会接续其脚位内容。0x7F
分隔每个脚位内容。这个资料不像版本编号与名称那样,开启 COM 就会自动回传,需要主动发送查询命令才行,所以我们在 port-transceiver.js
增加发送命令的功能吧。
需要在 firmata.js
新增一个取得命令资料内容的方法。
新增档案 cmd-define.js
,定义 cmd 名称与内容。
src\script\firmata\cmd-define.js
export default [
// queryCapability: 查询所有脚位与功能
{
key: 'queryCapability',
getValue() {
return [0xF0, 0x6B, 0xF7];
},
},
]
并在 firmata.js
新增 getCmdBytes()
Method。
src\script\firmata\firmata.js
/**
* @typedef {Object} ResponseParseResult 回应资料解析结果
* @property {string} key 回应 key
* @property {string} eventName 事件名称
* @property {number[]} oriBytes 原始回应值
* @property {Object} data 解析完成资料
*/
import responsesDefine from '@/script/firmata/response-define';
import cmdsDefinie from '@/script/firmata/cmd-define';
export default {
// ...
/** 取得命令资料
* @param {String} cmdKey
* @param {Object} params
* @return {Number[]}
*/
getCmdBytes(cmdKey, params) {
const target = cmdsDefinie.find(({ key }) =>
key === cmdKey
);
if (!target) {
throw new Error(`${cmdKey} 命令不存在`);
}
return target.getValue(params);
},
}
再来调整 port-transceiver.js
内容:
addCmd()
Method,finishReceive()
增加 emit 事件 undefined-response
,没有成功解析回应内容时触发。src\script\modules\port-transceiver.js
/**
* @typedef {Object} CmdQueueItem 命令项目
* @property {string} key 命令 key
* @property {Object} params 命令参数
* @property {numver[]} values 命令数值
*/
// ...
export default class extends EventEmitter2.EventEmitter2 {
/** @type {CmdQueueItem[]} */
cmdsQueue = []; // 命令伫列
// ...
/** 完成接收,emit 已接收资料 */
finishReceive() {
// 解析回应内容
const results = firmata.parseResponse(this.receiveBuffer);
if (results.length === 0) {
this.emit('undefined-response', this.receiveBuffer);
this.receiveBuffer.length = 0;
return;
}
// ...
}
/** 加入发送命令
* @param {string} cmdKey
* @param {Object} params
*/
addCmd(cmdKey, params = null) {
const cmdValues = firmata.getCmd(cmdKey, params);
/** @type {CmdQueueItem} */
const queueItem = {
key: cmdKey,
params,
values: cmdValues,
}
// console.log(`[ addCmd ] queueItem : `, queueItem);
this.cmdsQueue.push(queueItem);
return queueItem;
}
}
最後回到 app.vue
,增加以下内容。
portTransceiver.on('ready')
事件中,发送 queryFirmware 命令。portTransceiver.on('undefined-response')
事件,观察有没有接收到未定义资料。/**
* @typedef {import('@/script/modules/port-transceiver').default} PortTransceiver
*/
import { mapState } from 'vuex';
import DialogSystemSetting from '@/components/dialog-system-setting.vue';
export default {
name: 'App',
// ...
methods: {
initTransceiver() {
// ...
portTransceiver.on('ready', (data) => {
// ...
portTransceiver.addCmd('queryCapability');
});
portTransceiver.on('undefined-response', (data) => {
console.log(`[ portTransceiver on undefined-response ] data : `, data);
});
// ...
},
},
};
尝试看看会不会有讯息出现。
成功取得回应!
现在我们来仔细分析看看回应内容,将收到的数值转成 16 进位来看:
F0 6C 7F 7F 00 01 0B 01 01 01 04 0E 7F 00 01 0B 01 01 01 03 08 04 0E 7F 00 01 0B 01 01 01 04 0E 7F 00 01 0B 01 01 01 03 08 04 0E 7F 00 01 0B 01 01 01 03 08 04 0E 7F 00 01 0B 01 01 01 04 0E 7F 00 01 0B 01 01 01 04 0E 7F 00 01 0B 01 01 01 03 08 04 0E 7F 00 01 0B 01 01 01 03 08 04 0E 7F 00 01 0B 01 01 01 03 08 04 0E 7F 00 01 0B 01 01 01 04 0E 7F 00 01 0B 01 01 01 04 0E 7F 00 01 0B 01 01 01 02 0A 04 0E 7F 00 01 0B 01 01 01 02 0A 04 0E 7F 00 01 0B 01 01 01 02 0A 04 0E 7F 00 01 0B 01 01 01 02 0A 04 0E 7F 00 01 0B 01 01 01 02 0A 04 0E 06 01 7F 00 01 0B 01 01 01 02 0A 04 0E 06 01 7F F7
将资料依照命令特徵与分隔符号(0x7F
)换行并加上对应脚位编号,方便分析。
F0 6C
0 7F
1 7F
2 00 01 0B 01 01 01 04 0E 7F
3 00 01 0B 01 01 01 03 08 04 0E 7F
4 00 01 0B 01 01 01 04 0E 7F
5 00 01 0B 01 01 01 03 08 04 0E 7F
6 00 01 0B 01 01 01 03 08 04 0E 7F
7 00 01 0B 01 01 01 04 0E 7F
8 00 01 0B 01 01 01 04 0E 7F
9 00 01 0B 01 01 01 03 08 04 0E 7F
10 00 01 0B 01 01 01 03 08 04 0E 7F
11 00 01 0B 01 01 01 03 08 04 0E 7F
12 00 01 0B 01 01 01 04 0E 7F
13 00 01 0B 01 01 01 04 0E 7F
14 00 01 0B 01 01 01 02 0A 04 0E 7F
15 00 01 0B 01 01 01 02 0A 04 0E 7F
16 00 01 0B 01 01 01 02 0A 04 0E 7F
17 00 01 0B 01 01 01 02 0A 04 0E 7F
18 00 01 0B 01 01 01 02 0A 04 0E 06 01 7F
19 00 01 0B 01 01 01 02 0A 04 0E 06 01 7F
20 7F
可以很明确地看出脚位 0、1 不支援使用任何功能,接着再来看脚位功能。
因为 0、1 脚位固定作为 UART 通讯使用,所以不开放使用其他功能。
否则就不能正常通讯了 (́◉◞౪◟◉‵)
由说明可知「脚位模式(Mode)」与「模式解析度(Mode Resolution)」两两成对出现,所以把刚才的资料去芜存菁後分类一下。
2 (00 01) (0B 01) (01 01) (04 0E) 7F
3 (00 01) (0B 01) (01 01) (03 08) (04 0E) 7F
4 (00 01) (0B 01) (01 01) (04 0E) 7F
5 (00 01) (0B 01) (01 01) (03 08) (04 0E) 7F
6 (00 01) (0B 01) (01 01) (03 08) (04 0E) 7F
7 (00 01) (0B 01) (01 01) (04 0E) 7F
8 (00 01) (0B 01) (01 01) (04 0E) 7F
9 (00 01) (0B 01) (01 01) (03 08) (04 0E) 7F
10 (00 01) (0B 01) (01 01) (03 08) (04 0E) 7F
11 (00 01) (0B 01) (01 01) (03 08) (04 0E) 7F
12 (00 01) (0B 01) (01 01) (04 0E) 7F
13 (00 01) (0B 01) (01 01) (04 0E) 7F
14 (00 01) (0B 01) (01 01) (02 0A) (04 0E) 7F
15 (00 01) (0B 01) (01 01) (02 0A) (04 0E) 7F
16 (00 01) (0B 01) (01 01) (02 0A) (04 0E) 7F
17 (00 01) (0B 01) (01 01) (02 0A) (04 0E) 7F
18 (00 01) (0B 01) (01 01) (02 0A) (04 0E) (06 01) 7F
19 (00 01) (0B 01) (01 01) (02 0A) (04 0E) (06 01) 7F
看起来清楚多了,接着再比对模式代号表与解析度说明
脚位模式(Mode)
DIGITAL_INPUT (0x00)
DIGITAL_OUTPUT (0x01)
ANALOG_INPUT (0x02)
PWM (0x03)
SERVO (0x04)
SHIFT (0x05)
I2C (0x06)
ONEWIRE (0x07)
STEPPER (0x08)
ENCODER (0x09)
SERIAL (0x0A)
INPUT_PULLUP (0x0B)
// Extended modes
SPI (0x0C)
SONAR (0x0D)
TONE (0x0E)
DHT (0x0F)
模式解析度(Mode Resolution)
// resolution is 1 (binary)
DIGITAL_INPUT (0x00)
// resolution is 1 (binary)
DIGITAL_OUTPUT (0x01)
// analog input resolution in number of bits
ANALOG_INPUT (0x02)
// pwm resolution in number of bits
PWM (0x03)
// servo resolution in number of bits
SERVO (0x04)
// resolution is number number of bits in max number of steps
STEPPER (0x08)
// resolution is 1 (binary)
INPUT_PULLUP (0x0B)
我们可以发现所有可用的脚位都支援「数位输入(DIGITAL_INPUT
0x00
)」与「上拉数位输入(INPUT_PULLUP
0x0B
)」!
最後我们透过已知的资讯,反证看看判读有没有错误。
用过 Arduino Uno 的人应该都知道,Uno 比较特别的脚位功能为:
如上图所示。
回过头来看看,刚刚的资料是不是相符:
2 (00 01) (0B 01) (01 01) (04 0E) 7F
3 (00 01) (0B 01) (01 01) ~(03 08) (04 0E) 7F
4 (00 01) (0B 01) (01 01) (04 0E) 7F
5 (00 01) (0B 01) (01 01) ~(03 08) (04 0E) 7F
6 (00 01) (0B 01) (01 01) ~(03 08) (04 0E) 7F
7 (00 01) (0B 01) (01 01) (04 0E) 7F
8 (00 01) (0B 01) (01 01) (04 0E) 7F
9 (00 01) (0B 01) (01 01) ~(03 08) (04 0E) 7F
10 (00 01) (0B 01) (01 01) ~(03 08) (04 0E) 7F
11 (00 01) (0B 01) (01 01) ~(03 08) (04 0E) 7F
12 (00 01) (0B 01) (01 01) (04 0E) 7F
13 (00 01) (0B 01) (01 01) (04 0E) 7F
14 (00 01) (0B 01) (01 01) *(02 0A) (04 0E) 7F
15 (00 01) (0B 01) (01 01) *(02 0A) (04 0E) 7F
16 (00 01) (0B 01) (01 01) *(02 0A) (04 0E) 7F
17 (00 01) (0B 01) (01 01) *(02 0A) (04 0E) 7F
18 (00 01) (0B 01) (01 01) *(02 0A) (04 0E) (06 01) 7F
19 (00 01) (0B 01) (01 01) *(02 0A) (04 0E) (06 01) 7F
可以看到判读结果与现有已知的资料一致,太令人感动惹。。・゚・(つд`゚)・゚・
接下来我们来依据资料分析所有脚位功能。
以 Pin 2 为例:
2 (00 01) (0B 01) (01 01) (04 0E)
比对「脚位模式(Mode)」与「模式解析度(Mode Resolution)」後,可以知道 Pin 2 支援的功能有:
0x00
),解析度 1 bit(0x01)0x0B
),解析度 1 bit(0x01)0x01
),解析度 1 bit(0x01)0x04
),解析度 14 bit(0x0E)其他脚位以此类推,所以我们成功读懂脚位功能回应资料了,接下来就是在 response-define.js
增加回应定义,并将刚才的分析过程转换成 getData()
的解析程序。
由於需要将矩阵进行分割,所以在 utils.js
新增 arraySplit()
,可以根据指定元素分割矩阵。(逻辑同 String.split()
)
src\script\utils\utils.js
// ...
/** 根据指定元素分割矩阵
* separator 不会包含在矩阵中
* @param {Array} array
* @param {*} separator
*/
export function arraySplit(array, separator) {
const allIndex = indexOfAll(array, separator);
if (allIndex.length === 0) {
return [array];
}
const initArray = [];
const part = array.slice(0, allIndex[0]);
initArray.push(part);
const result = allIndex.reduce((acc, pos, index) => {
const start = pos;
const end = allIndex?.[index + 1] ?? null;
// end 不存在表示为最後一个
if (end === null) {
const part = array.slice(start + 1);
acc.push(part);
return acc;
}
const part = array.slice(start + 1, end);
acc.push(part);
return acc;
}, initArray);
return result;
}
接着引用 arraySplit()
,完成功能。
src\script\firmata\response-define.js
import { arraySplit, matchFeature } from '@/script/utils/utils';
export default [
// ...
// capabilitieResponse:
{
key: 'capabilitieResponse',
eventName: 'info',
/**
* @param {number[]} res
*/
matcher(res) {
const featureBytes = [0xF0, 0x6C];
return matchFeature(res, featureBytes);
},
/**
* @param {number[]} valuesIn
*/
getData(valuesIn) {
const values = valuesIn.filter((byte) => {
return ![0xF0, 0x6C, 0xF7].includes(byte);
});
const pinParts = arraySplit(values, 0x7F);
const pins = pinParts.map((pinPart, index) => {
// 每 2 个数值一组
const modeParts = [];
for (let i = 0; i < pinPart.length; i += 2) {
modeParts.push(pinPart.slice(i, i + 2));
}
// 第一个数值为模式,第二个数值为解析度
const capabilities = modeParts.map((modePart) => {
const [mode, resolution] = modePart;
return {
mode, resolution
}
});
return {
number: index,
capabilities,
}
});
return {
pins
};
},
},
]
接着在 board.store.js
中新增变数 pins
,储存 capabilitieResponse
回应资料。
src\store\modules\board.store.js
/**
* 管理 Firmata 版本、Pin 清单等等 MCU 开发版相关资料
*/
// ...
/** @type {Module} */
const self = {
namespaced: true,
state: () => ({
info: {
ver: null,
firmwareName: null,
pins: [],
},
}),
// ...
};
export default self;
试试看有没有成功。
成功取得 Arduino Uno 脚位清单与功能!
打开 Firmata Protocol,在「Analog Mapping Query」章节可以找到相关说明。
Analog messages are numbered 0 to 15, which traditionally refer to the Arduino pins labeled A0, A1, A2, etc. However, these pins are actually configured using "normal" pin numbers in the pin mode message, and when those pins are used for non-analog functions. The analog mapping query provides the information about which pins (as used with Firmata's pin mode message) correspond to the analog channels.
查询命令为:
0 START_SYSEX (0xF0)
1 analog mapping query (0x69)
2 END_SYSEX (0xF7)
回应资料为:
0 START_SYSEX (0xF0)
1 analog mapping response (0x6A)
2 analog channel corresponding to pin 0, or 127 if pin 0 does not support analog
3 analog channel corresponding to pin 1, or 127 if pin 1 does not support analog
4 analog channel corresponding to pin 2, or 127 if pin 2 does not support analog
... etc, one byte for each pin
N END_SYSEX (0xF7)
从以上说明可以得知:
[ 0xF0, 0x69, 0xF7 ]
0x6A
之後会接续映射资料。一样先新增命令。
src\script\firmata\cmd-define.js
export default [
// queryCapability: 查询所有脚位与功能
{ ... },
// queryAnalogMapping: 查询类比脚位映射
{
key: 'queryAnalogMapping',
getValue() {
return [0xF0, 0x69, 0xF7];
},
},
]
接着在 app.vue
之 portTransceiver.on('ready')
增加发送命令。
让 addCmd('queryCapability')
与上一个 addCmd('queryAnalogMapping')
命令之前延迟一小段时间,让回应两个命令的回应不要连在一起。
先在 src\script\utils\utils.js
新增 delay()
// ...
/** 延迟指定毫秒
* @param {number} millisecond
*/
export function delay(millisecond) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, millisecond);
});
}
接着在 src\app.vue
加入命令。
// ...
import { delay } from '@/script/utils/utils';
export default {
name: 'App',
// ...
methods: {
initTransceiver() {
// ...
portTransceiver.once('ready',async (data) => {
// ...
portTransceiver.addCmd('queryCapability');
await delay(100);
portTransceiver.addCmd('queryAnalogMapping');
});
// ...
},
},
};
命令发送成功!
接下来将内容转为 16 进位後分析一下。
F0 6A 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 7F 00 01 02 03 04 05 F7
依照文档说明换行并加上对应脚位编号。
F0 6A
0 7F
1 7F
2 7F
3 7F
4 7F
5 7F
6 7F
7 7F
8 7F
9 7F
10 7F
11 7F
12 7F
13 7F
14 00
15 01
16 02
17 03
18 04
19 05
F7
清楚明了的表示映射关系为:
其他以此类推。
再来就是实际解析回应资料。
src\script\firmata\response-define.js
import { arraySplit, matchFeature } from '@/script/utils/utils';
export default [
// ...
// analogPinMappingResponse:
{
key: 'analogPinMappingResponse',
eventName: 'info',
/**
* @param {number[]} res
*/
matcher(res) {
const featureBytes = [0xF0, 0x6A];
return matchFeature(res, featureBytes);
},
/**
* @param {number[]} values
*/
getData(values) {
const index = values.findIndex(byte => byte === 0x6A);
const dataBytes = values.slice(index + 1, -1);
const analogPinMap = dataBytes.reduce((map, byte, index) => {
if (byte === 127) {
return map;
}
map[`${index}`] = byte;
return map;
}, {});
return { analogPinMap };
},
},
]
并在 Vuex board.store.js
中新增变数 analogPinMap
,储存 analogPinMappingResponse
回应资料。
src\store\modules\board.store.js
/**
* 管理 Firmata 版本、Pin 清单等等 MCU 开发版相关资料
*/
// ...
/** @type {Module} */
const self = {
namespaced: true,
state: () => ({
info: {
ver: null,
firmwareName: null,
pins: [],
analogPinMap: {},
},
}),
// ...
};
export default self;
实测看看。
成功取得脚位映射资料 ✧*。٩(ˊᗜˋ*)و✧*。,接下来准备打开第一扇窗!
以上程序码已同步至 GitLab,大家可以前往下载:
>>: 前端工程师也能开发全端网页:挑战 30 天用 React 加上 Firebase 打造社群网站|Day22 修改会员名称
承上篇 先从信仰者(使用者)的角度说起 最早期乖乖先驱者都是来自於大型机房,NOC等,会在这些地方工...
阿嬷都看得懂的切版在干嘛 今天,让我们一起拿出童年回忆--贴纸簿。 如果你不是阿嬷而是乖孙,那我解释...
AWS Cloud9 是一种云端整合开发环境 (IDE),您只需要一个浏览器便能撰写、执行和侦错程序...
我原本预想是在 15 或 16 号开始进入专案实做,结果超进度了。 不过,差距不大,所以没什麽关系。...
解决方式: 不要升级 MacOS 到 10.15 以上 更换到 windows-based 开发环境...