Libre Calc Macro : Python threadを使う(旧バージョン)

Libre Calc

こちらは、旧バージョンで、過去の調査結果を残しているものです。
新しい調査結果は、こちらです。

Libre CalcでPython threadを使ってみました。
マクロを列動作させたい案件があったので、その下調べを行いました。

■ 実現したいこと

最終的には、Libre Calcのマクロ上でWebカメラのライブビュー(カメラのモニター)を作りたいと思っています。カメラ画像を表示しながらLibre Officeの操作を行いたいと考えています。
今回は、その下準備として、Libre Calc + Python threadが使用可能かどうかを調べてみました。
使えるのか、使えるとしたら、制約等はあるのかなどを調べました。

■ テストプログラム

動作確認を行うためのPythonスクリプトとシートです。
Pythonスクリプト:heartbeat_start()を実行すると、シート上のheartbeat1がカウントアップします。カウントアップ中にthread起動/停止フラグを0にセットするとスレッドを停止します。

〇 テストシート

〇 Pythonコード(対象部分のみ抜粋)

下記コードは、長くなるので、主要部分だけをぬきだしてあります。
削った部分のコードは、以下のセルアクセスIFです。
・prop_ctrl_xxx()は、thread起動/停止フラグの取得、設定を行います。
・prop_heartbeat1_xxx()は、heartbeat1の取得、設定を行います。
・prop_heartbeat2_xxx()は、heartbeat2の取得、設定を行います。
、prop_heartbeat3_xxx()は、heartbeat3の取得、設定を行います。

# coding: utf-8
from __future__ import unicode_literals

import uno
import threading
import time


#
#	purpose	:	entry to count heartbeat.
#
def heartbeat_monitor():
	heartbeat = 0

	while(True):
		if (prop_ctrl_get() == 0):
			break
	
		prop_heartbeat1_set(heartbeat)
		heartbeat = heartbeat + 1
		time.sleep(0.5)

#
#	purpose	:	entry to start heartbeat thread
#
def heartbeat_start(*args):
	prop_ctrl_set(1)
	prop_heartbeat1_set(0)
	prop_heartbeat2_set(0)
	prop_heartbeat3_set(0)

	th = threading.Thread(target=heartbeat_monitor)
	th.setDaemon(True)
	th.start()

■ 実行結果

Pythonスクリプトを実行した後は、普通に動作しています。
しかし、別のLibre Office文書を開く、または、閉じるとthreadが停止してしまいました。
原因は、XSCRIPTCONTEXT.getDocument()でドキュメントを取得してセルにアクセスしていることにありました。
アプリ起動時、終了時には、この参照でExceptionを起こしてしまうみたいです。

XSCRIPTCONTEXT.getDocument()を参照しないようにすればいいと思いますが、Calcのマクロである以上は、なかなか難しいような気がします。
しかも、サンプルでは、Pythonの外部変数の代わりにセルを参照させているので、回避不能です。

■ Libre 文書オープン時のthread停止対策

私の最終目は、WEBカメラのライブビューを表示しながら、撮影画像の確認ができればよく、シビアなタイミングで制御を行いたいわけでありません。
ここで、目を付けたのが、Basicマクロ。
Basicマクロは、Pythonのような制約はないので、Libre Officeで他文書をオープンしても止まることはありません。
少々汚い手ですが、以下のような再起動手順を組み込んでみました。
① BasicマクロでThread停止、Python関数のException検知を行い、スレッド停止チェックを行う
② ①でスレッドが停止していたら、以下を実施
・自身をアクティブにする→XSCRIPTCONTEXT.getDocument()を参照可能にする
・スレッド再起動

結果としては、うまく再起動はできました。
ただ、この方法でも、スレッド動作中に別のLibre Ofice文書を閲覧するのは可能ですが、編集するのは無理です。編集中はスレッドを停止するなどの運用を行うしかなさそうです。
私の場合は、自動で再起動してくれるだけでもよいので、Libre Officeの編集は、運用で逃げることにします。

