🌡️

農業IoTの準備として、TWELITEを使った環境情報の取得をやってみた-その1(環境情報の取得)

commits29 min read

知人が農業をやっているのですが、温度や湿度などを計測できないかと相談を受けました。
ラズベリーパイと無線マイコンTWELITEを使って、システムを考えてみました。

本記事のシリーズ

💡やりたいこと

Image from Gyazo

  1. 子機は、環境情報(温度、湿度等)を計測し、親機に送出する
  • 子機は、電池に動作する
    • 太陽光パネルの検討もしましたが、コストが上がるので却下
  • 子機は、送出後スリープし、省電力で動作する
  • 子機は、識別子を持つこととする
  • 簡単に設置できることが必要
  1. 親機は、子機の環境情報を受信し、ストレージに保存する
  • 今回は、csvファイルとして保存する
  1. 利用者は、データを確認できる
  • ※今回は実施しない
  • 後々、解析もできるようにする

子機は、TWELITE DIPを使用します。採用した理由は、

  • 省電力動作ができる
  • 無線通信可能である
  • 中継機としても使用できる(今回は使用なし)

https://mono-wireless.com/jp/products/TWE-Lite-DIP/index.html

また、親機は、ラズベリーパイを使用します。

今回は、データ取得について記載します。
クラウドにデータを送信するなどは省きます。

🏁つっくたもの

子機をブレッドボードで作りました↓。

Image from Gyazo

🔧パーツ一覧

子機と親機で分けて記載します。

子機

no 部品名 個数 備考
1 TWELITE DIP 1 Amazon - マッチ棒アンテナ実装済み
2 環境センサーBMP280 1 Amazon - 温度, 湿度, 気圧計測
3 3.3V出力昇圧DCDCコンバータ 1 秋月電子
4 単3電池 2 1本でも可能
5 電池ケース 1 使用する電池数によって1本用 or 2本用を使用のこと※本記事は2本で実施
6 ブレッドボード 1 必要に応じて
7 SBアダプター TWELITE R2-トワイライター2 & アタッチメントセット 1セット Amazon - TWELITE DIPへのプログラム書き込み

親機

no 部品名 個数 備考
1 ラズベリーパイ 1 今回は3を使用(4でも可能)
2 モノワイヤレスUSBスティック 1 Amazon

TWELITE DIPとモノワイヤレスUSBスティックで通信できます。
2.4GHz帯を使用してますが、独自規格のようです。

接続図

子機側

Image from Gyazo

TWELITEの入力電圧は、2.3V~3.6Vです。
電池1本でも動作させたかったため、昇圧し3.3VにしてTWELITEに入力します。

親機側

Image from Gyazo

USBを差し込むだけです。

TWELITE DIPおよびモノワイヤレスUSBスティックとも後述するファームウェアの書き込みが必要です。

💻環境

開発環境

作成するファームは3つとなります。

Image from Gyazo

子機側

  • ①子機側ファーム
    • Windows10 Home Edition - TWELITE Stage v1-0-9

親機側

  • ②モノワイヤレスUSBスティック側のファームウェア
    • Windows10 Home Edition - TWELITE Stage v1-0-9
  • ③モノワイヤレスUSBスティック制御用アプリ
    • RaspberryPi - Python3.7

TWELITE環境の構築

TWELITE Stageのインストールおよび操作方法については、公式のサイトにて確認ください。

  • TWELITE Stageのダウンロードサイト

https://mono-wireless.com/jp/products/stage/index.html
  • TWELITEのプログラミング

https://mono-wireless.com/jp/products/act/index.html
  • ライブラリの詳細
    • 各OS毎のインストール方法
    • サンプルプログラムの説明
    • API仕様の詳細

https://mwx.twelite.info/

RaspberryPiの補足

モジュール関連

シリアル通信のためにpyserial、
csvファイルの作成のためにpandasをインストールします。

$ sudo apt-get install python-pandas
$ python3 -m venv env
$ source env/bin/activate
(env) $ pip install pyserial
(env) $ pip install pandas

モノスティックの認識

モノスティックは、/dev/ttyUSB*にて認識されます。

 $ ls -l /dev/ttyUSB*
crw-rw---- 1 root dialout 188, 1  13 12:23 /dev/ttyUSB0

📝手順

