UMEHOSHI ITA TOP PAGE

ロボットの倒立振子の検討の記録


以下の検討の前で行った検討内容は、このリンク先で紹介しています。

モータの切り替え点を変動させて制御してみる。その3

前の検討では、傾き振動の頂点の傾き(下記傾きの矢印)を、各山や谷の頂点を調べて判断してみました。
(山の頂点の傾きが減ったら後進中と判断、 谷の頂点の傾きが減ったら前進中と判断)

もっと簡単に後進中と判断、 谷の頂点の傾きが減ったら前進中する方法として、各谷の頂点の変動に着目して、 この値が下がっる変化であればに後進中、上がった変化であればに前進中と判断するコードが次ぎのコードです。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#   TCPサーバープログラム(/usr/local/apps/raspiAPumeTest.py)
#  [Raspberry Pi 3 Model A+]と[UMEHOSHI ITA]を乗せたモータ付き台車のサービスから呼び出される
# 倒立振子を試すコード

import subprocess
from collections import deque
import os # ファイル有無を調べるため追加 ★
def exists(path):
   try:
      os.stat(path)
      return True
   except (OSError, AttributeError):
      # ファイルが存在しない、またはos.statが使えない場合にFalseを返す
      return False

#  前か後かの移動中を判断を、波の勾配(微分値)を使って判断すし、目標角の補正値を取得するクラス
from collections import deque
class Detector:
    def __init__(self, size: int = 6):
        self.dq_former = deque()# 前半の値を保持する変数(データはこちらからを入れる。注意:最新情報はこちら)
        self.dq_latter = deque()# 後半の値を保持する変数(注意:キューなのでこちらが時間的に前になる)
        self.size = size
        # 前半と後半の合計値を保持する変数
        self.sum_former = 0.0   # 「差分更新(スライディング・ウィンドウ)」という手法を使う
        self.sum_latter = 0.0
        self.prev_valley = 0 # 前の谷の値
        self.rtnVal = 0
        self.call_count = 0

    def append(self, data: float) -> float:     
        self.dq_former.append(data) # 新しいデータを追加       
        self.sum_former += data
        if len(self.dq_former) <= self.size:
            return 0,0
        # 以下は前半のキューが一杯になった後の追加処理
        removed = self.dq_former.popleft() # 前半」の先頭の追い出し   
        self.sum_former -= removed
        self.dq_latter.append(removed) # データを後半に移動
        self.sum_latter += data
        # 後半キューが一杯になるまでの処理
        if len(self.dq_latter) <= self.size:
                return 0,0
        # 以下は前半と後半が一杯になった。
        removed = self.dq_latter.popleft() # 前半」の先頭の追い出し   
        self.sum_latter -= removed
        #        
        tilt_former = self.dq_former[-1]-self.dq_former[0] # 最近の傾き
        tilt_latter = self.dq_latter[-1]-self.dq_latter[0] # 直前の傾き
        self.call_count += 1
        peak_flag = tilt_latter > 0 and tilt_former < 0 # 山の頂点
        valley_flag = tilt_latter < 0 and tilt_former > 0 # 谷の頂点

        if not( peak_flag or valley_flag ) or self.call_count < 10:
            return self.rtnVal,tilt_former  # 変曲点でない
        #
        dif_sum = abs(self.sum_former) - abs(self.sum_latter) # # 変曲点の前と後の傾きの差を取得 
        if abs(self.prev_valley-data) > 20: # 差が大きすぎる場合は雑音とsて無視
            return self.rtnVal,tilt_former
        #  
        if peak_flag :                  # -------------------上に山の変曲点 
            if False: pass
            # elif abs(tilt_former) < 1.5 :# 傾きが小さくなった
            #     self.rtnVal = -2.01
            # elif abs(tilt_former) < 1.5 + 0.5:
            #     self.rtnVal = -1.01
            else:
                self.rtnVal = -0.01
        elif valley_flag :              # -------------------下に谷の変曲点]
            if self.prev_valley != 0 and data>self.prev_valley+1: # 変曲点が以前が上がっている?
                self.rtnVal = -2.01
            elif self.prev_valley != 0 and data<self.prev_valley-1: # 変曲点が以前が下がっている?
                self.rtnVal = 2.01
            # elif abs(tilt_former) < 1.8:
            #     self.rtnVal = 2.51
            # elif abs(tilt_former) < 1.8 + 0.9:
            #     self.rtnVal = 1.51
            else:
                self.rtnVal = 0.01
            self.prev_valley = data # 谷の頂点を記憶
            self.call_count = 0
         #
        return self.rtnVal,tilt_former
    #
    def sum_tilt_former(self):
        return self.dq_former[-1]-self.dq_former[0] # 最近の傾き

