GPT4o に技術マニュアルから実装コードを模索する話(C2340R5でデバイス名を変更編)

生成AIを使ってコード生成をし始めて半年ぐらい経つわけですが、「いろいろなコード生成が自動的にできるよ!」という話よりも、実際に目の前のコードをどうやって生成していくのか?うまく動くようにAIが生成したコードをどうやって人(=自分)が修正していくのか?がプログラマには当面の課題になります。

まあ、どうやってChatGPTあるいはCopilotを使って、コード書きを促進させるか?という実例ですね。WEB アプリケーションのように巷に情報が溢れているならばまだしも、組み込みのような情報が閉鎖的(でもないのだけど)で少ない場合にはどうするのか?という例でもあります。実際のところ、ブロック崩しとか顔認識AIのような既にコードがある場合にはそれをコピペすれば言い訳で、ほどよく WEB サイトに散らばっているならば大丈夫なんですが、完全に未知なコード(少なくとも自分にとっては「未知」の状態)の場合には、ちょっと困るわけです。

C2340R5でBLEのデバイス名を変更したい

未知の人には完全に未知だと思います。まずは、「C2340R5」ってのが何なのかを調べないといけません。BLEを知っていればいいのだけど、デバイス名は何なのか、ぐらいは知っておいて欲しいものですが、まあ、そこからスタートしましょう。

ちなみに、CC2340R5 ってのは Bluetooth が入っている組み込み用のボードですね。ESP32 が入っている M5Stack 系で Bluetooth/BLE を扱えば結構情報が多いのですが、Texas Insturuments の CC23xx 系のボードだと情報が少ないのです。まあ、フォーラムがあるだけマシなのと、マニュアルが揃っているのでなんとかなりそうではあります。

https://www.ti.com/product/CC2340R5

実は SimpleLink というライブラリが TI から提供されていて、これに結構書いてあります。書いてはあるのですが、じゃあ「BLEのデバイス名を変更する」方法は書いていないのが難点ではあります(ほんとうのところ、書いていないのではなくて、デバイス名を変更する方法がない、ということなのですが)。

SWCU193 User guide | TI.com https://www.ti.com/document-viewer/lit/html/swcu193

初手は素直に聞いてみる

AIに質問をするときに「目的」と「手段」をうまく切り分けないと変な回答が出てくることが多いのですが、ひとまず初手としては素直に聞いてみるのがいちばんです。ぴったりとした答えがでてくればそれでよいし、まあ違ったとしたらそれを踏み台にして探索を続ければいいのです。

実はですね、この模索を再現しようと思って、初手に「C2340R5でBLEのデバイス名を変更したい」を再び入力したら、正解が出てくるんですよ。実は、SimpleLink のライブラリには動的にデバイス名を変更する API がなくて、BLE のアドバタイズ部分を再起動するしかありません。しかも配信するデバイス名は上記のように直書きになっているので、自前でバイト単位で書き出さなければ(memcpy使うけど)いけないのです。

まあ、このコードを見て「デバイス名が変更できる」と分かるには、その模索がってこそなのですが。

以下は、以前出した間違ったChatGTPの回答を上げておきます。

ChatGPT にマニュアルの PDF を入れる

ChatGPT にはファイルアップロードして、その中を優先的に探索してくれる機能があるので、それを使います。

初期値は英語になっているようですが、次のプロンプトで「日本語で。」というと日本語が主になります。

いろいろと探索する

前回の場合は「simplelink でデバイス名を変更するには?」で質問しています。この手の質問は、初手は漠然とした「目的」を示したほうがよいです。最終的には実装するための「手段」を探すことになるのですが、いきなり手段を示してしまうと、それ以外の手段を探すのが難しくなりがちです(人間の頭的に)。なので、最初は何もわからない振りをして、ChatGPTに尋ね、少し手間がかかりますが掘り込み方式で進めます。

  • TGAP_DEVICE_NAME という定義はどこにもない

  • GGS_DEVICE_NAME_ATT はあるが、デバイス名は変更されない。

  • GAPRole_SetParameter は存在しない。

  • BLEAppUtil_setAdvData は存在していない。

そんな訳で、なかなか正解に辿り着けません。実は、最初の質問ででてきたscanRespDataの書き換えが正解で、なんとか API 経由で書き換えようとしていたのですが、そんな API は存在しなくて、直接データを書き換えて BLEAppUtil_advStart で BLE のアドバタイズを再起動しないといけないのです。

仕方がないので、初期設定されているアドバタイズデータを書き換える

実は TI のプログラムは Code Composer Studio の *.syscfg というファイルを使っています。これが設定ファイルになっていて、各種設定をコード出力しているのです。Code Composer Studio でデバイス名(ble.deviceName)を書き換えると、うまく書き換わるわけで、そのあたりから更にコードを見ていきます。

最終的には BLEAppUtil_AdvInit_t 構造体があって、ここで初期設定されているのが肝です。コメントを見ると Sysconfig から変換されていることがわかるし、const struct になっているので、実にそれっぽいです。

//! Advertise param, needed for each advertise set, Generate by Sysconfig
const BLEAppUtil_AdvInit_t advSetInitParamsSet_1 =
{
    /* Advertise data and length */
    .advDataLen        = sizeof(advData1),
    .advData           = advData1,

    /* Scan respond data and length */
    .scanRespDataLen   = sizeof(scanResData1),
    .scanRespData      = scanResData1,

    .advParam          = &advParams1
};

まずは、const のままでは書き換えられないのでコードを修正します。

//! Advertise param, needed for each advertise set, Generate by Sysconfig
BLEAppUtil_AdvInit_t advSetInitParamsSet_1 =
{
    /* Advertise data and length */
    .advDataLen        = sizeof(advData1),
    .advData           = advData1,

    /* Scan respond data and length */
    .scanRespDataLen   = sizeof(scanResData1),
    .scanRespData      = scanResData1,

    .advParam          = &advParams1
};

その後に、appMain 関数の先頭で advDataLen と advData を書き換えます。

    // デバイス名の変更
    static uint8_t deviceName[] = "BLE-TEST";

    uint8_t deviceNameLen = strlen((const char*)deviceName);
    static uint8_t scanResData[32];
    uint8_t scanResDataLen = 0;
    scanResData[0] = strlen((const char*)deviceName) + 1;
    scanResData[1] = GAP_ADTYPE_LOCAL_NAME_COMPLETE;
    memcpy(&scanResData[2], deviceName, deviceNameLen);
    scanResData[2+deviceNameLen] = 0x02;
    scanResData[3+deviceNameLen] = GAP_ADTYPE_POWER_LEVEL;
    scanResData[4+deviceNameLen] = 0x00;
    scanResDataLen = 5 + deviceNameLen;

    memcpy( attDeviceName, deviceName, deviceNameLen+1 );
    // memcpy( scanResData1 + 2, deviceName, deviceNameLen );
    extern BLEAppUtil_AdvInit_t advSetInitParamsSet_1;
    advSetInitParamsSet_1.scanRespData = scanResData;
    advSetInitParamsSet_1.scanRespDataLen = scanResDataLen;

動的に変更したい場合は、BLEAppUtil_advStop でいったん止めてから BLEAppUtil_advStart すれば OK です。

ChatGPT の回答では、起動時の BLEAppUtil_advStart しか出て来ないのでなかなか正解に辿り着けないのですが、実は「動的に BLE のデバイス名を変えるにはどうすればいいですか?」と質問すると、かなりイイ線までいきます。

void updateDeviceName(const char* newDeviceName)
{
    // 新しいデバイス名を格納(長さチェックを含む)
    size_t nameLen = strlen(newDeviceName);
    if (nameLen > MAX_DEVICE_NAME_LEN)
    {
        nameLen = MAX_DEVICE_NAME_LEN;
    }
    strncpy(deviceName, newDeviceName, nameLen);

    // スキャンレスポンスデータを更新
    uint8_t scanRespData[] =
    {
        nameLen + 1,  // データ長
        GAP_ADTYPE_LOCAL_NAME_COMPLETE,  // デバイス名のタイプ
        // デバイス名データ
    };

    // デバイス名をコピー(新しい名前で上書き)
    memcpy(&scanRespData[2], deviceName, nameLen);

    // BLEアドバタイズ設定を更新
    BLEAppUtil_AdvInit_t advParams = {
        .advParamLegacy = { ... },  // 他のパラメータを設定
        .scanResponseData = scanRespData,  // 更新されたスキャンレスポンスデータをセット
    };

    // 既存のアドバタイズを停止
    BLEAppUtil_advStop();

    // 新しいアドバタイズを開始
    BLEAppUtil_advStart();
}