各プログラムの作成について記載します。
方針として、サンプルプログラムに対し修正を行います。

①子機側ファーム

子機側ファームは、サンプルプログラム - ActEx_Sns_BME280_SHT30をベースに修正します。

Windowsの場合、SDKがインストールされると、↓に配置されます
.\MWSTAGE\MWSDK\Act_extras\ActEx_Sns_BME280_SHT30

コード

// !!!add とコメントを入れているところがサンプルプログラムから改変した箇所です。

ActEx_Sns_BME280_SHT30.cpp
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <SNS_BME280>
#include <SNS_SHT3X>
#include <STG_STD>

/// if use with PAL board, define this.
#undef USE_PAL 
#ifdef USE_PAL
// X_STR makes string literals.
#define X_STR(s) TO_STR(s)
#define TO_STR(s) #s
#include X_STR(USE_PAL) // just use with PAL board (to handle WDT)
#endif

/*** Config part */
// application ID
const uint32_t DEF_APPID = 0x1234abcd;
uint32_t APP_ID = DEF_APPID;

// channel
uint8_t CHANNEL = 13;

// id
uint8_t u8ID = 0;

// application use
const char_t APP_NAME[] = "ENV SENSOR";
const uint8_t FOURCHARS[] = "SBS1";

uint8_t u8txid = 0;
uint32_t u32tick_tx;

// very simple state machine
enum class E_STATE {
    INIT = 0,
    CAPTURE_PRE,
    CAPTURE,
    TX,
    TX_WAIT_COMP,
	SETTING_MODE,
    SUCCESS,
    ERROR
};
E_STATE eState = E_STATE::INIT;

/*** Local function prototypes */
void sleepNow();

/*** sensor objects */
SNS_BME280 sns_bme280;
char_t bme280_model[8] = "BME280";
bool b_found_bme280 = false;
bool b_bme280_w_humid = false;

SNS_SHT3X sns_sht3x;
bool b_found_sht3x = false;

uint16_t u16_volt_vcc = 0;
uint16_t u16_volt_a1 = 0;