# ---- LED用の出力とタクトスイッチ用入力 のためのGPIO初期化-----
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(21, GPIO.OUT)  # GPIO21を出力に設定
GPIO.output(21, GPIO.HIGH)  # ON(3.3V)
for no in [6, 16, 17]:
    GPIO.setup(no, GPIO.IN, pull_up_down=GPIO.PUD_UP)  # プルアップ付き入力

#------ GPIO初期化 終了 -------------------------------------

import signal # システムが閉じることを判断するため追加 ★
import board
import busio
from adafruit_ssd1306 import SSD1306_I2C # SSD1306ディスプレイ用
import adafruit_vl53l1x # VL53L1X使用 レーザー測距センサーモジュール用
# オープンソースハードウェアの設計・製造・販売を行うアメリカの企業のAdafruit(エイダフルート)モジュール利用
from PIL import Image, ImageDraw, ImageFont
import time

i2c = busio.I2C(board.SCL, board.SDA)# --- I2C初期化 ---

# --- SSD1306ディスプレイ初期化 (128x64の場合) -----------
oled = SSD1306_I2C(128, 64, i2c)
oled.contrast(128) # 0?255

oled.fill(0)   # --- クリア
oled.show() # ---表示

# --- Pillowで描画領域を作成 ---
image = Image.new("1", (oled.width, oled.height))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default()# --- フォント設定 ---

def draw_text(txt: str, row=0, showFlag = True, newImageFlag = False, font=font, fill=255):
   global image,draw
   if newImageFlag: 
      image = Image.new("1", (oled.width, oled.height)) # イメージ作り直し(全体クリア)
      draw = ImageDraw.Draw(image)
   # --- テキスト描画 (0=黒、255=白)上記設定で、横21文字---
   draw.text((0, row*15), txt , font=font, fill=255)
   # --- 画面に表示 ---
   oled.image(image)
   if showFlag: oled.show()

draw_text(f"UMEHOSHI ITA",0)

# 9軸センサー BNO055 制御 ---------------------------------------------------
import smbus # I2C通信をPythonから簡単に扱うためのモジュール
import os

# BNO055 の初期化
BNO055_ADDRESS = 0x28  # BNO055のI2Cアドレス(ADRピンがGNDなら0x28、VDDなら0x29になります)
BNO055_OPR_MODE = 0x3D # 動作モードを設定するためのレジスタ
BNO055_EULER_H_LSB = 0x1A # オイラー角(方位・ロール・ピッチ)のデータが始まるアドレス
bus = smbus.SMBus(1)# 引数の1でRaspberry PiのボードGPIO2: SDA、GPIO3: SCLを指定

bno055_calib_bin_path="/usr/local/apps/bno055_calib.bin" # キャリブレーションデータファイルパス
# このキャリブレーションデータファイルが存在しなければ、作成する。
try:
   os.stat(bno055_calib_bin_path) # ファイルが存在しない場合は、エラー
