D08 - 今晚,我想来点「脚位清单」加「功能模式」,配「类比映射表」

电子助教:「所以标题提到的那些东西好吃吗?ლ(´∀`ლ)」

鳕鱼:「不,都不能吃 (́◉◞౪◟◉‵)」

电子助教:(乾骗我... ლ ( ´•̥̥̥ ω •̥̥̥` ლ ) )


在进入建立视窗组件之前,我们必须先取得必要 MCU 资讯,所以这个章节让我们继续取得更多 Firmata 资料吧。

接下来预计还要取得以下资讯:

  • MCU 所有脚位与其支援功能
  • 类比脚位映射表

取得脚位与功能

打开 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 分隔每个脚位内容。
  • 每个脚位支援模式以 2 bytes 表示,第一个 byte 表示「脚位模式(Mode),第二 byte 表示「模式解析度(Mode Resolution)」或某些特殊功能定义。

这个资料不像版本编号与名称那样,开启 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);
      });

      // ...
    },
  },
};

尝试看看会不会有讯息出现。

D08 - 发送 queryCapability 命令.gif

成功取得回应!

解析回应

现在我们来仔细分析看看回应内容,将收到的数值转成 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)」!

最後我们透过已知的资讯,反证看看判读有没有错误。

Untitled

图片来源:Arduino 官方

用过 Arduino Uno 的人应该都知道,Uno 比较特别的脚位功能为:

  • PWM 脚位:3、5、6、9、10、11
  • 类比输入脚位:A0 ~ A5(14 ~ 19)

如上图所示。

回过头来看看,刚刚的资料是不是相符:

  • PWM 代号为 03(用 ~ 标示)
  • 类比输入代号为 02(用 * 标示)
 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 支援的功能有:

  • 数位输入(DIGITAL_INPUT 0x00),解析度 1 bit(0x01)
  • 上拉输入(INPUT_PULLUP 0x0B),解析度 1 bit(0x01)
  • 数位输出(DIGITAL_OUTPUT 0x01),解析度 1 bit(0x01)
  • 伺服控制(SERVO 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;

试试看有没有成功。

D08 - 取得 queryCapability 回应.gif

成功取得 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 之後会接续映射资料。
  • 从 pin 0 开始依序排列,127 表示此 pin 不支援类比功能,其他数值则表示映射编号。

发送命令

一样先新增命令。

src\script\firmata\cmd-define.js

export default [
  // queryCapability: 查询所有脚位与功能
  { ... },

  // queryAnalogMapping: 查询类比脚位映射
  {
    key: 'queryAnalogMapping',
    getValue() {
      return [0xF0, 0x69, 0xF7];
    },
  },
]

接着在 app.vueportTransceiver.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');
      });

      // ...
    },
  },
};

D08 - 取得 queryAnalogMapping 回应.gif

命令发送成功!

解析回应

接下来将内容转为 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

清楚明了的表示映射关系为:

  • pin 14 → 0
  • pin 15 → 1
  • pin 16 → 2

其他以此类推。

再来就是实际解析回应资料。

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;

实测看看。

D08 - 取得 queryAnalogMapping 回应.gif

成功取得脚位映射资料 ✧*。٩(ˊᗜˋ*)و✧*。,接下来准备打开第一扇窗!

总结

  • 新增「取得脚位与功能」命令与解析
  • 新增「取得类比脚位映射表」命令与解析
  • 将以上两种资料储存至 Vuex

以上程序码已同步至 GitLab,大家可以前往下载:

GitLab - D08


<<:  Day 8:506. Relative Ranks

>>:  前端工程师也能开发全端网页:挑战 30 天用 React 加上 Firebase 打造社群网站|Day22 修改会员名称

Day5 回忆篇 那个关於乖乖的传说-2

承上篇 先从信仰者(使用者)的角度说起 最早期乖乖先驱者都是来自於大型机房,NOC等,会在这些地方工...

[Day 20] 阿嬷都看得懂的切版在干嘛

阿嬷都看得懂的切版在干嘛 今天,让我们一起拿出童年回忆--贴纸簿。 如果你不是阿嬷而是乖孙,那我解释...

[Day28] AWS Cloud9

AWS Cloud9 是一种云端整合开发环境 (IDE),您只需要一个浏览器便能撰写、执行和侦错程序...

[Day14] 引入 crate

我原本预想是在 15 或 16 号开始进入专案实做,结果超进度了。 不过,差距不大,所以没什麽关系。...

[Android Studio] intel-based MacOS 无法执行模拟器(AVD has terminated)

解决方式: 不要升级 MacOS 到 10.15 以上 更换到 windows-based 开发环境...