BLEAppUtil_AdvInit_t advParams が何処で使われているのか?(実際には BLEAppUtil_advStart の中身なんですが)、というのが不明なのでこのコードではうまくいかないのですが、先に説明した const struct BLEAppUtil_AdvInit_t までたどり着いています。

AI でコード出力をする場合、マニュアルにないコードは探せません。逆に言えば、マニュアルにあるコードならば結構な確率で探し出してくれます。数々の回答のコード(間違ってはいるけれども)もかつてのコードかもしれないし、うまく類推しているコードでしょう。

AI でコード出力と言うと「5分でなんとか」方式が多いので、あっという間に作れる雰囲気がありますが、実際のところはかなり違います。まあ、それでも何も分からないところがから、最初の下調べをしてくれてマニュアルからなんとなく導き出してくれるところは便利ですね。当然のことながら、コンパイルも含めて動作確認は人の手でやらないと無理なので、そのあたりで動作確認は必須なところです。

カテゴリー: 開発 | GPT4o に技術マニュアルから実装コードを模索する話(C2340R5でデバイス名を変更編) はコメントを受け付けていません

M5Stack で iBeacon を飛ばす(PlatformIO環境)

iBeacon をスマホで受信するテストしているときに、iBeacon 自体がないので困ることはありませんか?いや、困ってなくても作ってみるのがお勧めです。

たまに作ろうと思うと、ググってもうまく引っ掛からない(iBeaconを受信するツールはあるのですが、発信するほうはあまりないので)、自分のブログに残しておきます。

platformio.ini

初代の m5stack の platformio,ini は次の通り。m5stick cplus とかを使う場合は適宜かえておきます。

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino
monitor_speed = 115200

lib_deps = 
    m5stack/M5Stack@^0.3.9

RTOS を使わない場合

RTOS を使わずに framework の android で setup/loop 関数を使って作ります。iBeacon を発信するツールとして使いたいときはこれで十分です。

#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <M5Stack.h>

static BLEServer *pServer ;
static BLEAdvertising *advertising ;

#define BEACON_UUID "12345678-1111-2222-3333-56789abcdef0"
#define BEACON_MAJOR 0x0001
#define BEACON_MINOR 0x0001
#define BEACON_POWER 0xC5  // Measured power (at 1 meter distance)

void init_beacon() {
  BLEAdvertisementData ibeaconData;
  // 12345678-1111-2222-3333-56789abcdef0
  char UUID[] = { 
    0x12, 0x34, 0x56, 0x78, 
    0x11, 0x11, 
    0x22, 0x22, 
    0x33, 0x33, 
    0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 
    0x00};
    int major = BEACON_MAJOR ;
    int minor = BEACON_MINOR ;
    int txPower = BEACON_POWER;

    ibeaconData.setFlags(0x1A);
    std::string strServiceData = "";
    strServiceData += (char)0x4c;  // apple 
    strServiceData += (char)0x00;  // apple 
    strServiceData += (char)0x02;  // apple 
    strServiceData += (char)0x15;  // apple
    strServiceData += UUID ;
    strServiceData += (char)(major >> 8);    // major
    strServiceData += (char)(major & 0xFF);  // major
    strServiceData += (char)(minor >> 8);    // minor
    strServiceData += (char)(minor & 0xFF);  // minor
    strServiceData += (char)txPower;  // 
    ibeaconData.setManufacturerData(strServiceData);

    // アドバタイズの設定
    advertising = BLEDevice::getAdvertising();
    advertising->setAdvertisementData(ibeaconData);
    advertising->setScanResponse(false);

}

// Arduinoのsetup関数
void setup() {
    // M5Stack の初期化
    M5.begin();
    Serial.begin(115200);

    BLEDevice::init("M5Stack");
    BLEServer *pServer = BLEDevice::createServer();
    init_beacon();

    // taskBeacon 内で start している
    // 実は start 1回だけではうまくいかないことが多く、
    // start/stop を繰り返す必要がある
    // advertising->start();
}

// Arduinoのloop関数
void loop() {
    // メインタスクのループ
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(10, 10);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(2);
    M5.Lcd.print("Hello, M5Stack with FreeRTOS! ");
    M5.Lcd.print(BEACON_UUID);
    Serial.println("Hello, M5Stack with FreeRTOS! ");
    // vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
    advertising->start();
    delay(3000);
    advertising->stop();
    delay(3000);
}

iBeacon で送信するサービスUUIDは、Apple の 0x004c となっていて、内部データとして iBeacon として送信したい UUID(12345678-1111-2222-3333-56789abcdef0)を設定しています。本来ならば、BEACON_UUID から 16 bytes のデータを作るところでが、面倒なので UUID 配列に詰め込んでいます。ちなみに 0x1502 は特性ID(キャラクタリスティック)のようなもので固定値です。

このコードでは、init_beacon 関数内でビーコンの発信設定をしているので、advertising->start(); を 1回だけ呼び出せばビーコンが送信されるような気もするのですが、うまくいきません。

これが BLEAdvertising クラスの仕様なのか、M5Stack の制限なのか(あるいは手元の M5Stack が不調なのか)わかりませんが、loop 関数にあるように、ある程度の間隔で start/stop を繰り返してやらないと iBeacon のデータが飛びません。

iBeacon が無事飛んでいると、次のように別のツールで動作確認ができます。

これは dotnet で作ったツールで、こんなコードで動かしています。

using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Storage.Streams;

// BLEのスキャナ
BluetoothLEAdvertisementWatcher watcher;
// MACアドレスの保持(ランダムなので意味はない)
List<ulong> maclist = new List<ulong>();

Main(args);

void Main(string[] args)
{
    Console.WriteLine("Folkbears iBeaconCheck");

    watcher = new BluetoothLEAdvertisementWatcher()
    {
        ScanningMode = BluetoothLEScanningMode.Passive
    };
    // スキャンしたときのコールバックを設定
    watcher.Received += Watcher_Received;
    // スキャン開始
    watcher.Start();
    // キーが押されるまで待つ
    Console.WriteLine("Press any key to continue");
    Console.ReadLine();
}

void Watcher_Received(
    BluetoothLEAdvertisementWatcher sender,
    BluetoothLEAdvertisementReceivedEventArgs args)
{

    var uuids = args.Advertisement.ServiceUuids;
    var mac = string.Join(":",
                BitConverter.GetBytes(args.BluetoothAddress).Reverse()
                .Select(b => b.ToString("X2"))).Substring(6);
    var name = args.Advertisement.LocalName;
    var rssi = args.RawSignalStrengthInDBm;
    var time = args.Timestamp.ToString("yyyy/MM/dd HH:mm:ss.fff");
    

    if ( args.Advertisement.ManufacturerData.Count > 0)
    {
        var data = args.Advertisement.ManufacturerData[0];
        if ( data.CompanyId == 0x004c && data.Data.Length >= 23)
        {
            byte[] ibeacon = new byte[data.Data.Length];
            DataReader.FromBuffer(data.Data).ReadBytes(ibeacon);
            if (ibeacon[0] == 0x02 && ibeacon[1] == 0x15)
            {
                byte[] uuid = ibeacon[2..18];
                byte[] major = ibeacon[18..20];
                byte[] minor = ibeacon[20..22];
                byte txpower = ibeacon[22];

                int majorvalue = major[0] * 256 + major[1];
                int minorvalue = minor[0] * 256 + minor[1];
                Console.WriteLine($"{time} [{tohex(uuid)}] {rssi} dBm {mac} "
                    + string.Format("{0:x04}", majorvalue) + " "
                    + string.Format("{0:x04}", minorvalue) + " "
                    );
            }
        }
    }
    string tohex( byte[] data )
    {
        return BitConverter.ToString(data).Replace("-", "").ToLower();
    }
}

*.csproj ファイルはこんな感じです。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

RTOS を使う場合

先のコードでは iBeacon の発信と停止を loop 内で制御しましたが、RTOS を使えばタスク内で記述ができます。

#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <M5Stack.h>

static BLEServer *pServer ;
static BLEAdvertising *advertising ;

// タスクの定義
void task1(void *pvParameters) {
    while (1) {
        Serial.println("Hello from Task 1");
        vTaskDelay(pdMS_TO_TICKS(1000));  // 1秒待機
    }
}