except OSError:
   bus.write_byte_data(BNO055_ADDRESS, BNO055_OPR_MODE, 0x00) # 設定変更(OPR_MODE)でCONFIGモードに切り替える
   time.sleep(0.05)
   # センサーをリセット(0x3FのSYS_TRIGGERレジスタのビット7をセット)
   bus.write_byte_data(BNO055_ADDRESS,0x3F, 0x20)
   time.sleep(0.7)  # リセット後は再起動まで時間がかかる
   # 出力単位(UNIT_SEL)を設定(0x00で「角度=度(°)」単位)
   bus.write_byte_data(BNO055_ADDRESS,0x3B, 0x00)
   # センサーフュージョンを有効にするNDOFモードに変更
   bus.write_byte_data(BNO055_ADDRESS,BNO055_OPR_MODE, 0x0C) # NDOFモードへ
   time.sleep(0.05)
   while True: # NDOFモードで全てのセンサー(SYS, GYR, ACC, MAG)が 3 になるまで動かす。
      cal = bus.read_byte_data(BNO055_ADDRESS, 0x35)
      sys = (cal >> 6) & 0x03
      gyr = (cal >> 4) & 0x03
      acc = (cal >> 2) & 0x03
      mag = (cal >> 0) & 0x03
      draw_text(f"bno055 NDOF Calibrating...",0, showFlag = False,newImageFlag = True) # キャリブレーションの開始
      draw_text(f"SYS:{sys}, GYR:{gyr}, ACC:{acc}, MAG:{mag}", 1)
      time.sleep(0.5)
      if sys == 3 and gyr == 3 and acc == 3 and mag == 3 : break # キャリブレーション完了?
      # 各値が 3 になれば完全キャリブレーション完了です
   #
   calib_data = bus.read_i2c_block_data(BNO055_ADDRESS, 0x55, 22)
   with open(bno055_calib_bin_path, "wb") as f:
      f.write(bytearray(calib_data)) # オフセット値を読み出して保存
   #
   draw_text(f"End Calibration",2) # キャリブレーションデータファイル作成終了

# オフセット調整(センサーの取り付け位置や方向を自動補正)
bno055_offset_path="/usr/local/apps/bno055_offset.txt"
# 上記ファイル内に X軸が北を向く時にHeading、水平に置いた時にRollと Pitchが記憶される
make_offset_mode=False # "bno055_offset.txt"オフセット調整ファイル作成モード
try:
   with open(bno055_offset_path, "r") as fr:
      s = fr.readline() # 一行読み取り(改行を含めて)
   draw_text(f"bno055 offset setting",0,newImageFlag = True)
   a = s.split(",")
   heading_offset = float(a[0]) # オフセット調整値取得
   roll_offset = float(a[1])
   pitch_offset = float(a[2])
   print(f"offset value heading:{heading_offset}, roll:{roll_offset}, pitch:{pitch_offset}")
except:
   make_offset_mode = True # bno055_offset.txtのオフセット調整作成モード

bus.write_byte_data(BNO055_ADDRESS, BNO055_OPR_MODE, 0x00) # 設定変更(OPR_MODE)でCONFIGモードに切り替える
time.sleep(0.05)

with open(bno055_calib_bin_path, "rb") as f:
   calib_data = list(f.read(22)) # 別途bno055_calib_write.pyで行ったキャリブレーションの記憶ファイルを読む

# 設定モードへ
bus.write_byte_data(BNO055_ADDRESS, 0x3D, 0x00)
time.sleep(0.025)

# キャリブレーションデータを書き込んで、調整情報を復元
bus.write_i2c_block_data(BNO055_ADDRESS, 0x55, calib_data)

# NDOFモードに戻す
bus.write_byte_data(BNO055_ADDRESS, 0x3D, 0x0C)
time.sleep(0.05)

# 出力単位(UNIT_SEL)を設定(0x00で「角度=度(°)」単位)
bus.write_byte_data(BNO055_ADDRESS,0x3B, 0x00)

def to_signed(val):
   """16ビット値を符号付き整数に変換"""
   if val >= 0x8000:
      val -= 0x10000
   return val


