🌡️

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

2022/01/03に公開

知人が農業をやっているのですが、温度や湿度などを計測できないかと相談を受けました。
ラズベリーパイと無線マイコン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

接続図

子機側

Image from Gyazo

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

親機側

Image from Gyazo

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をベースに修正します。

コード

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をベースに修正します。

コード

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

コード

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