void task2(void *pvParameters) {
    while (1) {
        Serial.println("Hello from Task 2");
        vTaskDelay(pdMS_TO_TICKS(2000));  // 2秒待機
    }
}

void taskBeacon(void *pvParameters) {
    while (1) {
        Serial.println("task beacon start");
        advertising->start();
        vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
        Serial.println("task beacon stop");
        advertising->stop();
        vTaskDelay(pdMS_TO_TICKS(10000));  // 10秒待機
    }
}

#define BEACON_UUID "12345678-1111-2222-3333-56789abcdef0"
#define BEACON_MAJOR 0x0001
#define BEACON_MINOR 0x0001
#define BEACON_POWER 0xC5  // Measured power (at 1 meter distance)

void init_beacon() {
  BLEAdvertisementData ibeaconData;
  // 12345678-1111-2222-3333-56789abcdef0
  char UUID[] = { 
    0x12, 0x34, 0x56, 0x78, 
    0x11, 0x11, 
    0x22, 0x22, 
    0x33, 0x33, 
    0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 
    0x00};
    int major = BEACON_MAJOR ;
    int minor = BEACON_MINOR ;
    int txPower = BEACON_POWER;

    ibeaconData.setFlags(0x1A);
    std::string strServiceData = "";
    strServiceData += (char)0x4c;  // apple 
    strServiceData += (char)0x00;  // apple 
    strServiceData += (char)0x02;  // apple 
    strServiceData += (char)0x15;  // apple
    strServiceData += UUID ;
    strServiceData += (char)(major >> 8);    // major
    strServiceData += (char)(major & 0xFF);  // major
    strServiceData += (char)(minor >> 8);    // minor
    strServiceData += (char)(minor & 0xFF);  // minor
    strServiceData += (char)txPower;  // 
    ibeaconData.setManufacturerData(strServiceData);

    // アドバタイズの設定
    advertising = BLEDevice::getAdvertising();
    advertising->setAdvertisementData(ibeaconData);
    advertising->setScanResponse(false);
}

// Arduinoのsetup関数
void setup() {
    // M5Stack の初期化
    M5.begin();
    Serial.begin(115200);

    BLEDevice::init("M5Stack");
    BLEServer *pServer = BLEDevice::createServer();
    init_beacon();

    // taskBeacon 内で start している
    // 実は start 1回だけではうまくいかないことが多く、
    // start/stop を繰り返す必要がある
    // advertising->start();

    // タスクの作成
    xTaskCreate(&task1, "task1", 2048, NULL, 5, NULL);
    xTaskCreate(&task2, "task2", 2048, NULL, 5, NULL);
    xTaskCreate(&taskBeacon, "taskBeacon", 2048, NULL, 5, NULL);
}

// Arduinoのloop関数
void loop() {
    // メインタスクのループ
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(10, 10);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(2);
    M5.Lcd.print("Hello, M5Stack with FreeRTOS! ");
    M5.Lcd.print(BEACON_UUID);
    Serial.println("Hello, M5Stack with FreeRTOS! ");
    vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
}

ここでは、task1, task2, taskBeacon という3つのタスクが同時に動いています。優先度が同じなので、並列で動いているのですが、優先度を変えれば iBeacon 発信のタスクだけ優先して動かすことも可能でしょう(実験までしていませんが)。

先の iBeacon の発信/停止の切り替えは loop 関数から taskBeacon 内に移動しています。

m5stackの画面はこんな感じです。

カテゴリー: 開発 | M5Stack で iBeacon を飛ばす(PlatformIO環境) はコメントを受け付けていません

M5Stack で RTOS を試すときに main.cpp で動作させること

M5Stackシリーズを使うと、内部で ESP32のチップが使わているのでWi-FiやBluetooth周りのテストをするのに非常に便利です。手元の BLE 関係の通信も、いったん M5Stack で試し実装をしてみてスマホで動作確認をしてから、専用ボードに書き込むと動作がわかりやすいですよね。

手元でTIのボードを使ってBLE通信をしているのですが、そこでRTOSを使うことになっています。なぜRTOSを使うことになったのかはさておき、最近では AWS から FreeRTOSが配布されているので、これの準じたサンプルコードがあちこちで配布されています。当然ながらTIでも配布されているので、これを参考に作ります。

で、各ボードでのRTOS対応のコードは結構な職人プログラマが整備しているので、うまいことRTOSの説明や動きを解説することができあません。せっかくRTOSという統一されたOSのAPIを使っているわけですが、それぞれのボードに対応した専用関数で置き換えられているので(これはこれで仕事上は便利)コーディングがしやすいのですが、まあ、内部でどう動いているのか不可解なところがあるので、できることならば生のRTOSのAPIを叩いて試しておきたい。

ちなみに、私の場合 μiTronで仕事をしたことがあるのでリアルタイムOSまわりの動きはわかるのですが、RTOSのほうは初見ですね。

VSCode で PlatformIO を使う。

以前はESP32の開発環境を整えるのが一苦労だったのですが、最近は VSCode に「PlatformIO」という拡張をいれるだけで十分です。コンパイラ諸々をインストールしてくれます。

VSCode-PlatformIO IDEを使って、ESP32の開発環境を構築およびLチカ https://zenn.dev/kotaproj/articles/esp32_vscode_pio

デバッガ機能等便利なものが多いのですが、私の場合は Arduino IDE の上位互換機能があれば十分なので、build と upload だけあれば十分なところです。

Arduino IDE の場合はコード補完機能(インテリセンス機能)が効かないのでコーディングに苦労することが多いのですが、VSCode だと補完が効くし、GitHub Copilot を入れた現状だとかなりの部分をコード補完してくるので便利ですね。

RTOS対応のプロジェクトを作る

RTOSを使ってプログラミングをするのに、何かライブラリを入れないといけないのでは?と思いますが、実は ESP32の「espidf」というフレームワークには既に、RTOSのAPIが含まれています。他のボードだと結構苦労しそうな感じなのですが、少なくともM5StackシリーズでRTOSをやるときは特に追加のライブラリは必要ありません。

手元にあるのが、初期のm5stackなのでBoardには「M5Stack Core ESP32」を選択します。

ここでは、Framework に「Espidf」を選択していますが、実は「Android」でも構いません。どういうライブラリの構造かわかりませんが、ここで指定する「Android」のほうにもRTOSのライブラリが入っています。

最初、m5stackでRTOSプログラミングをするときのサンプルコードが、「Espidf」が多かったので、これにしたのが以下の躓きの始まりです。結論から言えば、(おそらくライブラリの容量の関係)「Android」で問題がないと思われます。

platformio.ini を比較する

プロジェクトを作成したら platformio.ini を確認しておきます。ここでフレームワークの指定やライブラリの追加ができます。

Framework が 「Espidf」の場合

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = espidf

Framework が Android の場合

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino

platformio.ini の中身は framework のところ以外は変わりません。

ただし、このままでは M5.Lcd のような、モニタを使ったコードが書けないので、lib_deps を追加しておきます。これで M5.* 系のライブラリが自動でインストールされます。

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino

lib_deps = 
    m5stack/M5Stack@^0.3.9

ChatGPT に RTOS を使ったサンプルコードを書いて貰う。

実は GPT4o を使うと、ほどよく M5Satck を使って RTOS のサンプルコードを書いてくれます。

以下は、提案してもらったコードなのですが、実はビルドができません。

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_system.h>
#include <esp_log.h>
#include <M5Stack.h>

// ログタグの定義
static const char *TAG = "main";

// タスクの定義
void task1(void *pvParameters) {
    while (1) {
        ESP_LOGI(TAG, "Hello from Task 1");
        vTaskDelay(pdMS_TO_TICKS(1000));  // 1秒待機
    }
}

void task2(void *pvParameters) {
    while (1) {
        ESP_LOGI(TAG, "Hello from Task 2");
        vTaskDelay(pdMS_TO_TICKS(2000));  // 2秒待機
    }
}

// メイン関数
void app_main(void) {
    // M5Stack の初期化
    M5.begin();

    // タスクの作成
    xTaskCreate(&task1, "task1", 2048, NULL, 5, NULL);
    xTaskCreate(&task2, "task2", 2048, NULL, 5, NULL);

    // メインタスクのループ
    while (1) {
        M5.Lcd.fillScreen(BLACK);
        M5.Lcd.setCursor(10, 10);
        M5.Lcd.setTextColor(WHITE);
        M5.Lcd.setTextSize(2);
        M5.Lcd.print("Hello, M5Stack with FreeRTOS!");
        vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
    }
}

