nacarの独り言

マイクロマウス作ってるらしいです

GASでPA-API(Amazon API)を使えるようにした

お久しぶりです.

 

 

はじめに

今回はAmazonアソシエイトに含まれるProduct Advertising API 5.0 (PA-API 5.0) をGAS(Google Apps Script)で実装した話です.

 

まずAmazonアソシエイトとはなんぞや,ってところです

アフィリエイトという言葉を日本語訳すると「提携する」ことを意味します。一般的なアフィリエイト・プログラムとは、個人や企業のホームページから、ECサイト(広告主)にリンクをはり、閲覧者がそのリンクを経由して広告主のサービスや商品を購入した場合にリンク元の運営者に報酬が支払われるという広告手段です。Amazon.co.jpではこれを独自にAmazon アソシエイト・プログラムと呼んでいます。  

ざっくりいうと自分の情報が含まれたリンクから誰かが商品を買ったときにいくばくかの報酬がもらえる,というものです.

これを利用するためには事前に登録して審査に合格する必要があるのですが,詳細は各々調べてみてください.

 

そんなAmazonアソシエイトの中にPA-APIと呼ばれる機能があります.

これを利用するとプログラムからAPIを叩くことで商品情報などを取得できるようになります.

PA-APIにはSDKと呼ばれる開発キットが用意されており,簡単に使うことができます.

が,用意されているSDKPHPJava,Node.js,Pythonに限られており,他の言語で使用する際には別途コードを書く必要があります.

Railsなどで実装してみた,という記事はいくつかあるのですが,GASで実装した例は見つからなかったため情報共有したいと思い,この記事を書いています.

 

本題

参考にしたページ

 

実装初期

API何もわからん,署名version4?なにそれ,なにもできん

...

とても困ったのでお世話になっている

intee(インティ) | スキルアップ型就活支援サービスのエンジニアの方など詳しい方から話を聞いて勉強しました.

Scratchpad使ったら一時的な署名が手に入るのか~など,右往左往しながら進んできたのですが,ここはあまり需要がないと思うので最短経路になるような部分だけ書くことにします.

 

実装の流れ

公式のドキュメント内にSDKを使わない実装例としてJavaPHPのコードが紹介されています.

私はどちらの言語もわからないのですが,XAMPPというフリーソフトを使ってPHPを動かすことができたので,今回はPHPのコードを参考にして実装することにしました.

2つのコードが紹介されているのですが,両方ダウンロードしてincludeしたら正しく常時されるようになりました.

PHPではechoを使って文字列の出力が行えるため,これを使ってありとあらゆるパラメータを表示してコードの内容を理解しました.

 

コードの流れ

  1.  payloadの作成
  2. 正規リクエストの作成(prepareCanonicalRequest)
  3. 署名文字列の作成(prepareStringToSign)
  4. 署名を計算する(calculateSignature)
  5. 署名を含めたヘッダーを作成
  6. APIを叩く

ここで2~4がPHPコード内の

    public function getHeaders() {
        $this->awsHeaders ['x-amz-date'] = $this->xAmzDate;
        ksort ( $this->awsHeaders );
        $canonicalURL = $this->prepareCanonicalRequest ();
        $stringToSign = $this->prepareStringToSign ( $canonicalURL );
        $signature = $this->calculateSignature ( $stringToSign );
        if ($signature) {
            $this->awsHeaders ['Authorization'] = $this->buildAuthorizationString ( $signature );
            return $this->awsHeaders;
        }
    }

に相当します.

 

実際のコード

function main() {

  var word = "宇宙よりも遠い場所";
  //日本語入力時のための処理
  word = escape(word);
  word = word.toLowerCase();
  word = word.replace(/%/g, "\\");
  word = word.replace(/\\2\d/g, " ");

  var Partner_Tag = PropertiesService.getScriptProperties().getProperty("PartnerTag");
  var Access_Key = PropertiesService.getScriptProperties().getProperty("AccessKey");
  var Secret_Key = PropertiesService.getScriptProperties().getProperty("SecretKey");

  //検索パラーメータ
  var payload = '{"ItemCount":10,"PartnerType":"Associates","PartnerTag":"' + Partner_Tag + '","Keywords":"' + word + '","SearchIndex":"Books","Resources":["Images.Primary.Medium","ItemInfo.Title","ItemInfo.ByLineInfo"]}';


  //日時の取得
  var now = new Date();
  var yyyymmdd = Utilities.formatDate(now, "GMT", "yyyyMMdd")
  var timestamp = yyyymmdd + "T" + Utilities.formatDate(now, "GMT", "HHmmss") + "Z";


  //署名の処理
  var canonicalURL = prepareCanonicalRequest(timestamp, payload);
  var stringToSign = prepareStringToSign(timestamp, yyyymmdd, canonicalURL);
  var signature = calculateSignature(Secret_Key, yyyymmdd, "us-west-2", "ProductAdvertisingAPI", stringToSign);

  var Authorization = "AWS4-HMAC-SHA256 Credential=" + Access_Key + "/" + yyyymmdd + "/us-west-2/ProductAdvertisingAPI/aws4_request,SignedHeaders=content-encoding;content-type;host;x-amz-date;x-amz-target,Signature=" + signature;

  var headers = {
    'content-type': 'application/json; charset=utf-8',
    'x-amz-date': timestamp,
    'x-amz-target': 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems',
    'content-encoding': 'amz-1.0',
    'Authorization': Authorization
  };

  var options = {
    method: 'POST',
    headers: headers,
    payload: payload,
    muteHttpExceptions: true,
  };

  var url = 'https://webservices.amazon.co.jp/paapi5/searchitems';
  var response = UrlFetchApp.fetch(url, options);
  Logger.log(response);

  //必要なデータのみ取り出す
  var json = JSON.parse(response.getContentText());
  var my_url = {};
  var my_author = {};
  var my_title = {};
  var my_picture_url = {};

  for (i = 0; i < 10; i++) {
    try {
      my_url[i] = json["SearchResult"]["Items"][i]["DetailPageURL"];
    } catch (e) {
      my_url[i] = "";
    }
    try {
      my_author[i] = json["SearchResult"]["Items"][i]["ItemInfo"]["ByLineInfo"]["Contributors"][0]["Name"];
    } catch (e) {
      my_author[i] = "";
    }
    try {
      my_title[i] = json["SearchResult"]["Items"][i]["ItemInfo"]["Title"]["DisplayValue"];
    } catch (e) {
      my_picture_url[i] = "";
    }
    try {
      my_picture_url[i] = json["SearchResult"]["Items"][i]["Images"]["Primary"]["Medium"]["URL"];
    } catch (e) {
      my_picture_url[i] = "";
    }
  }

  value = {
    affiliate_url: my_url,
    author: my_author,
    title: my_title,
    pictur_url: my_picture_url,
  };
  Logger.log(value);

  var result = {
    message: value
  }

  var out = ContentService.createTextOutput();

  //Mine TypeをJSONに
  out.setMimeType(ContentService.MimeType.JSON);

  //JSONテキストをセット
  out.setContent(JSON.stringify(result));

  return out;
}


