Bye Bye Moore

PoCソルジャーな零細事業主が作業メモを残すブログ

M5StackでCPUクロックを変更する前にシリアルのボーレートを弄ると表示がバグるので注意

M5StackのコアであるESP32系にはCPUクロックを落として省電力化する機能があります。
ただし、これの呼び出しタイミングを誤ると、シリアル通信などクロック依存のタスクに影響がでます。
そのため、CPUクロック変更命令はM5.begin()の前に呼び出しましょう。

実際のところ

状況

試作中の環境ノードの省電力化を試み、CPU速度を落としてみました。

void setup() {
  // M5Stack初期化
  M5.begin();
  M5.Power.begin();
  //...

  // CPU周波数削減(省電力化のため)
  setCpuFrequencyMhz(40);

  // シリアル通信初期化
  Serial.begin(115200);
  while (!Serial) {
    delay(100);
  }
  
  //...
}

目論見通り消費電力は落ち、計測ロジックも遅くならなかったので特に不満は無かったのですが……
ちょっと挙動を見ようとシリアルを繋いだところ

rst:0x1 (POWERON_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0030,len:1344
load:0x40078000,len:13964
load:0x40080400,len:3600
entry 0x400805f0
M5Stack initializing...
[ 377][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x00
[ 383][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 692][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x00
OK
�f␘����`f~␆��...

などと、それまで動いていたデバッグログが滅茶苦茶なことに

対策

M5stack BasicのコアであるESP32シリーズのシリアル通信は、(考えてみりゃ当然なのですが)CPUクロックを基準に生成されています。
beginした後にクロックを弄ると、ボーレートが狂うので今回のような事態になるという事。
という訳で、setCpuFrequencyMhzメソッドはM5.begin()の前に置かないとダメです。

void setup() {
  // CPU周波数削減(省電力化のため)
  // 重要: M5.begin()の前に設定しないとシリアル通信が文字化けする
  setCpuFrequencyMhz(40);

  // M5Stack初期化
  M5.begin();
  M5.Power.begin();

  // シリアル通信初期化
  Serial.begin(115200);
  while (!Serial) {
    delay(100);
  }
  
  //...
}

補足情報

M5stackは仕様上、先にCPUクロックを弄ってしまうとアウトのようですが、
素のESP32系ボードは必ずしもそういう仕様ではないようで

// ESP32の普通のボードなら以下のような書き方もOKなケースがある
uint32_t Freq = 0;

void setup() 
{
  pinMode(GPIO_pin, OUTPUT); 
  Serial.begin(115200);
  setCpuFrequencyMhz(10);
  Freq = getCpuFrequencyMhz();
  Serial.print("CPU Freq = ");
  Serial.print(Freq);
  Serial.println(" MHz");
  // ....

ESP32 Change CPU Speed (Clock Frequency) – DeepBlue

参考もと

github.com

NeoHex等のGroveに繋げるFastLED対応の光物をI2Cに繋いでもちゃんと動かないよ(戒め)

M5stack BasicのGroveポートに一個つないでた時には動いたNeoHex(WS2812B)が、
その列にエンコーダーユニット (M5Stack-U135)をつなぐと0番と1番のLEDだけ最大発光の緑や黄色になる奇妙な現象が発生。
まぁ何のことはなく、NeoHexはI2Cじゃないので無理にほかのと繋ぐと妙な挙動をするというだけの話でした。
NeoHexはGPIO26に、ほかのI2C系はGPIO21につなぐ事で無事に動作しました。

実際のところ



動いたスクリプト

/*
 * M5Stack Basic v2.7 NeoHex Encoder Controller
 * 
 * 【接続方法】
 * - NeoHex: GPIO26 (3線通信 WS2812B)
 * - エンコーダーユニット (M5Stack-U135): I2C (GPIO21/22)
 * 
 * 【注意事項】
 * - I2Cと3線通信は異なる通信方式のため、同じピンは使用不可
 * - NeoHexは37個のWS2812B LEDを使用
 * - エンコーダーで明度を0-254の範囲で調整可能
 */

#include <Arduino.h>
#include <M5Stack.h>
#include <FastLED.h>
#include <Wire.h>

// 色定数の定義(M5Stack用)
#ifndef BLACK
#define BLACK 0x0000
#endif
#ifndef WHITE
#define WHITE 0xFFF
#endif

// NeoHexの設定
#define LED_PIN 26        // データピン(GPIO26)
#define NUM_LEDS 37       // NeoHexのLED数(37個)
#define LED_TYPE WS2812B  // LEDタイプ
#define COLOR_ORDER GRB    // カラーオーダー

// エンコーダーユニット(M5Stack-U135)の設定
#define ENCODER_ADDR 0x40  // エンコーダーユニットのI2Cアドレス
#define ENCODER_REG 0x10   // エンコーダー値のレジスタアドレス(0x10から2バイト)

// デバッグ用の設定
#define DEBUG_MODE true   // デバッグモードを有効にする

// LED配列の定義
CRGB leds[NUM_LEDS];

// グローバル変数
int16_t lastEncoderValue = 0;
uint8_t currentBrightness = 0;

// 関数宣言
int16_t readEncoderValue();
void setNeoHexBrightness(uint8_t brightness);
void updateDisplay(int16_t encoderValue, uint8_t brightness);
void testInitialLEDs();
void scanI2CDevices();
void initializeEncoder();

void setup() {
  // M5Stack Basic v2.7の初期化
  M5.begin();
  
  // I2C通信の初期化
  Wire.begin();
  
  // シリアル通信の初期化
  Serial.begin(115200);
  delay(1000);
  Serial.println("M5Stack Basic v2.7 NeoHex Encoder Controller Started");
  
  // 画面の初期化
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(10, 10);
  M5.Lcd.println("NeoHex Encoder");
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(10, 40);
  M5.Lcd.println("Rotate encoder to adjust brightness");
  
  // デバッグ情報を表示
  Serial.printf("LED Pin: %d\n", LED_PIN);
  Serial.printf("Number of LEDs: %d\n", NUM_LEDS);
  Serial.printf("LED Type: WS2812B\n");
  Serial.printf("Color Order: GRB\n");
  Serial.printf("Encoder I2C Address: 0x%02X\n", ENCODER_ADDR);
  
  // FastLEDの初期化(段階的に実行)
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.setBrightness(0);  // 最初は明度0に設定
  
  // 電源制限設定(M5Stack Basic対応)
  //FastLED.setMaxPowerInVoltsAndMilliamps(5, 1000);  // 5V, 1000mA制限
  
  // 全LEDを確実にクリア(複数回実行)
  for (int clearCount = 0; clearCount < 3; clearCount++) {
    FastLED.clear(true);  // バッファをゼロで埋める
    for (int i = 0; i < NUM_LEDS; i++) {
      leds[i] = CRGB::Black;
    }
    FastLED.show();
    delay(50);
  }
  
  
  // 明度を段階的に上げる
  for (int brightness = 0; brightness <= 255; brightness += 10) {
    FastLED.setBrightness(brightness);
    delay(10);
  }
  FastLED.setBrightness(255);
  
  Serial.println("NeoHex and Encoder initialized successfully");
  Serial.println("Rotate encoder to adjust brightness (0-254)");
  
  // I2Cスキャン
  scanI2CDevices();
  
  // エンコーダーユニットの初期化
  initializeEncoder();
  
  // 初期テスト
  testInitialLEDs();
}

void loop() {
  // エンコーダーの値を読み取り
  int16_t encoderValue = readEncoderValue();
  
  // デバッグ用:常にエンコーダー値を表示
  static unsigned long lastDebugTime = 0;
  if (millis() - lastDebugTime > 1000) {
    Serial.printf("Debug - Encoder Value: %d, Last Value: %d\n", encoderValue, lastEncoderValue);
    lastDebugTime = millis();
  }
  
  // エンコーダー値が変化した場合のみ処理
  if (encoderValue != lastEncoderValue) {
    // エンコーダー値を0-254の明度にマッピング
    // エンコーダーの範囲を仮定(-30から30の範囲)
    uint8_t brightness = map(encoderValue, -30, 30, 0, 254);
    brightness = constrain(brightness, 0, 254);
    
    Serial.printf("Encoder changed: %d -> %d, Brightness: %d\n", lastEncoderValue, encoderValue, brightness);
    
    // NeoHexの明度を設定
    setNeoHexBrightness(brightness);
    
    // 画面表示を更新
    updateDisplay(encoderValue, brightness);
    
    lastEncoderValue = encoderValue;
    currentBrightness = brightness;
  }
  
  delay(50);  // 短い遅延でスムーズな動作
}

// エンコーダーの値を読み取る関数
int16_t readEncoderValue() {
  // I2C通信の開始(レジスタアドレスを指定)
  Wire.beginTransmission(ENCODER_ADDR);
  Wire.write(ENCODER_REG);
  uint8_t error = Wire.endTransmission(false);  // falseで再起動を防ぐ
  
  if (error != 0) {
    Serial.printf("I2C Error: %d\n", error);
    return lastEncoderValue;  // 前回の値を返す
  }
  
  // データの要求(2バイト)
  Wire.requestFrom(ENCODER_ADDR, 2);
  
  if (Wire.available() >= 2) {
    uint8_t lowByte = Wire.read();
    uint8_t highByte = Wire.read();
    int16_t value = (int16_t)((highByte << 8) | lowByte);
    Serial.printf("Raw encoder bytes: 0x%02X 0x%02X, Value: %d\n", highByte, lowByte, value);
    return value;
  } else {
    Serial.printf("I2C read failed, available: %d\n", Wire.available());
    return lastEncoderValue;  // 前回の値を返す
  }
}

// NeoHexの明度を設定する関数
void setNeoHexBrightness(uint8_t brightness) {
  Serial.printf("Setting brightness to: %d\n", brightness);
  
  // 段階的なクリア処理
  FastLED.clear(true);  // バッファをゼロで埋める
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CRGB::Black;
  }
  FastLED.show();
  delay(5);
  
  // 全LEDに白色を設定(段階的に)
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CRGB(brightness, brightness, brightness);  // 白色で点灯
  }
  
  FastLED.show();
  delay(10);

  Serial.println("LEDs updated");
}

// 画面表示更新
void updateDisplay(int16_t encoderValue, uint8_t brightness) {
  M5.Lcd.fillRect(10, 60, 300, 40, BLACK);
  M5.Lcd.setCursor(10, 60);
  M5.Lcd.printf("Encoder: %d", encoderValue);
  M5.Lcd.setCursor(10, 80);
  M5.Lcd.printf("Brightness: %d", brightness);
}

// 初期LEDテスト
void testInitialLEDs() {
  Serial.println("Starting initial LED test...");
  
  // 段階的なLEDテスト
  for (int i = 0; i < 3; i++) {
    Serial.printf("Test cycle %d\n", i + 1);
    
    // 赤色で全LED点灯
    FastLED.clear(true);  // バッファをゼロで埋める
    for (int j = 0; j < NUM_LEDS; j++) {
      leds[j] = CRGB(50, 0, 0);  // 赤色(明度を下げる)
    }
    FastLED.show();
    delay(300);
    
    // 緑色で全LED点灯
    FastLED.clear(true);  // バッファをゼロで埋める
    for (int j = 0; j < NUM_LEDS; j++) {
      leds[j] = CRGB(0, 50, 0);  // 緑色(明度を下げる)
    }
    FastLED.show();
    delay(300);
    
    // 青色で全LED点灯
    FastLED.clear(true);  // バッファをゼロで埋める
    for (int j = 0; j < NUM_LEDS; j++) {
      leds[j] = CRGB(0, 0, 50);  // 青色(明度を下げる)
    }
    FastLED.show();
    delay(300);
    
    // 消灯
    FastLED.clear(true);  // バッファをゼロで埋める
    for (int j = 0; j < NUM_LEDS; j++) {
      leds[j] = CRGB::Black;
    }
    FastLED.show();
    delay(300);
  }
  
  // 最終的な完全消灯処理(複数回実行)
  for (int clearCount = 0; clearCount < 5; clearCount++) {
    FastLED.clear(true);  // バッファをゼロで埋める
    for (int i = 0; i < NUM_LEDS; i++) {
      leds[i] = CRGB::Black;
    }
    FastLED.show();
    delay(100);
  }
  
  Serial.println("Initial LED test completed");
}

// I2Cデバイススキャン関数
void scanI2CDevices() {
  Serial.println("Scanning I2C devices...");
  int deviceCount = 0;
  
  for (uint8_t address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    uint8_t error = Wire.endTransmission();
    
    if (error == 0) {
      Serial.printf("I2C device found at address 0x%02X\n", address);
      deviceCount++;
    }
  }
  
  if (deviceCount == 0) {
    Serial.println("No I2C devices found!");
  } else {
    Serial.printf("Found %d I2C device(s)\n", deviceCount);
  }
}

// エンコーダーユニットの初期化関数
void initializeEncoder() {
  Serial.println("Initializing encoder unit...");
  
  // エンコーダーユニットの初期化コマンドを送信
  Wire.beginTransmission(ENCODER_ADDR);
  Wire.write(0x00);  // 初期化レジスタ
  Wire.write(0x01);  // 初期化コマンド
  uint8_t error = Wire.endTransmission();
  
  if (error == 0) {
    Serial.println("Encoder unit initialized successfully");
  } else {
    Serial.printf("Encoder initialization failed with error: %d\n", error);
  }
  
  delay(100);  // 初期化待機
}

プルアップ抵抗まわりの問題の可能性も

テープ型の事例になりますが、通信用のポートをプルアップしてるとダメというケースもある様子。



n8nとollamaを連動する その2:n8nチャットとpython製webhook的受けツールで試す

実際のところ

外観


IF節

とりあえずテスト実装なので

{{ $json.chatInput }} 
contains
Hello

Code節

return [
  {
    json: {
      text: "Invalid msg"
    }
  }
];

Basic LLM Chain節

とりあえず挙動制御ように
Systemに

You're a polite teacher. You understand the user is not good at english.

受け手のPythonスクリプト

#!/usr/bin/env python3
# webhook_receiver.py

from http.server import HTTPServer, BaseHTTPRequestHandler
import json
from datetime import datetime
import sys

class WebhookHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        # Content-Lengthを取得
        content_length = int(self.headers.get('Content-Length', 0))
        
        # リクエストボディを読み取り
        body = self.rfile.read(content_length).decode('utf-8')
        
        # タイムスタンプ
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        
        # JSONパース試行
        try:
            data = json.loads(body)
            
            # valueフィールドがあれば表示
            if 'value' in data:
                print(f'\n[{timestamp}] --- Message Content ---')
                print(data['value'])
                print()
            else:
                # valueがない場合は全体を表示
                print(f'\n[{timestamp}] --- Message Content ---')
                print(json.dumps(data, indent=2, ensure_ascii=False))
                print()
        except json.JSONDecodeError:
            # JSONでない場合はそのまま表示
            print(f'\n[{timestamp}] --- Message Content ---')
            print(body)
            print()
        
        sys.stdout.flush()
        
        # レスポンスを返す
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        
        response = {
            'status': 'received',
            'timestamp': timestamp
        }
        self.wfile.write(json.dumps(response).encode('utf-8'))
    
    def log_message(self, format, *args):
        # アクセスログを抑制
        pass

def run_server(port=9999, host='0.0.0.0'):
    server_address = (host, port)
    httpd = HTTPServer(server_address, WebhookHandler)
    
    print('Webhook Receiver Server')
    print(f'Listening on: {host}:{port}')
    print(f'Docker access: http://172.17.0.1:{port}')
    print('Press Ctrl+C to stop\n')
    
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print('\n\nServer stopped.')
        sys.exit(0)

if __name__ == '__main__':
    import argparse
    
    parser = argparse.ArgumentParser(description='Simple Webhook Receiver')
    parser.add_argument('-p', '--port', type=int, default=9999,
                        help='Port number (default: 9999)')
    parser.add_argument('--host', default='0.0.0.0',
                        help='Host address (default: 0.0.0.0)')
    
    args = parser.parse_args()
    run_server(port=args.port, host=args.host)

n8nとollamaを連動する その1:n8nチャットとnetcatでためす

ターゲットによってはローカルでやらざるを得ないこともあるだろうし

実際のところ

外観

n8nのdocker

まず、lobechatとollamaを実験したときのネットワーク名を

$ docker inspect ollama | jq '.[0].NetworkSettings.Networks | keys'
[
  "lobe-net"
]

この名前をネットワーク名として変数に保存

NETWORK_NAME="lobe-net"
$ docker run -d \
  --name n8n_docker \
  --network $NETWORK_NAME \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  --restart unless-stopped \
  n8nio/n8n:1.100.1

ollama

openaiのときと同様、認証してモデル選択をします


HTTPリクエスト

key
{{ $json.text }}

受け手のnetcat

$ nc -l -w 3 -p 9999
POST / HTTP/1.1
Accept: application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7
Content-Type: application/json
User-Agent: axios/1.8.3
Content-Length: 278
Accept-Encoding: gzip, compress, deflate, br
Host: 172.17.0.1:9999
Connection: close

{"value":"Awesome! That's great to hear. Local n8n and local Ollama can be really powerful – and cool! \n\nSo, what's the test about? Are you running the test with a specific dataset in Ollama and configuring the workflow in n8n?\n\nLet's go! 😊 \n\nHow's it going so far?"}

FastAPIとuvicornサーバーを使った簡易ロボットサーバー その2:JSONをうける

実際のところ

スクリプト

# ownserver.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Literal

app = FastAPI()

# リクエストボディのスキーマ定義
class RobotCommand(BaseModel):
    direction: Literal["right", "left", "forward", "backward", "up", "down"]
    distance_mm: float = Field(gt=0, description="移動距離(ミリメートル)")

class CommandRequest(BaseModel):
    cmd: RobotCommand

@app.get("/")
async def root():
    return {"message": "Robot API Server"}

@app.get("/health")
async def health_check():
    return {"status": "ok"}

@app.post("/robot/move")
async def move_robot(request: CommandRequest):
    """
    ロボットを移動させるエンドポイント
    """
    try:
        direction = request.cmd.direction
        distance = request.cmd.distance_mm
        
        # ここに実際のロボット制御コードを書く
        print(f"Moving robot: {direction} by {distance}mm")
        
        return {
            "status": "success",
            "command": {
                "direction": direction,
                "distance_mm": distance
            },
            "message": f"Robot moved {direction} by {distance}mm"
        }
    
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Robot control error: {str(e)}")

動作検証

$ curl -X POST http://localhost:8000/robot/move   -H "Content-Type: application/json"   -d '{"cmd": {"direction": "up", "distance_mm": 25.5}}'
{"status":"success","command":{"direction":"up","distance_mm":25.5},"message":"Robot moved up by 25.5mm"}

ドキュメント

http://localhost:8000/redoc にアクセスすると、API仕様を確認できます
これはFastAPIの標準機能で大変便利なところ