/*** setup procedure (run once at cold boot) */
void setup() {

	// !!!add - wait
	delay(1000); // just in case, wait for devices to listen furthre I2C comm.
	Serial << crlf << format("run : setup() - %08Xms", 1000*1);

	/*** SETUP section */
#ifdef USE_PAL
	/// use PAL board (for WDT handling)
    auto&& brd = the_twelite.board.use<USE_PAL>(); // register board (PAL)
#endif

	/// interactive mode settings
	auto&& set = the_twelite.settings.use<STG_STD>();
			// declare to use interactive setting.
			// once activated, use `set.serial' instead of `Serial'.

	set << SETTINGS::appname(APP_NAME)          // set application name appears in interactive setting menu.
		<< SETTINGS::appid_default(DEF_APPID); // set the default application ID.

	set.hide_items(
		  E_STGSTD_SETID::POWER_N_RETRY
		, E_STGSTD_SETID::OPTBITS
		, E_STGSTD_SETID::OPT_DWORD2
		, E_STGSTD_SETID::OPT_DWORD3
		, E_STGSTD_SETID::OPT_DWORD4
		, E_STGSTD_SETID::ENC_MODE
		, E_STGSTD_SETID::ENC_KEY_STRING
	);

	// read SET pin (DIO12)
	pinMode(PIN_DIGITAL::DIO12, PIN_MODE::INPUT_PULLUP);
	if (digitalRead(PIN_DIGITAL::DIO12) == LOW) {
		set << SETTINGS::open_at_start();      // start interactive mode immediately.
		eState = E_STATE::SETTING_MODE;        // set state in loop() as dedicated mode for settings.
		return;                                // skip standard initialization.
	}

	// acquired EEPROM saved data	
	set.reload(); // must call this before getting data, if configuring method is called.

	APP_ID = set.u32appid();
	CHANNEL = set.u8ch();
	u8ID = set.u8devid();

	// the twelite main object.
	the_twelite
		<< TWENET::appid(APP_ID)     // set application ID (identify network group)
		<< TWENET::channel(CHANNEL); // set channel (pysical channel)

	// Register Network
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
	nwk << NWK_SIMPLE::logical_id(u8ID); // set Logical ID. (0xFE means a child device with no ID)

	/*** BEGIN section */
	/*** INIT message */
	Serial << crlf << "--- " << APP_NAME << ":" << FOURCHARS << " ---";

	Serial << crlf << format("..APP_ID   = %08X", APP_ID);
	Serial << crlf << format("..CHANNEL  = %d", CHANNEL);
	Serial << crlf << format("..LID      = %d(0x%02X)", u8ID, u8ID);

	// sensors.setup() may call Wire during initialization.
	Wire.begin(WIRE_CONF::WIRE_100KHZ);

	// setup analogue
	Analogue.setup();

	// check SHT3x
	{
		bool b_alt_id = false;
		sns_sht3x.setup();
		if (!sns_sht3x.probe()) {
			bool b_alt_id = false;
			delayMicroseconds(100); // just in case, wait for devices to listen furthre I2C comm.
			sns_sht3x.setup(0x45); // alternative ID
			if (sns_sht3x.probe()) b_found_sht3x = true;
		} else {
			b_found_sht3x = true;
		}
		if (b_found_sht3x) {
			Serial << crlf << "..found sht3x" << (b_alt_id ? " at 0x45" : " at 0x44");
		}
	}

	delayMicroseconds(100); // just in case, wait for devices to listen furthre I2C comm.
	
	// check BMx280
	{
		bool b_alt_id = false;
		sns_bme280.setup();
		if (!sns_bme280.probe()) {
			b_alt_id = true;
			delayMicroseconds(100); // just in case, wait for devices to listen furthre I2C comm.
			sns_bme280.setup(0x77); // alternative ID
			if (sns_bme280.probe()) b_found_bme280 = true;
		} else {
			b_found_bme280 = true;
		}

		if (b_found_bme280) {
			// check if BME280 or BMP280	
			if ((sns_bme280.sns_stat() & 0xFF) == 0x60) {
				b_bme280_w_humid = true;
			} else
			if ((sns_bme280.sns_stat() & 0xFF) == 0x58) {
				b_bme280_w_humid = false;
				bme280_model[2] = 'P';
			}
			Serial << crlf
				<< format("..found %s ID=%02X", bme280_model, (sns_bme280.sns_stat() & 0xFF))
				<< (b_alt_id ? " at 0x77" : " at 0x76");
		}
	}

	/*** let the_twelite begin! */
	the_twelite.begin(); // start twelite!

}