prev1_Pitch = 0 # 前のPitchの測定値
prev2_Pitch = 0 # 前のPitchの測定値
prev3_Pitch = 0 # 前のPitchの測定値
def read_euler():
   ''' make_offset_modeがTrueの場合は、現在の
   heading:左右への向き, roll:左右の傾き, pitch:上下の傾きを返す。
   make_offset_modeがFalseの場合は、調整過程のheading, roll, pitchを返す'''
   #
   global prev1_Pitch,prev2_Pitch,prev3_Pitch # 前のPitchの測定値
   data = bus.read_i2c_block_data(BNO055_ADDRESS, BNO055_EULER_H_LSB, 6)
   # 各要素が 1 バイト(0〜255)の整数を6個のリストで得られる。(1 LSB = 1/16 度)

   # データはリトルエンディアン形式(下位→上位の順)
   heading = (data[1] << 8) | data[0]  # 方位角(北基準のYAW)
   roll    = (data[3] << 8) | data[2]  # ロール角(左右の傾き)
   pitch   = (data[5] << 8) | data[4]  # ピッチ角(前後の傾き)

   # ロールとピッチは符号付き
   roll = to_signed(roll) # 16ビット値を符号付き整数に変換
   pitch = to_signed(pitch) # 16ビット値を符号付き整数に変換

   # スケーリング(1 LSB = 1/16 度)
   heading = heading / 16.0 # 
   roll    = roll / 16.0
   pitch   = pitch / 16.0

   if make_offset_mode == False: #オフセット調整処理
      # X軸が北を向く時にHeadingが0、水平に置いた時にRollと Pitchが0になるオフセット調整
      heading = heading - heading_offset # headingズレ補正
      heading = heading if heading >= 0 else heading + 360
      roll = roll - roll_offset # rollズレ補正
      if roll > 90:
         roll = -(90 - roll)
      elif roll < -90:
         roll = -(-90 - roll)
      pitch = pitch - pitch_offset # pitchズレ補正
      if pitch > 180:
         pitch = -(360 - pitch)
      elif pitch < -180:
         pitch = -(-360 - pitch)
   #
   prev3_Pitch = prev2_Pitch # 前の測定値記憶
   prev2_Pitch = prev1_Pitch # 前の測定値記憶
   prev1_Pitch = pitch       # 前の測定値記憶
   return heading, roll, pitch # 左右への向き, 左右の傾き, 上下の傾きを返す


if make_offset_mode: #オフセット調整処理
      count = 100 # ロボットを水平にしてはX軸が北を向いてから安定に必要な予想回数
      while True:
         h, r, p = read_euler()
         print(f"Heading: {h:7.2f}, Roll: {r:7.2f}, Pitch: {p:7.2f}")
         count -= 1
         print(f"  {count}が0になるまでに、ロボットを水平にしてはX軸が北を向くように置いてください。")
         draw_text(f"Before CountReaches 0",0,showFlag = False, newImageFlag = True)
         draw_text(f"Place it horizontally",1,showFlag = False)
         draw_text(f"   and point it north",2,showFlag = False)
         draw_text(f" count:{count}",3)
         time.sleep(0.2)
         if count <= 0: 
            with open(bno055_offset_path, "w") as fw:
               fw.write(f"{h},{r},{p}\n") # オフセット調整データ書き込み
            heading_offset, roll_offset,pitch_offset=h,r,p
            break # 上記でX軸が北を向く時にHeading、水平に置いた時にRollと Pitchを記憶

# --------------------------------------------------------------------------------
vl53 = adafruit_vl53l1x.VL53L1X(i2c)# VL53L1X使用 レーザー測距センサーモジュール初期化
print("VL53L1X Start measuring...")
vl53.start_ranging()

distance = vl53.distance
time.sleep(0.5)
print(f"Distance: {distance} mm")

# umetcp umeusb 通信関連----------------------------------------------------
import os
import umetcp
from umetcp import send_message, receiveData
import socket
import umeusb
import traceback

sock = None # クライアントと通信するソケット