〇 Basic呼び出し部:test()

長いですが、test()がメイン関数です。
heartbeat_examine()が、threadの生存チェック関数。
少々簡易的にExceptionが発生していたら一定時間を待つようにしてあります。

'
'	here's the configuration of this file.
'
Private const cnf_task_yield = 100		' time slice = 100.

Private const cnf_thread_wait = 500		' is the time slice of the target thread

'
'	purpose	:	モニタースレッドの起動
'
sub heartbeat_start()
	Dim pv as object
	Dim sc as object

	on error goto fatal_error
	pv = ThisComponent.getScriptProvider()
	sc = pv.getScript("vnd.sun.star.script:test.py$heartbeat_start?language=Python&location=user")
	sc.invoke(array(), array(), array())
fatal_error:
end sub

'
'	purpose	:	ハートビートのチェック
'
function heartbeat_examine() as integer
	Dim pv as object
	Dim sc as object

	on error goto fatal_error	
	pv = ThisComponent.getScriptProvider()
	sc = pv.getScript("vnd.sun.star.script:test.py$heartbeat?language=Python&location=user")
	heartbeat_examine = sc.invoke(array(), array(), array())
	exit function
fatal_error:
	heartbeat_examine = 0                 # Exception検知時は、常にThread停止として扱う
	Wait(cnf_thread_wait)                 # 対象thread確実に止まるまで待つ
end function

'
'	purpose	:	スレッドテストプログラム
'
sub test()
	Dim cell_ctrl as object
	
	'
	'	起動停止フラグの取得
	'
	cell_ctrl = ThisComponent.Sheets.getbyname("test").getCellByPosition(1, 4)

	'
	'	スレッドのスタート
	'
	call heartbeat_start()
	cnt = 0
	do while (cell_ctrl.Value = 1)		
		'
		' バックグランド処理(今はない)
		'

		
		'
		' モニタースレッドの生存確認
		'
		if (heartbeat_examine() = 0) then
			'
			'	ウインドをアクティブにしないとうまくpythonが動作しないので、切替
			'
			ThisComponent.getCurrentController().getFrame().Activate()
			call heartbeat_start()		
		end if
		
		'
		' タスクのYield
		'
		call DoEvents()	
		call Wait(cnf_task_yield)
 	loop
end sub

〇 Python追加関数:heartbeat()

Basicから一定間隔で呼び出されることを前提にしています。
Exception以外での停止もありえるので、threadの実行遅延を10回検知すると、thread停止と判断して、Basicマクロへ停止中と返すようにしておきます。

#
#	purpose	:	entry to capture image.
#	returns	:	1	...	ok.
#		:	0	...	monitor is dead.
#
def heartbeat(*args):
	if (prop_ctrl_get() != 0):
		# スレッド実行時の処理
		val1 = prop_heartbeat1_get()
		val2 = prop_heartbeat2_get()
		# heartbeat1、heartbeat2を比べて一致していたら、thread遅延と判断
		# 10回以上遅延が続いたら、停止と判断
		if (val1 == val2):                  
			val3 = prop_heartbeat3_get()
			if (val3 >= 10):
				prop_heartbeat3_set(0)
				return(0)
			else:
				prop_heartbeat3_set(val3 + 1)
				return(1)
		else:
			prop_heartbeat2_set(val1) #正常時:heartbeat1をheartbeat2にコピー      
			prop_heartbeat3_set(0)    #リトライカウンタをリセット
			return(1)
	else:
		return(1)             # 未起動時は、常にOK

■ 応用:Webカメラへの応用結果

サンプルプログラムを改造して、OpenCVを使ってライブビューをやってみました。
動きとしては、まあまあ、満足いく結果でした。(今のところは)
こちらは、別途作成中のモノで使う予定です。

■ サンプルプログラム

上記で使用したサンプルプログラムです。
自己責任での御使用をお願いいたします。

タイトルとURLをコピーしました