/*** loop procedure (called every event) */
void loop() {
	bool new_state;

	if (Analogue.available()) {
		if (!u16_volt_vcc) {
			u16_volt_vcc = Analogue.read(PIN_ANALOGUE::VCC);
			u16_volt_a1 =  Analogue.read(PIN_ANALOGUE::A1);
		}
	}

	do {
		new_state = false;

		switch(eState) {			
		case E_STATE::SETTING_MODE: // while in setting (interactive mode)
			break;

		case E_STATE::INIT:
			Serial << crlf << format("..%04d/start sensor capture.", millis() & 8191);
			
			// start sensor capture
			Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.

			if (b_found_sht3x) {
				sns_sht3x.begin();
			}

			if (b_found_bme280) {
				sns_bme280.begin();
			}

			eState =  E_STATE::CAPTURE_PRE;
			break;

		case E_STATE::CAPTURE_PRE: // wait for sensor capture completion
			if (TickTimer.available()) {
				if (b_found_bme280 && !sns_bme280.available()) {
					sns_bme280.process_ev(E_EVENT_TICK_TIMER);
				}
				if (b_found_sht3x && !sns_sht3x.available()) {
					sns_sht3x.process_ev(E_EVENT_TICK_TIMER);
				}

				// both sensors are finished.
				if (	(!b_found_bme280 || (b_found_bme280 && sns_bme280.available()))
					&&	(!b_found_sht3x  || (b_found_sht3x && sns_sht3x.available()))
				) {
					new_state = true; // do next state immediately.
					eState =  E_STATE::CAPTURE;
				}
			}
			break;

		case E_STATE::CAPTURE: // display sensor results
			if (b_found_sht3x) {
					Serial 
						<< crlf << format("..%04d/finish sensor capture.", millis() & 8191)
						<< crlf << "  SHT3X    : T=" << sns_sht3x.get_temp() << 'C'
						<< " H=" << sns_sht3x.get_humid() << '%';
			}
			if (b_found_bme280) {
					Serial
						<< crlf << "  " << bme280_model << "   : T=" << sns_bme280.get_temp() << 'C'
						<< " P=" << int(sns_bme280.get_press()) << "hP";
				if (b_bme280_w_humid)
					Serial 
						<< " H=" << sns_bme280.get_humid() << '%';
			}
			if (1) {
					Serial
						<< crlf << format("  ADC      : Vcc=%dmV A1=%04dmV", u16_volt_vcc, u16_volt_a1);
			}
			
			new_state = true; // do next state immediately.
			eState =  E_STATE::TX;
			break;

		case E_STATE::TX: // place TX packet requiest.
			eState = E_STATE::ERROR; // change this when success TX request...

			if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
				// set tx packet behavior
				pkt << tx_addr(0x00)  // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
					<< tx_retry(0x1) // set retry (0x1 send two times in total)
					<< tx_packet_delay(0, 0, 2); // send packet w/ delay

				// prepare packet payload
				pack_bytes(pkt.get_payload() // set payload data objects.
					, make_pair(FOURCHARS, 4)  // just to see packet identification, you can design in any.
					, uint16_t(sns_sht3x.get_temp_cent()) // temp
					, uint16_t(sns_sht3x.get_humid_per_dmil())
					, uint16_t(sns_bme280.get_temp_cent()) // temp
					, uint16_t(sns_bme280.get_humid_per_dmil())
					, uint16_t(sns_bme280.get_press())
					, uint16_t(u16_volt_vcc)
					, uint16_t(u16_volt_a1)
				);

				// do transmit
				MWX_APIRET ret = pkt.transmit();
				Serial << crlf << format("..%04d/transmit request by id = %d.", millis() & 8191, ret.get_value());
				
				if (ret) {
					u8txid = ret.get_value() & 0xFF;
					u32tick_tx = millis();
					eState = E_STATE::TX_WAIT_COMP;
				} else {
					Serial << crlf << "!FATAL: TX REQUEST FAILS. reset the system." << crlf;
				}
			} else {
				Serial << crlf << "!FATAL: MWX TX OBJECT FAILS. reset the system." << crlf;
			}
			break;

		case E_STATE::TX_WAIT_COMP: // wait TX packet completion.
			if (the_twelite.tx_status.is_complete(u8txid)) {
				Serial << crlf << format("..%04d/transmit complete.", millis() & 8191);
		
				// success on TX
				eState = E_STATE::SUCCESS;
				new_state = true;
			} else if (millis() - u32tick_tx > 3000) {
				Serial << crlf << "!FATAL: MWX TX OBJECT FAILS. reset the system." << crlf;
				eState = E_STATE::ERROR;
				new_state = true;
			} 

        	// !!!add - error
			if( false == b_found_bme280 )
			{
				Serial << crlf << "!FATAL: not found bme280. reset the system." << crlf;
				eState = E_STATE::ERROR;
				new_state = true;
			}
			break;

		case E_STATE::ERROR: // FATAL ERROR
			Serial.flush();
			delay(100);
			the_twelite.reset_system();
			break;

		case E_STATE::SUCCESS: // NORMAL EXIT (go into sleeping...)
			sleepNow();
			break;
		}
	} while(new_state);
}

// perform sleeping
void sleepNow() {
	// !!!add - sleep
	// uint32_t u32ct = 1750 + random(0,500);
	uint32_t u32ct = 60 * 1000 * 5;	//5 min
	Serial << crlf << format("..%04d/sleeping %dms.", millis() % 8191, u32ct);
	Serial.flush();

	the_twelite.sleep(u32ct);
}

// wakeup procedure
void wakeup() {
	Wire.begin();

	Serial	<< crlf << "--- " << APP_NAME << ":" << FOURCHARS << " wake up ---";

	eState = E_STATE::INIT; // go into INIT state in the loop()
}