def my_tcp_receive_file_func(filepath):
   ''' 受信したumehoshiアプリ用「.umh」のファイルより、
   「UME専用Hexコマンド」の文字列をusbへ出力する'''
   name,ext = os.path.splitext(filepath)
   if not ext == ".umh" : return
   with open( filepath ) as f: ss=f.readlines()
   ss="".join(ss[1:])
   print(ss)
   umeusb.send_cmd(ss, quantity=0) # TCPで受信した 「.umh」データを[UMEHOSHI ITA]へ送る

umetcp.tcp_receive_file_func = my_tcp_receive_file_func # tcpファイル受信データの処理を置き換え

def my_tcp_recieve_message(msg):
   if msg.startswith("G:"):
      file_path = msg[2:]
      if exists(file_path):
         umetcp.send_file(sock, file_path)
      else:send_message(sock, f"G:{file_path}Not Operation\n")
   else: umeusb.send_cmd(msg)

umetcp.tcp_receive_message_func=my_tcp_recieve_message  # TCO受信文字列(UME専用Hexコマンド)処理を置き換え

def my_usb_receive_func(bin):
   'usb受信のイベントで実行するデフォルト処理'
   global sock
   #print("-----------",bin)
   ss = bin.decode('utf-8')
   a=ss.split('\n')
   for s in a:
      s = s.strip()
      if s == "": continue 
      if sock != None:
         print(f"USB receive:{s}")
         send_message(sock, s) # [UMEHOSHI ITA]からの応答メッセージをTCPで返す
      else: print( f"USB receive:{s}" )

umeusb.usb_receive_func = my_usb_receive_func # USB受信データの処理を置き換える。

umeusb.init_sub()
import threading
t_id = threading.Thread(target=umeusb.read_loop)
t_id.start()

#ip="192.168.4.1"
ip=umetcp.get_wlan0_ip() # IPアドレス取得
while ip == None:
   ip=umetcp.get_wlan0_ip()
   if ip : break
   time.sleep(0.1)

#umeusb.send_cmdfile("/usr/local/apps/uStartInit.umh") # ロボット初期化("R009D020010004E")
umeusb.send_cmdfile("/usr/local/apps/pwm_pi3ma_init.c.umh") # ロボット初期化
server_addr =(umetcp.get_wlan0_ip(), 59154)
hostname=socket.gethostname()
print("Serverの情報:",hostname, server_addr)

def info_show2( pitch , target_angle):
   ''' IPアドレスpとポート番号の情報と、ピッチ角度、目標角度をSSD1306ディスプレイに表示する'''
   draw_text(f"{server_addr[0]},{server_addr[1]}",0,showFlag = False, newImageFlag = True)
   draw_text(f"Pitch:{pitch:5.1f}  {target_angle}",2,showFlag = True)

def save_results(results, last_str): # 結果保存用
   with open('/usr/local/apps/log2.txt', 'w') as fw:
      for t in results:
         fw.write(f'{t[0]}:{t[1]},pitch:{t[2]},periodo:{t[3]},tilt:{t[4]},ajv:{t[5]}\n')
      fw.write(last_str)