error “This library only supports boards with ESP32 processor.”

なエラーが M5Stack.h 内で出ています。

よくわからないので、framework が「Android」のほうに、main.c をコピーすると以下のようなエラーがでます。

“class” がないというエラーなのですが、実は main.c をそのままコピーしたので C言語で扱われているからです。なので、main.cpp にして C++ でコンパイルされるようにします。

実は SD.h がないとか諸々エラーになることもあるのですが、結論としては main.c のままなのが原因で、main.cpp にすれば ok です。

コンパイルは順調に進むのですが、最後のリンクで失敗します。

どうやら、

  • framework が espidf のときはC言語で、app_main がエントリー関数(俗にいうmain関数)
  • framework が android のときはC++で、setup と loop が呼び出される

という違いがあるようですね。TI のサンプルコードが C言語のほうの *.c に限られていたので、Android のほうが C++ のほうの *.cpp に統一されていたのを忘れていました、というオチです。

で、最終的には以下のコードで動くようになります。

#include <M5Stack.h>

// タスクの定義
void task1(void *pvParameters) {
    while (1) {
        Serial.println("Hello from Task 1");
        vTaskDelay(pdMS_TO_TICKS(1000));  // 1秒待機
    }
}

void task2(void *pvParameters) {
    while (1) {
        Serial.println("Hello from Task 2");
        vTaskDelay(pdMS_TO_TICKS(2000));  // 2秒待機
    }
}

// Arduinoのsetup関数
void setup() {
    // M5Stack の初期化
    M5.begin();
    Serial.begin(115200);

    // タスクの作成
    xTaskCreate(&task1, "task1", 2048, NULL, 5, NULL);
    xTaskCreate(&task2, "task2", 2048, NULL, 5, NULL);
}

// Arduinoのloop関数
void loop() {
    // メインタスクのループ
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(10, 10);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(2);
    M5.Lcd.print("Hello, M5Stack with FreeRTOS!");
    vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
}


先頭部分の「freertos/FreeRTOS.h」あたりは、M5Stack.h から自動的にインクルードされているらしく必要ありません。RTOS 特有の関数としては、タスク生成の xTaskCreate の部分で、これが呼び出せていれば RTOS でビルドできています。

This page describes the RTOS xTaskCreate() FreeRTOS API function which is part of the RTOS task control API. FreeRTOS is a professional grade, small footprint, open source RTOS for microcontrollers. https://www.freertos.org/a00125.html

アップロードして動作させる

アップロードでして動作させると m5stack のモニタに「Hello, M5Stack with FreeRTOS!」の表示がでて、シリアルポートには、以下のような2つのタスクが交互っぽい形で動いていることがわかります。

ここまでで半日費やしてしまったので力尽きて、何をやろうとしていたのか忘れてしまいました。まあ、いいんですが。

後日、BLE通信のコードをちょっと入れ込んで試しおくつもりです。

余談

ちなみに、これらのサンプルコードは ChatGPT の GPT4o を使って尋ねているのですが、最終的に main.c と main.cpp に気付いた部分はこれになります。

いやいやいや、そもそも ESP-IDF フレームワークと Android フレームワークを混在させてきたのは君なんですが!をぐっとこらえてw

まあ、それでも GPT4o を使って、何度も「このコードだと動かないのですが、どうしたらいいですか?」を繰り返している訳で。なかなか一発で解決にはなりませんね。

それでも最初のスタートダッシュは生成AIを使うと便利です。

カテゴリー: 開発 | M5Stack で RTOS を試すときに main.cpp で動作させること はコメントを受け付けていません

CrawdStrike 関係で Windows のカーネルドライバーの動きを少し解説

NHK の解説

【最新】システム障害 マイクロソフトは「サービスは回復」と発表 影響は一部で継続も収束にむかう | NHK | 航空 https://www3.nhk.or.jp/news/html/20240720/k10014517151000.html

Microsoft からの復旧手順

KB5042421: CrowdStrike issue impacting Windows endpoints causing an 0x50 or 0x7E error message on a blue screen – Microsoft Support https://support.microsoft.com/en-us/topic/kb5042421-crowdstrike-issue-impacting-windows-endpoints-causing-an-0x50-or-0x7e-error-message-on-a-blue-screen-b1c700e0-7317-4e95-aeee-5d67dd35b92f

Recovery options for Azure Virtual Machines (VM) affected by CrowdStrike Falcon agent – Microsoft Community Hub https://techcommunity.microsoft.com/t5/azure-compute-blog/recovery-options-for-azure-virtual-machines-vm-affected-by/ba-p/4196798

どのようにNULLポインタアクセスなのか?

画面キャプチャだけ抜き出し。

このツイートでは、00000000 0000009c ゆえに NULL ポインタアクセスという指摘になっているが、(確か)実際は Windows 起動時はリアルモードで実行されるので DS レジスタが有効になっていて DS:009c がアクセスされる。つまり 002b:009c から DWORD の 4バイトの読み込みでアクセスエラーとなっているので、NULL ポインタアクセスとは限らない(まあ、この DS が間違っている可能性もあるので)。ここは私も見逃したところのなので、特に言及はしない(私が間違っている可能性あるし)以下はひとつの考察である。

アセンブリコードを見ると、