ポイント

  • 起動 - setup()

    • 起動時、setup()が呼ばれる
    • 基板内のeepromより、APP_ID, CHANNEL, u8IDをロードする
      • APP_ID : アプリケーションID(子機、親機とも同じIDでないと、親機の通信ができない)
      • CHANNEL : 周波数帯(子機、親機とも同じ周波数帯でないと、親機の通信ができない)
      • u8ID : 子機の識別子として使用する
      • =>↑の設定は、TWELITE Stageのインタラクティブモードより変更可能です
    • check BME280の箇所
      • BME280用のAPIが用意されている
      • I2CラインにBME280のデバイスが見つかった場合、b_found_bme280をtrue。
  • イベントループ - loop()

    • イベント(データを受信するなど)が発生するたびに本関数が呼ばれる
    • 正常時、本loopは以下の状態を遷移する
      • INIT -> CAPTURE_PRE -> CAPTURE -> TX -> TX_WAIT_COMP -> SUCCESS
    • INIT
      • BME280アクセスための準備
    • CAPTURE_PRE, CAPTURE
      • BME280から環境情報の取得
    • TX, TX_WAIT_COMP
      • 環境情報を無線で送信
      • 送信には時間がかかるため、TX_WAIT_COMPで完了待ち
    • SUCCESS
      • 5分を指定し、省電力モードへ遷移
      • =>5分後、起床しwakeup()へ遷移
      • wakeup() -> loop()-INITへ遷移
  • 追加箇所(!!!add)について

    • // !!!add - wait
      • まれに、I2Cアクセスに失敗することがあり、起動時に追加
    • // !!!add - error
      • bme280が見つからない場合は、即時リセットしたかったため追加
    • // !!!add - sleep
      • サンプルプログラムはランダム値になっていたため、明示的に5分に変更

②モノワイヤレスUSBスティック側のファームウェア

親機側ファームは、サンプルプログラム - Parent-MONOSTICKをベースに修正します。

Windowsの場合、SDKがインストールされると、↓に配置されます
.\MWSTAGE\MWSDK\Act_samples\Parent-MONOSTICK

コード

// !!!add とコメントを入れているところがサンプルプログラムから改変した箇所です。
また、本コードは汎用的に作られているため、一部コードを省略しています

Parent-MONOSTICK.cpp
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <MONOSTICK>
#include <STG_STD>

/*** Config part */
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;

/*** function prototype */
bool analyze_payload(packet_rx& rx);

/*** application defs */

/*** setup procedure (run once at cold boot) */
void setup() {
	/*** SETUP section */
	auto&& brd = the_twelite.board.use<MONOSTICK>();
	auto&& set = the_twelite.settings.use<STG_STD>();
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();

	// settings: configure items
	set << SETTINGS::appname("PARENT");
	set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
	set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
	set << SETTINGS::lid_default(0x00); // set default LID
	set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
	set.reload(); // load from EEPROM.
	OPT_BITS = set.u32opt1(); // this value is not used in this example.

	// the twelite main class
	the_twelite
		<< set                    // apply settings (appid, ch, power)
		<< TWENET::rx_when_idle() // open receive circuit (if not set, it can't listen packts from others)
		;

	// Register Network
	nwk << set;							// apply settings (LID and retry)
	nwk << NWK_SIMPLE::logical_id(0x00) // set Logical ID. (0x00 means parent device)
		;

	// configure hardware
	brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
	brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)

	/*** BEGIN section */
	the_twelite.begin(); // start twelite!

	/*** INIT message */
	Serial << "--- MONOSTICK_Parent act ---" << mwx::crlf;
}

/*** loop procedure (called every event) */
void loop() {
}