# # ピッチなどの計測と、その制御ループ----------------------------------------------------------
measure_loop_flag=True # ピッチなどの計測と、その制御ループを続けるためのフラグ
def measure_loop():
   global measure_loop_flag # 測定監視ループフラグ
   next_measure_time=0 # 測定間隔制御用(次の測定の時間を記憶する)
   flag_push = False # 倒立制御開始ボタンが押されてから倒立制御を終えるまでTrue
   flag_control = False # 倒立制御中である間だけTrue
   start_control_time=0 # 制御開始の時間(秒)
   start_forward_time = 0.05 # 制御開始前のモータ前進を行う期間(秒)
   target_angle=23.5 # 目標角度 理想予測値
   # target_angle = 22.4 # 実験用
   # target_angle = 22.5 # 実験用
   # target_angle = 23.0 # 実験用
   target_angle = 23.2 # 実験用
   time_now=0 # 現在の測定時間
   motor_ctrl_time=0 # motor制御用コマンドをSUB送信した時の時間を記憶
   results = [] # 制御履歴を残すリスト(制御終了時にファイル化)
   detector=None # 変更点判定
   ajv=0 # target_angleに対する調整値
   pwmS=""
   pwmS_bak=""
   pwdN=0   # PWM デューティ比の大きさ(0〜6)
   pwdN_bak=6 # 上記の設定前の状態
   pitch_bak=0
   umeusb.usb_send("R0080005600005B", quantity=0) # 最大値setPWM6の(0x0BFFF)
   #
   while measure_loop_flag:
      if GPIO.input(6) == GPIO.LOW: # SW2スイッチ(緑)が押された? リブート処理------------
         measure_loop_flag=False
         draw_text(f"Rebooting.",0,showFlag = True, newImageFlag = True)
         time.sleep(0.01)
         try: # shell=False(デフォルト)で呼び出すのが安全
            sock.close()
            subprocess.run(["sudo", "reboot"], check=True)
         except subprocess.CalledProcessError as e:
             draw_text(f"Reboot fail.",0,showFlag = True, newImageFlag = True)
         break
      #
      if GPIO.input(17) == GPIO.LOW: # SW4スイッチ(黒色)が押された? 制御終了-----------
         flag_push = False
         flag_control = False
         save_results(results, last_str="End SW\n") # 結果を保存
      #
      if flag_push==False and GPIO.input(16) == GPIO.LOW: # 制御開始用SW3スイッチ(黄色)が押された?
         time.sleep(2)
         results = []
         detector=Detector(size=6) # 判定器
         ajv=0 # target_angleに対する調整値
         pwmS_bak=''
         umeusb.usb_send("R0080005600005B", quantity=0) # 最大値:setPWM6の(0x0BFFF)
         pwdN_bak=6
         # umeusb.usb_send("R0080005500005C", quantity=0) # setPWM5の(0x07FFF)
         # pwdN_bak=5
         start_control_time = time.time() # 制御スタート時間
         flag_push = True
         umeusb.usb_send("R009D020200004D", quantity=0) # Forward 最初の助走
      #
      time_now=time.time()
      if next_measure_time > time_now:
         time.sleep(next_measure_time-time_now)
         continue # 次の周期まで待つ
      next_measure_time = time_now + 0.002 # 次の測定時間を更新
      #
      startM=time.time()
      # for ck in range(3): #  # ジャイロ測定処理(雑音対策の繰り返し)
      #    heading, roll, pitch = read_euler() # ーーージャイロ測定ーーー
      #    if pitch < 150: break # 適正と予測される値?
      heading, roll, pitch = read_euler() # ーーージャイロ測定ーーー
      if pitch > 150: pitch = pitch_bak
      else: pitch_bak=pitch
      endM=time.time()
      if flag_push == False : info_show2(pitch,target_angle) # 測定値の表示
      #
      if flag_push and flag_control == False: # 倒立制御に入るまでの処理
         if time.time() > start_control_time + start_forward_time: 
            umeusb.usb_send("R009D020300004C", quantity=0) # Back 最初の倒立のための逆転
            if pitch > target_angle+ajv-6.5: # 倒立角に達した?
               flag_control = True
      #
      if flag_control :# 倒立振子制御中
         ajv,tilt_former=detector.append(pitch) # 変曲点を求めるためのデータ記憶
         #ajv=0
         pwdN=0
         if False: pass
         elif pitch < target_angle+ajv-6.5 or pitch > target_angle+ajv+6.5:
            pwdN=6
            if pwdN!=pwdN_bak: 
               umeusb.usb_send("R0080005600005B", quantity=0) # 最大値:setPWM6の(0x0BFFF)
         elif pitch < target_angle+ajv-1.5 or pitch > target_angle+ajv+1.5:
            pwdN=5
            if pwdN!=pwdN_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R0080005500005C", quantity=0) # setPWM5の(0x07FFF)
         elif pitch < target_angle+ajv-0.7 or pitch > target_angle+ajv+0.7:
            pwdN=4
            if pwdN!=pwdN_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R0080005400005D", quantity=0) # setPWM4の(0x4FFF)
         elif pitch < target_angle+ajv-0.5 or pitch > target_angle+ajv+0.5:
            pwdN=3
            if pwdN!=pwdN_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R0080005300005E", quantity=0) # setPWM3の(0x38FF)
         elif pitch < target_angle+ajv-0.3 or pitch > pitch < target_angle+ajv+0.3:
            pwdN=2
            if pwdN!=pwdN_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R0080005200005F", quantity=0) # setPWM2の(0x1FFF)
         elif pitch < target_angle+ajv-0.2 or pitch > target_angle+ajv+0.2:
            pwdN=1
            if pwdN!=pwdN_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R00800051000060", quantity=0) # 最小:setPWM1の(0x0FFF)
         
         pwdN_bak = pwdN
         # モータの停止、逆転、正転
         if pwdN == 0: # motor停止
            pwmS=f'S{pwdN}'
            if  startM-motor_ctrl_time>0.01  or pwmS != pwmS_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R009D020A00003E", quantity=0) # STOP      
               pwmS_bak = pwmS
               motor_ctrl_time = startM
            #  
         elif pitch <= target_angle+ajv: # 逆転
            pwmS=f'B{pwdN}'
            if startM-motor_ctrl_time>0.01 or pwmS != pwmS_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R009D020300004C", quantity=0) # Back
               pwmS_bak = pwmS
               motor_ctrl_time = startM # モータ制御コマンドを送った時間をメモ
            #          
         else: # (pitch > target_angle+ajv): #正転
            pwmS=f'F{pwdN}'
            if  startM-motor_ctrl_time>0.01  or pwmS != pwmS_bak: # USB送信量を抑える工夫
               umeusb.usb_send("R009D020200004D", quantity=0) # Forward
               pwmS_bak = pwmS
               motor_ctrl_time = startM
            #               
         #
         results.append( ( f'{pwmS}', startM, pitch, endM-startM, tilt_former, ajv ) ) # ファイル化情報
         umeusb.usb.flush() 
         if pitch < 5 or pitch > 70 or startM > start_control_time + 10: #この時間で制御を終了
            umeusb.usb_send("R009D020A00003E", quantity=0) # STOP
            flag_push = False
            flag_control = False
            save_results(results, last_str=f"End pitch:{pitch} time:{startM-start_control_time}.\n") # 結果を保存
         #
      #
   #