mov r9d,dword [r8] ds:002b:00000000`0000009c=????????

r8 レジスタが示すメモリつまり [002b:009c] なんだけど、きちんとデータセグメントにあるっぽいのに、read access error を起こしているのはかなり変な状況になっている感じがする。

が!!!、再考すると、

  • カーネルドライバは「プロテクトモード」で動いている
  • セグメントレジスタの値(特にcs=0010, ss=0018, ds=002bなど)は、プロテクトモードのGDT内のエントリを指している可能性が高い。

とのことなので、これは「プロテクトモード」っぽい。

となると、先のツイートにあるように、なんらかの変数A(配列の先頭と思われる)に対して「オフセットで、0x9cの場所を参照しようとしている。つまり

DWORD x = A[ 0x9c ] ;

みたいなコードで、DWORD(4バイト)読み込みをしようとしているのだが、最初の変数 A が NULL ポインタなので(おそらく初期化漏れ)、アクセスエラーを発生している。大抵のメモリでは、先頭 0x1000 位が NULL ポインタチェック用に用意されていることが多く、それに引っ掛かったのだろう。

DWORD *A = 0x00 ;
// ここで変数 A の初期化漏れ
DWORD x = A[ 0x9c ] ;

この部分、変数 A の初期化漏れを Rust で回避できるかどうかなんだけど、Rust の場合は初期化していない変数を利用しようとするとコンパイルエラーになるので、これは回避できるかも。

「インサイド Windows」下巻の第12章より

ここの「ドライバー実行環境(DXE)」のときに、既にプロテクトモードになっているそうだ。なので、リアルモードのセグメント化でつかう DS レジスタは、既に GDT を示しているのだけなので、00000000 0000009c がフラットなアドレスと指定される。メモリを示すときはこんな小さなアドレスを示すことはないので、配列の先頭を示すアドレスの初期化漏れ(しかもわざわざ 0x00 が入っていた)と考えるのが正しい。

参考先

Windows カーネルドライバーって Visual Studio のプロジェクトテンプレートにあるらしく、結構便利になっている。

Write a Hello World Windows Driver (KMDF) – Windows drivers | Microsoft Learn https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/writing-a-very-small-kmdf–driver

追記 2024/07/25

Technical Details: Falcon Update for Windows Hosts | CrowdStrike https://www.crowdstrike.com/blog/falcon-update-for-windows-hosts-technical-details/

名前付きパイプの読み取りで失敗して、C-00000291-*.sys の読み込みあたりでエラーが発声しているので、Although Channel Files end with the SYS extension, they are not kernel drivers. カーネルドライバーではないよ、という理由らしい。C-00000291-*.sys ファイルを消すと正常動作するので、読み取り先のデータ領域っぽい(多分、ファイルをオープンして、メモリマップドしていると思う)ので間違いではないが、NULLポインタエラーの否定にはなっていないし、おそらく

  • C-00000291-*.sys ファイルが新規に追加される
  • C-00000291-*.sys ファイルをオープンする。
  • 名前付きパイプでアクセスする(大抵はメモリマップドである)
  • このときに NULL チェックを怠ったか、C-00000291-*.sys の先が NULL 書き込みされていたかでアクセスエラー発生

という手順だろう。

ほぼ100%再現性があるっぽいので(何万台というレベルで発生いるため)、これ最初に動作確認したのか?が危ぶまれるのだが CrawdStrike社としてはどうなのだろうか?

単体テストというか、リリース前の運用テストをスキップしてしまった感じがする。

カテゴリー: 開発 | CrawdStrike 関係で Windows のカーネルドライバーの動きを少し解説 はコメントを受け付けていません

詳細設計書を「効果的に」活用するためのパターン

発端はこのツイート

スナップショットはこちら(ロードするたびに割合がかわってしまうので)

前提条件

いろいろ議論が発散してしまうので、前提条件を確認しておきます。

「詳細設計」というのは、PMBOKの「Detail Design」にあたる、とします。

・要件定義
・概要設計、外部設計、基本設計、画面設計
・詳細設計、内部設計
・実装
・各種のテスト

PMBOKで云うと概要設計と基本設計が同じになります。最近は「基本設計」の方が言われることが多かった(多分、新聞などで使われることになったからだと思う)のですが、いわゆる顧客からの要件定義を受けて、システムを構築するための設計のことです。

実は、大手の会社でもここの分類が分かれるところで「自社だと違う」というパターンが多いでしょう。実際に画面設計(ユーザーインターフェース)やネットワーク設計などの仕様書が必要になることが多く、PMBOKが規定する分類にあてはまるとは限りません。

この分類は、漠然としたものではなく「誰が誰のために作るのか?」という視点で分けるとわかりやすいです。

「要件定義」は、顧客の要望を文書化したものです。顧客自身が書いてもいいし、請け負った会社が書いても構いません。

概要設計(基本設計や画面設計やネットワーク設計など)は、要望をどのように実現するのか?を記述していきます。これは、要件定義と対になるもので、いわゆるプロジェクト予算を作るときに利用します。ただし、実際のITプロジェクトの契約では、要件定義段階で契約することが多く、概要設計を作らずに(あるいは概要の概要みたいなものは作る)契約してしまうことも多いのですが、これは別の問題なのでここでは議論しません。

詳細設計は、概要に従ったものを具体的にクラスやフレームワーク、関数や、シーケンスなどを考慮して記述していきます。近年「詳細設計」を飛ばしてしまうことが多いのは、概要設計から実装(WEBの各種フレームワークやプロトタイプ、ノンコーディングできる実装環境など)が繋ぎやすいところにあります。いわゆる、スクラッチビルドが試せる環境が整っていたことがあります。逆に言えば、プロトタイプビルディングができない環境である場合は、詳細設計が必須です。

実装は、実際にコーディングをして動かす部分です。これに対しても以前はモジュール設計、クラス設計などを記述して分離させていた時期もあるのですが、最近では詳細設計に入れてしまう場合が多いです。これは会社のプロジェクトによって異なるところです。

UMLは、かつては概要設計と詳細設計の両方で使われる可能性があったのですが、現在はあまりUMLで書いてある仕様書を見かけません。これは有償のケースツールの衰退もあるのと、UML自体が細分化されてしまって、忌諱されてしまったためでしょう。

UMLを記述する場合は、最低限

・クラス図
・シーケンス図
・オブジェクト図(これはなくてもよい)
・状態遷移図
・ユースケース記述

だけ覚えればOKでしょう。これは時間と具象/抽象の4象限でわけたものです。

ただし、これ自体はここでは議論しません。

詳細設計は誰のためにつくるのか?

さて、詳細設計は誰のために作るのか?をパターン分けしておきましょう。先のアンケートを分類すると、

  1. 詳細設計書と実装が別の人
  2. 詳細設計書と実装が同じ人
  3. 詳細設計を書かず、実装のみ

のパターンに分けられています。それぞれ、意味があるので何がよいという訳ではありません。ITプロジェクトの状態に沿った形で、詳細設計を書くかあるいは省略するかすればよいのです。

詳細設計と実装が別の会社の場合

契約上、詳細設計と実装が異なる会社の場合があります。かつての中国オフショアなどがそれで、日本で詳細設計書を書いて中国のプログラマに発注していた時期があります。オフショアに限らず、子会社は外注に出すパターンはこの組み合わせが多いです。ただし、WEBサイト開発のような場合は、概要設計や画面設計から実装へ進む場合も多いので、これは情報システムや銀行系の場合に多いですね。

それぞれの会社が別なので「瑕疵」という契約が発生します。どのような場合に「瑕疵」となるのか、不具合をどのように直すのか?が問われるところなので、契約のために「詳細設計書」が必要になります。

別の会社であっても、派遣社員や準派遣の場合は、発注会社が責任を持つことが多いので、詳細設計書を省略しても契約上問題ありません。

詳細設計と実装が同じ会社(人)の場合

同じ会社であり同じ人であるならば詳細設計を作らずに、画面設計からいきなり実装に移ることも多いでしょう。保守運用的に詳細設計を残す或いは残さないの判断もあるのですが、実装する人の経験が浅かったり、ロジックが複雑怪奇であったりする場合には、詳細設計書を書くのがベターです。

詳細設計といっても、コードと逐一同じものを書くのではなく、UMLなどを使いながら簡略化していきます。

私の新人教育では、

・箇条書きで詳細設計を行う
・シーケンス図を記述する

ことにしています。シーケンス図はプログラムコードが入り組んでいた時、タイミングが微妙なときを考察するのに便利です。基本的に詳細設計は使い捨てです。そのままコードのドキュメントとして残すか(文芸的プログラミングのように)、単なるメモ書きとして記録しておくだけでよいでしょう。

コードレベルの詳細設計は書かない

先のアンケートの結果を見ればわかる通り、私は「コードレベルの詳細設計」は書きません。しかし、場合によってはシーケンス図のような詳細設計を書きます。

このアンケートで答えた人がどの位の「詳細設計書」を考えているかわかりませんが、分布としては形式的な詳細設計書はなくなっていく傾向にあります。

ここで注意しておきたいのは、

・アジャイル開発だからといって、詳細設計が存在しない訳ではない
・ウォーターフォール開発だからといって、詳細設計が必須な理由はない

ということです。ITプロジェクトのスタイル(アジャイル開発、計画駆動、イテレーション開発などなど)に問われるものではありません。アジャイル開発においても「設計書」は情報共有のために必要となるので、そこに囚われる必要はないでしょう。

設計書からコードを出力するツールはあるのか?

あると言えばあるんですよ。

記事を見ると解る通り、みずほ銀行の件は失敗しています。失敗していますが、詳細設計書からコードを出力するツールはあるんです。あるんですが、失敗しています、ということを覚えておくことが重要です。

個人的には MDA の復活になると思うのですが、それがいつ頃になるかわかりません。Scratch とか Node-RED のツールとかがそれに近いです。各種のノンコードツールもそれに近いものがあります。ノンコードツールの延長線上にあるとは思えないのですが、一種ではあると思われます。

参考先

図解即戦力 PMBOK第6版の知識と手法がこれ1冊でしっかりわかる教科書

図解即戦力 アジャイル開発の基礎知識と導入方法がこれ1冊でしっかりわかる教科書 Kindle版

図解入門 よくわかる最新 システム開発者のための仕様書の基本と仕組み

カテゴリー: 開発 | 詳細設計書を「効果的に」活用するためのパターン はコメントを受け付けていません

MS-MVP の再受賞(14回目)と学習効果の話

今年も無事、Microsoft MVP を受賞することができました。ぱちぱちぱち。

ひとまず、Microsoft系の書籍を書いている限りは、申請を出すつもりなので、引き続きよろしくお願い致します。

「Azure OpenAI Service 入門」のほうも合わせて。

さて学習効果の話

ツイートを失念してしまったのですが、学習効果が複利的に逓増するか?というのが再び流行りました。もともとの図自体はかなり前に流行ったものなので、目新しいことではないのですが、賛同だけではなく、疑問符な方たちが結構いたようなので、学習効果についてちょっと記述しておきます。

誰が云ったか忘れてしまったのですが、「1.01の法則と0.99の法則」というものです。

1.01を365日続けると37倍になり、0.99で努力が減ると0.03になるという話で、最初でてきた当時も「1年間続けたとしても、さすがに37倍にはならんやろ」という結論がでています。まあ、1.01の努力ってのもたとえ話なので、実際に37倍になるかどうかは不明です。

さて、これを数式で表すと、次のように複利的にΠで計算するか、加算ということでΣで表すかという違いになります。

果たして、微々たる努力(1.00のところを1%だけ上げるので、勤務時間8時間であれば、8x60x0.01 = 4.8分/日ということになる)で、じゃあ、1年後に37倍になるか?という話になり、まあそうなんらんやろという結論で、Bの加算程度じゃないだろうか?というツイートが今回散見しました。努力が加算されるという意味ですね。

なにかの作業的なものであれば、たかだか1日のうち5分間というのは大した効果ではありません。単純に自給換算にで言えば、8時間勤務が8時間5分勤務になったというだけなのです。

が、Aのような複利効果を考えるとき、実はベースとなる実力そのものが増大するため、その効果はその時々において確かに複利的になる(常に元金が増えるような状態なので)ものです。たとえ話のように、356日連続で向上するとなるとなにやらうさん臭くなってしまいますが、これが「1か月単位で、ある程度実力が向上する」となると少し話が違ってきます。

たとえば、プログラムのコードを書く作業を生産性とみなし(詳細設計や単体試験まどを含めた、バグのないコードをどのくらいのペースで書けるか?ということを考えてみましょう)たときに、1か月のうちで、1.00の時間ところを0.99の時間で書けるようになったとしましょう。たかだか1か月のうちで1%しかスピードアップはしませんが、これが12か月になると、0.99^12 = 0.886 となり、おおよそ 90%程度になります。つまり、1割位の短縮ができるわけです。

プログラムを書くスピードはそのプロログラム言語の習得率や設計のうまさ、テスト環境の整え方など各人の経験で向上できるところの多い分野です。この「経験」は、Σのように加算ではなく、Πのように累積されることは、いわゆるプログラマ力の個人差が大きいところを見ると明らかでしょう。スタート時点での経験の差(あるいは習得率の差)は、その差のまま続くのではなく、1年間のうちに「経験」として向上するものです。さらに、1か月ごとの累積で考えるならば、1か月単位のスタートで「経験」に対して累積していくと言えるでしょう。スタートとなる土台は、次のスタートの時には土台が上がっていると考えられます。

このあたりは町工場などの職人も同じですね。学習前の土台を常々上げることによって、次のステップが楽になります(段差が小さくなる状態にしておきます)。

ここでは仮に1か月につき1%の向上にしましたが、5%と仮定したときは、当初1.0だった見積もりが0.54程度に住むことになります(0.95^12 = 0.54)。つまり1年たてば、最初の見積もり期間よりも半分の工数でコードができあがる、という予定になります。

実際、プログラミング言語の習得だけではこの向上は見込めませんが、

  • フレームワークの使い方の習得
  • サンプルコードやテストコードを作ることで、落とし穴を回避する
  • あるいは、あらかじめ落とし穴に落ちておく(踏み抜いておく)
  • テストコードや不具合対処の勘がさえてくる(経験上)
  • プログラミング言語自体の習熟度が上がっている

などの要因もあり、単純な加算ではなく複利的に実力が向上することが見込まれます。

実は、似たところはプロジェクトのスケジューリングにも言えるのです。プロジェクトの当初では未知の部分も多く、プロジェクトで利用するライブラリや環境にも不慣れな状態からスタートします。しかし、プロジェクトが終盤になれば、プロジェクトのメンバーはライブラリや環境にも習熟して、数々の不具合を素早く対処できるようになってきています。

終盤で下手に炎上しているプロジェクトではなければ(実は炎上しているプロジェクトであったとしても、プロジェクトメンバー自身は成長しているのですが)、プロジェクトの最初の力量よりもプロジェクト終了時の力量のほうが上であると考えられます。

つまり、この習熟度の高い状態を疑似的に作り出せば、プロジェクトはより安全に安定期(予測可能という意味で)に入ることができます。

  • プロジェクトで使うフレームワークを事前にサンプル等を作って試しておく
  • 運用直前でトラブルになりそうなリスク要件をあらかじめ確認しておく
  • もともと、習熟度の高いメンバーを入れる(可能であれば)
  • プロジェクト途中で「学習」を重視する

という様々な方法が考えられます。

そんな訳で、私的には学習による複利的効果(基礎力をアップするという意味で)を期待して、基礎力のアップに努めたほうがプロジェクトも安定しやすいだろう、ということです。

加筆

これは正しいwww

カテゴリー: 開発 | MS-MVP の再受賞(14回目)と学習効果の話 はコメントを受け付けていません

2024年 都知事選を予測&検証する

都知事選は結果を見る通り、小池氏が全得票の半数弱を取る結果となった。

いろいろな後付けの感想がツイッター(X)に流れている訳だが、実はひとつの実験として、行動経済学の視点から実地の数値から都知事選を予測することをやっていた。

結果の資料

東京都知事選挙2024 立候補者紹介・選挙速報(7月7日投票) https://www3.nhk.or.jp/senkyo2/shutoken/20336/skh54664.html

あわせて開票所別も

東京都議会議員補欠選挙2024 立候補者紹介・選挙速報(7月7日投票) https://www3.nhk.or.jp/senkyo2/shutoken/20337/

東京都知事選挙 NHK出口調査結果|NHK 首都圏のニュース https://www3.nhk.or.jp/shutoken-news/20240707/1000106259.html

支持層割合、とくに無党派層の割合を調べる

都知事選 【結果】2024 現職の小池百合子氏が3回目の当選 石丸伸二氏 蓮舫氏らを抑える | NHK | 選挙 https://www3.nhk.or.jp/news/html/20240707/k10014502181000.html

年代別用

私の予想

投票日の1週間前位に投票数を予想している。

私としては、

  • 組織長(首長)の三選は禁止したい派なので、非小池知事を希望
  • 板橋区というのもあり地元当選の蓮舫氏を支持

ということで、蓮舫陣営寄りの観察になりがちなので、これを補正するためにきちんと事前のデータだけを利用することにした。感情的にはツイッター(X)での様相に左右されがちなのだけど、これは安野氏や暇空氏支持でも同じことだろう。どうしてもエコーチェンバーになりがちになってします。このために、行動経済学における「見たものからしかデータを得られない」というバイアスがかかってしまう。また、どうしても期待を込めてしまうために、ちょっとでも希望が持てそうな事実を重く取り上げてしまう。いわゆる、損失バイアスをかけた状態になってしまうのだ。

投票率は 55% 程度、全体で600万票と予想する。各党の支持率は、東京都の比例代表からとってきている(ここは、前回の出口調査から類するするべきであった。無支持層が少なすぎる)。

上段のものが、比例代表などから想定したもの。各党のまとめ方は、組織票(公明と共産)が集まりやすいものと、分散しやすい層(自民、立憲)とわかれている。実は、都内なので都民ファーストの率が高いのであるが、国政に都民ファーストが入っていないので票が読めなかった。また、2020年の選挙でのれいわの票の行先が不明であった。

得票予想としてはとしては、

  • 小池氏 250万票 41% 程度
  • 蓮舫氏 125万票 20%程度

となっている。

実際のところは、

  • 小池氏 290万票 43%程度
  • 蓮舫氏 128万票 19%程度

となるので、ほぼこれで合ってる。石丸氏が得票数を伸ばした(1/4の支持を得ている)ことは予想できなかったし、田母神氏がこれほど低い得票率であったこと、泡沫候補がトータルでも5%未満であることは予想し得なかったので、ちょっと偶然の具合も強いのだが、最初の予想はあっている。

予測時点で、蓮舫氏を当選させるシミュレーションとしては、

  • 自民票が、半分以下にとどまること
  • 立憲、共産が票を強くまとめること
  • 蓮舫氏が無党派層の割合をもう少しあげること

であったが、それでも160万票ぐらいにしか届かない。このためボーダーラインとしては、150万票から200万票ぐらいと考えていたわけだが、黄色のセルをどう工夫しても蓮舫氏の勝ち目は薄かったところである。

それでも期待バイアス(損失バイアスの反対)を掛けて、蓮舫氏の当選を工夫しようとするのだが…結果的には、当初の予測値に近い形になっている。

無慈悲な統計のほうがあたり率が高いというのは、やっぱり行動経済学ですね、という具合になってしまう。

支持政党別を補正して予測し直す

先の予測では、比例代表から支持政党の割合を決めたのだが、無支持層の割合が低すぎる(9.4%)になっている。今回の出口調査を見ると 48% である。

これを使って予測し直してみよう。

得票率は61%となるので、前回より得票率が高い。有効投票数は690万票と100万票ぐらい増えている。

これをベースにして計算し直すと、

  • 小池氏 290万票 42.1%
  • 蓮舫氏 106万票 15.4%

ということで、これは概ね合っている。維新、れいわは資料がないので按分し、都民ファーストはほとんどを小池氏にいれる計算になる。

実際の割合はこれになるが、統計上2桁位で計算すればよいので、1%,2%である、れいわは誤差として無視してよいだろう(前回の山本太郎候補の66万票 10% 程度、が、れいわ新選組以外からの票が多かったことを示している)。

行動経済学の損失/期待バイアスはシステム1を無視すれば、バイアスを避けられる

この実験は、予測値をきちんとした実測値(実測値自体の精度もあるのだが)を使えば、心理学的にシステム1(損失バイアス)を回避して、うまくシステム2の予測ができるだろうか?という実証実験である。

結論から言えば、実測値を忠実に扱えば、システム1に引っ張られることを避けることができる。さらに言えば、実測値をシステム2の思考で忠実に扱えば、かなり近い形で未来の予測が可能であろう、ということになる。まあ、N=1の実験結果でしかないので、都知事選については過去のデータに遡って予測を繰り返してみるとよいだろう。

応用としては、ITプロジェクトの進捗予測や不具合予測に利用可能である。

プロジェクトマネジメントにおける直感はシステム1に引っ張られる可能性もあるが、同時に専門職による経験と勘は内部的にはシステム2の思考を利用している可能性も高く重要である。傍目からみると(自分でさえ)その思考手順がシステム1なのかシステム2なのかを知ることはできない。このため、バイアスが存在するかどうかが判断し辛いのだが、直感と実測からの予測を見比べることで、ずれが生じていれば、それは「システム1に引っ張られているかもしれない」という懸念を持つことが重要だろう。こうすると、経験と勘を捨てずに、精度の高い予測を行うことが可能と考えられる。

ただし、この予測値においては石丸氏の170万票(24%)は予測できていない。無党派層の2割(日経新聞より)からだけでは、24%という高い得票率は想定し得ない。私の予想では、石丸氏、田母神氏がギリギリ供託金の60万票をまぬがれるかどうか、と思っていたのだが、実際のところでは田母神氏は保守票をまとめきれず 27万票しか得られていない。

個人的には、安野氏や暇空氏に注目するところ(賛同ではないけれど)ではあったのだが、2%前後という低い割合になったのは意外なところだった。2%というと、学校の学年で2,3人ぐらいの支持なわけで、クラスで徒党を組むことも難しい。その位のマイノリティということになる。マイノリティにはマイノリティによる戦い方があるので、ここではゲリラ戦ですよね :)

参考資料

都知事選2024予想(Excel)

https://1drv.ms/x/s!AmXmBbuizQkXg4Ag0jTmpCT4KbShow?e=4TavxZ

カテゴリー: 開発 | 2024年 都知事選を予測&検証する はコメントを受け付けていません

組み込みシステムとWEBアプリのグローバル変数の扱いについて

元ネタは、以下からなんですが、もともと新人が C++ で書いたプロトコルがばしばしグローバル変数を叩いていてなんともならん!が発端らしいので、ちょっと筋が違うかもしれないけど、ネタ的に興味深いところがあるので、ちょっと書き連ねておきます。

結論から先に言えば、組み込みシステムの割り込みで使うグローバル変数と、WEBアプリ(ブラウザで作る方)の割り込みはかなり違うので比較ができません。加えて、サーバーサイドのWEBアプリというかWEBシステムも「割り込み」や「非同期」の扱いが、組み込みの非同期とはかなり違います。

組み込みの非同期

先のツイートについては、組み込みの非同期については釈迦に説法のような気もするのですが、このブログの対象としてはあまり知らない分野だと思うので、いちおう。

組み込みにしてもOSあり/なしがあるわけですが、OSを使うにせよ使わないにせよ、「割り込み処理」(インターラプト)内の処理は気を付ける必要があります。いわゆる、ハードウェアの割り込み処理(I/Oポートからのインターラプト)とOSのシグナル、ソフトウェアからの割り込み処理に分けられるわけで、この割り込み処理の中では、

  • スタックが異なる
  • ランタイム(C言語ランタイムなど)の初期化有無がある
  • スレッド(メモリ空間)が異なる場合がある
  • CPUレベルでスイッチングしている

という面がある。

組み込みのC言語プログラミングの場合、C言語ランタイムが初期化されていない場合が多いので通常の関数(printfとかputsとか)が使えないときがある。スタックの切り替えやメモリ空間の切り替えが起こっている可能性もある(ハードウェアのインターラプトの場合はそう)のだけど、これは組み込み用のCPUの場合は、仮想メモリを使わない場合が多いので、プロセスごとに気を付ける必要があるという面もある。

そんなわけで、IntelのようにCPUレベルで仮想化されているわけではないので、プロセス(iTronプロセスみたいに)ごとに相手のプロセス空間を汚さないように注意する必要がある。と同時に、グローバル変数にデータを置くと、自動的に共有メモリとして扱えるので、インターラプト関係では結構便利なので、ついつい使ってしまう、という弱点がある。

先のツイートの元ネタのプロトコル通信でグローバル変数を使っていてあかん、というのはそれで、通信自体はハードウェア的に非同期で走っていてかつ、受信側も非同期で動いているとグローバル変数のメモリ空間は取り合いになってしまうという面がある。そのあたりは、iTron型のリアルタイムOSなのか、OSなしなのかが不明なのでなんとも言えないけど、

  • 全体のメモリが少ないので、プロセス間通信はグローバル変数で受け渡ししてしまうことが多い。
  • ただし、ハード割り込みの関係で、ポーリング形式になりがちな解析部分での非同期化が必要になり、単純な1個のグローバル変数では難しく、リングバッファにするかー、とか考えないといけない。場合によるが。

そんなわけで、組み込みプログラミングに置いて、グローバル変数と割り込みは絶妙な感じにしないといけない。あと、メモリ周りの制限がきついので C++ で大量に new/delete するのは困る。別途 allocater を作るわけだが…という話が待っている。

WEBアプリ(クライアントサイド)の非同期

React.js や Vue.js のように一見、async/await や Promise クラスを使って非同期処理をしているように見えるが、ベースは JavaScript なのでシングルスレッドで動いている。このあたりは見かけ上、非同期処理(特に GUI部分と)をしているように見える、あるいはプログラミングできるというだけになる。

特にシングルページアプリケーション(SPA)の場合は、非同期処理とはいえWeb APIを呼び出している間「待たない」という処理だけなので、組み込みプログラミングでいうところのハードウェア割り込みというものが存在しない。ボタン操作やキーボード操作などは、イベント処理として扱われているので、低レベルな話で言えば OS のポーリング処理に属している。

なので、非同期処理内(ラムダ式とか)であっても、他のクラスを使うこともできるし、グローバル変数(のようなもの)に対しての処理もプログラム言語での仕様でしかなく、ハードウェア上の制限ではない。

特にブラウザアプリケーションの場合は、グローバル変数というものは存在しない。コード内に var 変数として書くこともできるが、これはソースコードを跨げない。ちょうど、C言語のファイル内のスコープと同じになる。唯一、ブラウザ自身を示す window オブジェクトというのがあって、これを媒介してグローバル変数として扱うこともできる。乱暴だが window.a とすれば、a というグローバル変数が作れて別のソースコードから参照可能になる。

まあ、何が言いたいかというと、react.js や vue.js に flux, vuex というストアがあってですね。実質グローバル変数化されるわけですが、これ、ルールを決めて window.* にアクセスすればそれで充分では?と思うのです。実際 vuex が廃れてしまって mvvm パターンの pinia 標準化されつつあるわけで、「グローバル変数」を嫌うあまりに、プロセス/アプリケーションが共通でもつべきメモリ空間、の置き所に混乱が生じてしまっているような気がするのです。まあ、結局のところグローバル変数に落ち着きそうですが。

で、WEBアプリの場合は非同期でラムダ式を使っていたとしても、実質的にはシングルスレッドで動いているのでグローバル変数(windows.* へのアクセス)は競合しません。将来的にネイティブな typescript が wasm 上で実装さえたりすると競合するようになるかもしれませんが、いまのところ大丈夫です。

余談ですが、react, vue ともにビルドをするとひとつの javascript に圧縮されるので、中身で書いた var 変数はグローバル変数化します。ですが、ビルドする前に別のソースコードの変数を参照する手段がない(export/importすれば別だけど)ので、実質グローバル変数がないのです。これは、そういう文法を作ればクリアできる問題かもしれません。

そんなわけで、組み込みプログラミングとWEBアプリでの「グローバル変数」の立ち位置が異なるのと、それにかかわる「非同期処理」のレベルが異なるので、一概にどっちがどうという訳ではありませんね。という話です。

カテゴリー: 開発 | 組み込みシステムとWEBアプリのグローバル変数の扱いについて はコメントを受け付けていません

Microsoft Store で配布されているアプリがネイティブアプリかストアアプリ(UWP)かを見分ける方法

ストア形式といえば、忘れ去れてている感がある Microsoft Store ですが、現状ではサン土木すで制限された UWP アプリの他にネイティブ Windows アプリを登録できます。

UWP アプリは、「UWP」は「ユニバーサル Windows プラットフォーム」の略で、iPhone の App Store や Android の Google Play で配布されているアプリのように、安全にダウンロード&インストールしたのち、安全にプログラムが実行される(ユーザーがアプリの権限を不許可にしたりして制限ができるなど)アプリのことです。Windows 8 からの Microsoft 肝いり企画だったわけですが、Mobile Windows の廃止により、その価値は激減しています。

実行ファイルを配布するときに、どれを選ぶのかがややこしいのですが、

  • ストアでUWPアプリ
    一番制限がきついパターンだが、一番安全に配布できる
  • ストアでWindows アプリ
    一応セキュリティ審査/署名があるが、実行はネイティブなので、UWP アプリより少し制限が緩い
  • インストーラーを作って配布
    従来の一般的な配布。ただし、ストアよりも目に惹かれないので、できればストア配布を推奨…とされる
  • EXEファイルを直接配布
    社内での配布の場合はこれで十分なことが多い。 

久しく制限のきつい開発環境≒大手IT会社のハコヅメ環境をやっていないので、今の実情はわからないのですが、社内でツールの制限をかけるときに「何処からダウンロードするのか?」「どのツールを使うのか?」が求めらえるところでしょう。

例えば Line Desktop は Windows アプリ

Microsoft Store で「Line」を検索すると「LINE Desktop」がでてきます。このアプリは従来ならば UWP アプリで作るところですが、このアプリはネイティブの Windows アプリです。

判別が難しいのが難点なのですが、追加情報のところの「このアプリでは次のことができます」の中で「すべてのシステムリソースを使用する」となっていると、ネイティブな Windows アプリです。

一見すると、このアプリが Windows の秘密機能をあれこれやってキーロガーとか個人情報とかを抜きとるように見えますが(実際、できるんだけど)、実はそうではありません。ネイティブの Windows アプリなので「特に制限されてない」だけです。制限はされていませんが、一般的な Windows アプリと同様に、Administor 権限とか System 権限が必要なものは、それが使えるユーザー(管理ユーザー)でしか動きません。

一方で電卓アプリは UWP アプリ

一方で電卓アプリは、UWP アプリとして登録されています。

同じように機能の制限を見ると、「インターネット接続にアクセスする」ことだけが許可されています。

この文言は非常に解り辛いのですが、

  • 「すべてのシステムリソースを使用する」の表示があれば、Windows アプリ
  • それ以外の制限が書いてあれば、UWP アプリ

という違いがあります。前述した通り、UWP アプリとして作られている電卓アプリのほうが安全です。

プロセスを比較する

タスクマネージャで両者のプロセスを見ていきましょう。

LINE Desktop の場合は、各種のプロエスが動いています。

プロセスのプロパティを見ると、「C:\Users\masuda\AppData\Local\LINE\bin\current」のような場所で動いています。

エクスプローラーでフォルダーを見ると、Qt フレームワークを使って書かれていることがわかります。他にも DLL があって、一般的な Windows アプリと同じです。

同じように「電卓」をみてみましょう。

プロパティを見てみると、「C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_11.2405.2.0_x64__8wekyb3d8bbwe」のように、C:\Program Files\WindowsApps フォルダーの配下にアプリケーションが配置されています。

この WindowsApps フォルダーがサンドボックスの役割をしています。このフォルダーは閲覧が制限されていて Powershell などを管理者モードにしないと見ることができません。

ls コマンドなどで、インストールされているアプリの正式名を知ることができます。このそれぞれのフォルダーに UWP アプリの実体が含まれています。

ためしに Temas のフォルダーを見ていきましょう。

この中で使われている DLL や JS コードなどを見ることで、ああ JavaScript で作られているな、とか判断したりするわけです。

カテゴリー: 開発 | Microsoft Store で配布されているアプリがネイティブアプリかストアアプリ(UWP)かを見分ける方法 はコメントを受け付けていません

Semantic Kernel を Windows フォームアプリで使う

「Azure OpenAI Service 入門」のサンプルコードを Semantic Kernel に書き換えるシリーズの第3弾は、Windows フォームを使ってみる、の巻です。

こんな感じで、必要項目を埋めるといい感じのブログ記事を書いてくれるツールを作ります。いわゆるブログ記事の量産ツールではあるのですが、自社製品である架空の「製品ABC」の紹介をブログ記事の中に織り込みます。書籍では第5章にあります。

第8章で、いろいろなツール(スマホ、ブラウザ、JSなど)から使える解説をするのですが、最初に WinForms を持ってきたのは諸々の技術を使うよりも初心者には手っ取り早いからです。私的には WPF で書いたほうが手っ取り早いのですが、まあ、一定の読者層を見込んでというところです。

SemanticKernel パッケージを使う

	<ItemGroup>
		<PackageReference Include="Microsoft.SemanticKernel" Version="1.14.1" />
	</ItemGroup>

画面をデザインする

デザイナで作って、コントロールに適当に名前を付けておきます。

実装するのは「生成ボタン」だけです。

生成ボタンをクリックしたときの処理

/// <summary>
/// 生成ボタンをクリックしたときの処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button1_Click(object sender, EventArgs e)
{

    string blogTheme = textBox1.Text;   // テーマ
    string blogTerms = textBox2.Text;   // 専門用語
    DateTime blogDate = dateTimePicker1.Value; // 作成日
    string authorName = textBox3.Text;  // 作成者
    string attention = textBox4.Text;   // 注意事項
    string additionalInfo = textBox5.Text; // 追加情報

    string prompt = $"""
        テーマ:{blogTheme}
        専門用語:{blogTerms}
        作成日:{blogDate.ToString("yyyy年MM月dd日")}
        作成者:{authorName}
        注意事項:{attention}

        このテーマと専門用語を用いて、詳細かつ情報豊富なブログ記事を作成してください。

        追加情報:
        #追加情報のはじまり
        {additionalInfo}
        #追加情報のおわり

        出力フォーマット:
        #出力フォーマットはじまり

        ■タイトル
        [タイトルをここに記入]

        ■ブログ記事
        [記事の詳細な内容をここに記入]

        作成日:[作成日をここに記入] 
        作成者:[作成者をここに記入]
        ※注意事項 
        [注意事項をここに記入]

        #出力フォーマットおわり

        ブログ記事:
        """;

    var builder = Kernel.CreateBuilder();
    builder.AddAzureOpenAIChatCompletion("test-x", endpoint, apiKey);
    var kernel = builder.Build();

    var result = await kernel.InvokePromptAsync(prompt);
    string generatedText = result.GetValue<string>() ?? "";
    string blogPost = generatedText.Trim().Replace("\n","\r\n");
    textBox6.Text = blogPost;
}

それぞれのテキストボックスから値を取ってきて、プロンプトを作成します。semantic kernel にはプロンプトテンプレートという機能があるのですが、C#の場合はそのまま変数の埋め込みができるので、先にプロンプト自体を作ってしまったほうがわかりやすいでしょう。

プロンプト自体は、半年前に少し工夫したものですが、ポイントとしては

  • 箇条書きにする
  • 見出しに「■」や「#」のマークを付ける
  • AI に入れて欲しいときは「[作成日をここに記入]」のように括弧で括ると適用しやすい

ことです。

書籍にも書きましたが、下手にプロンプトエンジニアリングの本を読むよりも、最近では「○○を出力するためのプロンプトを作ってください」と AI 自身に頼んだほうが、AI が解釈しやすいプロンプトを吐き出してくれます。

区切りとなるコロン「:」も、全角と半角が混在しても大丈夫です。何度か試してみて、補助的な指示を工夫してみてください。

このプログラムを実行すると、次のようなブログ記事が得られます。

カテゴリー: 開発 | Semantic Kernel を Windows フォームアプリで使う はコメントを受け付けていません