void on_rx_packet(packet_rx& rx, bool_t &handled) {
	Serial << ".. coming packet (" << int(millis()&0xffff) << ')' << mwx::crlf;

	// output type1 (raw packet)
	//   uint8_t  : 0x01
	//   uint8_t  : src addr (LID)
	//   uint32_t : src addr (long)
	//   uint32_t : dst addr (LID/long)
	//   uint8_t  : repeat count
	//     total 11 bytes of header.
	//     
	//   N        : payload
    // !!!add - enable - raw packet
	if(1) { // this part is disabed.
		serparser_attach pout;
		pout.begin(PARSER::ASCII, rx.get_psRxDataApp()->auData, rx.get_psRxDataApp()->u8Len, rx.get_psRxDataApp()->u8Len);

		Serial << "RAW PACKET -> ";
		pout >> Serial;
		Serial.flush();
	}

	// output type2 (generate ASCII FORMAT)
	//  :0DCC3881025A17000000008D000F424154310F0D2F01D200940100006B39
	//   *1*2*3*4------*5------*6*7--*8
	if (1) {
		smplbuf_u8<256> buf;
		pack_bytes(buf
			, uint8_t(rx.get_addr_src_lid())		// *1:src addr (LID)
			, uint8_t(0xCC)							// *2:cmd id (0xCC, fixed)
			, uint8_t(rx.get_psRxDataApp()->u8Seq)	// *3:seqence number
			, uint32_t(rx.get_addr_src_long())		// *4:src addr (long)
			, uint32_t(rx.get_addr_dst())			// *5:dst addr
			, uint8_t(rx.get_lqi())					// *6:LQI
			, uint16_t(rx.get_length())				// *7:payload length
			, rx.get_payload() 						// *8:payload
				// , make_pair(rx.get_payload().begin() + 4, rx.get_payload().size() - 4)
				//   note: if you want the part of payload, use make_pair().
		);

		serparser_attach pout;
		pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
		
		Serial << "ASCII FMT -> ";
		pout >> Serial;
		Serial.flush();
	}

	// packet analyze
	analyze_payload(rx);
}

bool analyze_payload(packet_rx& rx) {
    省略
}

ポイント

  • 起動 - setup()

    • 起動時、setup()が呼ばれる
    • APP_ID, CHANNEL, u8IDを設定する(以下デフォルト値を記載)
      • APP_ID : DEFAULT_APP_ID = 0x1234abcd(※子機と一致しているため、通信可)
      • CHANNEL : DEFAULT_CHANNEL = 13(※子機と一致しているため、通信可)
      • ID : 0を指定(lid_default(0x00), logical_id(0x00))。 0は、ネットワーク上の親機を指す
      • =>↑の設定は、TWELITE Stageのインタラクティブモードより変更可能です
  • データ受信 - on_rx_packet()

    • 子機からのデータ受信時、本関数が呼ばれる
    • 受信したデータを "RAW PACKET -> "の文字列をつけて、シリアル通信で送信する
      • このシリアル通信のデータを③のプログラムで受信し、データをパースする
  • 追加箇所(!!!add)について

    • // !!!add - enable - raw packet
      • データの中身を把握したかったため、生データをみるためにenableに変更
      • ③で本データを解析していますが、"ASCII FMT -> "でも可

③モノワイヤレスUSBスティック制御用アプリ

モノワイヤレスUSBスティック制御用アプリは、twelite_read_write.txtをベースに修正します。

https://mono-wireless.com/jp/products/TWE-APPS/App_Twelite/python_twelite/twelite_read_write.txt

コード

以下の方針で修正しています。
Python2関連のコードの削除、csvファイルの保存処理の追加

app_twelite.py
from serial import *
from sys import stdout, stdin, stderr, exit
import threading
from datetime import datetime
import time
import pandas as pd
import os

ENVTEMP_CSV = "env_temp.csv"
SENSOR_NAME = "twelite"

# global
ser = None # serial port
t1 = None  # thread
bTerm = False # flag

def readThread():
    """Interpret data from the serial port one line at a time.
    """
    global ser, bTerm
    while True:
        time.sleep(0.1)
        if bTerm:
            return
        line = ser.readline().rstrip()
        
        bCommand = False
        bStr = False

        if len(line) > 0:
            print(datetime.now())
            print(line)
            c = line[0]
            # 58 - 0x3a":" <= Delimiter
            if c == 58:
                bCommand = True
                
        if not bCommand and b'RAW PACKET -> :' in line:
            print("recv:", datetime.now())
            add_csv(line)


def DoTerminate():
    """end process
    """
    global bTerm

    # stop - thread
    bTerm = True
    print ("... quitting")
    time.sleep(1.0) # スリープでスレッドの終了待ちをする
    exit(0)


def add_csv(rdata, sensor_name=SENSOR_NAME, csv_file=ENVTEMP_CSV):
    """Create a csv file
    """
    # devid
    val = int(rdata[17:(17+2)], 16)
    devid = str(val)
    # temperature
    val = int(rdata[53:(53+4)], 16)
    val = (-1) * (0x10000 - val) if val > 0x7FFF else val
    temp = str(val / 100)
    # humidity
    hum = str(int(rdata[57:(57+4)], 16) / 100)
    # pressure
    pressure = str(int(rdata[61:(61+4)], 16))
    # row data
    l = [sensor_name, datetime.now(), devid, temp, hum, pressure]
    # header?
    hd_flg = False if os.path.isfile(ENVTEMP_CSV) else True
    df = pd.DataFrame([l], columns=['date', 'sensor', 'devid', 'temp', 'hum', 'pressure'])
    df.to_csv(csv_file, index=False, encoding="utf-8", mode='a', header=hd_flg)
    return