#info_show() # ディスプレイへ表示
t_id2 = threading.Thread(target=measure_loop)
t_id2.start()

# 終了時に実行したい処理 ★
def handle_shutdown(signum, frame):
   global measure_loop_flag
   if measure_loop_flag == False: return
   print(f"シャットダウンを検知しました (Signal: {signum})")
   measure_loop_flag = False
   draw_text(f"Wait....",0,showFlag = True, newImageFlag = True)
   time.sleep(10)
   draw_text(f"Shutdown.",0,showFlag = True, newImageFlag = True)
   time.sleep(0.01)

# SIGTERM(システム終了時の標準信号)を登録 ★
signal.signal(signal.SIGTERM, handle_shutdown) # ★

# TCPサーバー起動処理
try:
   serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   serversock.bind(server_addr)  # IPとポート番号を指定します
   print(f"{server_addr}:接続要求を待つ")
   serversock.listen()
except : serversock = None
while serversock != None:
   print("接続を待って、接続してきたら許可")
   sock, address = serversock.accept()#サーバの接続受け入れ
   print("接続相手:",address)
   try:
      receiveData( sock ) # 受信ループ
   except Exception as e:
      print(f"例外の種類: {type(e)}")
      print(f"スタックトレース: {traceback.format_exc()}")
      draw_text(f"wait next",0,showFlag = True, newImageFlag = True)
   sock.close()

またそれ以外にも、PWMの切り替えタイミングを変更しています。
残念ながら、大きな好転は得られませんでした。(コードが少なく、同じ効果のように感じる)