function prepareCanonicalRequest(timestamp, payload) {
  var canonicalUrl = "POST\n";
  canonicalUrl = canonicalUrl + "/paapi5/searchitems" + "\n\n";
  canonicalUrl = canonicalUrl + "content-encoding:amz-1.0" + "\n";
  canonicalUrl = canonicalUrl + "content-type:application/json; charset=utf-8" + "\n";
  canonicalUrl = canonicalUrl + "host:webservices.amazon.co.jp" + "\n";
  canonicalUrl = canonicalUrl + "x-amz-date:" + timestamp + "\n";
  canonicalUrl = canonicalUrl + "x-amz-target:com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems" + "\n\n";
  canonicalUrl = canonicalUrl + "content-encoding;content-type;host;x-amz-date;x-amz-target" + "\n";
  canonicalUrl = canonicalUrl + SHA256(payload);
  return canonicalUrl;
}

function prepareStringToSign(timestamp, yyyymmdd, canonicalURL) {
  var stringToSign = "AWS4-HMAC-SHA256" + "\n";
  stringToSign = stringToSign + timestamp + "\n";
  stringToSign = stringToSign + yyyymmdd + "/us-west-2/ProductAdvertisingAPI/aws4_request" + "\n";
  stringToSign = stringToSign + SHA256(canonicalURL);
  return stringToSign;
}



function calculateSignature(secretAccessKey, currentDate, regionName, serviceName, stringToSign) {
  var kDate = my_HMAC_keytext(currentDate, "AWS4" + secretAccessKey);
  var kRegion = my_HMAC_keyB64(regionName, kDate);
  var kService = my_HMAC_keyB64(serviceName, kRegion);
  var kSigning = my_HMAC_keyB64("aws4_request", kService);
  var signatureKey = kSigning;
  var signature = my_HMAC_keyB64_HEXOutput(stringToSign, signatureKey);
  // Logger.log(signature);
  return signature;
}



function SHA256(input) {
  var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, input);
  var txtHash = '';
  for (i = 0; i < rawHash.length; i++) {
    var hashVal = rawHash[i];
    if (hashVal < 0) {
      hashVal += 256;
    }
    if (hashVal.toString(16).length == 1) {
      txtHash += '0';
    }
    txtHash += hashVal.toString(16);
  }
  return txtHash;
}


function my_HMAC_keytext(value, key) {
  const shaObj = new jsSHA("SHA-256", "TEXT", {
    hmacKey: { value: key, format: "TEXT" },
  });
  shaObj.update(value);
  const hmac = shaObj.getHash("B64");
  return hmac;
}

function my_HMAC_keytext_HEXOutput(value, key) {
  const shaObj = new jsSHA("SHA-256", "TEXT", {
    hmacKey: { value: key, format: "TEXT" },
  });
  shaObj.update(value);
  const hmac = shaObj.getHash("HEX");
  return hmac;
}

function my_HMAC_keyB64(value, key) {
  const shaObj = new jsSHA("SHA-256", "TEXT", {
    hmacKey: { value: key, format: "B64" },
  });
  shaObj.update(value);
  const hmac = shaObj.getHash("B64");
  return hmac;
}

function my_HMAC_keyB64_HEXOutput(value, key) {
  const shaObj = new jsSHA("SHA-256", "TEXT", {
    hmacKey: { value: key, format: "B64" },
  });
  shaObj.update(value);
  const hmac = shaObj.getHash("HEX");
  return hmac;
}

