Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions SerialController/Gamepad.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def __init__(
is_active: Callable[[], bool] | None = None,
reader: Any | None = None,
on_display: Callable[[dict[str, Any]], None] | None = None,
deadzone: int = 0,
deadzone: int = 1638,
) -> None:
"""sender へ差分申告する。sender が None なら作れない。

Expand All @@ -112,7 +112,7 @@ def __init__(
on_display は読むたびに呼ぶ表示口(仮想パッドの鏡用)。
読取スレッドから呼ぶため、Tk を触る処理は呼び出し側で
root.after 経由にすること。
deadzone は SDL 経路のスティック遊び(0〜8192)。
deadzone は SDL 経路のスティック遊び(0〜8192、既定1638=最大値の5%)。
XInput 経路は既定のまま変えない。
"""
if sender is None:
Expand Down
5 changes: 5 additions & 0 deletions SerialController/Menubar.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ def OpenBlocklyEditor(self) -> None:
reload_commands=self.app.reloadCommands,
get_frame=blockly_capture.build_get_frame(lambda: self.camera),
run_host=self.app,
record_blocked=lambda: (
"実行中は記録できません(止めてから始めてください)"
if self.app.runner.is_busy()
else None
),
)

def OpenErrorReport(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion SerialController/Settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self, profile: str = "") -> None:
value=general.getint("gamepad_index", fallback=0)
)
self.gamepad_deadzone = tk.IntVar(
value=general.getint("gamepad_deadzone", fallback=0)
value=general.getint("gamepad_deadzone", fallback=1638)
)
# マウスでのスティック操作。GUI のチェックボックスと1対1で対応する
self.is_use_left_stick_mouse = tk.BooleanVar(
Expand Down
13 changes: 13 additions & 0 deletions SerialController/Window.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,19 @@ def _init_state(self) -> None:
# 走行記録の受け先。SerialService が担い、runner が開始・終了で呼ぶ。
# 記録の失敗で実行は壊さない(runner・service 側で握る)。
self.runner.set_diag(self.serial, self.profile)

# 記録中の通常コマンドの開始拒否。判定は編集側の記録を見る。
# ui への直参照は作らず、遅延読みで循環を避ける(runner は ui を
# 知らない。Window が両者をつなぐ)。
def _recording_now() -> bool:
try:
from ui import blockly_editor

return bool(blockly_editor.is_recording())
except Exception:
return False

self.runner.set_record_guard(_recording_now)
# 履歴の受け先。開始・終了で 1 行ずつ積む。渡さなければ残さない。
self.runner.set_history(self.command_history, self._commandKind)
self._closing = False
Expand Down
4 changes: 2 additions & 2 deletions SerialController/assets/blockly/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -2100,9 +2100,9 @@ <h2 id="newdialogtitle">作例から始める</h2>
on('pausebtn', 'click', function () { runControl(run.paused ? 'resume' : 'pause'); });
on('stepbtn', 'click', function () { runControl('step'); });
on('stopbtn', 'click', function () { runControl('stop'); });
// ---- 操作の記録(本体で押した操作を押すブロックの並びにする) ----
// ---- 操作の記録(本体で押した操作をブロックの並びにする) ----
// 本体で実際に押した操作(送信線へ出た入力)を記録し、止めたときに
// pokecon_press ブロックの並びにして挿入する。同時押しの再現はしない。
// 単独押しは pokecon_press、同時押しは hold・holdEnd の連鎖にして挿入する。
var rec = { active: false };
function setRecUi(active) {
rec.active = !!active;
Expand Down
31 changes: 27 additions & 4 deletions SerialController/assets/blockly/pokecon_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// motion 材料 1 件をブロックにする。press 手順なら null を返す。
// kind: stick → pokecon_motion、stick2 → pokecon_motion2、
// hold/hold_end → pokecon_hold/pokecon_hold_end、
// wait → pokecon_wait(sec・wait の順で秒を読む)、
// release_stick は直前の motion 系ブロックの END を RELEASE にする。
var lastMotionBlock = null;
function pairText(pair) {
Expand Down Expand Up @@ -66,11 +67,28 @@
} else if (s.kind === "hold") {
b = ws.newBlock("pokecon_hold");
var target = String(s.target || "Button.A");
setFields(b, { TARGET: target, WAIT: 0.1 });
// chord 連鎖の持ち時間(at_ms 由来の wait も同じ欄で来る)。
// 数値が無ければ従来どおり既定の 0.1。
var holdWait = Number(s.wait);
if (!isFinite(holdWait)) {
holdWait = 0.1;
}
setFields(b, { TARGET: target, WAIT: holdWait });
} else if (s.kind === "hold_end") {
b = ws.newBlock("pokecon_hold_end");
var endTarget = String(s.target || "Button.A");
setFields(b, { TARGET: endTarget });
} else if (s.kind === "wait") {
// chord 連鎖の待ち(変換器は sec・wait のどちらかで秒を載せる)。
// press フォールバックへ落とすと押下になるためここで受ける。
b = ws.newBlock("pokecon_wait");
var sec = Number(s.sec);
if (!isFinite(sec)) {
sec = Number(s.wait);
}
if (isFinite(sec)) {
setFields(b, { SEC: sec });
}
} else if (s.kind === "release_stick") {
// 直前の motion 系の END を RELEASE にする(ブロックは増やさない)。
if (lastMotionBlock) {
Expand All @@ -80,6 +98,10 @@
}
lastMotionBlock = null;
return null;
} else if (s.kind === "press") {
// chord 連鎖・motion クリップ外の press は従来の pokecon_press 扱い。
// 欄への具体値は insertSteps 側の press フォールバックが付ける。
return null;
} else {
return null;
}
Expand Down Expand Up @@ -637,11 +659,12 @@

// 記録した手順をブロックの並びにして挿入する。
// steps は [{target, duration, wait}](press 手順、services/blockly_record
// の形)か、motion 材料({kind: stick/stick2/hold/hold_end/release_stick})
// の混在。target は Button.A / Hat.TOP / Direction.UP の形で、BUTTON 欄の
// の形。{kind: 'press', ...} も同じ扱い)か、motion 材料({kind:
// stick/stick2/hold/hold_end/wait/release_stick})の混在。target は
// Button.A / Hat.TOP / Direction.UP の形で、BUTTON 欄の
// 書式に合わせる(ボタンだけ素の名 A、方向は Hat./Direction. のまま)。
// motion 材料は pokecon_motion / pokecon_motion2・pokecon_hold・
// pokecon_hold_end ブロックになる。
// pokecon_hold_end・pokecon_wait ブロックになる。
// 挿入位置は、選択中の文ブロックがあればその直後(元の後続は最後の
// 新ブロックの後ろへつけ直す)、無ければプログラムの DO の末尾、
// プログラムが無ければ空いている所。全体を1つの取り消し単位にする。
Expand Down
8 changes: 4 additions & 4 deletions SerialController/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ def default_sections() -> dict[str, dict[str, Any]]:
"is_use_keyboard": True,
"is_use_gamepad": False,
"gamepad_index": 0,
# ゲームパッドのスティック遊び(0〜8192、既定0=なし)。
# XInput 既定 7849・約24%、Pro コン実測ブレ考慮なら 1600〜2600。
"gamepad_deadzone": 0,
# ゲームパッドのスティック遊び(0〜8192、既定1638=最大値の5%)。
# XInput 既定 7849・約24% より浅く、Pro コン実測ブレ(1600前後)を抑える。
"gamepad_deadzone": 1638,
"is_use_left_stick_mouse": False,
"is_use_right_stick_mouse": False,
"is_take_stick_log": False,
Expand Down Expand Up @@ -270,7 +270,7 @@ def complete_missing(parser: configparser.ConfigParser) -> list[str]:
"baud_rate": (9600, lambda value: value > 0),
"fps": (45, lambda value: value in (5, 15, 30, 45, 60)),
"gamepad_index": (0, lambda value: 0 <= value <= 3),
"gamepad_deadzone": (0, lambda value: 0 <= value <= 8192),
"gamepad_deadzone": (1638, lambda value: 0 <= value <= 8192),
}
for key, (default, valid) in integer_rules.items():
try:
Expand Down
17 changes: 13 additions & 4 deletions SerialController/core/pad_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,22 @@ def scan(self) -> list[int]:
return sorted(found)

def names(self) -> dict[int, str]:
"""番号 → 表示名。SDL で分かる分だけ載る。"""
"""番号 → 表示名。XInput 側は汎名、SDL で分かる分は実名で返す。"""
try:
names = self.sdl.names()
return {int(k): str(v) for k, v in names.items()}
out = {int(k): str(v) for k, v in names.items()}
except Exception:
return {}
out = {}
for index in range(MAX_PADS):
if index in out:
continue
try:
state = self.xinput.read(index)
except Exception:
continue
if state.connected:
out[index] = "XInput パッド"
return out

def route_of(self, index: int) -> str:
"""その番号がいまどちらの口で読めているか(検証・表示用)。"""
return self._route.get(int(index), "")
38 changes: 22 additions & 16 deletions SerialController/core/serial/sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -1723,10 +1723,9 @@ def _logLiveError(self) -> None:
def startLiveWorker(self, transport: Any = None) -> bool:
"""workerを1本だけ起動し、現在姿勢を最初に送る。

確認と起動を _live_lock の中で行う。二重起動で worker が
漏れると、古い方が止められず線が2本になる。
準備(列への投入)は外で行う。錠の中で putLive すると
同じ錠を取り直して詰まる(Lock は再入不可)。
確認・登録・起動を _live_lock の一つの臨界区間で行う。二重起動で
worker が漏れると、古い方が止められず線が2本になる。
準備の写し(snapshot)は外で取る。姿勢の錠と順序逆転で詰むため。
動いているときは帳簿に触らず False で抜ける。
"""
return self._startLiveWorker(transport) is not None
Expand All @@ -1742,25 +1741,23 @@ def _startLiveWorker(self, transport: Any = None) -> threading.Thread | None:
transport = getattr(self, "transport", None)
if transport is None:
return None
# 起動準備の写しは錠の外で取る。snapshot は姿勢の錠(_lock)を
# 取るため、_live_lock の中で呼ぶと順序逆転で詰む(Lock は再入不可)。
# putLive は scheduler 側の錠だけを取り、_live_lock は統計の
# 一瞬だけなので、ここで呼んでも起動の臨界区間には入らない。
pending_snap = self.snapshot()
with self._live_lock:
thread = self._live_thread
if thread is not None and thread.is_alive():
return None
self._live_stop.clear()
with self._live_lock:
self._live_stop.clear()
self._live_last_snap = None
self._live_last_sent_at = None
self._live_last_shown_at = None
self._live_last_shown_snap = None
self.putLive(self.snapshot())
with self._live_lock:
thread = self._live_thread
if thread is not None and thread.is_alive():
return None
# 生成・登録・起動まで錠の中で行う。登録と起動の間に錠を離すと、
# 別の起動が「登録済みだが未起動(is_alive False)」を見て
# もう1本起こし、線が2本になる。start() は錠を取らないため、
# ここで起こしても詰まらない。
# 生成・登録・起動まで一つの臨界区間で行う。確認と起動の
# 間に錠を離すと、並行した起動が同じ隙へ入り線が2本になる。
# start() は錠を取らないため、ここで起こしても詰まらない。
started = threading.Thread(
target=self._liveLoop,
args=(transport,),
Expand All @@ -1769,7 +1766,16 @@ def _startLiveWorker(self, transport: Any = None) -> threading.Thread | None:
)
self._live_thread = started
started.start()
return started
started_alive = started.is_alive()
if not started_alive:
# 起動直後に死んでいたら登録を外す(isLiveWorkerRunning と
# 二重起動防止の判定が「死んだ登録」を残さないため)。
with self._live_lock:
if self._live_thread is started:
self._live_thread = None
return None
self.putLive(pending_snap)
return started

def discardLive(self) -> bool:
"""scheduler の未送出を破棄する。
Expand Down
Loading
Loading