if __name__=='__main__':
    # Check the parameters.
    if len(sys.argv) != 2:
        print ("%s {serial port name}" % sys.argv[0])
        exit(1)
    
    # Open the serial port.
    try:
        ser = Serial(sys.argv[1], 115200, timeout=0.1)
        print ("open serial port: %s" % sys.argv[1])
    except:
        print ("cannot open serial port: %s" % sys.argv[1])
        exit(1)
        
    # Start the read thread.
    t1 = threading.Thread(target=readThread)
    t1.setDaemon(True)
    t1.start()
    
    # Input processing from stdin
    while True:
        time.sleep(1.0)
        try:
            l = stdin.readline().rstrip()
            
            if len(l) > 0:
                if l[0] == 'q': # quit
                    DoTerminate()
                    
                if l[0] == ':': # tx
                    cmd = l + "\r\n"
                    print ("--> "+ l)
                    ser.write(cmd)
        except KeyboardInterrupt: # Ctrl+C
            DoTerminate()
        except SystemExit:
            exit(0)
        except:
            print ("... unknown exception detected")
            break
    
    exit(0)

実行例

^C... quitting
(env) $ python app_twelite.py /dev/ttyUSB0
open serial port: /dev/ttyUSB0
2022-01-03 21:01:26.924440
b'.. coming packet (59050)'
2022-01-03 21:01:27.029359
b'RAW PACKET -> :010D810BFBA30000000000534253310000000009140B7703E80D0009A36C'
recv: 2022-01-03 21:01:27.029540
2022-01-03 21:01:27.148278
b'ASCII FMT -> :0DCC01810BFBA300000000840012534253310000000009140B7703E80D0009A30A'
2022-01-03 21:01:27.251004
b'SBS1(ID=13/LQ=132)-> ..not analyzed..'
2022-01-03 21:06:27.222859
b'.. coming packet (31644)'
2022-01-03 21:06:27.327868
b'RAW PACKET -> :010D810BFBA30000000000534253310000000009C50A7003E80D0009A3C3'
recv: 2022-01-03 21:06:27.328069
2022-01-03 21:06:27.444305
...

作成されるcsvファイルは、以下のような感じです。

date sensor devid temp hum pressure
2022-01-03 21:00:02.709852 twelite 13 22.97 30.2 1000
2022-01-03 21:00:24.469994 twelite 13 23.84 28.9 1000
2022-01-03 21:01:27.029656 twelite 13 23.24 29.35 1000
2022-01-03 21:06:27.328218 twelite 13 25.01 26.72 1000
2022-01-03 21:11:27.546072 twelite 13 25.24 26.56 1000
2022-01-03 21:16:27.772859 twelite 13 25.41 26.32 1000
2022-01-03 21:21:27.943229 twelite 13 25.65 26.14 1000
2022-01-03 21:23:08.479403 twelite 13 24.63 27.07 1000
2022-01-03 21:28:08.676965 twelite 13 26.02 25.74 1000

ポイント

  • 受信スレッド - readThread
    • シリアルで下記のコマンドを監視します
    • b'RAW PACKET -> :010D810BFBA30000000000534253310000000009C50A7003E80D0009A3C3'

重要な部分を記載します。

no data 役割 備考
0バイト目 01 スタートデータ 固定値
1バイト目 0D デバイスID 子機に割り振られたID
19-20バイト目 09C5 温度 0x09C5 / 100 => 25.01℃
21-22バイト目 0A70 湿度 0x0A70 / 100 => 26.72%
23-24バイト目 03E8 気圧 0x03E8 => 1000hPa

さいごに

1日程度エージングを行い、温度/湿度とも安定して取得できていることを確認できました。
実際の運用を考えると足りない部分がまだまだあるので、いろいろと検討していきたいと思います。

GitHubで編集を提案

Discussion

ログインするとコメントできます