サポート切れのサイクルナビASG-CM14をまだまだ使うので、AIプログラミングしてみる

10年前のナビがまだまだ動く

そろそろ新しいサイクルナビが欲しいのですが、10年前に買ったASG-CM14がまだまだ動くのです。スマホとは連動しませんが、PCで作ったGPXファイルを読み込ませることができます。

agyay.hatenablog.com

ただ、上記のスクリプトはStravaからダウンロードしたGPXファイルを完全に変換できないことに気がつきました。複数行に跨る <metadata>要素が残ってしまいます。そこでブラウザ上でGPXファイルを変換できるアプリに作り直しました。

AIでアプリを作る

最近の流行りに挑戦してみます。今回CopilotとGeminiを使いましたが、どちらも大体同じような結果でした。

スクリプト


次の仕様でアプリケーションを作成してください。
1. アプリの本体はローカルディスクに保存されている単一のシンプルなhtmlファイル。
 CSS、javascriptはそのhtmlファイル内に含まれる。
2. アプリを起動するときは htmlファイルをダブルクリックする。
 するとブラウザがそのhtmlファイルを表示する。
 このアプリケーションのウィンドウ全体で、
 ファイルがドラッグ&ドロップされた時のブラウザのデフォルトの動作(ファイルのダウンロード)を無効とする。
3. アプリの画面には、次の要素がある。
(1) 表題 「Strava -> ASG-CM14 GPXファイル変換」
(2) ローカルディスクに保存されている .gpx ファイルをドラッグ&ドロップできる領域。
 そこには「ここに.gpxファイルをドラッグ&ドロップしてください」と表示されている。
(3) 「変換」というラベルが表示されているボタン。デフォルト状態では無効になっている
(4) GPXファイルの内容を表示する領域。デフォルト状態では空白になっている
(5) 「ダウンロード」というラベルが表示されているボタン。デフォルト状態では無効になっている。
4. gpxファイルがドラッグ&ドロップされたらファイルが下記のルールに従っているか確認する。
(1) ファイルの拡張子が 「.gpx」である
(2) ファイルの内容が 正しいxmlの構文に従っている。
5. ファイルがルールに従っていたら
(1) ドラッグ&ドロップされたファイル名をドラッグ&ドロップする領域に表示する。
(2) 「変換」ボタンを有効にする。
(3) GPXファイルの内容を表示する領域にドラッグ&ドロップされたGPXファイルの内容を表示する。
6. ファイルがルールに従っていなかったら
(1)「正しいGPXファイルをドラッグ&ドロップして下さい」と表示する。
(2)「変換」ボタンと「ダウンロード」ボタンを無効にする。
6. 「変換」ボタンが有効な時に「変換」ボタンが押されたら次の動作を行う。
(1) GPXファイルの内容を表示する領域に表示されているGPXファイルの内容を、
 GPXファイルを変換するルールに従って変換して再表示する。
(2)「ダウンロード」ボタンを有効にする。
7. GPXファイルを変換するルール
(1) 改行コードは LF にする
(2) <!— で始まり —> で終わるコメントを全て削除する。
(3) 下記のXML要素を全て削除する。
 子要素を含む場合は子要素ごと削除する。
 XML要素は複数の行に跨ることがある。また空要素のこともある。
 metadata, wpt, desc, link, text, time, year, author, license, keywords, ele, copyright
(4) trkpt 要素の子要素になっている name 要素とdesc要素を全て削除する。
 trk 要素の子要素の name要素は残す。
(5) <![CDATA[任意の文字列]]> から<![CDATA[ と]] を取り除いて 任意の文字列 だけにする。
(6) 上記の変換を行った結果、改行だけ、あるいは空白だけになってしまった行を削除する。
7. 「ダウンロード」ボタンを押すと、ファイルをダウンロードするダイアログを表示する。
 ファイル名の拡張子は「.gpx」とする。
8. ファイルをダウンロードするダイアログで適切なダウンロードするディレクトリ、ファイル名が
 入力されてOKが押されたらGPXファイルの内容を表示する領域に表示されたGPXファイルの内容を
 プレーンテキストで出力する。
 ファイル名の拡張子は「.gpx」とする。