/**
* A JavaScript implementation of the SHA family of hashes - defined in FIPS PUB 180-4, FIPS PUB 202,
* and SP 800-185 - as well as the corresponding HMAC implementation as defined in FIPS PUB 198-1.
*
* Copyright 2008-2020 Brian Turek, 1998-2009 Paul Johnston & Contributors
* Distributed under the BSD License
* See http://caligatio.github.com/jsSHA/ for more information
*
* Two ECMAScript polyfill functions carry the following license:
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED,
* INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
* MERCHANTABLITY OR NON-INFRINGEMENT.
*
* See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
*/
!function (t, r) { "object" == typeof exports && "undefined" != typeof module ? module.exports = r() : "function" == typeof define && define.amd ? define(r) : (t = t || self).jsSHA = r() }(this, (function () { "use strict"; var t = function (r, n) { return (t = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function (t, r) { t.__proto__ = r } || function (t, r) { for (var n in r) r.hasOwnProperty(n) && (t[n] = r[n]) })(r, n) }; var r = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; function n(t, r, n, i) { var e, o, u, s = r || [0], f = (n = n || 0) >>> 3, h = -1 === i ? 3 : 0; for (e = 0; e < t.length; e += 1)o = (u = e + f) >>> 2, s.length <= o && s.push(0), s[o] |= t[e] << 8 * (h + i * (u % 4)); return { value: s, binLen: 8 * t.length + n } } function i(t, i, e) { switch (i) { case "UTF8": case "UTF16BE": case "UTF16LE": break; default: throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE") }switch (t) { case "HEX": return function (t, r, n) { return function (t, r, n, i) { var e, o, u, s; if (0 != t.length % 2) throw new Error("String of HEX type must be in byte increments"); var f = r || [0], h = (n = n || 0) >>> 3, a = -1 === i ? 3 : 0; for (e = 0; e < t.length; e += 2) { if (o = parseInt(t.substr(e, 2), 16), isNaN(o)) throw new Error("String of HEX type contains invalid characters"); for (u = (s = (e >>> 1) + h) >>> 2; f.length <= u;)f.push(0); f[u] |= o << 8 * (a + i * (s % 4)) } return { value: f, binLen: 4 * t.length + n } }(t, r, n, e) }; case "TEXT": return function (t, r, n) { return function (t, r, n, i, e) { var o, u, s, f, h, a, c, w, v = 0, E = n || [0], A = (i = i || 0) >>> 3; if ("UTF8" === r) for (c = -1 === e ? 3 : 0, s = 0; s < t.length; s += 1)for (u = [], 128 > (o = t.charCodeAt(s)) ? u.push(o) : 2048 > o ? (u.push(192 | o >>> 6), u.push(128 | 63 & o)) : 55296 > o || 57344 <= o ? u.push(224 | o >>> 12, 128 | o >>> 6 & 63, 128 | 63 & o) : (s += 1, o = 65536 + ((1023 & o) << 10 | 1023 & t.charCodeAt(s)), u.push(240 | o >>> 18, 128 | o >>> 12 & 63, 128 | o >>> 6 & 63, 128 | 63 & o)), f = 0; f < u.length; f += 1) { for (h = (a = v + A) >>> 2; E.length <= h;)E.push(0); E[h] |= u[f] << 8 * (c + e * (a % 4)), v += 1 } else for (c = -1 === e ? 2 : 0, w = "UTF16LE" === r && 1 !== e || "UTF16LE" !== r && 1 === e, s = 0; s < t.length; s += 1) { for (o = t.charCodeAt(s), !0 === w && (o = (f = 255 & o) << 8 | o >>> 8), h = (a = v + A) >>> 2; E.length <= h;)E.push(0); E[h] |= o << 8 * (c + e * (a % 4)), v += 2 } return { value: E, binLen: 8 * v + i } }(t, i, r, n, e) }; case "B64": return function (t, n, i) { return function (t, n, i, e) { var o, u, s, f, h, a, c = 0, w = n || [0], v = (i = i || 0) >>> 3, E = -1 === e ? 3 : 0, A = t.indexOf("="); if (-1 === t.search(/^[a-zA-Z0-9=+/]+$/)) throw new Error("Invalid character in base-64 string"); if (t = t.replace(/=/g, ""), -1 !== A && A < t.length) throw new Error("Invalid '=' found in base-64 string"); for (o = 0; o < t.length; o += 4) { for (f = t.substr(o, 4), s = 0, u = 0; u < f.length; u += 1)s |= r.indexOf(f.charAt(u)) << 18 - 6 * u; for (u = 0; u < f.length - 1; u += 1) { for (h = (a = c + v) >>> 2; w.length <= h;)w.push(0); w[h] |= (s >>> 16 - 8 * u & 255) << 8 * (E + e * (a % 4)), c += 1 } } return { value: w, binLen: 8 * c + i } }(t, n, i, e) }; case "BYTES": return function (t, r, n) { return function (t, r, n, i) { var e, o, u, s, f = r || [0], h = (n = n || 0) >>> 3, a = -1 === i ? 3 : 0; for (o = 0; o < t.length; o += 1)e = t.charCodeAt(o), u = (s = o + h) >>> 2, f.length <= u && f.push(0), f[u] |= e << 8 * (a + i * (s % 4)); return { value: f, binLen: 8 * t.length + n } }(t, r, n, e) }; case "ARRAYBUFFER": try { new ArrayBuffer(0) } catch (t) { throw new Error("ARRAYBUFFER not supported by this environment") } return function (t, r, i) { return function (t, r, i, e) { return n(new Uint8Array(t), r, i, e) }(t, r, i, e) }; case "UINT8ARRAY": try { new Uint8Array(0) } catch (t) { throw new Error("UINT8ARRAY not supported by this environment") } return function (t, r, i) { return n(t, r, i, e) }; default: throw new Error("format must be HEX, TEXT, B64, BYTES, ARRAYBUFFER, or UINT8ARRAY") } } function e(t, n, i, e) { switch (t) { case "HEX": return function (t) { return function (t, r, n, i) { var e, o, u = "", s = r / 8, f = -1 === n ? 3 : 0; for (e = 0; e < s; e += 1)o = t[e >>> 2] >>> 8 * (f + n * (e % 4)), u += "0123456789abcdef".charAt(o >>> 4 & 15) + "0123456789abcdef".charAt(15 & o); return i.outputUpper ? u.toUpperCase() : u }(t, n, i, e) }; case "B64": return function (t) { return function (t, n, i, e) { var o, u, s, f, h, a = "", c = n / 8, w = -1 === i ? 3 : 0; for (o = 0; o < c; o += 3)for (f = o + 1 < c ? t[o + 1 >>> 2] : 0, h = o + 2 < c ? t[o + 2 >>> 2] : 0, s = (t[o >>> 2] >>> 8 * (w + i * (o % 4)) & 255) << 16 | (f >>> 8 * (w + i * ((o + 1) % 4)) & 255) << 8 | h >>> 8 * (w + i * ((o + 2) % 4)) & 255, u = 0; u < 4; u += 1)a += 8 * o + 6 * u <= n ? r.charAt(s >>> 6 * (3 - u) & 63) : e.b64Pad; return a }(t, n, i, e) }; case "BYTES": return function (t) { return function (t, r, n) { var i, e, o = "", u = r / 8, s = -1 === n ? 3 : 0; for (i = 0; i < u; i += 1)e = t[i >>> 2] >>> 8 * (s + n * (i % 4)) & 255, o += String.fromCharCode(e); return o }(t, n, i) }; case "ARRAYBUFFER": try { new ArrayBuffer(0) } catch (t) { throw new Error("ARRAYBUFFER not supported by this environment") } return function (t) { return function (t, r, n) { var i, e = r / 8, o = new ArrayBuffer(e), u = new Uint8Array(o), s = -1 === n ? 3 : 0; for (i = 0; i < e; i += 1)u[i] = t[i >>> 2] >>> 8 * (s + n * (i % 4)) & 255; return o }(t, n, i) }; case "UINT8ARRAY": try { new Uint8Array(0) } catch (t) { throw new Error("UINT8ARRAY not supported by this environment") } return function (t) { return function (t, r, n) { var i, e = r / 8, o = -1 === n ? 3 : 0, u = new Uint8Array(e); for (i = 0; i < e; i += 1)u[i] = t[i >>> 2] >>> 8 * (o + n * (i % 4)) & 255; return u }(t, n, i) }; default: throw new Error("format must be HEX, B64, BYTES, ARRAYBUFFER, or UINT8ARRAY") } } var o = [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580, 3835390401, 4022224774, 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, 2554220882, 2821834349, 2952996808, 3210313671, 3336571891, 3584528711, 113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, 2730485921, 2820302411, 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, 1955562222, 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, 3329325298], u = [3238371032, 914150663, 812702999, 4144912697, 4290775857, 1750603025, 1694076839, 3204075428], s = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]; function f(t) { var r = { outputUpper: !1, b64Pad: "=", outputLen: -1 }, n = t || {}, i = "Output length must be a multiple of 8"; if (r.outputUpper = n.outputUpper || !1, n.b64Pad && (r.b64Pad = n.b64Pad), n.outputLen) { if (n.outputLen % 8 != 0) throw new Error(i); r.outputLen = n.outputLen } else if (n.shakeLen) { if (n.shakeLen % 8 != 0) throw new Error(i); r.outputLen = n.shakeLen } if ("boolean" != typeof r.outputUpper) throw new Error("Invalid outputUpper formatting option"); if ("string" != typeof r.b64Pad) throw new Error("Invalid b64Pad formatting option"); return r } function h(t, r) { return t >>> r | t << 32 - r } function a(t, r) { return t >>> r } function c(t, r, n) { return t & r ^ ~t & n } function w(t, r, n) { return t & r ^ t & n ^ r & n } function v(t) { return h(t, 2) ^ h(t, 13) ^ h(t, 22) } function E(t, r) { var n = (65535 & t) + (65535 & r); return (65535 & (t >>> 16) + (r >>> 16) + (n >>> 16)) << 16 | 65535 & n } function A(t, r, n, i) { var e = (65535 & t) + (65535 & r) + (65535 & n) + (65535 & i); return (65535 & (t >>> 16) + (r >>> 16) + (n >>> 16) + (i >>> 16) + (e >>> 16)) << 16 | 65535 & e } function p(t, r, n, i, e) { var o = (65535 & t) + (65535 & r) + (65535 & n) + (65535 & i) + (65535 & e); return (65535 & (t >>> 16) + (r >>> 16) + (n >>> 16) + (i >>> 16) + (e >>> 16) + (o >>> 16)) << 16 | 65535 & o } function d(t) { return h(t, 7) ^ h(t, 18) ^ a(t, 3) } function l(t) { return h(t, 6) ^ h(t, 11) ^ h(t, 25) } function R(t) { return "SHA-224" == t ? u.slice() : s.slice() } function U(t, r) { var n, i, e, u, s, f, R, U, y, b, T, m, F = []; for (n = r[0], i = r[1], e = r[2], u = r[3], s = r[4], f = r[5], R = r[6], U = r[7], T = 0; T < 64; T += 1)F[T] = T < 16 ? t[T] : A(h(m = F[T - 2], 17) ^ h(m, 19) ^ a(m, 10), F[T - 7], d(F[T - 15]), F[T - 16]), y = p(U, l(s), c(s, f, R), o[T], F[T]), b = E(v(n), w(n, i, e)), U = R, R = f, f = s, s = E(u, y), u = e, e = i, i = n, n = E(y, b); return r[0] = E(n, r[0]), r[1] = E(i, r[1]), r[2] = E(e, r[2]), r[3] = E(u, r[3]), r[4] = E(s, r[4]), r[5] = E(f, r[5]), r[6] = E(R, r[6]), r[7] = E(U, r[7]), r } return function (r) { function n(t, n, e) { var o = this; if ("SHA-224" !== t && "SHA-256" !== t) throw new Error("Chosen SHA variant is not supported"); var u = e || {}; return (o = r.call(this, t, n, e) || this).t = o.i, o.o = !0, o.u = -1, o.s = i(o.h, o.v, o.u), o.A = U, o.p = function (t) { return t.slice() }, o.l = R, o.R = function (r, n, i, e) { return function (t, r, n, i, e) { for (var o, u = 15 + (r + 65 >>> 9 << 4), s = r + n; t.length <= u;)t.push(0); for (t[r >>> 5] |= 128 << 24 - r % 32, t[u] = 4294967295 & s, t[u - 1] = s / 4294967296 | 0, o = 0; o < t.length; o += 16)i = U(t.slice(o, o + 16), i); return "SHA-224" === e ? [i[0], i[1], i[2], i[3], i[4], i[5], i[6]] : i }(r, n, i, e, t) }, o.U = R(t), o.T = 512, o.m = "SHA-224" === t ? 224 : 256, o.F = !1, u.hmacKey && o.B(function (t, r, n, e) { var o = t + " must include a value and format"; if (!r) { if (!e) throw new Error(o); return e } if (void 0 === r.value || !r.format) throw new Error(o); return i(r.format, r.encoding || "UTF8", n)(r.value) }("hmacKey", u.hmacKey, o.u)), o } return function (r, n) { function i() { this.constructor = r } t(r, n), r.prototype = null === n ? Object.create(n) : (i.prototype = n.prototype, new i) }(n, r), n }(function () { function t(t, r, n) { var i = n || {}; if (this.h = r, this.v = i.encoding || "UTF8", this.numRounds = i.numRounds || 1, isNaN(this.numRounds) || this.numRounds !== parseInt(this.numRounds, 10) || 1 > this.numRounds) throw new Error("numRounds must a integer >= 1"); this.g = t, this.Y = [], this.H = 0, this.S = !1, this.I = 0, this.C = !1, this.L = [], this.N = [] } return t.prototype.update = function (t) { var r, n = 0, i = this.T >>> 5, e = this.s(t, this.Y, this.H), o = e.binLen, u = e.value, s = o >>> 5; for (r = 0; r < s; r += i)n + this.T <= o && (this.U = this.A(u.slice(r, r + i), this.U), n += this.T); this.I += n, this.Y = u.slice(n >>> 5), this.H = o % this.T, this.S = !0 }, t.prototype.getHash = function (t, r) { var n, i, o = this.m, u = f(r); if (this.F) { if (-1 === u.outputLen) throw new Error("Output length must be specified in options"); o = u.outputLen } var s = e(t, o, this.u, u); if (this.C && this.t) return s(this.t(u)); for (i = this.R(this.Y.slice(), this.H, this.I, this.p(this.U), o), n = 1; n < this.numRounds; n += 1)this.F && o % 32 != 0 && (i[i.length - 1] &= 16777215 >>> 24 - o % 32), i = this.R(i, o, 0, this.l(this.g), o); return s(i) }, t.prototype.setHMACKey = function (t, r, n) { if (!this.o) throw new Error("Variant does not support HMAC"); if (this.S) throw new Error("Cannot set MAC key after calling update"); var e = i(r, (n || {}).encoding || "UTF8", this.u); this.B(e(t)) }, t.prototype.B = function (t) { var r, n = this.T >>> 3, i = n / 4 - 1; if (1 !== this.numRounds) throw new Error("Cannot set numRounds with MAC"); if (this.C) throw new Error("MAC key already set"); for (n < t.binLen / 8 && (t.value = this.R(t.value, t.binLen, 0, this.l(this.g), this.m)); t.value.length <= i;)t.value.push(0); for (r = 0; r <= i; r += 1)this.L[r] = 909522486 ^ t.value[r], this.N[r] = 1549556828 ^ t.value[r]; this.U = this.A(this.L, this.U), this.I = this.T, this.C = !0 }, t.prototype.getHMAC = function (t, r) { var n = f(r); return e(t, this.m, this.u, n)(this.i()) }, t.prototype.i = function () { var t; if (!this.C) throw new Error("Cannot call getHMAC without first setting MAC key"); var r = this.R(this.Y.slice(), this.H, this.I, this.p(this.U), this.m); return t = this.A(this.N, this.l(this.g)), t = this.R(r, this.m, this.T, t, this.m) }, t }()) }));

 

 今回のコードを作成するにあたって

  • とにかく早く実装すること
  • 使用する機能が変わらない

という前提があったため,本来なら変数を使うべきところを直接書き込んでいるなど,よろしくない点はありますが,ご了承ください.

例えば,検索カテゴリを本だけに限定している,検索パラメータの固定など....

もしこのコードを参考にされる方は自己責任の上,適宜変更してください.

 

工夫した点

日本語検索への対応

アルファベットで構成された文字列で検索する場合には不要な処理なのですが,日本語を含む文字列を含んだ情報を署名化しようとすると適切な値が取得されませんでした.

そこでお手本としたPHPのコードを参考に,文字をエンコードすればいいのだろうと推測し,escape関数とtoLowerCase()メソッドを用いて文字列の変換を行っています.

これだけでは半角スペースなどがおかしくなるので例外処理をその下に記述しています.

 

取得データの分解

Amazonから取得したデータは物によってデータが欠けていたりするため,try...catch文を用いてエラーが起きてもコードが継続するようにしています.

 

SHA256

ハッシュ関数としてGASで使えるcomputeHmacSha256Signatureではなく,

jsSHA - SHA Hashes in JavaScriptを利用しています.

これはPHPコード内の"hash_hmac"関数の引数がtrueとなっており,バイナリデータが出力されているのですが,computeHmacSha256Signatureではこれが実現できなさそうだったためです.

ただ,jsSHAを導入してから明らかにコードが重くなったので別のやり方があればご教授いただきたいですね.

 

まとめ

今回はGASを用いてPA-APIを実装した話を簡単にまとめました.

もしかしたら内容的に間違っている点やおかしい点があるかもしれませんが,お許しください.

ご指摘いただければ幸いです.

実装ははじめに悩んでいた期間も含めれば2週間ほどかかりました.

結構苦しんだので,「よく頑張ったな」,って方は

www.amazon.co.jp

このリンクを踏んでから何か購入してくれると紹介料が入るのでお願いします.

 

また,今回の実装はNoCodeCamp杯 – コードを使わないアプリ開発に参加するなかで必要な機能でした.

そのため今回の機能をAPI化し,NoCodeツールであるBubbleから呼び出しています.

そっちの話も余裕があれば書こうと思っているので,書ければ書きます.

 

ハッカソンで実際に制作したアプリが

shark0731.bubbleapps.ioこちらです.

まだベータ版の段階で,現状PA-APIの機能は実装されていませんが,今後実装して改良するつもりなので,ぜひ触ってみてフィードバックいただければ幸いです.

 

以上,長くなりましたが今回はここまでです.

読んでいただき,ありがとうございました.

 

生存報告とAmazonで買ってよかったもの

お久しぶりです.

皆さんいかがお過ごしでしょうか.

自分は研究・就活・ハッカソンに追われていて忙しい気がしています.

それ以外だとGASとか使ってSlackとかLINEと連携するアプリ作ったりしてます.

 

今回この記事を書いたのはハッカソンのためにとある条件をクリアする必要があるためです.

なので,完全に私的な利用であり,マウス界隈の方に有益な情報はないと思います.

 

本題

今回の記事ではAmazonで購入したことがあるor友人に勧められている商品を紹介したいと思います.

興味を持つ方がいたら買ってみてはいかがでしょうか?

ではいきます.

 

  • はんだこて&こて先
白光 こて先 1C型 T18-C1

白光 こて先 1C型 T18-C1

  • メディア: Tools & Hardware
 

 せっかくなので1つだけですが,電子工作関連の商品です.

今の時代一家に一台はんだごて,当たり前ですよね?

fx-600は王道のはんだごてで流石,という感じです.

C1のこてさきは先輩から教えていただきましたが,表面実装を行う際にはとても重宝するこて先です.

表面実装で苦しんでいる方はご検討ください.

 

まず1つ目がこちらの電動歯ブラシです.

研究室の先輩におすすめしていただいたのですが,とても良かったです!

価格はお手頃なのですが,とても歯がツルツルになりました.

虫歯などが気になる方はぜひ!

 

  • SSD(M.2 500GB)

 パソコンの起動を早めたい方,いかがでしょう.

ただM.2のSSDに対応しているPCってまだあまり多くないような気はしてます.

 

  • 卓上リング照明

最近就活していてWeb面接とか説明会とかに参加する機会があるのですが,部屋のレイアウトの都合上,どうしても逆光になってしまうため,顔が暗くなってしまいます.

そこで買ったのがこちらの商品です.

低価格の割に大きく,光量も全く困らないレベルでした💡

ただ,三脚部分が緩く,高さの調整が難しい,という難点はありました.

 

  • 浄水ボトル

これは 今年のはじめに購入した浄水ボトルです.

健康のために水を積極的に飲もう,と思ったのですが水道水を直接飲むのはちょっと...という気持ちがあったので購入しました.

出先でも水道があればいつでも給水できますし,デザインも悪くないのでとても気に入っています.

30分に一度水を飲むことで風邪にかかりにくくなる,という噂もありますのでこまめに水を飲みたい方にもおすすめです.(科学的根拠はしらん)

 

 ここにきて本命です.

去年の年末に買ったのですが,とてもいいです!

高校生くらいまではそれなりに本を読んでいたのですが,大学に入ってからはあまり読んでいなかったこともあり,自分にプレッシャーをかける意味も込めて購入しました.

おかげさまで最近は本を読むようになりました.

これは防水機能も付いてるのでお風呂でも読むことが出来ます.(僕は念の為ジップロックに入れてますが)

ただ,ひとつ難点を上げるとすると電源ボタンの位置がとてもだめ.

普通に持っているだけで電源offになることがあるので,これだけは改善してほしい点ですね.

 

  •  小分けナッツ

 勉強中・勤務中に小腹がすいたあなたにオススメ!

小分けなので食べ過ぎることもないですし,味もGoodです.

 

  • その他

ここは気にしないでください....

 

では.

 

 

BIJELAの探索アルゴリズムについて

この記事は

Mice Advent Calendar 2019 - Adventar

の15日目の記事です.

昨日の投稿はstarknighthoodさんの...あれ?

現在静岡逃亡中ということで更新されるのを心待ちにしましょう.

 

 

前回の投稿が8月で,そのあと大会に出ているのにブログ書いて無くて悲しくなっております.

もう少し頻繁に書きたいなと,毎度のことながら思います.

 

今回の内容

さて,今回は春に行われたMiceの技術交流会で紹介した探索アルゴリズムについて書きたいと思います.

はじめから書くと途中で力尽きてしまうので,この記事では壁情報の記録や更新,足立法が理解できている前提で話を進めさせていただきます。

まあ,足立法の解説はたくさんありますから問題ないでしょう.

マイクロマウス特化資料 [Mice Wiki]

Miceで主流な壁情報の記録方法 - あえて賢かれ

今年初のハーフマウスであるTIGAを作成したにも関わらず,去年のクラシックマウスであるBIJELAの名前を使っているのは,TIGAで安定した探索ができるまで調整できなかったためです.

今回紹介する探索アルゴリズムは普通の足立法に比べ,1回の走行当たりの走行量が増えてしまうため,全日本ではコケてしまい走り切れませんでした.

不甲斐ねぇ...

 

足立法の改良案

去年のマウスであるBIJELAの探索アルゴリズムについて書く前に,足立法を少しいじって便利にする方法を考えます.

4マスゴール対応

まず一番に思いつくのが4マスゴール対応です.

4マスゴール対応とはその名の通り,2×2の4マスすべてをゴールとして認識することを意味します.

もちろん1マスゴールの足立法でも4マスのうちのどれかをゴールとして設定していれば必ずゴールすることができますが,ゴール内である特定のマスを目指して動くのは無駄であると思います.

また,探索のことを考えても4マスゴールにしてデメリットはないと思いますので,是非とも4マスゴール対応させましょう.

実装は簡単で,歩数マップ更新の際に1マスのゴールにセットしていた初期値を残りの3マスにもセットするだけです.

umuoumu.blog.fc2.com

Queueを用いた歩数マップ展開

一番簡単な歩数マップの更新方法は,ある歩数nを更新するために全区画の中からnを探して隣接するマスの歩数を更新,次はn+1を全区画の中から探して...という手法だと思います.

しかし,この方法ではある値を探すために全区画,16*16マスを探すためとても効率が悪く,時間がかかります.

 

BIJELAの場合,探索走行中に

  1. 区画中心で壁データの更新
  2. 新しい壁データを使って歩数マップを展開
  3. 歩数マップを用いて進路決定
  4. 直進orスラロームorUターン

という処理を繰り返し行っています.

BIJELAはマップの更新などの計算中も距離を加算しているため,次の動作が直進であれば,計算中に10mm進もうが100㎜進もうが問題ありません.

しかし,次の動作がスラロームである場合,3番の進路決定の段階でスラロームの前距離を超過しているとターンが崩れてしまいます.

BIJELAの600mm/s探索においてスラロームの前距離は約20mmでしたので,壁データの追加から進路決定までを33ms以内の時間で行う必要があります.

正直,そこそこのマイコンを使って計算すれば愚直にfor文を回しても34msもかからないと思います.

しかし,今後探索速度の向上や,より複雑な歩数マップ更新・進路決定を行う場合に困ることもあるかと思いますので,計算の高速化を行うに越したことはありません.

 

そのために紹介するアルゴリズムがQueue(キュー)というものです.

qiita.comQueueの詳細な解説は省略しますが,FIFO (First-In-First-Out),つまり溜まっているデータを古いものから順々に処理していくアルゴリズムです.

これを歩数マップ展開に当てはめると,更新したマスの座標データを溜めて置き,そのデータを用いて更新されたマスの周辺のみ歩数の更新を行う処理が実現できます.

これによって更新されていないマスを探す,といった無駄が削減されるため,演算量の低減が可能となります.

初代のSH7125マイコンを使っていたステッパーではそこそこ計算時間が早くなった記憶があります(半分くらいとかだったかな?覚えてないですが)

 

詳しい実装までは書かないですが,データを追加するpush,取り出すpopの関数を丁寧に作成し,リングバッファとするためheadとtailの管理をうまく行えば実装できるはずです.

また,データをためるリングバッファとなる配列の数が少ないと簡単にtailがheadに追いつき,挙動がおかしくなってしまうため注意が必要です.

 

直線優先歩数マップ

これは斜めを走らない人や苦手な人,既知区間加速をたくさん使いたい,という人におすすめの話です.

ここでいう直線優先歩数マップとは,隣接するマスの歩数が同じ時に直線を選ぶ,というものではなく,直線となる方向への歩数更新を小さい値とし,ターンする方向への歩数更新を大きな値とするものを指します.

つまり,パターンに応じて更新する歩数の重みを変える必要があります.

(ちなみに,更新する歩数の値が大きくなると配列の型によってはオーバーフローが発生したりする可能性があると思いますので注意してください)

実装の方法はいくつかあると思いますが,BIJELAでは更新する方向とは反対方向の壁情報と歩数をもとに直進かどうか判定していました.

 

既知区間加速

既知区間加速とは探索走行中に既知区間(すでに通ったことある経路など)を通過する際に,加速して通過することで探索時間の短縮を図るものです.

これも実装の方法は何通りもあると思いますが,BIJELAは仮想的に座標を動かしていき,未知壁にヒットする座標まで早く移動する,という考えで実装しています.

基本的には直線のみ加速する人が多いですが,中には斜め走行も含めた既知区間斜めをするマウスもあるとか.

すごいです.

 

BIJELAの探索アルゴリズム

それでは本題としてBIJELAの探索アルゴリズムを紹介しようと思います.

このアルゴリズムのモチベーションとしては,5回しかない走行回数を何度も使って探索するのは嫌だ,という所です.

また,普通の往復足立法では「あの経路が出てたら最短したいけどあそこもう探索したっけ?」といったように最短経路が見つけられているかわからない,といった点も嫌でした.

(本記事において"最短経路=全迷路データを持っている状況での足立法ルート"とします)

つまり,1走目が終わった時点で必ず最短経路が見つけられていることが確約されるアルゴリズムを作成することを目指しました.

 

アルゴリズムの流れ

この探索アルゴリズムの目標はスタート座標(0,0)からゴール座標までの最短経路を出す過程で,未知壁がなくなることです.

そのため

  1. 未知壁は無いものとして歩数マップ展開
  2. 仮想的にスタートからゴールまで歩数マップを使って移動
  3. その過程で未知壁があったらその周辺マスを一時的なゴールとする(無ければ探索終了)
  4. 現在地から一時的なゴールまで足立法探索する
  5. 一時的なゴールについたら1に戻る

 という流れをとればよいでしょう.

しかし,このアルゴリズムでは迷路の形状によっては一時的なゴールが更新されるたびに同じ経路を往復してしまい,時間を消費する割に,情報量が増えないという問題が発生します.

 

そこでこの問題を解決するため,未知区間優先歩数マップを作成し,この考え方を一時的なゴールへ向かう探索(4の動作)に適応させます.

するとすでに通っている区間を避けて移動しようとするため,同じ経路を取りづらくなり,多少遠回りしますが,新し壁データを収集しながら移動します.

この未知区間優先歩数マップの実装は直進優先のようにパターンに応じて更新する歩数を変更すればよいでしょう.

BIJELAでは一時的なゴールへの移動に使う歩数マップを直進優先と未知区間優先をハイブリットさせることで既知区間加速を有効に使いながら,無駄な往復を繰り返さないように工夫しています.

この直進優先と未知区間での重みをいじることで経路は変わってきます.

色々試してみる必要があると思います.

 

まとめ

さてこれでBIJELAに実装していた探索アルゴリズムの紹介は終了です.

 

このアルゴリズムの欠点としては,足立法の経路が出ることは確約されていますが,最短経路を別のアルゴリズムで出そうとしたときに,欲しい壁が未知だとその経路が走れなくなってしまいます.

そんな問題を解決できるのが全面探索ではないでしょうか.

僕は実装しないと思いますが....

便利であることは疑いようがありません.

 

一般的な足立法探索しか実装できていない方,ぜひ探索アルゴリズムいじってみてください.

楽しいですけれど辛かったです.

 

おわりに

去年の技術交流会の内容を書き直してみました.

なんとか読める記事になっているでしょうか?

間違いなどあればご指摘いただければ幸いです.

 

今年の機体や大会の振り返りもやりたいですが,あまり長くしたくないのでまたの機会としましょう.

 

また,この記事には自分が実装した機能しか書いていませんが,まだまだ工夫できる点はあると思います.

新しく実装したい機能もいくつかあるのですが,これらは実装してうまく動いたときにまた紹介できればと思います.

来年は最短経路導出もいじっていきたいと考えています.

そのためにも迷路シミュレータなど欲しいですが,卒論などですぐに身動きが取れず悲しい.

 

また,今年未知区間加速やクラッシュからの自動復帰を実装していた「けりさん」のマウスを見てすごいな,と心から感じました.

kerikeri.top

自分もけりさんのように頭のいいマウスを作れるよう努力していきたいな,と感じました.

 

明日の記事はayataka2591さんの「今年のマウスまとめ」です.

お楽しみに!

それでは

 

 

 

 

 

 

 

忙しい忙しい(ほんまか?)

みなさんお久しぶりです.

 

しばらく更新してなかったのですが,友達がブログを開設して「おい,書けよ」と煽られてしまったので少しだけ書こうと思います.

↓↓↓

rennoi.hatenablog.comシャレオツなブログやな~

 

近況報告になりますのでマウスの話はほとんどないです.

 

  • マウス合宿の話

7/6-7でマウス合宿が早稲田大学WMMC主催で開催され,参加してきました.

ss-sholaw-wmmc.hatenablog.com

個人的には走るマウスもなく申し訳ねぇって気持ちでしたが,いろんな方とお話ができてとても楽しむことができました.

運営してくださったWMMCの皆さん,素晴らしいイベントを本当にありがとうございました.

お疲れさまでした.

 

  • 研究室の話

今年の3月から研究室配属になり,信号処理系の研究室に配属となりました.

第一希望の研究室でしたし,同じく希望していた友人達も一緒に入れたのでとてもよかったです.

が,うちの研究室は同学科の研究室の中では真面目で忙しい部類に属しています(と思っています)

そのため,なかなか思うようにマウスの進捗が産めていません.

が,純粋に忙しいからではなく集中力不足でグダグダやっているからという気もします.(タイトル回収)

ただ,8月末にかけて忙しいのはマジなので身体を壊さない程度に頑張ります.

落ち着いたら温泉行ってサウナ入って水風呂に入って無敵になりたいですね.

(サウナ→水風呂出来たことないのですがリベンジしたい)

 

 

  • マウスの話

忙しいと思っている中ですが,時間ができたらマウスの設計をしていました.

今年はハーフ(マイクロマウス)に初挑戦しています.

オリジナリティをもってマウスを作りたい気持ちはあるのですが,中々そうもできないので,1作目と言い訳しながら急ぎながら作っています.

(あまり凝りすぎても進まないとどうしようもない,と思ってる人間なので)

先日研究室に泊まった時に配線ゴリゴリやって発注した基板&部品が届いたので,また時間を見つけてはんだ付けしていきたい所存.

ミスがないといいのですが・・・

f:id:kt33-JASALMA:20190805215501j:plain

新作基板

ちなみに今年はインドネシア語です.

 

 

少しはんだ付けして問題なさそうなら3Dプリントも発注しなくては

お,お金がかかる...

 

 

  • その他

8月19-20でMice夏合宿です.

今年は千葉の上総一ノ宮という所で開催らしいです.

楽しみです.

そこで圧倒的進捗が産めたらいいな.

 

東日本大会まであと1ヶ月しかないってま.?

もうエントリー開始してるし・・・

 

厳しい・・・

 

書きながら思い出しましたが,Mice技術交流会で発表した探索法の話もブログにまとめようと考えていたのですがすっかり忘れていました・・・

もし興味があるという方がいれば「書けよ」と圧力かけてください.

といっても新作マウスもやりたいので書くかはわかりませんが.

 

 あ,このまえ3つ(4つ説もある?)の花火大会を同時にみて「ほーん」って気持ちになりました.

 

 

現実逃避で書いてましたが,締め切りが近いタスクがあるのでそっちに戻ります.

相変わらず,殴り書き雑記事ですがここまで読んでくれた方,ありがとうございました.

また大会でお会いできるのを楽しみにしています.

お互いマウスがビュンビュン走ってるといいですね・・・_(:3 」∠)_

 

 

Nucleoボードをぶった切って書き込みとUART通信した

手を滑らせて秋月で買ったNucleo Board(F411)をST-LINK部分とSTM32の部分に分割してしまったのでその状態で書き込み(デバッグ)とUART通信した話についてです.

 

 

f:id:kt33-JASALMA:20190212212510p:plain

ちゃんと公式のドキュメントに記述あるからね

 

 はじめに

 この記事では割らない状態で書き込みとUART通信が出来ている前提で話を進めていきます.

まだの方は

などを参考に頑張ってみてください.

 

環境

OS Windows10
IDE TrueSTUDIO
デバッグ構成 J-LINK(SWD)
Nucleo STM32F411RE
外部電源 9V乾電池

 

分割

 うっかり机の上から落としたくらいでは割れないので頑張って切ります.

今回は部室のバンドソーでカットしました.

 

諸々の処理

Nucleoのデータシートからデバッグ用のピンはCN4であり,その割り振りは以下の通りです.

f:id:kt33-JASALMA:20190212215445p:plain

デバッグコネクタ

ここで6pinのSWOは使用していない(理解してないので使えない) ので無視すると以下のようになります.

Pin ボードのPin 名称
1 CN7-16 3.3V
2 CN7-15 PA14
3 CN7-20 GND
4 CN7-13 PA13
5 CN-14 RESET

 (ただし,3.3V,GNDはボード内の同じ機能の他のピンでも問題ないはずです)

 

また,Nucleoボードはついてるジャンパや0Ω抵抗のありなし(Solder brige)を変更することで配線や機能を変更することが出来ます.

今回は大きく分けて3つの変更が必要なのでまとめていきます.

1つ目

ST-LINKのボードに載っているCN2のジャンパ2つを取り外す.

これによってCN4のSWDのピンが有効になるっぽい(?)

 

2つ目

JP5のジャンパがpin1とpin2にセットされているのでこれをpin2とpin3にずらし,外部電源(VIN or E5V)の入力を有効にする.

 

f:id:kt33-JASALMA:20190214140508p:plain

外部電源

今回は9Vの電池を使っているのでCN7の22,24pin(GNDとVIN)に電池を接続しました.

 

3つ目

これはUSART2で使用している信号線をCN10の35,37ピンまでつなぐのに必要な処理です.

ボードを分割しているときには意味がないような気もしますが,SB13,14は開放するべき,との記載があるので一応0Ω抵抗を外しておきました.

また,USART2の信号線としてCN3のTX,RXピンとCN10の35,37ピンを接続します.

TXとRXがクロスするように接続しなければいけないので注意してください.

 

実行

 上に記したように接続したら,今までできていたのと同じようにデバッグを実行したら書き込みができると思います.

またUSART2も繋いでいればこちらも使えるのでprintf文なんかが使えるようになっていると思います.

 

注意

CubeMXでは今回使用したNucleoボードのUSART2はデフォルトで有効になっており,このボーレートは115200Bits/sに設定されています.

そのためこれを弄っていないならTera Termなりターミナルなりの設定もこれに合わせてあげる必要があります.

ここでトラブルが生じました.

分割する前は115200のまま通信が出来たのですが,分割した後,ケーブルを使って接続するとこの設定では早すぎるのか通信できなくなってしまいました.

この設定を9600くらいにすると通信できたので,まあきっとそういうことなんだろうな,と思います.

 

その他

 以下適当なことです.

 

CN4の1pinはターゲットデバイスの電圧で今回は3.3Vです.

またST-LINKも同様に3.3Vで駆動しているのでこれを活用してみようと思います.

ST-LINKのボードに存在するJP1の左側(USB-miniの端子じゃないほう)のピンが3.3Vなのでこれを繋ぐ.

→問題なく動く

 

またCN4の5pinがNRSTでSTM32のRESETと繋いでいますがこれも外してみる

→なんか知らんけど動く

 

こうするとマイコン本体との接続にはGND,SWCLK,SWDIO,TX(USART)の4本で事足ります(←ほんとにぃ~?)

 (※USARTは一方向しか使わないので1本にできる)

 

正直このNRSTの扱い(マイコンの書き込みの仕組み)に関してはちゃんと理解できてないので,何かあれば気軽にご指摘お願いします....

 

近況報告

このままいくとまたブログを書かなくなってしまいそうだったので雑にブログ更新したいと思います.

 

振り返ってみると元日の段階ではすでに回路図の作成が始まっていて,1/9にはSTMマイコンでPWMの出力が出来ていることがわかります.

 

 

 

が,2/6現在,回路図は終わらず,3DCADも一切手付かずです.

ま,まあテストもありましたしね....

 

 ...??ゲーム...??

 

まあなにはともあれテストも終わり,無事冬休みに突入したのでここから少しづつでも作業していけたらと思っています.

幸いテスト期間中に買ったエスコンも2,3周してひと段落したので少しは集中してマウスに打ち込めるかと思います.

 

今ハーフの回路図が発光回路とUIまわり,マイコンのピンの割り当て以外の部分は終わったので,残りはその他の部分の回路と3DCADの設計です.

足回りやエンコーダなどクラシックとは変わってくる部分も出て来ますが,できるだけ早く終わらせたいです.

四月になって新入生が入ってくると自分の作業がやりにくくなるので最低でもそれまでには基板が届いているような状況にしたいですね.(あくまでも"最低でも"ですが)

 

その他では電通大プチコンやうちの技術交流会も控えているので楽しみです.

 もっとも技術交流会は発表しないといけないのでその準備も進めないといけませんが....

 

 なんだか書き始めたのはいいものの,ブログを更新するだけの記事になってしまいました

少し先の話にはなってしまうけど新入生,沢山入ってくれるといいな~

 

あと最後にですが,STM32のF413の48ピンを使おうと思っているのですが,データのやり取りはSWOとUSARTとどちらがいいんでしょう

おすすめあれば教えてください

BIJELAというマウス(2018年終了のお知らせ)

もう年末ですね.

 

みなさん今年はどんな1年でしたか?

 マウスがビュンビュン走った人や,残念ながら全日本に出れなかった人など様々だと思います.

個人的には部長をさせていただき,多大の方々ともたくさんお話しできていい年だったと思います.

心残りといえば,残った新入生が少なかったことですね...

すまないが現部長の活躍に期待するしかないです.

必要ならば2月にでもちょっとした勉強会(座談会)を開いたりしたいな,と思います.

何か意見などあればください.

 

さて,前置きはここまでにして,今日は今年作成したクラシックマウスのBIJELA(ビエラ)についてまとめようと思います.

f:id:kt33-JASALMA:20181229143031p:plain

部内写真集向けに作ったよ

基本データ

よくある1717を使った変則4輪・吸引マウスですね.

部品リストなどは以下に示す通りです.

機体名 BIJELA
全長 [mm] 90.0
車幅 [mm] 70.0
重量 [mm] 108.0
マイコン RX631 64pin
ジャイロ ICM-20648
壁センサ ST-1KL3A+SFH4550
モーター 1717T003SR
エンコーダー IEH2-4096(付属)
モータードライバ TB6614
ピニオンギア 0.5M 11T(kkpmo)
スパーギア 0.5M 40T(DS成形平歯車)
ベアリング F682zz (Amazon)
ホイール外径 [mm] 19.5
タイヤ ミニッツレーサータイヤ20度
吸引モーター 10CL-1801-DJK
吸引ファン径 [mm] 25.0
Li-po Hyperion 180mAh 2S

回路図や配線図はこの記事です.

BIJELA以外の候補,BALTAとBIANCOだったんですね...なんだか懐かしい

3Dデータはこんな感じ. 前の記事でEAGLEとFusion360連携させて部品干渉確認しような!とかほざいてたやつが見事にやってなくて笑いました.

だから干渉したんやぞ,という気持ちです.

 

雑な感想など

 部品はモーターマウント(POM)だけ部室CNCで切削し,ファン,ファンフレーム,ホイールはDMM.makeの3Dプリントですべてナイロンで作成しました.

周りの人の話や体感から,工作精度として

  • 切削>3Dプリント(ナイロン)

というのを強く感じました.

が,積層方向が問題なければクラシックマウスくらいなら問題なさそう(...?)

(ホイールが偏心しているような気もしましたが)

モーターマウントの精度が良かったおかげなのか,重心がうまい具合だったのかわかりませんが,4点接地がきれいに決まってたのはいい点でした.

 

あと,CAD上では存在しているセンサーホルダーなのですが,1つだけ切削で作り,はんだ付けの際の冶具として使用しました.

全部作ってもいいかな,と考えてはいたのですが「まあ,いっか」となったので未採用となりました.

 

今回のマウスの目標として「吸引」とういう実績の解除を目指し,ファンが小さい気はしながらも,配線や実装の都合上これでいいや,と考えファン径25mmとしました.

が,実際に吸引すると70gくらいが限界で,添える程度の効果しかなかったのかなと思います.

というかそもそもジャイロ,角速度制御あたりのベースがしっかりしておらず,しっかりとした制御が出来ていませんでした.

これを吸引でごまかしていたような形になっていたので来年はもう少し基礎をしっかり固めないとな,と思います.

が,解決策が思いついてないのでどうしようね.

 

また,BIJELAには大きな音の出るスピーカー(UGCT7525AN4)を搭載していたのですが,これも本領発揮させてあげられなかった(優先順位が低かった)ので少し残念です.

また,サイズ的にもこれが結構配線を苦しめていたので来年はもっと小さいものにしようと考えています.

 

BIJELAはハード的には何もひねりのないつまらない機体なので少しでも探索アルゴリズムや経路導出などのソフト面を改良しようと考えていました.

実際には経路導出は何もいじれませんでしたが,探索は一度目の探索で確実に通りたい経路を見れるようになったのでこれはよかったです(全面探索ではない).

 

壁制御に関しては中部地区大会までP制御のみだったのですが,全日本前にD制御を追加し,PD制御としました.

これによって高速域の走行が安定するようになったと思います.

また,これも直前になって櫛対策の制御を追加しました.

...が,考えが浅すぎたのか吸い込まれっぽい挙動したり,発散したような挙動となり改悪だったな,と感じます.

ここも来年の課題ですね.

 

パラメータ

探索 加速度[m/s²] 7
速度 [mm/s²] 600
最短(直線) 加速度[m/s²] 15
速度 [mm/s²] 3500
斜め 加速度[m/s²] 10
速度 [mm/s²] 35
ターン 重心速度 [m/s²] 1.2(1.3)

ここで示している最短のパラメータはすべて安定(笑)のパラメータでこれ以上のギャンブルみたいなのもいくつかあります.

実際の大会で走ったものもありますが,まあ誤差みたいなもんなので省略です.

改めて「吸引してる割にターン遅すぎね?」って感じました.

 

来年の予定

 来年はクラシックとハーフと2種類のマウスを作りたいと考えています!

が,正直3月に配属が決まる研究室に大きく依存しますし,そもそも厳しい気がしているので優先度はハーフ>>クラシックとし,最低限ハーフの感想を目指したいと思います.

クラシックはBIJELAのマイナーチェンジ(1717使用),ハーフは非吸引の4輪にしようと考えています.

詳細はここでは書かない(というかほぼ未定)です.

 

個人的に創造性や独創性がなく,悲しくなってくるのですが,毎度のごとくハーフはありきたりな感じ,クラシックはファンを少し弄ってみようと考えています.

 

それでは,2019年が皆様にとってより良い一年となりますように...

よいお年を!