Microsoft Copilot が生成したhtmlファイル

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Strava → ASG-CM14 GPXファイル変換</title>
  <style>
    body {
      font-family: sans-serif;
      margin: 20px;
    }
    h1 {
      text-align: center;
    }
    #drop-zone {
      border: 2px dashed #aaa;
      padding: 40px;
      text-align: center;
      margin-bottom: 20px;
      background-color: #f9f9f9;
    }
    #file-name {
      margin-top: 10px;
      font-weight: bold;
    }
    button {
      padding: 10px 20px;
      margin-right: 10px;
      font-size: 16px;
    }
    #gpx-content {
      white-space: pre-wrap;
      background-color: #f0f0f0;
      padding: 10px;
      border: 1px solid #ccc;
      height: 300px;
      overflow-y: scroll;
    }
    #message {
      color: red;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>Strava → ASG-CM14 GPXファイル変換</h1>
 
  <div id="drop-zone">
    ここに.gpxファイルをドラッグ&ドロップしてください
    <div id="file-name"></div>
    <div id="message"></div>
  </div>
 
  <button id="convert-btn" disabled>変換</button>
  <button id="download-btn" disabled>ダウンロード</button>
 
  <div id="gpx-content"></div>
 
  <script>
    document.addEventListener('dragover', e => e.preventDefault());
    document.addEventListener('drop', e => e.preventDefault());
 
    const dropZone = document.getElementById('drop-zone');
    const fileNameDisplay = document.getElementById('file-name');
    const messageDisplay = document.getElementById('message');
    const convertBtn = document.getElementById('convert-btn');
    const downloadBtn = document.getElementById('download-btn');
    const gpxContentDisplay = document.getElementById('gpx-content');
 
    let originalContent = '';
    let convertedContent = '';
 
    dropZone.addEventListener('drop', async (e) => {
      e.preventDefault();
      const file = e.dataTransfer.files[0];
      if (!file || !file.name.endsWith('.gpx')) {
        showError('正しいGPXファイルをドラッグ&ドロップして下さい');
        return;
      }
 
      const text = await file.text();
      try {
        new DOMParser().parseFromString(text, "application/xml");
        originalContent = text;
        fileNameDisplay.textContent = file.name;
        gpxContentDisplay.textContent = originalContent;
        convertBtn.disabled = false;
        downloadBtn.disabled = true;
        messageDisplay.textContent = '';
      } catch {
        showError('正しいGPXファイルをドラッグ&ドロップして下さい');
      }
    });
 
    convertBtn.addEventListener('click', () => {
      convertedContent = convertGPX(originalContent);
      gpxContentDisplay.textContent = convertedContent;
      downloadBtn.disabled = false;
    });
 
    downloadBtn.addEventListener('click', () => {
      const blob = new Blob([convertedContent], { type: 'text/plain' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'converted.gpx';
      a.click();
    });
 
    function showError(msg) {
      fileNameDisplay.textContent = '';
      gpxContentDisplay.textContent = '';
      convertBtn.disabled = true;
      downloadBtn.disabled = true;
      messageDisplay.textContent = msg;
    }
 
    function convertGPX(text) {
      let lines = text.split(/\r?\n/);
 
      // LFに統一
      let result = lines.join('\n');
 
      // コメント削除
      result = result.replace(/<!--[\s\S]*?-->/g, '');
 
      // 指定要素削除(複数行対応)
      const tagsToRemove = ['metadata', 'wpt', 'desc', 'link', 'text', 'time', 'year', 'author', 'license', 'keywords', 'ele', 'copyright','type'];
      tagsToRemove.forEach(tag => {
        const regex = new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi');
                            const emptyTagRegex = new RegExp(`<${tag}[^>]*/>`, "gi");
        result = result.replace(regex, '');
                            result = result.replace (emptyTagRegex,'');
      });
 
      // trkpt内のname, desc削除
      result = result.replace(/<trkpt[^>]*>[\s\S]*?<name>[\s\S]*?<\/name>/gi, '');
      result = result.replace(/<trkpt[^>]*>[\s\S]*?<desc>[\s\S]*?<\/desc>/gi, '');
 
      // CDATA削除
      result = result.replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1');
 
      // 空行・空白行削除
      result = result.split('\n').filter(line => line.trim() !== '').join('\n');
 
      return result;
    }
  </script>
</body>
</html>

Mac/Windows, Safari/Chrome/Edgeで動きます。これはgpxファイルをドロップしたところで、変換を押すとASGが解釈できない行を削除します。

AIコーディングは途中までは簡単だが、最後を詰めるのが面倒

既に色々な人が書かれている通りですね。今回も良いところまでプログラムしてくれたのですが、最後が手間取りました。ファイルをドラッグ&ドロップするとそのファイルが直ぐにダウンロードフォルダにエクスポートされてしまいます。AIに修正を頼むと修正しましたと回答するのですが、やっぱり治っていません。チャットのたびに違うコードを吐き出すので、コードを読むのも大変です。色々調べて

document.addEventListener('dragover', e => e.preventDefault());
document.addEventListener('drop', e => e.preventDefault());

の2行が必要だということがわかったので、プロンプトに

このアプリケーションのドキュメント全体で、
ファイルがドラッグ&ドロップされた時のブラウザのデフォルトの動作(ファイルのダウンロード)を無効とする。

と追記したらうまくいきました。しかしこれもたまたまで、ダウンロードの動作をさせるな、とかだとダメでした。

Overhaul a Trek Checkpoint ALR 5

買ってから2年経ったので小オーバーホール

今回初

  1. チューブレスレディタイヤ

    TREK GR1 TEAM ISSUE 430g → Panaracer GRAVELKING R 400g
    外すのも嵌めるのもタイヤレバーに頼った。ビード上げはなんとかフロアポンプで成功。出先でパンクした時にチューブを入れるのは相当難しそうなので考えないことにした。
    先にシーラントを注射器で抜き取ったがそれでも外すときに大量に零れた。シーラントはメーカーの指定量だと多すぎる気がする。少なめにしてその代わり乾く前に補充した方が良さそう。
  2. ブレーキフルード

    Shimanoのマニュアル、Youtubeのやり方がばらばらで何が正解だか。今回は上から入れて下から注射器で古いフルードを引き抜いて捨てた。その後もう一度注射器を繋いでブリーディング。
    反省点多数。古いフルードを捨てる量が不十分だった。リアのブレーキパッドを作業前に外すのを忘れてフルードを溢してしまってダメにした。チューブのクランプの正しい使い方ができなかった。じょうごを外すときに蓋を閉じるのを忘れて大量に溢した。
  3. フリーボディ
    あまり汚れていなかった。次回は3年後で良いかも。Youtubeで分解方法を探しても同じ形式のものは見つからず。ナットを外す時は反対側の軸をプライヤーでそのまま掴むしかない。
  4. ヘッドパーツ

    下側に少しだけ汚れ。次回は3年後で良いかも。
  5. プーリー交換

    歯が全然減っていなかったので多分早すぎた。

半年ごとの作業

  1. チェーン、スプロケット

  2. ブレーキディスク

    リアのディスクが歪んでいるので補正したが不十分。センター出しも完全ではないので、そのうち交換する。

やらなかった事

  1. バーテープ、シフトワイヤー
    2ヶ月前に交換したので見送り
  2. ボトムブラケット清掃
    シフトアウターワイヤーを交換するときに外したので見送り
  3. チェーン、スプロケット、チェーンリング交換
    チェーン伸びが許容量の半分だったので見送り
  4. ペダルの分解清掃
    細かい部品が多くて開けるのは怖い

次回以降の計画

  1. タイヤにインサートを入れる。チューブを携帯しないので、プラグで補修し切れないパンクが怖い。

    https://www.vittoriajapan.co.jp/vittoria_all_products/vittoria_tires_inserts/

  2. 駆動系を12速に全交換
    TREK Checkpointのマニュアルにフリーボディの形式が明記されていない。付いているスプロケットがCS-HG700-11 スペーサ付きなので、HG スプライン Lだと思われる。今度測ってみないと。これが正しいとすると12速ロード用は付くが、12速MTB用は付かない。フロントシングル化するにはホイールごと交換するしかない。俺の貧脚にはフロントシングルのギア比の方が合っているのだが。

    https://productinfo.shimano.com/ja/compatibility/C-731

    Di2のグループセットは現時点の実売価格が25万円程度で、Checkpoint ALR5の本体価格とほぼ等しい。年金生活に入ってからだと捻出が
    難しいかも。

他に欲しいもの

  1. ナビ
    手持ちのナビは出かける前にPCでルートを作っておく必要がある。専用ソフトは2019年にサポート終了しているし。
  2. ドラレコ
    やっぱり事故に備えたい。前後カメラ、4K手ぶれ補正、電池持ち8時間以上が出たら買いたいのだが。

Replace the rear shift cable of Trek Checkpoint ALR5 MY23

最近のロードバイクは高性能になったが整備が難しい

リアシフトケーブルの交換

2年前に買ったTrek Checkpoint ALR5のリアシフトケーブルを交換しました。他の車種ではケーブルが一旦BBの外に出ていることが多いのですが、ALR5ではレバーからディレイラーまでフレームに内装された一本のアウターケーブルが繋がっています。内装ケーブルを交換するときのセオリーに従ってまずインナーケーブルだけ交換し、次にインナーケーブルをガイドにしてアウターケーブルを交換しようとしました。しかしアウターケーブルが途中までしか入りません。サービスマニュアルを見るとボトムブラケットの辺りが怪しいです。

TK_CheckpointALR_MY23_ServiceManual

ボトムブラケットの取り外し

どうやってもアウターケーブルが入らないので専用工具を買ってボトムブラケットを外すことにしました。

praxiscycles.com

外してみるとこんな感じです。BBを付け直すと、奥に見えるシェルとフレームの間の非常に狭い隙間でアウターケーブルが急角度に曲がっています。これでは外側からアウターケーブルを押し込んでも入っていかないわけです。

増えていく工具

最近のロードバイクはパーツ毎に各メーカーの専用工具が必要になっています。もうすぐブレーキフルードチューブレスタイヤの交換時期ですが、初めての試みなのでまた一生のうちに数回しか使わない工具が増えそうです。

 

ドルツ ジェットウォッシャー EW-DJ42 電池交換

ドルツ ジェットウォッシャー EW-DJ42 電池交換

私はEW-DJ52を修理しながら使っていたのですが、何度も水没させたせいかとうとう完全に故障しました。

agyay.hatenablog.com

その後、EW-DJ42に買い替えて1年半使っているのですが、そろそろ電池が消耗してきた気がします。EW-DJ42の電池を交換したときに買った2個セットの電池が1個余っているので交換することにします。まず上蓋を外します。爪が3箇所あります。

充電端子のカバーを上に引き抜いてから外します。

3本のネジを外したあと、3箇所のくぼみをドライバー等でこじて上部の部品を引き抜きます。ネジ穴、ノズル取付口の裏、部品の周りなどにOリングが付いているので無くさないように気をつけます。

充電端子を外します。取水パイプも外れます。

 

空気取り込み口にはフィルターが付いていて水が外にこぼれないようになっています。ボタンの隙間に汚れが入り込んでいますが、柔らかいプラスチックに完全に貼り付けられているので完全に掃除するのは難しそうです。

電池はどちら向きでも付いてしまうので、+-の表示を間違えないように交換します。パナソニック電動歯ブラシやシェーバーなどとも共通です。長い事放置していたので充電します。

 

EW-DJ42のときは電池交換後に水没させてしまったので今度はあらかじめシリコングリスをOリングに塗ってから慎重に取り付けます。

さて、今度は大丈夫か。









GRXにバッシュガードを付けました

ロードバイクでズボンの裾が汚れる問題

ロードバイクはチェーンががむき出しなので、長ズボン(スラックス、パンツ?)で乗ると裾が汚れます。ズボンを履いてロードバイクに乗るなという話ですが、私の肌は日焼けに極端に弱いのです。冬はサイクルタイツを履きますが、春秋にゆっくり走るときには短いサイクルパンツの上にモンベルのライトトレッキングパンツを履いています。

バッシュガードを付けた

Trek Checkpoint にはGRXグラベルクランクセット2x11が付いています。バッシュガードの取り付けは全く想定されていないようです。

  • チェーンリングに取付用のネジ穴が無い
  • ホローテック2にはセンターボルトが無いのでここにも付かない
  • クランクとチェーンリングの間が狭いので全周型のガードは入らない

GRX チェーン リング クランク ガード バッシュ カバー プロテクター chain ring crank guard bash cover protector 110 BCD asymmetric Shimano 4 bolt などのキーワードでAmazonやAliexpressを探しましたが良いものが見つかりません。それでも探すとハンドルコラム用のプレッシャーアンカーでバッシュガードを固定した人がいました。

7bei.blog.fc2.com

真似をさせていただいたところうまく付きました。しかしSW-720は真ん中が膨らんでいるため、ペダリング中にシューズを擦ってしまうのです。フラットペダルなら足を少し外側に置くので問題ないのでしょう。

もっと薄いバッシュガードを探しました。

今度はホローテック2の穴が途中から狭くなっていてプレッシャーアンカーを奥まで差し込めません。

プレッシャーアンカーの円柱部の3つのパーツのうち1つを外して残りの2つをヤスリで少し削って細くし、さらに間に切ったゴムを入れて対角線上にパーツが来るようにします。バッシュガードとフロントディレイラー接触しない高さまで押し込み、ホローテック2 が変形すると困るのであまり力を入れずにボルトで固定します。

ボルトには内側にもネジが切ってあるので、M5ボルトでバッシュガードを取り付けます。

ライトトレッキングパンツは裾がかなりヒラヒラしているのでやはり裾バンドが必要ですが、バッシュガードと併用すれば裾が汚れることがありません。

その他の対策

スクワート ルブ

ワックスタイプのルブを使うと裾にほとんど汚れが付きません。

各社のドライルブやワックスルブを試しましたが、使用前にチェーンを完全に脱脂洗浄する必要があるなど癖がありますが squirt chain lube が一番ベタつかず抵抗が小さかったのでお勧めです。

マウスのボタン修理

ロジクールのマウスM590を480円で修理する

WindowsMacでマウスを共有する

複数PCで使えるマウスはいくつかありますが私はロジクールM590を使っています。製造中止になってしまったので公式店ではもう売っていません。他の店を探せばまだ在庫があるようですが、1万円以上します。

M590は上面に切り替えボタンがあり切り替えがとても便利です。後継機?のM570、Anywhere3は切り替えボタンが裏に移動してしまいホイールのチルトも省略されて私には使いにくくなってしまいました。

酷使で右クリックボタンが壊れた

愛用していたM590ですが酷使がたたり右クリックが効かなくなってしまいました。後継機は高いし機能も私には合わないしでなんとか修理したいところです。互換スイッチに交換したところ無事治りました。使ったはんだごての熱容量が小さくて苦労したので、修理前に大きな部品の外し方やスルーホール内のハンダの除去方法をYoutube等で調べておくと良いと思います。

 

Trek Checkpoint ALR5 のダウンチューブにツールボトルを付ける

自転車を買い替えました

ロードバイクの品薄状態が続いていましたが、解消されてきたようです。Trekが大型セールをやっていたのを機に新調しました。

Trek Checkpoint ALR5

Checkpoint SLと違ってALRには内蔵ストレージが無いので、ツールを入れるストレージが欲しくなります。私は飛び石の保護も兼ねてダウンチューブ下に大きめのツールボトルを付けました。できるだけ下に取り付けられるケージを選んだので、前輪とボトルの間には十分な余裕があります。フレームサイズは52です。