From a111c54582041733db235deaecb07257ad93b008 Mon Sep 17 00:00:00 2001 From: "COT001\\DEV" <871066422@qq.com> Date: Thu, 9 Apr 2026 09:22:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=87=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/600泵/config.json | 2 +- configs/600泵/table.py | 294 +-- pcm_influxdb/config-1.2-debug.yaml | 300 +++ pcm_influxdb/docker-compose.yml | 101 + pcm_influxdb/pcm-influxdb-debug.py | 24 +- pcm_influxdb/pcm-influxdb-debug0402 copy.py | 2148 +++++++++++++++++++ pcm_influxdb/pcm-influxdb-debug0402.py | 2148 +++++++++++++++++++ 7 files changed, 4892 insertions(+), 125 deletions(-) create mode 100644 pcm_influxdb/config-1.2-debug.yaml create mode 100644 pcm_influxdb/docker-compose.yml create mode 100644 pcm_influxdb/pcm-influxdb-debug0402 copy.py create mode 100644 pcm_influxdb/pcm-influxdb-debug0402.py diff --git a/configs/600泵/config.json b/configs/600泵/config.json index 4cbcb4e..fe25013 100644 --- a/configs/600泵/config.json +++ b/configs/600泵/config.json @@ -167,7 +167,7 @@ "10" ] ], - "scriptFile": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uDQojIC0qLSBjb2Rpbmc6IHV0Zi04IC0qLQ0KIiIiDQrmtYvor5Xpg6jkvY3muKnluqborrDlvZXooajnlJ/miJDohJrmnKzvvIjluKbotJ/ovb3nirbmgIHnrZvpgInvvIkNCg0KLSDlv73nlaXkvKDlhaXnmoQgZXhwZXJpbWVudFByb2Nlc3PvvIzoh6rooYzmnoTpgKDlm7rlrprnu5PmnoTnmoTmlbDmja4NCi0g5LuOIEluZmx1eERCIOafpeivouavj+S4qua1i+ivlemDqOS9jeWcqOWQhOaXtumXtOeCueeahOeerOaXtua4qeW6puWAvA0KLSDmt7vliqAgbG9hZF9zdGF0dXMgPSAxIOeahOetm+mAieadoeS7tu+8jOehruS/neWPquWcqOecn+ato+mHh+mbhuaVsOaNruaXtuiOt+WPlua4qeW6pg0KLSDovpPlh7rmoLzlvI/kuI7lupTnlKjkuK3nmoQgc2NyaXB0VGFibGUg5Y2g5L2N56ym5YW85a65DQotIOm7mOiupOaKiiB7c2NyaXB0VGFibGUxfSDmlL7lnKgi5rWL6K+V6YOo5L2NIuaJgOWcqOeahOWNleWFg+agvA0KDQrnjq/looPlj5jph4/vvJoNCiAgICBUQUJMRV9UT0tFTiAgICAgICAgIOebruagh+WNoOS9jeespu+8jOm7mOiupCBzY3JpcHRUYWJsZTENCiAgICBUQUJMRV9TVEFSVF9ST1cgICAgIOWGmeWFpei1t+Wni+ihjOWBj+enu++8jOm7mOiupCAwDQogICAgVEFCTEVfU1RBUlRfQ09MICAgICDlhpnlhaXotbflp4vliJflgY/np7vvvIzpu5jorqQgMA0KICAgIFRBQkxFX1RJTUVfU0xPVFMgICAg6YCX5Y+35YiG6ZqU55qE5pe26Ze05Yi75bqm77yM6buY6K6kICIwLjVoLDFoLDEuNWgsMmgsMi41aCwzaCwzLjVoIg0KICAgIFRBQkxFX01PVE9SX1NQRUVEICAg55S15py66L2s6YCf5qCH562+77yM6buY6K6kICI5ODBSUE0iDQogICAgRVhQRVJJTUVOVF9TVEFSVCAgICAg5a6e6aqM5byA5aeL5pe26Ze077yISVNPIDg2MDEg5qC85byP77yM5aaCIDIwMjQtMDEtMDFUMTA6MDA6MDBa77yJDQogICAgRVhQRVJJTUVOVF9FTkQgICAgICAg5a6e6aqM57uT5p2f5pe26Ze077yISVNPIDg2MDEg5qC85byP77yJDQogICAgSU5GTFVYX1VSTCAgICAgICAgICAgSW5mbHV4REIgVVJMDQogICAgSU5GTFVYX09SRyAgICAgICAgICAgSW5mbHV4REIg57uE57uHDQogICAgSU5GTFVYX1RPS0VOICAgICAgICAgSW5mbHV4REIg5Luk54mMDQogICAgSU5GTFVYX0JVQ0tFVCAgICAgICAgSW5mbHV4REIg5qG25ZCN77yM6buY6K6kIFBDTQ0KICAgIElORkxVWF9NRUFTVVJFTUVOVCAgIEluZmx1eERCIOa1i+mHj+WQje+8jOm7mOiupCBQQ01fTWVhc3VyZW1lbnQNCiIiIg0KDQpmcm9tIF9fZnV0dXJlX18gaW1wb3J0IGFubm90YXRpb25zDQoNCmltcG9ydCBqc29uDQppbXBvcnQgbG9nZ2luZw0KaW1wb3J0IG9zDQppbXBvcnQgc3lzDQpmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRldGltZSwgdGltZWRlbHRhDQpmcm9tIHR5cGluZyBpbXBvcnQgQW55LCBEaWN0LCBMaXN0LCBPcHRpb25hbA0KDQoNCkxPR0dFUiA9IGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKQ0KDQoNCmRlZiBfbWFza19zZWNyZXQodmFsdWU6IE9wdGlvbmFsW3N0cl0pIC0+IHN0cjoNCiAgICAiIiLmjqnnoIHmlY/mhJ/kv6Hmga8iIiINCiAgICBpZiBub3QgdmFsdWU6DQogICAgICAgIHJldHVybiAiPGVtcHR5PiINCiAgICBpZiBsZW4odmFsdWUpIDw9IDg6DQogICAgICAgIHJldHVybiAiKiIgKiBsZW4odmFsdWUpDQogICAgcmV0dXJuIHZhbHVlWzo0XSArICIqIiAqIChsZW4odmFsdWUpIC0gOCkgKyB2YWx1ZVstNDpdDQoNCg0KZGVmIF9zZXR1cF9sb2dnaW5nKCkgLT4gTm9uZToNCiAgICAiIiLorr7nva7ml6Xlv5ciIiINCiAgICBsb2dfbGV2ZWxfc3RyID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX0xPR19MRVZFTCIsICJERUJVRyIpLnVwcGVyKCkNCiAgICBsb2dfbGV2ZWwgPSBnZXRhdHRyKGxvZ2dpbmcsIGxvZ19sZXZlbF9zdHIsIGxvZ2dpbmcuREVCVUcpDQogICAgDQogICAgIyDphY3nva7moLnml6Xlv5forrDlvZXlmagNCiAgICBsb2dnaW5nLmJhc2ljQ29uZmlnKA0KICAgICAgICBsZXZlbD1sb2dfbGV2ZWwsDQogICAgICAgIGZvcm1hdD0nJShhc2N0aW1lKXMgWyUobGV2ZWxuYW1lKXNdICUobmFtZSlzOiAlKG1lc3NhZ2UpcycsDQogICAgICAgIGhhbmRsZXJzPVsNCiAgICAgICAgICAgIGxvZ2dpbmcuU3RyZWFtSGFuZGxlcihzeXMuc3RkZXJyKQ0KICAgICAgICBdDQogICAgKQ0KICAgIA0KICAgICMg5aaC5p6c5oyH5a6a5LqG5pel5b+X5paH5Lu277yM5re75Yqg5paH5Lu25aSE55CG5ZmoDQogICAgbG9nX2ZpbGUgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfTE9HX0ZJTEUiLCAiIikuc3RyaXAoKQ0KICAgIGlmIGxvZ19maWxlOg0KICAgICAgICB0cnk6DQogICAgICAgICAgICBmaWxlX2hhbmRsZXIgPSBsb2dnaW5nLkZpbGVIYW5kbGVyKGxvZ19maWxlLCBlbmNvZGluZz0ndXRmLTgnKQ0KICAgICAgICAgICAgZmlsZV9oYW5kbGVyLnNldExldmVsKGxvZ19sZXZlbCkNCiAgICAgICAgICAgIGZpbGVfaGFuZGxlci5zZXRGb3JtYXR0ZXIobG9nZ2luZy5Gb3JtYXR0ZXIoDQogICAgICAgICAgICAgICAgJyUoYXNjdGltZSlzIFslKGxldmVsbmFtZSlzXSAlKG5hbWUpczogJShtZXNzYWdlKXMnDQogICAgICAgICAgICApKQ0KICAgICAgICAgICAgbG9nZ2luZy5nZXRMb2dnZXIoKS5hZGRIYW5kbGVyKGZpbGVfaGFuZGxlcikNCiAgICAgICAgICAgIExPR0dFUi5pbmZvKCLml6Xlv5fmlofku7blt7LphY3nva46ICVzIiwgbG9nX2ZpbGUpDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCLphY3nva7ml6Xlv5fmlofku7blpLHotKU6ICVzIiwgZSkNCg0KDQpkZWYgX2dldF9pbmZsdXhfY29uZmlnKCkgLT4gRGljdFtzdHIsIHN0cl06DQogICAgIiIi6I635Y+WSW5mbHV4RELphY3nva4iIiINCiAgICBjb25maWcgPSB7DQogICAgICAgICd1cmwnOiBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX1VSTCIsICIiKS5zdHJpcCgpLA0KICAgICAgICAnb3JnJzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9PUkciLCAiIikuc3RyaXAoKSwNCiAgICAgICAgJ3Rva2VuJzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9UT0tFTiIsICIiKS5zdHJpcCgpLA0KICAgICAgICAnYnVja2V0Jzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9CVUNLRVQiLCAiUENNIikuc3RyaXAoKSwNCiAgICAgICAgJ21lYXN1cmVtZW50Jzogb3MuZW52aXJvbi5nZXQoIklORkxVWF9NRUFTVVJFTUVOVCIsICJQQ01fTWVhc3VyZW1lbnQiKS5zdHJpcCgpLA0KICAgIH0NCiAgICANCiAgICBMT0dHRVIuZGVidWcoDQogICAgICAgICJJbmZsdXhEQumFjee9rjogdXJsPSVzIG9yZz0lcyB0b2tlbj0lcyBidWNrZXQ9JXMgbWVhc3VyZW1lbnQ9JXMiLA0KICAgICAgICBjb25maWdbJ3VybCddIG9yICI8ZW1wdHk+IiwNCiAgICAgICAgY29uZmlnWydvcmcnXSBvciAiPGVtcHR5PiIsDQogICAgICAgIF9tYXNrX3NlY3JldChjb25maWdbJ3Rva2VuJ10pLA0KICAgICAgICBjb25maWdbJ2J1Y2tldCddLA0KICAgICAgICBjb25maWdbJ21lYXN1cmVtZW50J10sDQogICAgKQ0KICAgIA0KICAgIHJldHVybiBjb25maWcNCg0KDQpkZWYgX3BhcnNlX2V4cGVyaW1lbnRfdGltZXMoKSAtPiB0dXBsZVtPcHRpb25hbFtkYXRldGltZV0sIE9wdGlvbmFsW2RhdGV0aW1lXV06DQogICAgIiIi6Kej5p6Q5a6e6aqM5pe26Ze077yM5YmN56uv5Lyg5YWl5pys5Zyw5pe26Ze077yM6L2s5o2i5Li6VVRD55So5LqOSW5mbHV4RELmn6Xor6IiIiINCiAgICBmcm9tIGRhdGV0aW1lIGltcG9ydCB0aW1lem9uZSwgdGltZWRlbHRhDQogICAgDQogICAgc3RhcnRfc3RyID0gb3MuZW52aXJvbi5nZXQoIkVYUEVSSU1FTlRfU1RBUlQiLCAiIikuc3RyaXAoKQ0KICAgIGVuZF9zdHIgPSBvcy5lbnZpcm9uLmdldCgiRVhQRVJJTUVOVF9FTkQiLCAiIikuc3RyaXAoKQ0KICAgIA0KICAgIExPR0dFUi5kZWJ1Zygi5Y6f5aeL5pe26Ze05a2X56ym5LiyOiBTVEFSVD0lcywgRU5EPSVzIiwgc3RhcnRfc3RyLCBlbmRfc3RyKQ0KICAgIA0KICAgIHN0YXJ0X3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSA9IE5vbmUNCiAgICBlbmRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdID0gTm9uZQ0KICAgIA0KICAgIGlmIHN0YXJ0X3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTIiwgIiVZLSVtLSVkVCVIOiVNOiVTLiVmIl06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBzdGFydF90aW1lID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCBmbXQpDQogICAgICAgICAgICAgICAgICAgICMg5pys5Zyw5pe26Ze0LTjlsI/ml7Y9VVRDDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSBzdGFydF90aW1lIC0gdGltZWRlbHRhKGhvdXJzPTgpDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSBzdGFydF90aW1lLnJlcGxhY2UodHppbmZvPXRpbWV6b25lLnV0YykNCiAgICAgICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCLop6PmnpBTVEFSVDog5pys5ZywPSVzIOKGkiBVVEM9JXMiLCBzdGFydF9zdHIsIHN0YXJ0X3RpbWUpDQogICAgICAgICAgICAgICAgICAgIGJyZWFrDQogICAgICAgICAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6DQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgICAgICBpZiBzdGFydF90aW1lIGlzIE5vbmU6DQogICAgICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIuaXoOazleino+aekEVYUEVSSU1FTlRfU1RBUlQ6ICVzIiwgc3RhcnRfc3RyKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBMT0dHRVIuZXJyb3IoIuino+aekEVYUEVSSU1FTlRfU1RBUlTlpLHotKUgJyVzJzogJXMiLCBzdGFydF9zdHIsIGUpDQogICAgDQogICAgaWYgZW5kX3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTIiwgIiVZLSVtLSVkVCVIOiVNOiVTLiVmIl06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBlbmRfdGltZSA9IGRhdGV0aW1lLnN0cnB0aW1lKGVuZF9zdHIsIGZtdCkNCiAgICAgICAgICAgICAgICAgICAgIyDmnKzlnLDml7bpl7QtOOWwj+aXtj1VVEMNCiAgICAgICAgICAgICAgICAgICAgZW5kX3RpbWUgPSBlbmRfdGltZSAtIHRpbWVkZWx0YShob3Vycz04KQ0KICAgICAgICAgICAgICAgICAgICBlbmRfdGltZSA9IGVuZF90aW1lLnJlcGxhY2UodHppbmZvPXRpbWV6b25lLnV0YykNCiAgICAgICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCLop6PmnpBFTkQ6IOacrOWcsD0lcyDihpIgVVRDPSVzIiwgZW5kX3N0ciwgZW5kX3RpbWUpDQogICAgICAgICAgICAgICAgICAgIGJyZWFrDQogICAgICAgICAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6DQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgICAgICBpZiBlbmRfdGltZSBpcyBOb25lOg0KICAgICAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCLml6Dms5Xop6PmnpBFWFBFUklNRU5UX0VORDogJXMiLCBlbmRfc3RyKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBMT0dHRVIuZXJyb3IoIuino+aekEVYUEVSSU1FTlRfRU5E5aSx6LSlICclcyc6ICVzIiwgZW5kX3N0ciwgZSkNCiAgICANCiAgICByZXR1cm4gc3RhcnRfdGltZSwgZW5kX3RpbWUNCg0KDQpkZWYgX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cjogc3RyKSAtPiBmbG9hdDoNCiAgICAiIiLop6PmnpDml7bpl7Tmp73lrZfnrKbkuLLkuLrlsI/ml7bmlbAiIiINCiAgICBpZiBub3Qgc2xvdF9zdHI6DQogICAgICAgIHJldHVybiAwLjANCiAgICANCiAgICBzbG90X3N0ciA9IHNsb3Rfc3RyLnN0cmlwKCkubG93ZXIoKQ0KICAgIA0KICAgIGlmIHNsb3Rfc3RyLmVuZHN3aXRoKCdoJyk6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgIHJldHVybiBmbG9hdChzbG90X3N0cls6LTFdKQ0KICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICAgICAgICAgIHBhc3MNCiAgICANCiAgICB0cnk6DQogICAgICAgIHJldHVybiBmbG9hdChzbG90X3N0cikNCiAgICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICAgICAgcGFzcw0KICAgIA0KICAgIHJldHVybiAwLjANCg0KDQpkZWYgX3RpbWVfc2xvdHMoKSAtPiBMaXN0W3N0cl06DQogICAgcmF3ID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX1RJTUVfU0xPVFMiLCAiIikuc3RyaXAoKQ0KICAgIGlmIG5vdCByYXc6DQogICAgICAgICMg5qC55o2u5Zu+54mH77yM5pe26Ze05Yi75bqm5piv77yaMC41aCwgMWgsIDEuNWgsIDJoLCAyLjVoLCAzaCwgMy41aO+8iDfliJfvvIkNCiAgICAgICAgcmV0dXJuIFsiMC41aCIsICIxaCIsICIxLjVoIiwgIjJoIiwgIjIuNWgiLCAiM2giLCAiMy41aCJdDQogICAgc2xvdHMgPSBbc2xvdC5zdHJpcCgpIGZvciBzbG90IGluIHJhdy5zcGxpdCgiLCIpXQ0KICAgIHJldHVybiBbc2xvdCBmb3Igc2xvdCBpbiBzbG90cyBpZiBzbG90XQ0KDQoNCmRlZiBfZGVmYXVsdF9zZWN0aW9ucygpIC0+IExpc3RbRGljdFtzdHIsIEFueV1dOg0KICAgICMgbmFtZSAtPiByb3dzIHVuZGVybmVhdGjvvIhlbnRyaWVz77yJDQogICAgIyDmr4/kuKogZW50cnkg5a+55bqU5LiA5Liq5rWL6K+V6YOo5L2N77yM6ZyA6KaB5pig5bCE5YiwIEluZmx1eERCIOeahCBmaWVsZCDmiJYgdGFnDQogICAgcmV0dXJuIFsNCiAgICAgICAgeyJuYW1lIjogIuS4u+i9tOaJvyIsICJlbnRyaWVzIjogWw0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMSIsICJmaWVsZCI6ICLkuLvovbTmib8jMSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzEifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzIiLCAiZmllbGQiOiAi5Li76L205om/IzIiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuS4u+i9tOaJvyMyIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiMzIiwgImZpZWxkIjogIuS4u+i9tOaJvyMzIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLkuLvovbTmib8jMyJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNCIsICJmaWVsZCI6ICLkuLvovbTmib8jNCIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzQifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLljYHlrZflpLQiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzEiLCAiZmllbGQiOiAi5Y2B5a2X5aS0IzEiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWNgeWtl+WktCMxIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiMyIiwgImZpZWxkIjogIuWNgeWtl+WktCMyIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLljYHlrZflpLQjMiJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMyIsICJmaWVsZCI6ICLljYHlrZflpLQjMyIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Y2B5a2X5aS0IzMifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLlh4/pgJ/nrrHlsI/ovbTmib8iLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzHvvIjovpPlhaXms5XlhbDnq6/vvIkiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/MSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5bCP6L205om/IzEifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzIiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/IzIiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWHj+mAn+euseWwj+i9tOaJvyMyIn0sDQogICAgICAgIF19LA0KICAgICAgICB7Im5hbWUiOiAi5YeP6YCf566x5aSn6L205om/IiwgImVudHJpZXMiOiBbDQogICAgICAgICAgICB7ImxhYmVsIjogIiMz77yI5aSn56uv55uW56uv77yJIiwgImZpZWxkIjogIuWHj+mAn+euseWkp+i9tOaJvyMzIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLlh4/pgJ/nrrHlpKfovbTmib8jMyJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNCIsICJmaWVsZCI6ICLlh4/pgJ/nrrHlpKfovbTmib8jNCIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5aSn6L205om/IzQifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnmuKkiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIiwgImZpZWxkIjogIm1lYW4iLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIua2pua7keayuea4qSJ9LCAicmVzdWx0X2tleSI6ICLmtqbmu5HmsrnmuKkifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnljosiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiKFBzaSkiLCAiZmllbGQiOiAibWVhbiIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAi5ram5ruR5rK55Y6LIn0sICJyZXN1bHRfa2V5IjogIua2pua7keayueWOiyJ9LA0KICAgICAgICBdfSwNCiAgICBdDQoNCmRlZiBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIGluZmx1eF91cmw6IHN0ciwNCiAgICBpbmZsdXhfb3JnOiBzdHIsDQogICAgaW5mbHV4X3Rva2VuOiBzdHIsDQogICAgaW5mbHV4X2J1Y2tldDogc3RyLA0KICAgIGluZmx1eF9tZWFzdXJlbWVudDogc3RyLA0KKSAtPiBMaXN0W0RpY3Rbc3RyLCBBbnldXToNCiAgICAiIiLmn6Xor6LmlbTkuKrlrp7pqozmnJ/pl7TnmoRsb2FkX3N0YXR1c+aXtumXtOe6v+aVsOaNriIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3IgYXMgZToNCiAgICAgICAgTE9HR0VSLmVycm9yKCJJbmZsdXhEQuWuouaIt+err+WvvOWFpeWksei0pTogJXPvvIzor7flronoo4U6IHBpcCBpbnN0YWxsIGluZmx1eGRiLWNsaWVudCBwYW5kYXMiLCBlKQ0KICAgICAgICByZXR1cm4gW10NCg0KICAgIHRyeToNCiAgICAgICAgY2xpZW50ID0gSW5mbHV4REJDbGllbnQodXJsPWluZmx1eF91cmwsIG9yZz1pbmZsdXhfb3JnLCB0b2tlbj1pbmZsdXhfdG9rZW4pDQogICAgICAgIHF1ZXJ5X2FwaSA9IGNsaWVudC5xdWVyeV9hcGkoKQ0KDQogICAgICAgICMg56Gu5L+d5L2/55SoVVRD5pe26Ze05qC85byP5p+l6K+iDQogICAgICAgIHN0YXJ0X3JmYyA9IHN0YXJ0X3RpbWUuc3RyZnRpbWUoJyVZLSVtLSVkVCVIOiVNOiVTWicpDQogICAgICAgIGVuZF9yZmMgPSBlbmRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgDQogICAgICAgIExPR0dFUi5kZWJ1Zygi5p+l6K+ibG9hZF9zdGF0dXPml7bpl7TojIPlm7Q6ICVzIOWIsCAlcyIsIHN0YXJ0X3JmYywgZW5kX3JmYykNCg0KICAgICAgICBmbHV4ID0gZicnJw0KZnJvbShidWNrZXQ6ICJ7aW5mbHV4X2J1Y2tldH0iKQ0KICB8PiByYW5nZShzdGFydDoge3N0YXJ0X3JmY30sIHN0b3A6IHtlbmRfcmZjfSkNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX21lYXN1cmVtZW50Il0gPT0gIntpbmZsdXhfbWVhc3VyZW1lbnR9IikNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiZGF0YV90eXBlIl0gPT0gIkJyZWFrZXIiKQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfZmllbGQiXSA9PSAibG9hZF9zdGF0dXMiKQ0KICB8PiBzb3J0KGNvbHVtbnM6IFsiX3RpbWUiXSkNCiAgfD4geWllbGQobmFtZTogImxvYWRfc3RhdHVzX3RpbWVsaW5lIikNCicnJy5zdHJpcCgpDQoNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJMb2FkIHN0YXR1cyB0aW1lbGluZSBxdWVyeTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zIG9yICdfdGltZScgbm90IGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBMT0dHRVIud2FybmluZygiTm8gbG9hZF9zdGF0dXMgdGltZWxpbmUgZGF0YSBmb3VuZCIpDQogICAgICAgICAgICByZXR1cm4gW10NCg0KICAgICAgICAjIOi9rOaNouS4uuaXtumXtOe6v+aVsOaNru+8jOS/neaMgVVUQ+aXtuWMug0KICAgICAgICBmcm9tIGRhdGV0aW1lIGltcG9ydCB0aW1lem9uZQ0KICAgICAgICB0aW1lbGluZSA9IFtdDQogICAgICAgIGZvciBfLCByb3cgaW4gZGYuaXRlcnJvd3MoKToNCiAgICAgICAgICAgIHRpbWVfb2JqID0gcGQudG9fZGF0ZXRpbWUocm93WydfdGltZSddKQ0KICAgICAgICAgICAgIyDnoa7kv53ovazmjaLkuLpVVEPml7bljLrnmoRkYXRldGltZeWvueixoQ0KICAgICAgICAgICAgaWYgaGFzYXR0cih0aW1lX29iaiwgJ3R6X2xvY2FsaXplJyk6DQogICAgICAgICAgICAgICAgaWYgdGltZV9vYmoudHogaXMgTm9uZToNCiAgICAgICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50el9sb2NhbGl6ZSh0aW1lem9uZS51dGMpDQogICAgICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50el9jb252ZXJ0KHRpbWV6b25lLnV0YykNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgaWYgaGFzYXR0cih0aW1lX29iaiwgJ3RvX3B5ZGF0ZXRpbWUnKToNCiAgICAgICAgICAgICAgICB0aW1lX29iaiA9IHRpbWVfb2JqLnRvX3B5ZGF0ZXRpbWUoKQ0KICAgICAgICAgICAgICAgIA0KICAgICAgICAgICAgdGltZWxpbmUuYXBwZW5kKHsNCiAgICAgICAgICAgICAgICAndGltZSc6IHRpbWVfb2JqLA0KICAgICAgICAgICAgICAgICdsb2FkX3N0YXR1cyc6IGZsb2F0KHJvd1snX3ZhbHVlJ10pDQogICAgICAgICAgICB9KQ0KDQogICAgICAgIExPR0dFUi5pbmZvKCJMb2FkIHN0YXR1cyB0aW1lbGluZTogJWQgZGF0YSBwb2ludHMgZnJvbSAlcyB0byAlcyIsIA0KICAgICAgICAgICAgICAgICAgIGxlbih0aW1lbGluZSksIHN0YXJ0X3RpbWUsIGVuZF90aW1lKQ0KICAgICAgICANCiAgICAgICAgIyDosIPor5XvvJrmo4Dmn6Xml7bpl7Tlr7nosaHnsbvlnosNCiAgICAgICAgaWYgdGltZWxpbmU6DQogICAgICAgICAgICBmaXJzdF90aW1lID0gdGltZWxpbmVbMF1bJ3RpbWUnXQ0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJUaW1lbGluZSBmaXJzdCB0aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgICAgICBmaXJzdF90aW1lLCB0eXBlKGZpcnN0X3RpbWUpLCBnZXRhdHRyKGZpcnN0X3RpbWUsICd0emluZm8nLCBOb25lKSkNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJzdGFydF90aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUsIHR5cGUoc3RhcnRfdGltZSksIGdldGF0dHIoc3RhcnRfdGltZSwgJ3R6aW5mbycsIE5vbmUpKQ0KICAgICAgICBMT0dHRVIuZGVidWcoImVuZF90aW1lOiAlcyAodHlwZTogJXMsIHR6aW5mbzogJXMpIiwgDQogICAgICAgICAgICAgICAgICAgIGVuZF90aW1lLCB0eXBlKGVuZF90aW1lKSwgZ2V0YXR0cihlbmRfdGltZSwgJ3R6aW5mbycsIE5vbmUpKQ0KICAgICAgICANCiAgICAgICAgcmV0dXJuIHRpbWVsaW5lDQoNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgbG9hZF9zdGF0dXMgdGltZWxpbmU6ICVzIiwgZSkNCiAgICAgICAgcmV0dXJuIFtdDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX2NhbGN1bGF0ZV9lZmZlY3RpdmVfdGltZV9wb2ludHMoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBpbmZsdXhfY29uZmlnOiBEaWN0W3N0ciwgc3RyXQ0KKSAtPiBEaWN0W3N0ciwgT3B0aW9uYWxbZGF0ZXRpbWVdXToNCiAgICAiIiLorqHnrpfln7rkuo7mnInmlYjov5DooYzml7bpl7TntK/orqHnmoTnnJ/lrp7ml7bpl7TngrkiIiINCiAgICANCiAgICAjIDEuIOiOt+WPlmxvYWRfc3RhdHVz5pe26Ze057q/DQogICAgdGltZWxpbmUgPSBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgICAgIHN0YXJ0X3RpbWUsIGVuZF90aW1lLA0KICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSwgaW5mbHV4X2NvbmZpZ1snb3JnJ10sIGluZmx1eF9jb25maWdbJ3Rva2VuJ10sDQogICAgICAgIGluZmx1eF9jb25maWdbJ2J1Y2tldCddLCBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddDQogICAgKQ0KICAgIA0KICAgIGlmIG5vdCB0aW1lbGluZToNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoIk5vIGxvYWRfc3RhdHVzIHRpbWVsaW5lIGRhdGEsIGZhbGxiYWNrIHRvIG9yaWdpbmFsIHRpbWUgY2FsY3VsYXRpb24iKQ0KICAgICAgICAjIOWbnumAgOWIsOWOn+Wni+aXtumXtOiuoeeulw0KICAgICAgICByZXN1bHQgPSB7fQ0KICAgICAgICBmb3Igc2xvdF9zdHIgaW4gdGltZV9zbG90czoNCiAgICAgICAgICAgIHNsb3RfaG91cnMgPSBfcGFyc2VfdGltZV9zbG90KHNsb3Rfc3RyKQ0KICAgICAgICAgICAgcmVzdWx0W3Nsb3Rfc3RyXSA9IHN0YXJ0X3RpbWUgKyB0aW1lZGVsdGEoaG91cnM9c2xvdF9ob3VycykNCiAgICAgICAgcmV0dXJuIHJlc3VsdA0KICAgIA0KICAgICMgMi4g6K6h566X5pyJ5pWI6L+Q6KGM5pe26Ze05q61DQogICAgZWZmZWN0aXZlX3BlcmlvZHMgPSBbXQ0KICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gTm9uZQ0KICAgIA0KICAgIGZvciBpLCBwb2ludCBpbiBlbnVtZXJhdGUodGltZWxpbmUpOg0KICAgICAgICBpZiBwb2ludFsnbG9hZF9zdGF0dXMnXSA9PSAxLjA6DQogICAgICAgICAgICBpZiBjdXJyZW50X3BlcmlvZF9zdGFydCBpcyBOb25lOg0KICAgICAgICAgICAgICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gcG9pbnRbJ3RpbWUnXQ0KICAgICAgICBlbHNlOiAgIyBsb2FkX3N0YXR1cyAhPSAxLjANCiAgICAgICAgICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgIGVmZmVjdGl2ZV9wZXJpb2RzLmFwcGVuZCh7DQogICAgICAgICAgICAgICAgICAgICdzdGFydCc6IGN1cnJlbnRfcGVyaW9kX3N0YXJ0LA0KICAgICAgICAgICAgICAgICAgICAnZW5kJzogcG9pbnRbJ3RpbWUnXSwNCiAgICAgICAgICAgICAgICAgICAgJ2R1cmF0aW9uX2hvdXJzJzogKHBvaW50Wyd0aW1lJ10gLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgICAgICAgICAgfSkNCiAgICAgICAgICAgICAgICBjdXJyZW50X3BlcmlvZF9zdGFydCA9IE5vbmUNCiAgICANCiAgICAjIOWkhOeQhuacgOWQjuS4gOS4quWRqOacn++8iOWmguaenOWunumqjOe7k+adn+aXtuS7jeWcqOi/kOihjO+8iQ0KICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICBlZmZlY3RpdmVfcGVyaW9kcy5hcHBlbmQoew0KICAgICAgICAgICAgJ3N0YXJ0JzogY3VycmVudF9wZXJpb2Rfc3RhcnQsDQogICAgICAgICAgICAnZW5kJzogZW5kX3RpbWUsDQogICAgICAgICAgICAnZHVyYXRpb25faG91cnMnOiAoZW5kX3RpbWUgLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgIH0pDQogICAgDQogICAgdG90YWxfZWZmZWN0aXZlX2hvdXJzID0gc3VtKHBlcmlvZFsnZHVyYXRpb25faG91cnMnXSBmb3IgcGVyaW9kIGluIGVmZmVjdGl2ZV9wZXJpb2RzKQ0KICAgIExPR0dFUi5pbmZvKCJFZmZlY3RpdmUgcnVubmluZyBwZXJpb2RzOiAlZCBwZXJpb2RzLCB0b3RhbCAlLjNmIGhvdXJzIiwgDQogICAgICAgICAgICAgICBsZW4oZWZmZWN0aXZlX3BlcmlvZHMpLCB0b3RhbF9lZmZlY3RpdmVfaG91cnMpDQogICAgDQogICAgZm9yIHBlcmlvZCBpbiBlZmZlY3RpdmVfcGVyaW9kczoNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJFZmZlY3RpdmUgcGVyaW9kOiAlcyDihpIgJXMgKCUuM2YgaG91cnMpIiwNCiAgICAgICAgICAgICAgICAgICAgcGVyaW9kWydzdGFydCddLnN0cmZ0aW1lKCclSDolTTolUycpLA0KICAgICAgICAgICAgICAgICAgICBwZXJpb2RbJ2VuZCddLnN0cmZ0aW1lKCclSDolTTolUycpLA0KICAgICAgICAgICAgICAgICAgICBwZXJpb2RbJ2R1cmF0aW9uX2hvdXJzJ10pDQogICAgDQogICAgIyAzLiDorqHnrpfmr4/kuKrml7bpl7Tmp73lr7nlupTnmoTnnJ/lrp7ml7bpl7TngrkNCiAgICBlZmZlY3RpdmVfdGltZV9wb2ludHMgPSB7fQ0KICAgIA0KICAgIGZvciBzbG90X3N0ciBpbiB0aW1lX3Nsb3RzOg0KICAgICAgICB0YXJnZXRfZWZmZWN0aXZlX2hvdXJzID0gX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cikNCiAgICAgICAgDQogICAgICAgIGlmIHRhcmdldF9lZmZlY3RpdmVfaG91cnMgPD0gMDoNCiAgICAgICAgICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50c1tzbG90X3N0cl0gPSBOb25lDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICANCiAgICAgICAgaWYgdGFyZ2V0X2VmZmVjdGl2ZV9ob3VycyA+IHRvdGFsX2VmZmVjdGl2ZV9ob3VyczoNCiAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCJUYXJnZXQgZWZmZWN0aXZlIHRpbWUgJS4zZmggZXhjZWVkcyB0b3RhbCBlZmZlY3RpdmUgdGltZSAlLjNmaCBmb3Igc2xvdCAlcyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRhcmdldF9lZmZlY3RpdmVfaG91cnMsIHRvdGFsX2VmZmVjdGl2ZV9ob3Vycywgc2xvdF9zdHIpDQogICAgICAgICAgICBlZmZlY3RpdmVfdGltZV9wb2ludHNbc2xvdF9zdHJdID0gTm9uZQ0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgDQogICAgICAgICMg5Zyo5pyJ5pWI5pe26Ze05q615Lit5p+l5om+57Sv6K6h6L+Q6KGMdGFyZ2V0X2VmZmVjdGl2ZV9ob3Vyc+Wwj+aXtueahOaXtumXtOeCuQ0KICAgICAgICBjdW11bGF0aXZlX2hvdXJzID0gMC4wDQogICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gTm9uZQ0KICAgICAgICANCiAgICAgICAgZm9yIHBlcmlvZCBpbiBlZmZlY3RpdmVfcGVyaW9kczoNCiAgICAgICAgICAgIHBlcmlvZF9kdXJhdGlvbiA9IHBlcmlvZFsnZHVyYXRpb25faG91cnMnXQ0KICAgICAgICAgICAgDQogICAgICAgICAgICBpZiBjdW11bGF0aXZlX2hvdXJzICsgcGVyaW9kX2R1cmF0aW9uID49IHRhcmdldF9lZmZlY3RpdmVfaG91cnM6DQogICAgICAgICAgICAgICAgIyDnm67moIfml7bpl7TngrnlnKjov5nkuKrlkajmnJ/lhoUNCiAgICAgICAgICAgICAgICByZW1haW5pbmdfaG91cnMgPSB0YXJnZXRfZWZmZWN0aXZlX2hvdXJzIC0gY3VtdWxhdGl2ZV9ob3Vycw0KICAgICAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gcGVyaW9kWydzdGFydCddICsgdGltZWRlbHRhKGhvdXJzPXJlbWFpbmluZ19ob3VycykNCiAgICAgICAgICAgICAgICBicmVhaw0KICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICBjdW11bGF0aXZlX2hvdXJzICs9IHBlcmlvZF9kdXJhdGlvbg0KICAgICAgICANCiAgICAgICAgZWZmZWN0aXZlX3RpbWVfcG9pbnRzW3Nsb3Rfc3RyXSA9IHRhcmdldF90aW1lX3BvaW50DQogICAgICAgIA0KICAgICAgICBpZiB0YXJnZXRfdGltZV9wb2ludDoNCiAgICAgICAgICAgIExPR0dFUi5pbmZvKCJTbG90ICVzOiBlZmZlY3RpdmUgJS4zZmgg4oaSIGFjdHVhbCB0aW1lICVzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgc2xvdF9zdHIsIHRhcmdldF9lZmZlY3RpdmVfaG91cnMsIHRhcmdldF90aW1lX3BvaW50LnN0cmZ0aW1lKCclSDolTTolUycpKQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIkNvdWxkIG5vdCBjYWxjdWxhdGUgZWZmZWN0aXZlIHRpbWUgcG9pbnQgZm9yIHNsb3QgJXMiLCBzbG90X3N0cikNCiAgICANCiAgICByZXR1cm4gZWZmZWN0aXZlX3RpbWVfcG9pbnRzDQoNCg0KZGVmIF9xdWVyeV9pbmZsdXhkYl9yYW5nZV93aXRoX2xvYWRfc3RhdHVzKA0KICAgIGZpZWxkX25hbWU6IHN0ciwNCiAgICBzdGFydF90aW1lOiBkYXRldGltZSwNCiAgICBlbmRfdGltZTogZGF0ZXRpbWUsDQogICAgaW5mbHV4X3VybDogc3RyLA0KICAgIGluZmx1eF9vcmc6IHN0ciwNCiAgICBpbmZsdXhfdG9rZW46IHN0ciwNCiAgICBpbmZsdXhfYnVja2V0OiBzdHIsDQogICAgaW5mbHV4X21lYXN1cmVtZW50OiBzdHIsDQogICAgZmlsdGVyczogT3B0aW9uYWxbRGljdFtzdHIsIHN0cl1dID0gTm9uZSwNCikgLT4gT3B0aW9uYWxbZmxvYXRdOg0KICAgICIiIuafpeivoiBJbmZsdXhEQiDojrflj5bmjIflrprlrZfmrrXlnKjml7bpl7TojIPlm7TlhoXnmoTlubPlnYflgLzvvIjku4XlvZMgbG9hZF9zdGF0dXMgPSAxIOaXtu+8iSIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgIExPR0dFUi53YXJuaW5nKCJJbmZsdXhEQiBjbGllbnQgbm90IGF2YWlsYWJsZSwgc2tpcCBxdWVyeSBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICByZXR1cm4gTm9uZQ0KDQogICAgdHJ5Og0KICAgICAgICBjbGllbnQgPSBJbmZsdXhEQkNsaWVudCh1cmw9aW5mbHV4X3VybCwgb3JnPWluZmx1eF9vcmcsIHRva2VuPWluZmx1eF90b2tlbikNCiAgICAgICAgcXVlcnlfYXBpID0gY2xpZW50LnF1ZXJ5X2FwaSgpDQoNCiAgICAgICAgIyDnoa7kv53kvb/nlKhVVEPml7bpl7TmoLzlvI8NCiAgICAgICAgc3RhcnRfcmZjID0gc3RhcnRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgZW5kX3JmYyA9IGVuZF90aW1lLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICANCiAgICAgICAgTE9HR0VSLmRlYnVnKCLmn6Xor6LlrZfmrrUgJXMg5pe26Ze06IyD5Zu0OiAlcyDliLAgJXMiLCBmaWVsZF9uYW1lLCBzdGFydF9yZmMsIGVuZF9yZmMpDQoNCiAgICAgICAgIyDmnoTlu7rov4fmu6TmnaHku7YNCiAgICAgICAgdGFnX2ZpbHRlcnMgPSAiIg0KICAgICAgICBpZiBmaWx0ZXJzOg0KICAgICAgICAgICAgZm9yIGtleSwgdmFsdWUgaW4gZmlsdGVycy5pdGVtcygpOg0KICAgICAgICAgICAgICAgIHRhZ19maWx0ZXJzICs9IGYnXG4gIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIntrZXl9Il0gPT0gInt2YWx1ZX0iKScNCg0KICAgICAgICAjIOWvueS6jueOr+Wig+a4qeW6pu+8jOWPluWFqOmDqOmdnjDmlbDmja7nmoTlnYflgLzvvJvlhbbku5blrZfmrrXku43pnIBsb2FkX3N0YXR1cz0x562b6YCJDQogICAgICAgIGlmIGZpZWxkX25hbWUgPT0gIueOr+Wig+a4qeW6piI6DQogICAgICAgICAgICBmbHV4ID0gZicnJw0KZnJvbShidWNrZXQ6ICJ7aW5mbHV4X2J1Y2tldH0iKQ0KICB8PiByYW5nZShzdGFydDoge3N0YXJ0X3JmY30sIHN0b3A6IHtlbmRfcmZjfSkNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX21lYXN1cmVtZW50Il0gPT0gIntpbmZsdXhfbWVhc3VyZW1lbnR9IikNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX2ZpZWxkIl0gPT0gIntmaWVsZF9uYW1lfSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl92YWx1ZSJdICE9IDAuMCl7dGFnX2ZpbHRlcnN9DQogIHw+IG1lYW4oKQ0KICB8PiB5aWVsZChuYW1lOiAibWVhbl9ub25femVybyIpDQonJycuc3RyaXAoKQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtzdGFydF9yZmN9LCBzdG9wOiB7ZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9maWVsZCJdID09ICJ7ZmllbGRfbmFtZX0iKXt0YWdfZmlsdGVyc30NCiAgfD4gbWVhbigpDQogIHw+IHlpZWxkKG5hbWU6ICJtZWFuX3RlbXBlcmF0dXJlX2RhdGEiKQ0KJycnLnN0cmlwKCkNCg0KICAgICAgICBMT0dHRVIuZGVidWcoIkZsdXjmn6Xor6Lor63lj6UgKHJhbmdlKTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zOg0KICAgICAgICAgICAgaWYgZmllbGRfbmFtZSA9PSAi546v5aKD5rip5bqmIjoNCiAgICAgICAgICAgICAgICBMT0dHRVIuZGVidWcoIk5vIHZhbGlkIHJhbmdlIGRhdGEgZm91bmQgZm9yIGZpZWxkPSVzIChub24temVybyBkYXRhKSIsIGZpZWxkX25hbWUpDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiTm8gdmFsaWQgcmFuZ2UgZGF0YSBmb3VuZCBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICAgICAgcmV0dXJuIE5vbmUNCiAgICAgICAgICAgIA0KICAgICAgICBtZWFuX3ZhbHVlID0gZGZbJ192YWx1ZSddLmlsb2NbMF0NCiAgICAgICAgaWYgcGQuaXNuYShtZWFuX3ZhbHVlKToNCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiTWVhbiB2YWx1ZSBpcyBOYU4gZm9yIGZpZWxkPSVzIiwgZmllbGRfbmFtZSkNCiAgICAgICAgICAgIHJldHVybiBOb25lDQoNCiAgICAgICAgdmFsdWUgPSBmbG9hdChtZWFuX3ZhbHVlKQ0KICAgICAgICBpZiBmaWVsZF9uYW1lID09ICLnjq/looPmuKnluqYiOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJGaWVsZD0lcyByYW5nZV9tZWFuX3ZhbHVlPSUuM2YgKG5vbi16ZXJvIGRhdGEpIiwgZmllbGRfbmFtZSwgdmFsdWUpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBMT0dHRVIuZGVidWcoIkZpZWxkPSVzIHJhbmdlX21lYW5fdmFsdWU9JS4zZiIsIGZpZWxkX25hbWUsIHZhbHVlKQ0KICAgICAgICByZXR1cm4gdmFsdWUNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgSW5mbHV4REIgcmFuZ2UgZm9yIGZpZWxkPSVzOiAlcyIsIGZpZWxkX25hbWUsIGUpDQogICAgICAgIHJldHVybiBOb25lDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX3F1ZXJ5X2luZmx1eGRiX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgZmllbGRfbmFtZTogc3RyLA0KICAgIHRhcmdldF90aW1lOiBkYXRldGltZSwNCiAgICBpbmZsdXhfdXJsOiBzdHIsDQogICAgaW5mbHV4X29yZzogc3RyLA0KICAgIGluZmx1eF90b2tlbjogc3RyLA0KICAgIGluZmx1eF9idWNrZXQ6IHN0ciwNCiAgICBpbmZsdXhfbWVhc3VyZW1lbnQ6IHN0ciwNCiAgICBmaWx0ZXJzOiBPcHRpb25hbFtEaWN0W3N0ciwgc3RyXV0gPSBOb25lLA0KKSAtPiBPcHRpb25hbFtmbG9hdF06DQogICAgIiIi5p+l6K+iIEluZmx1eERCIOiOt+WPluaMh+WumuWtl+auteWcqOaMh+WumuaXtumXtOeCueeahOeerOaXtuWAvO+8iOS7heW9kyBsb2FkX3N0YXR1cyA9IDEg5pe277yJIiIiDQogICAgdHJ5Og0KICAgICAgICBmcm9tIGluZmx1eGRiX2NsaWVudCBpbXBvcnQgSW5mbHV4REJDbGllbnQNCiAgICAgICAgaW1wb3J0IHBhbmRhcyBhcyBwZA0KICAgICAgICBpbXBvcnQgd2FybmluZ3MNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQuY2xpZW50Lndhcm5pbmdzIGltcG9ydCBNaXNzaW5nUGl2b3RGdW5jdGlvbg0KICAgIGV4Y2VwdCBJbXBvcnRFcnJvcjoNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoIkluZmx1eERCIGNsaWVudCBub3QgYXZhaWxhYmxlLCBza2lwIHF1ZXJ5IGZvciBmaWVsZD0lcyIsIGZpZWxkX25hbWUpDQogICAgICAgIHJldHVybiBOb25lDQoNCiAgICB0cnk6DQogICAgICAgIGNsaWVudCA9IEluZmx1eERCQ2xpZW50KHVybD1pbmZsdXhfdXJsLCBvcmc9aW5mbHV4X29yZywgdG9rZW49aW5mbHV4X3Rva2VuKQ0KICAgICAgICBxdWVyeV9hcGkgPSBjbGllbnQucXVlcnlfYXBpKCkNCg0KICAgICAgICAjIOehruS/neS9v+eUqFVUQ+aXtumXtA0KICAgICAgICB0YXJnZXRfdGltZV9yZmMgPSB0YXJnZXRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgDQogICAgICAgIExPR0dFUi5kZWJ1ZygNCiAgICAgICAgICAgICLmn6Xor6LlrZfmrrU9JXMg55uu5qCH5pe26Ze0PSVzIChVVEMpIOi/h+a7pOWZqD0lcyIsDQogICAgICAgICAgICBmaWVsZF9uYW1lLA0KICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcmZjLA0KICAgICAgICAgICAgZmlsdGVycyBvciB7fSwNCiAgICAgICAgKQ0KDQogICAgICAgICMg5L2/55So5pe26Ze056qX5Y+j5p+l5om+5pyA5o6l6L+R55qE5pWw5o2u54K5DQogICAgICAgIHdpbmRvd19taW51dGVzID0gMTANCiAgICAgICAgDQogICAgICAgIHF1ZXJ5X3N0YXJ0ID0gdGFyZ2V0X3RpbWUgLSB0aW1lZGVsdGEobWludXRlcz13aW5kb3dfbWludXRlcykNCiAgICAgICAgcXVlcnlfZW5kID0gdGFyZ2V0X3RpbWUgKyB0aW1lZGVsdGEobWludXRlcz13aW5kb3dfbWludXRlcykNCiAgICAgICAgDQogICAgICAgIHF1ZXJ5X3N0YXJ0X3JmYyA9IHF1ZXJ5X3N0YXJ0LnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICBxdWVyeV9lbmRfcmZjID0gcXVlcnlfZW5kLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICANCiAgICAgICAgTE9HR0VSLmRlYnVnKCLmn6Xor6Lnqpflj6M6ICVzIOWIsCAlcyIsIHF1ZXJ5X3N0YXJ0X3JmYywgcXVlcnlfZW5kX3JmYykNCg0KICAgICAgICAjIOaehOW7uui/h+a7pOadoeS7tg0KICAgICAgICB0YWdfZmlsdGVycyA9ICIiDQogICAgICAgIGlmIGZpbHRlcnM6DQogICAgICAgICAgICBmb3Iga2V5LCB2YWx1ZSBpbiBmaWx0ZXJzLml0ZW1zKCk6DQogICAgICAgICAgICAgICAgdGFnX2ZpbHRlcnMgKz0gZidcbiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsie2tleX0iXSA9PSAie3ZhbHVlfSIpJw0KDQogICAgICAgICMg5p+l6K+i5rip5bqm5pWw5o2u77yI5LiN6ZyA6KaBbG9hZF9zdGF0dXPnrZvpgInvvIzlm6DkuLrlt7Lnu4/ln7rkuo7mnInmlYjml7bpl7Tngrnmn6Xor6LvvIkNCiAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtxdWVyeV9zdGFydF9yZmN9LCBzdG9wOiB7cXVlcnlfZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9maWVsZCJdID09ICJ7ZmllbGRfbmFtZX0iKXt0YWdfZmlsdGVyc30NCiAgfD4gc29ydChjb2x1bW5zOiBbIl90aW1lIl0pDQogIHw+IGxhc3QoKQ0KICB8PiB5aWVsZChuYW1lOiAiaW5zdGFudGFuZW91c19hdF9lZmZlY3RpdmVfdGltZSIpDQonJycuc3RyaXAoKQ0KDQogICAgICAgIExPR0dFUi5kZWJ1ZygiRmx1eOafpeivouivreWPpTpcbiVzIiwgZmx1eCkNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgICMg6I635Y+W556s5pe25YC877yI5pyA6L+R55qE5LiA5Liq5pyJ5pWI5pWw5o2u54K577yJDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJObyB2YWxpZCBkYXRhIGZvdW5kIGZvciBmaWVsZD0lcyBhdCBlZmZlY3RpdmUgdGltZSBwb2ludCIsIGZpZWxkX25hbWUpDQogICAgICAgICAgICByZXR1cm4gTm9uZQ0KICAgICAgICAgICAgDQogICAgICAgICMg5Y+W56ys5LiA6KGM55qE5YC877yI5Zug5Li65p+l6K+i5bey57uP5o6S5bqP5bm25Y+W5LqGbGFzdCgp77yJDQogICAgICAgIGluc3RhbnRfdmFsdWUgPSBkZlsnX3ZhbHVlJ10uaWxvY1swXQ0KICAgICAgICBpZiBwZC5pc25hKGluc3RhbnRfdmFsdWUpOg0KICAgICAgICAgICAgTE9HR0VSLmRlYnVnKCJJbnN0YW50YW5lb3VzIHZhbHVlIGlzIE5hTiBmb3IgZmllbGQ9JXMiLCBmaWVsZF9uYW1lKQ0KICAgICAgICAgICAgcmV0dXJuIE5vbmUNCg0KICAgICAgICB2YWx1ZSA9IGZsb2F0KGluc3RhbnRfdmFsdWUpDQogICAgICAgIA0KICAgICAgICAjIOWmguaenOacieaXtumXtOS/oeaBr++8jOiusOW9leWunumZheeahOaVsOaNruaXtumXtOeCuQ0KICAgICAgICBpZiAnX3RpbWUnIGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBhY3R1YWxfdGltZSA9IGRmWydfdGltZSddLmlsb2NbMF0NCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiRmllbGQ9JXMgaW5zdGFudGFuZW91c192YWx1ZT0lLjNmIGFjdHVhbF90aW1lPSVzIChhdCBlZmZlY3RpdmUgdGltZSkiLCANCiAgICAgICAgICAgICAgICAgICAgICAgIGZpZWxkX25hbWUsIHZhbHVlLCBhY3R1YWxfdGltZSkNCiAgICAgICAgZWxzZToNCiAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygiRmllbGQ9JXMgaW5zdGFudGFuZW91c192YWx1ZT0lLjNmIChhdCBlZmZlY3RpdmUgdGltZSkiLCBmaWVsZF9uYW1lLCB2YWx1ZSkNCiAgICAgICAgICAgIA0KICAgICAgICByZXR1cm4gdmFsdWUNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIExPR0dFUi5lcnJvcigiRXJyb3IgcXVlcnlpbmcgSW5mbHV4REIgZm9yIGZpZWxkPSVzOiAlcyIsIGZpZWxkX25hbWUsIGUpDQogICAgICAgIHJldHVybiBOb25lDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX2xvYWRfdGVtcGVyYXR1cmVfZGF0YV93aXRoX2xvYWRfc3RhdHVzKA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBzZWN0aW9uczogTGlzdFtEaWN0W3N0ciwgQW55XV0sDQogICAgc3RhcnRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdLA0KICAgIGVuZF90aW1lOiBPcHRpb25hbFtkYXRldGltZV0sDQopIC0+IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXToNCiAgICAiIiLku44gSW5mbHV4REIg5p+l6K+i5omA5pyJ5rWL6K+V6YOo5L2N5Zyo5ZCE5pe26Ze054K555qE556s5pe25rip5bqm5YC877yI5LuF5b2TIGxvYWRfc3RhdHVzID0gMSDml7bvvIkiIiINCiAgICBpZiBub3Qgc3RhcnRfdGltZSBvciBub3QgZW5kX3RpbWU6DQogICAgICAgIExPR0dFUi5pbmZvKCJTa2lwIGRhdGEgcXVlcnk6IG1pc3Npbmcgc3RhcnQvZW5kICglcywgJXMpIiwgc3RhcnRfdGltZSwgZW5kX3RpbWUpDQogICAgICAgIHJldHVybiB7fQ0KICAgIA0KICAgIGluZmx1eF9jb25maWcgPSBfZ2V0X2luZmx1eF9jb25maWcoKQ0KICAgIA0KICAgIGlmIG5vdCBhbGwoW2luZmx1eF9jb25maWdbJ3VybCddLCBpbmZsdXhfY29uZmlnWydvcmcnXSwgaW5mbHV4X2NvbmZpZ1sndG9rZW4nXSwgDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1snYnVja2V0J10sIGluZmx1eF9jb25maWdbJ21lYXN1cmVtZW50J11dKToNCiAgICAgICAgTE9HR0VSLndhcm5pbmcoDQogICAgICAgICAgICAiU2tpcCBkYXRhIHF1ZXJ5OiBtaXNzaW5nIEluZmx1eCBjb25maWcgdXJsPSVzIGJ1Y2tldD0lcyBtZWFzdXJlbWVudD0lcyIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSBvciAiPGVtcHR5PiIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWydidWNrZXQnXSBvciAiPGVtcHR5PiIsDQogICAgICAgICAgICBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddIG9yICI8ZW1wdHk+IiwNCiAgICAgICAgKQ0KICAgICAgICByZXR1cm4ge30NCiAgICANCiAgICAjIOiuoeeul+aAu+aXtumVv++8iOWwj+aXtu+8iQ0KICAgIHRvdGFsX2R1cmF0aW9uID0gKGVuZF90aW1lIC0gc3RhcnRfdGltZSkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgTE9HR0VSLmluZm8oDQogICAgICAgICJGZXRjaCBpbnN0YW50YW5lb3VzIHRlbXBlcmF0dXJlIGRhdGEgKGxvYWRfc3RhdHVzPTEpIHdpbmRvdz0lc+KGkiVzIHRvdGFsX2hvdXJzPSUuM2YgdGltZV9wb2ludHM9JXMiLA0KICAgICAgICBzdGFydF90aW1lLmlzb2Zvcm1hdCgpLA0KICAgICAgICBlbmRfdGltZS5pc29mb3JtYXQoKSwNCiAgICAgICAgdG90YWxfZHVyYXRpb24sDQogICAgICAgICIsIi5qb2luKHRpbWVfc2xvdHMpLA0KICAgICkNCiAgICANCiAgICAjIOaUtumbhuaJgOaciemcgOimgeafpeivoueahOWtl+autQ0KICAgIHF1ZXJ5X3RhcmdldHM6IExpc3RbdHVwbGVbc3RyLCBEaWN0W3N0ciwgQW55XV1dID0gW10NCiAgICBmb3Igc2VjdGlvbiBpbiBzZWN0aW9uczoNCiAgICAgICAgZW50cmllcyA9IHNlY3Rpb24uZ2V0KCJlbnRyaWVzIikgb3IgW10NCiAgICAgICAgZm9yIGVudHJ5IGluIGVudHJpZXM6DQogICAgICAgICAgICBpZiBpc2luc3RhbmNlKGVudHJ5LCBkaWN0KToNCiAgICAgICAgICAgICAgICBmaWVsZF9uYW1lID0gZW50cnkuZ2V0KCJmaWVsZCIsICIiKQ0KICAgICAgICAgICAgICAgIGlmIGZpZWxkX25hbWU6DQogICAgICAgICAgICAgICAgICAgIHF1ZXJ5X3RhcmdldHMuYXBwZW5kKChmaWVsZF9uYW1lLCBlbnRyeSkpDQoNCiAgICBpZiBub3QgcXVlcnlfdGFyZ2V0czoNCiAgICAgICAgcmV0dXJuIHt9DQogICAgDQogICAgIyDorqHnrpfln7rkuo7mnInmlYjov5DooYzml7bpl7TntK/orqHnmoTnnJ/lrp7ml7bpl7TngrkNCiAgICBMT0dHRVIuaW5mbygiPT09IOW8gOWni+iuoeeul+acieaViOaXtumXtOeCuSA9PT0iKQ0KICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50cyA9IF9jYWxjdWxhdGVfZWZmZWN0aXZlX3RpbWVfcG9pbnRzKA0KICAgICAgICBzdGFydF90aW1lLCBlbmRfdGltZSwgdGltZV9zbG90cywgaW5mbHV4X2NvbmZpZw0KICAgICkNCiAgICANCiAgICAjIOS4uuavj+S4quacieaViOaXtumXtOeCueafpeivoua4qeW6puaVsOaNrg0KICAgIHRlbXBlcmF0dXJlX2RhdGE6IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXSA9IHt9DQogICAgDQogICAgZm9yIGlkeCwgc2xvdF9zdHIgaW4gZW51bWVyYXRlKHRpbWVfc2xvdHMpOg0KICAgICAgICB0YXJnZXRfdGltZV9wb2ludCA9IGVmZmVjdGl2ZV90aW1lX3BvaW50cy5nZXQoc2xvdF9zdHIpDQogICAgICAgIA0KICAgICAgICBpZiB0YXJnZXRfdGltZV9wb2ludCBpcyBOb25lOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIk5vIGVmZmVjdGl2ZSB0aW1lIHBvaW50IGZvciBzbG90ICVzLCB1c2luZyBzaW1wbGUgb2Zmc2V0Iiwgc2xvdF9zdHIpDQogICAgICAgICAgICBzbG90X2hvdXJzID0gX3BhcnNlX3RpbWVfc2xvdChzbG90X3N0cikNCiAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50ID0gc3RhcnRfdGltZSArIHRpbWVkZWx0YShob3Vycz1zbG90X2hvdXJzKQ0KICAgICAgICAgICAgaWYgdGFyZ2V0X3RpbWVfcG9pbnQgPiBlbmRfdGltZToNCiAgICAgICAgICAgICAgICBMT0dHRVIud2FybmluZygiVGltZSBwb2ludCAlcyBleGNlZWRzIGVuZCB0aW1lLCBza2lwcGluZyIsIHNsb3Rfc3RyKQ0KICAgICAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgIA0KICAgICAgICBMT0dHRVIuZGVidWcoIlByb2Nlc3Npbmcgc2xvdCAlcyBhdCBlZmZlY3RpdmUgdGltZSBwb2ludCAlcyIsIA0KICAgICAgICAgICAgICAgICAgICBzbG90X3N0ciwgdGFyZ2V0X3RpbWVfcG9pbnQuc3RyZnRpbWUoJyVZLSVtLSVkICVIOiVNOiVTJykpDQogICAgICAgIA0KICAgICAgICBmb3IgZmllbGRfbmFtZSwgZW50cnkgaW4gcXVlcnlfdGFyZ2V0czoNCiAgICAgICAgICAgIHJlc3VsdF9rZXkgPSBlbnRyeS5nZXQoInJlc3VsdF9rZXkiKSBvciBmaWVsZF9uYW1lDQogICAgICAgICAgICBpZiBub3QgcmVzdWx0X2tleToNCiAgICAgICAgICAgICAgICByZXN1bHRfa2V5ID0gZmllbGRfbmFtZQ0KICAgICAgICAgICAgZW50cnlfZmlsdGVycyA9IGVudHJ5LmdldCgiZmlsdGVycyIpIGlmIGlzaW5zdGFuY2UoZW50cnksIGRpY3QpIGVsc2UgTm9uZQ0KICAgICAgICAgICAgaWYgcmVzdWx0X2tleSBub3QgaW4gdGVtcGVyYXR1cmVfZGF0YToNCiAgICAgICAgICAgICAgICB0ZW1wZXJhdHVyZV9kYXRhW3Jlc3VsdF9rZXldID0ge30NCg0KICAgICAgICAgICAgIyDkvb/nlKjntKLlvJXkvZzkuLprZXnvvIzlm6DkuLrlj6/og73mnInph43lpI3nmoTml7bpl7TliLvluqYNCiAgICAgICAgICAgIHNsb3Rfa2V5ID0gZiJ7aWR4fV97c2xvdF9zdHJ9IiAgIyDkvb/nlKjntKLlvJUr5pe26Ze05Yi75bqm5L2c5Li65ZSv5LiAa2V5DQoNCiAgICAgICAgICAgICMg5p+l6K+i556s5pe25YC877yI5Zyo5pyJ5pWI5pe26Ze054K577yJDQogICAgICAgICAgICB2YWx1ZSA9IF9xdWVyeV9pbmZsdXhkYl93aXRoX2xvYWRfc3RhdHVzKA0KICAgICAgICAgICAgICAgIGZpZWxkX25hbWUsDQogICAgICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQsDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1sndXJsJ10sDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1snb3JnJ10sDQogICAgICAgICAgICAgICAgaW5mbHV4X2NvbmZpZ1sndG9rZW4nXSwNCiAgICAgICAgICAgICAgICBpbmZsdXhfY29uZmlnWydidWNrZXQnXSwNCiAgICAgICAgICAgICAgICBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddLA0KICAgICAgICAgICAgICAgIGZpbHRlcnM9ZW50cnlfZmlsdGVycyBpZiBlbnRyeV9maWx0ZXJzIGVsc2UgTm9uZSwNCiAgICAgICAgICAgICkNCg0KICAgICAgICAgICAgaWYgdmFsdWUgaXMgbm90IE5vbmU6DQogICAgICAgICAgICAgICAgdGVtcGVyYXR1cmVfZGF0YVtyZXN1bHRfa2V5XVtzbG90X2tleV0gPSB2YWx1ZQ0KICAgICAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygNCiAgICAgICAgICAgICAgICAgICAgIlNsb3Q9JXMgZmllbGQ9JXMgdmFsdWU9JS4zZiBhdCBlZmZlY3RpdmVfdGltZT0lcyIsDQogICAgICAgICAgICAgICAgICAgIHNsb3Rfa2V5LA0KICAgICAgICAgICAgICAgICAgICByZXN1bHRfa2V5LA0KICAgICAgICAgICAgICAgICAgICB2YWx1ZSwNCiAgICAgICAgICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQuc3RyZnRpbWUoJyVIOiVNOiVTJykNCiAgICAgICAgICAgICAgICApDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgIExPR0dFUi5kZWJ1ZygNCiAgICAgICAgICAgICAgICAgICAgIlNsb3Q9JXMgZmllbGQ9JXMgbm9fZGF0YSBhdCBlZmZlY3RpdmVfdGltZT0lcyIsDQogICAgICAgICAgICAgICAgICAgIHNsb3Rfa2V5LA0KICAgICAgICAgICAgICAgICAgICByZXN1bHRfa2V5LA0KICAgICAgICAgICAgICAgICAgICB0YXJnZXRfdGltZV9wb2ludC5zdHJmdGltZSgnJUg6JU06JVMnKQ0KICAgICAgICAgICAgICAgICkNCg0KICAgIHJldHVybiB0ZW1wZXJhdHVyZV9kYXRhDQoNCg0KZGVmIF9idWlsZF9jZWxsc193aXRoX2xvYWRfc3RhdHVzKA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBzZWN0aW9uczogTGlzdFtEaWN0W3N0ciwgQW55XV0sDQogICAgbW90b3Jfc3BlZWQ6IHN0ciwNCiAgICBzdGFydF90aW1lOiBPcHRpb25hbFtkYXRldGltZV0sDQogICAgZW5kX3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSwNCiAgICB0ZW1wZXJhdHVyZV9kYXRhOiBEaWN0W3N0ciwgRGljdFtzdHIsIGZsb2F0XV0sDQogICAgdXNlX2RlZmF1bHRzOiBib29sID0gRmFsc2UsDQopIC0+IExpc3RbRGljdFtzdHIsIEFueV1dOg0KICAgICIiIuaehOW7uuWNleWFg+agvOaVsOaNru+8iOWfuuS6jiBsb2FkX3N0YXR1cyA9IDEg55qE5pyJ5pWI5pWw5o2u77yJLSDkuI7ljp/lp4vohJrmnKznu5PmnoTlrozlhajkuIDoh7QiIiINCiAgICBjZWxsczogTGlzdFtEaWN0W3N0ciwgQW55XV0gPSBbXQ0KDQogICAgZGVmIGFkZF9jZWxsKHJvdzogaW50LCBjb2w6IGludCwgdmFsdWU6IHN0ciA9ICIiLCByb3dzcGFuOiBpbnQgPSAxLCBjb2xzcGFuOiBpbnQgPSAxKSAtPiBOb25lOg0KICAgICAgICBwYXlsb2FkOiBEaWN0W3N0ciwgQW55XSA9IHsicm93Ijogcm93LCAiY29sIjogY29sLCAidmFsdWUiOiB2YWx1ZX0NCiAgICAgICAgaWYgcm93c3BhbiA+IDE6DQogICAgICAgICAgICBwYXlsb2FkWyJyb3dzcGFuIl0gPSByb3dzcGFuDQogICAgICAgIGlmIGNvbHNwYW4gPiAxOg0KICAgICAgICAgICAgcGF5bG9hZFsiY29sc3BhbiJdID0gY29sc3Bhbg0KICAgICAgICBjZWxscy5hcHBlbmQocGF5bG9hZCkNCg0KICAgICMg5qih5p2/5bem5L6n5qCH6aKY5YiX5bey57uP5Y676Zmk77yM6L+Z6YeM5LuF55Sf5oiQ57qv5pWw5o2u5Yy677yM5LuOICgwLDApIOW8gOWni+Whq+WFpeaVsOWAvOOAgg0KICAgICMgY3VycmVudF9yb3cg5a+55bqU5qih5p2/5Lit55qE5a6e6ZmF5pWw5o2u6KGM57Si5byV44CCDQogICAgY3VycmVudF9yb3cgPSAwDQogICAgZm9yIHNlY3Rpb24gaW4gc2VjdGlvbnM6DQogICAgICAgIGVudHJpZXMgPSBzZWN0aW9uLmdldCgiZW50cmllcyIpIG9yIFtdDQogICAgICAgIGlmIG5vdCBlbnRyaWVzOg0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgIyDmr4/kuKrmtYvor5Xpg6jkvY3lrZDpobnlr7nlupTmqKHmnb/kuK3nmoTkuIDooYwNCiAgICAgICAgZm9yIGVudHJ5IGluIGVudHJpZXM6DQogICAgICAgICAgICAjIOaUr+aMgeaWsOagvOW8j++8iOW4piBmaWVsZCDmmKDlsITvvInlkozml6fmoLzlvI/vvIjnuq/lrZfnrKbkuLLvvIkNCiAgICAgICAgICAgIGlmIGlzaW5zdGFuY2UoZW50cnksIGRpY3QpOg0KICAgICAgICAgICAgICAgIGZpZWxkX25hbWUgPSBlbnRyeS5nZXQoImZpZWxkIiwgIiIpDQogICAgICAgICAgICAgICAgZW50cnlfZmlsdGVycyA9IGVudHJ5LmdldCgiZmlsdGVycyIpDQogICAgICAgICAgICAgICAgZW50cnlfa2V5ID0gZW50cnkuZ2V0KCJyZXN1bHRfa2V5Iikgb3IgZmllbGRfbmFtZQ0KICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICBmaWVsZF9uYW1lID0gIiINCiAgICAgICAgICAgICAgICBlbnRyeV9maWx0ZXJzID0gTm9uZQ0KICAgICAgICAgICAgICAgIGVudHJ5X2tleSA9ICIiDQoNCiAgICAgICAgICAgICMg5LuF6L6T5Ye65pWw5YC85YiX77ya5YiX57Si5byV55u05o6l5a+55bqU5pe26Ze05q61DQogICAgICAgICAgICAjIOW8uuWItuWhq+WFheaJgOacieWIl++8jOS8mOWFiOS9v+eUqOafpeivouaVsOaNru+8jOWQpuWImeS9v+eUqOm7mOiupOWAvA0KICAgICAgICAgICAgaWYgZmllbGRfbmFtZToNCiAgICAgICAgICAgICAgICB0YXJnZXRfa2V5ID0gZW50cnlfa2V5IG9yIGZpZWxkX25hbWUNCg0KICAgICAgICAgICAgICAgICMg6YGN5Y6G5omA5pyJ5pe26Ze05q615YiX77yM56Gu5L+d5q+P5LiA5YiX6YO95pyJ5pWw5o2uDQogICAgICAgICAgICAgICAgZm9yIGNvbF9pZHgsIHNsb3QgaW4gZW51bWVyYXRlKHRpbWVfc2xvdHMpOg0KICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IE5vbmUNCg0KICAgICAgICAgICAgICAgICAgICAjIOS8mOWFiOS9v+eUqOafpeivouWIsOeahOaVsOaNrg0KICAgICAgICAgICAgICAgICAgICBpZiB0ZW1wZXJhdHVyZV9kYXRhOg0KICAgICAgICAgICAgICAgICAgICAgICAgc2xvdF9kYXRhID0gdGVtcGVyYXR1cmVfZGF0YS5nZXQodGFyZ2V0X2tleSwge30pDQogICAgICAgICAgICAgICAgICAgICAgICBpZiBzbG90X2RhdGE6DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgc2xvdF9rZXkgPSBmIntjb2xfaWR4fV97c2xvdH0iDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBzbG90X2RhdGEuZ2V0KHNsb3Rfa2V5KQ0KDQogICAgICAgICAgICAgICAgICAgIGlmIHZhbHVlIGlzIE5vbmUgYW5kIHVzZV9kZWZhdWx0czoNCiAgICAgICAgICAgICAgICAgICAgICAgICMg5L2/55So5Z+656GA6buY6K6k5YC8ICsg5pe26Ze05q615YGP56e777yI5q+P5Liq5pe26Ze05q615aKe5YqgMC4x5bqm77yJDQogICAgICAgICAgICAgICAgICAgICAgICBkZWZhdWx0X2Jhc2VfdmFsdWUgPSAyNS4wICAjIOeugOWMlueahOm7mOiupOWAvA0KICAgICAgICAgICAgICAgICAgICAgICAgdGltZV9vZmZzZXQgPSBjb2xfaWR4ICogMC4xDQogICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IGRlZmF1bHRfYmFzZV92YWx1ZSArIHRpbWVfb2Zmc2V0DQoNCiAgICAgICAgICAgICAgICAgICAgaWYgdmFsdWUgaXMgTm9uZToNCiAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlX3N0ciA9ICIiDQogICAgICAgICAgICAgICAgICAgIGVsc2U6DQogICAgICAgICAgICAgICAgICAgICAgICAjIOagvOW8j+WMluS4uuWtl+espuS4su+8iOS/neeVmTHkvY3lsI/mlbDvvIkNCiAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlX3N0ciA9IGYie3ZhbHVlOi4xZn0iDQoNCiAgICAgICAgICAgICAgICAgICAgYWRkX2NlbGwoY3VycmVudF9yb3csIGNvbF9pZHgsIHZhbHVlX3N0cikNCiAgICAgICAgICAgIGVsc2U6DQogICAgICAgICAgICAgICAgIyDlpoLmnpzmsqHmnInlrZfmrrXlkI3vvIzloavlhYXnqbrlrZfnrKbkuLINCiAgICAgICAgICAgICAgICBmb3IgY29sX2lkeCBpbiByYW5nZShsZW4odGltZV9zbG90cykpOg0KICAgICAgICAgICAgICAgICAgICBhZGRfY2VsbChjdXJyZW50X3JvdywgY29sX2lkeCwgIiIpDQogICAgICAgICAgICBjdXJyZW50X3JvdyArPSAxDQoNCiAgICByZXR1cm4gY2VsbHMNCg0KDQpkZWYgYnVpbGRfdGVtcGVyYXR1cmVfdGFibGVfd2l0aF9sb2FkX3N0YXR1cyhfOiBEaWN0W3N0ciwgQW55XSkgLT4gRGljdFtzdHIsIEFueV06DQogICAgIiIi5p6E5bu65rip5bqm6KGo5qC85pWw5o2u77yI5LuF5L2/55SoIGxvYWRfc3RhdHVzID0gMSDnmoTmnInmlYjmlbDmja7vvIkiIiINCiAgICBfc2V0dXBfbG9nZ2luZygpDQogICAgDQogICAgdG9rZW4gPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfVE9LRU4iLCAic2NyaXB0VGFibGUxIikNCiAgICByb3dfb2Zmc2V0ID0gaW50KG9zLmVudmlyb24uZ2V0KCJUQUJMRV9TVEFSVF9ST1ciLCAiMCIpIG9yIDApDQogICAgY29sX29mZnNldCA9IGludChvcy5lbnZpcm9uLmdldCgiVEFCTEVfU1RBUlRfQ09MIiwgIjAiKSBvciAwKQ0KICAgIG1vdG9yX3NwZWVkID0gb3MuZW52aXJvbi5nZXQoIlRBQkxFX01PVE9SX1NQRUVEIiwgIjk4MFJQTSIpDQogICAgDQogICAgIyDop6PmnpDlrp7pqozml7bpl7TojIPlm7QNCiAgICBzdGFydF90aW1lLCBlbmRfdGltZSA9IF9wYXJzZV9leHBlcmltZW50X3RpbWVzKCkNCiAgICANCiAgICB0aW1lX3Nsb3RzID0gX3RpbWVfc2xvdHMoKQ0KICAgIHNlY3Rpb25zID0gX2RlZmF1bHRfc2VjdGlvbnMoKQ0KICAgIA0KICAgICMg5p+l6K+i5rip5bqm5pWw5o2u77yI5LuF5b2TIGxvYWRfc3RhdHVzID0gMSDml7bvvIkNCiAgICB0ZW1wZXJhdHVyZV9kYXRhID0gX2xvYWRfdGVtcGVyYXR1cmVfZGF0YV93aXRoX2xvYWRfc3RhdHVzKHRpbWVfc2xvdHMsIHNlY3Rpb25zLCBzdGFydF90aW1lLCBlbmRfdGltZSkNCiAgICANCiAgICAjIOWni+e7iOemgeatoum7mOiupOaVsOaNru+8jOS/neivgeafpeivouS4jeWIsOWAvOaXtuS/neaMgeepuueZvQ0KICAgIHVzZV9kZWZhdWx0cyA9IEZhbHNlDQogICAgDQogICAgY2VsbHMgPSBfYnVpbGRfY2VsbHNfd2l0aF9sb2FkX3N0YXR1cygNCiAgICAgICAgdGltZV9zbG90cywgDQogICAgICAgIHNlY3Rpb25zLCANCiAgICAgICAgbW90b3Jfc3BlZWQsIA0KICAgICAgICBzdGFydF90aW1lLCANCiAgICAgICAgZW5kX3RpbWUsIA0KICAgICAgICB0ZW1wZXJhdHVyZV9kYXRhLA0KICAgICAgICB1c2VfZGVmYXVsdHM9dXNlX2RlZmF1bHRzDQogICAgKQ0KICAgIA0KICAgICMg5bqU55So6KGM5YGP56e7DQogICAgZm9yIGNlbGwgaW4gY2VsbHM6DQogICAgICAgIGNlbGxbInJvdyJdICs9IDQNCiAgICANCiAgICAjIOa3u+WKoOWunumqjOaXtumXtOS/oeaBr++8iOS4juWOn+Wni+iEmuacrOWujOWFqOS4gOiHtOeahOmAu+i+ke+8iQ0KICAgIHN0YXJ0X3RpbWVfcm93ID0gMQ0KICAgIHN0YXJ0X3RpbWVfdmFsdWVfY29sID0gMQ0KICAgIGVuZF90aW1lX3ZhbHVlX2NvbCA9IDMNCiAgICANCiAgICAjIOiOt+WPluWOn+Wni+aXtumXtOWtl+espuS4sui/m+ihjOWkhOeQhu+8iOS4juWOn+Wni+iEmuacrOS/neaMgeS4gOiHtO+8iQ0KICAgIHN0YXJ0X3N0ciA9IG9zLmVudmlyb24uZ2V0KCJFWFBFUklNRU5UX1NUQVJUIiwgIiIpLnN0cmlwKCkNCiAgICBpZiBzdGFydF9zdHIgYW5kIHN0YXJ0X3RpbWU6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgICMg5bCd6K+V5bim5pe25Yy65ZKM5LiN5bim5pe25Yy65Lik56eN5qC85byPDQogICAgICAgICAgICB0cnk6DQogICAgICAgICAgICAgICAgdXRjX2F3YXJlX2R0ID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCAiJVktJW0tJWRUJUg6JU06JVMleiIpDQogICAgICAgICAgICAgICAgbG9jYWxfZHQxID0gdXRjX2F3YXJlX2R0LmFzdGltZXpvbmUodHo9Tm9uZSkNCiAgICAgICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOg0KICAgICAgICAgICAgICAgICMg5LiN5bim5pe25Yy677yM55u05o6l6Kej5p6Q5Li65pys5Zyw5pe26Ze0DQogICAgICAgICAgICAgICAgbG9jYWxfZHQxID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCAiJVktJW0tJWRUJUg6JU06JVMiKQ0KICAgICAgICAgICAgDQogICAgICAgICAgICBsb2NhbF9kdDIgPSBsb2NhbF9kdDEgKyB0aW1lZGVsdGEoaG91cnM9My41KQ0KICAgICAgICAgICAgc3RhcnRfdGltZV92YWx1ZSA9IGxvY2FsX2R0MS5zdHJmdGltZSgiJVktJW0tJWQgJUg6JU06JVMiKQ0KICAgICAgICAgICAgZW5kX3RpbWVfdmFsdWUgPSBsb2NhbF9kdDIuc3RyZnRpbWUoIiVZLSVtLSVkICVIOiVNOiVTIikNCiAgICAgICAgICAgIGNlbGxzLmFwcGVuZCh7InJvdyI6IHN0YXJ0X3RpbWVfcm93LCAiY29sIjogc3RhcnRfdGltZV92YWx1ZV9jb2wsICJ2YWx1ZSI6IHN0YXJ0X3RpbWVfdmFsdWV9KQ0KICAgICAgICAgICAgY2VsbHMuYXBwZW5kKHsicm93Ijogc3RhcnRfdGltZV9yb3csICJjb2wiOiBlbmRfdGltZV92YWx1ZV9jb2wsICJ2YWx1ZSI6IGVuZF90aW1lX3ZhbHVlfSkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOg0KICAgICAgICAgICAgTE9HR0VSLndhcm5pbmcoIkZhaWxlZCB0byBwcm9jZXNzIGV4cGVyaW1lbnQgdGltZSBzdHJpbmdzOiAlcyIsIGUpDQogICAgDQogICAgIyDmn6Xor6Lnjq/looPmuKnluqbvvIjkuI7ljp/lp4vohJrmnKzlrozlhajkuIDoh7TnmoTpgLvovpHvvIkNCiAgICBpbmZsdXhfdXJsID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9VUkwiLCAiIikuc3RyaXAoKQ0KICAgIGluZmx1eF9vcmcgPSBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX09SRyIsICIiKS5zdHJpcCgpDQogICAgaW5mbHV4X3Rva2VuID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9UT0tFTiIsICIiKS5zdHJpcCgpDQogICAgaW5mbHV4X2J1Y2tldCA9IG9zLmVudmlyb24uZ2V0KCJJTkZMVVhfQlVDS0VUIiwgIlBDTSIpLnN0cmlwKCkNCiAgICBpbmZsdXhfbWVhc3VyZW1lbnQgPSBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX01FQVNVUkVNRU5UIiwgIlBDTV9NZWFzdXJlbWVudCIpLnN0cmlwKCkNCiAgICANCiAgICBpZiBzdGFydF90aW1lIGFuZCBlbmRfdGltZToNCiAgICAgICAgIyDlr7nkuo7njq/looPmuKnluqbvvIzkvb/nlKjml7bpl7TojIPlm7Tmn6Xor6LvvIjkuI7ljp/lp4vohJrmnKzpgLvovpHkuIDoh7TvvIkNCiAgICAgICAgdmFsdWUgPSBfcXVlcnlfaW5mbHV4ZGJfcmFuZ2Vfd2l0aF9sb2FkX3N0YXR1cygNCiAgICAgICAgICAgICLnjq/looPmuKnluqYiLA0KICAgICAgICAgICAgc3RhcnRfdGltZSwNCiAgICAgICAgICAgIGVuZF90aW1lLA0KICAgICAgICAgICAgaW5mbHV4X3VybCwNCiAgICAgICAgICAgIGluZmx1eF9vcmcsDQogICAgICAgICAgICBpbmZsdXhfdG9rZW4sDQogICAgICAgICAgICBpbmZsdXhfYnVja2V0LA0KICAgICAgICAgICAgaW5mbHV4X21lYXN1cmVtZW50LA0KICAgICAgICAgICAgZmlsdGVycz17ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LA0KICAgICAgICApDQogICAgICAgICMg56Gu5L+ddmFsdWXkuI3mmK9Ob25l77yM6YG/5YWNV29yZCBDT03mk43kvZzlvILluLjvvIjkuI7ljp/lp4vohJrmnKzkuIDoh7TvvIkNCiAgICAgICAgaWYgdmFsdWUgaXMgbm90IE5vbmU6DQogICAgICAgICAgICBjZWxscy5hcHBlbmQoeyJyb3ciOiAwLCAiY29sIjogMSwgInZhbHVlIjogZiJ7dmFsdWU6LjFmfeKEgyJ9KQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgY2VsbHMuYXBwZW5kKHsicm93IjogMCwgImNvbCI6IDEsICJ2YWx1ZSI6ICIifSkNCiAgICANCiAgICBMT0dHRVIuaW5mbygNCiAgICAgICAgIlRlbXBlcmF0dXJlIHRhYmxlIGJ1aWx0IHdpdGggbG9hZF9zdGF0dXM9MSBmaWx0ZXI6IHRva2VuPSVzIGNlbGxzPSVkIHRpbWVfc2xvdHM9JXMiLA0KICAgICAgICB0b2tlbiwNCiAgICAgICAgbGVuKGNlbGxzKSwNCiAgICAgICAgIiwiLmpvaW4odGltZV9zbG90cyksDQogICAgKQ0KICAgIA0KICAgIHJldHVybiB7DQogICAgICAgICJ0b2tlbiI6IHRva2VuLA0KICAgICAgICAic3RhcnRSb3ciOiByb3dfb2Zmc2V0LA0KICAgICAgICAic3RhcnRDb2wiOiBjb2xfb2Zmc2V0LA0KICAgICAgICAiY2VsbHMiOiBjZWxscywNCiAgICB9DQoNCg0KZGVmIF9sb2FkX3BheWxvYWQoKSAtPiBEaWN0W3N0ciwgQW55XToNCiAgICAiIiLku47moIflh4bovpPlhaXmiJbnjq/looPlj5jph4/liqDovb1wYXlsb2Fk5pWw5o2uIiIiDQogICAgdHJ5Og0KICAgICAgICAjIOWwneivleS7juagh+WHhui+k+WFpeivu+WPlkpTT04NCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgaW1wb3J0IHNlbGVjdA0KICAgICAgICAgICAgaWYgc2VsZWN0LnNlbGVjdChbc3lzLnN0ZGluXSwgW10sIFtdLCAwLjApWzBdOg0KICAgICAgICAgICAgICAgIHBheWxvYWRfc3RyID0gc3lzLnN0ZGluLnJlYWQoKS5zdHJpcCgpDQogICAgICAgICAgICAgICAgaWYgcGF5bG9hZF9zdHI6DQogICAgICAgICAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHBheWxvYWRfc3RyKQ0KICAgICAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgICAgICAjIFdpbmRvd3PkuIpzZWxlY3Tlj6/og73kuI3lj6/nlKjvvIzlsJ3or5Xnm7TmjqXor7vlj5YNCiAgICAgICAgICAgIGltcG9ydCBtc3ZjcnQNCiAgICAgICAgICAgIGlmIG1zdmNydC5rYmhpdCgpOg0KICAgICAgICAgICAgICAgIHBheWxvYWRfc3RyID0gc3lzLnN0ZGluLnJlYWQoKS5zdHJpcCgpDQogICAgICAgICAgICAgICAgaWYgcGF5bG9hZF9zdHI6DQogICAgICAgICAgICAgICAgICAgIHJldHVybiBqc29uLmxvYWRzKHBheWxvYWRfc3RyKQ0KICAgIGV4Y2VwdCBFeGNlcHRpb246DQogICAgICAgIHBhc3MNCiAgICANCiAgICAjIOWmguaenOayoeacieagh+WHhui+k+WFpe+8jOi/lOWbnuepuuWtl+WFuA0KICAgIHJldHVybiB7fQ0KDQoNCmRlZiBfbG9nX2Vudmlyb25tZW50X3ZhcmlhYmxlcygpIC0+IE5vbmU6DQogICAgIiIi6K6w5b2V55u45YWz546v5aKD5Y+Y6YePIiIiDQogICAgZW52X3ZhcnMgPSBbDQogICAgICAgICJUQUJMRV9UT0tFTiIsICJUQUJMRV9TVEFSVF9ST1ciLCAiVEFCTEVfU1RBUlRfQ09MIiwgIlRBQkxFX1RJTUVfU0xPVFMiLCAiVEFCTEVfTU9UT1JfU1BFRUQiLA0KICAgICAgICAiRVhQRVJJTUVOVF9TVEFSVCIsICJFWFBFUklNRU5UX0VORCIsDQogICAgICAgICJJTkZMVVhfVVJMIiwgIklORkxVWF9PUkciLCAiSU5GTFVYX1RPS0VOIiwgIklORkxVWF9CVUNLRVQiLCAiSU5GTFVYX01FQVNVUkVNRU5UIg0KICAgIF0NCiAgICANCiAgICBmb3IgdmFyIGluIGVudl92YXJzOg0KICAgICAgICB2YWx1ZSA9IG9zLmVudmlyb24uZ2V0KHZhciwgIiIpDQogICAgICAgIGlmICJUT0tFTiIgaW4gdmFyIGFuZCB2YWx1ZToNCiAgICAgICAgICAgIHZhbHVlID0gX21hc2tfc2VjcmV0KHZhbHVlKQ0KICAgICAgICBMT0dHRVIuZGVidWcoIkVOViAlcz0lcyIsIHZhciwgdmFsdWUgb3IgIjxlbXB0eT4iKQ0KDQoNCmRlZiBtYWluKCkgLT4gaW50Og0KICAgIHRyeToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgaWYgbm90IGxvZ2dpbmcuZ2V0TG9nZ2VyKCkuaGFuZGxlcnM6DQogICAgICAgICAgICAgICAgbG9nX2xldmVsX25hbWUgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfTE9HX0xFVkVMIiwgIkRFQlVHIikuc3RyaXAoKSBvciAiREVCVUciDQogICAgICAgICAgICAgICAgbG9nX2xldmVsID0gZ2V0YXR0cihsb2dnaW5nLCBsb2dfbGV2ZWxfbmFtZS51cHBlcigpLCBsb2dnaW5nLkRFQlVHKQ0KICAgICAgICAgICAgICAgIGxvZ19maWxlX3JhdyA9IG9zLmVudmlyb24uZ2V0KCJUQUJMRV9MT0dfRklMRSIsICJ0ZXN0LmxvZyIpLnN0cmlwKCkgb3IgInRlc3QubG9nIg0KICAgICAgICAgICAgICAgIGxvZ19maWxlID0gb3MucGF0aC5hYnNwYXRoKGxvZ19maWxlX3JhdykNCg0KICAgICAgICAgICAgICAgIGxvZ2dpbmcuYmFzaWNDb25maWcoDQogICAgICAgICAgICAgICAgICAgIGxldmVsPWxvZ19sZXZlbCwNCiAgICAgICAgICAgICAgICAgICAgZm9ybWF0PSIlKGFzY3RpbWUpcyBbJShsZXZlbG5hbWUpc10gJShuYW1lKXM6ICUobWVzc2FnZSlzIiwNCiAgICAgICAgICAgICAgICAgICAgaGFuZGxlcnM9Ww0KICAgICAgICAgICAgICAgICAgICAgICAgbG9nZ2luZy5GaWxlSGFuZGxlcihsb2dfZmlsZSwgZW5jb2Rpbmc9InV0Zi04IiksDQogICAgICAgICAgICAgICAgICAgICAgICBsb2dnaW5nLlN0cmVhbUhhbmRsZXIoc3lzLnN0ZGVyciksDQogICAgICAgICAgICAgICAgICAgIF0sDQogICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgICAgIExPR0dFUi5pbmZvKCJMb2dnaW5nIGluaXRpYWxpemVkIC0+IGZpbGU9JXMgbGV2ZWw9JXMiLCBsb2dfZmlsZSwgbG9nZ2luZy5nZXRMZXZlbE5hbWUobG9nX2xldmVsKSkNCiAgICAgICAgICAgICAgICBfbG9nX2Vudmlyb25tZW50X3ZhcmlhYmxlcygpDQogICAgICAgICAgICBzeXMuc3Rkb3V0LnJlY29uZmlndXJlKGVuY29kaW5nPSJ1dGYtOCIpICAjIHR5cGU6IGlnbm9yZVthdHRyLWRlZmluZWRdDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246DQogICAgICAgICAgICBwYXNzDQogICAgICAgIA0KICAgICAgICBwYXlsb2FkID0gX2xvYWRfcGF5bG9hZCgpDQogICAgICAgIHRhYmxlX3NwZWMgPSBidWlsZF90ZW1wZXJhdHVyZV90YWJsZV93aXRoX2xvYWRfc3RhdHVzKHBheWxvYWQpDQogICAgICAgIHJlc3VsdCA9IHsidGFibGVzIjogW3RhYmxlX3NwZWNdfQ0KICAgICAgICBwcmludChqc29uLmR1bXBzKHJlc3VsdCwgZW5zdXJlX2FzY2lpPUZhbHNlKSkNCiAgICAgICAgcmV0dXJuIDANCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGV4YzoNCiAgICAgICAgcHJpbnQoZiJlcnJvcjoge2V4Y30iLCBmaWxlPXN5cy5zdGRlcnIpDQogICAgICAgIHJldHVybiAxDQoNCg0KaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoNCiAgICBzeXMuZXhpdChtYWluKCkpDQo=", + "scriptFile": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uDQojIC0qLSBjb2Rpbmc6IHV0Zi04IC0qLQ0KIiIiDQrmtYvor5Xpg6jkvY3muKnluqborrDlvZXooajnlJ/miJDohJrmnKzvvIjluKbotJ/ovb3nirbmgIHnrZvpgInvvIkNCg0KLSDlv73nlaXkvKDlhaXnmoQgZXhwZXJpbWVudFByb2Nlc3PvvIzoh6rooYzmnoTpgKDlm7rlrprnu5PmnoTnmoTmlbDmja4NCi0g5LuOIEluZmx1eERCIOafpeivouavj+S4qua1i+ivlemDqOS9jeWcqOWQhOaXtumXtOeCueeahOeerOaXtua4qeW6puWAvA0KLSDmt7vliqAgbG9hZF9zdGF0dXMgPSAxIOeahOetm+mAieadoeS7tu+8jOehruS/neWPquWcqOecn+ato+mHh+mbhuaVsOaNruaXtuiOt+WPlua4qeW6pg0KLSDovpPlh7rmoLzlvI/kuI7lupTnlKjkuK3nmoQgc2NyaXB0VGFibGUg5Y2g5L2N56ym5YW85a65DQotIOm7mOiupOaKiiB7c2NyaXB0VGFibGUxfSDmlL7lnKgi5rWL6K+V6YOo5L2NIuaJgOWcqOeahOWNleWFg+agvA0KDQrnjq/looPlj5jph4/vvJoNCiAgICBUQUJMRV9UT0tFTiAgICAgICAgIOebruagh+WNoOS9jeespu+8jOm7mOiupCBzY3JpcHRUYWJsZTENCiAgICBUQUJMRV9TVEFSVF9ST1cgICAgIOWGmeWFpei1t+Wni+ihjOWBj+enu++8jOm7mOiupCAwDQogICAgVEFCTEVfU1RBUlRfQ09MICAgICDlhpnlhaXotbflp4vliJflgY/np7vvvIzpu5jorqQgMA0KICAgIFRBQkxFX1RJTUVfU0xPVFMgICAg6YCX5Y+35YiG6ZqU55qE5pe26Ze05Yi75bqm77yM6buY6K6kICIwLjVoLDFoLDEuNWgsMmgsMi41aCwzaCwzLjVoIg0KICAgIFRBQkxFX01PVE9SX1NQRUVEICAg55S15py66L2s6YCf5qCH562+77yM6buY6K6kICI5ODBSUE0iDQogICAgRVhQRVJJTUVOVF9TVEFSVCAgICAg5a6e6aqM5byA5aeL5pe26Ze077yISVNPIDg2MDEg5qC85byP77yM5aaCIDIwMjQtMDEtMDFUMTA6MDA6MDBa77yJDQogICAgRVhQRVJJTUVOVF9FTkQgICAgICAg5a6e6aqM57uT5p2f5pe26Ze077yISVNPIDg2MDEg5qC85byP77yJDQogICAgSU5GTFVYX1VSTCAgICAgICAgICAgSW5mbHV4REIgVVJMDQogICAgSU5GTFVYX09SRyAgICAgICAgICAgSW5mbHV4REIg57uE57uHDQogICAgSU5GTFVYX1RPS0VOICAgICAgICAgSW5mbHV4REIg5Luk54mMDQogICAgSU5GTFVYX0JVQ0tFVCAgICAgICAgSW5mbHV4REIg5qG25ZCN77yM6buY6K6kIFBDTQ0KICAgIElORkxVWF9NRUFTVVJFTUVOVCAgIEluZmx1eERCIOa1i+mHj+WQje+8jOm7mOiupCBQQ01fTWVhc3VyZW1lbnQNCiIiIg0KDQpmcm9tIF9fZnV0dXJlX18gaW1wb3J0IGFubm90YXRpb25zDQoNCmltcG9ydCBqc29uDQppbXBvcnQgbG9nZ2luZw0KaW1wb3J0IG9zDQppbXBvcnQgc3lzDQpmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRldGltZSwgdGltZWRlbHRhDQpmcm9tIHR5cGluZyBpbXBvcnQgQW55LCBEaWN0LCBMaXN0LCBPcHRpb25hbA0KDQoNCmRlZiBfbWFza19zZWNyZXQodmFsdWU6IE9wdGlvbmFsW3N0cl0pIC0+IHN0cjoNCiAgICAiIiLmjqnnoIHmlY/mhJ/kv6Hmga8iIiINCiAgICBpZiBub3QgdmFsdWU6DQogICAgICAgIHJldHVybiAiPGVtcHR5PiINCiAgICBpZiBsZW4odmFsdWUpIDw9IDg6DQogICAgICAgIHJldHVybiAiKiIgKiBsZW4odmFsdWUpDQogICAgcmV0dXJuIHZhbHVlWzo0XSArICIqIiAqIChsZW4odmFsdWUpIC0gOCkgKyB2YWx1ZVstNDpdDQoNCg0KY2xhc3MgX0R1bW15TG9nZ2VyOg0KICAgIGRlZiBkZWJ1ZyhzZWxmLCAqYXJncywgKiprd2FyZ3MpOiBwYXNzDQogICAgZGVmIGluZm8oc2VsZiwgKmFyZ3MsICoqa3dhcmdzKTogcGFzcw0KICAgIGRlZiB3YXJuaW5nKHNlbGYsICphcmdzLCAqKmt3YXJncyk6IHBhc3MNCiAgICBkZWYgZXJyb3Ioc2VsZiwgKmFyZ3MsICoqa3dhcmdzKTogcGFzcw0KDQpMT0dHRVIgPSBfRHVtbXlMb2dnZXIoKQ0KDQoNCmRlZiBfc2V0dXBfbG9nZ2luZygpIC0+IE5vbmU6DQogICAgcGFzcw0KDQoNCmRlZiBfZ2V0X2luZmx1eF9jb25maWcoKSAtPiBEaWN0W3N0ciwgc3RyXToNCiAgICAiIiLojrflj5ZJbmZsdXhEQumFjee9riIiIg0KICAgIGNvbmZpZyA9IHsNCiAgICAgICAgJ3VybCc6IG9zLmVudmlyb24uZ2V0KCJJTkZMVVhfVVJMIiwgIiIpLnN0cmlwKCksDQogICAgICAgICdvcmcnOiBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX09SRyIsICIiKS5zdHJpcCgpLA0KICAgICAgICAndG9rZW4nOiBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX1RPS0VOIiwgIiIpLnN0cmlwKCksDQogICAgICAgICdidWNrZXQnOiBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX0JVQ0tFVCIsICJQQ00iKS5zdHJpcCgpLA0KICAgICAgICAnbWVhc3VyZW1lbnQnOiBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX01FQVNVUkVNRU5UIiwgIlBDTV9NZWFzdXJlbWVudCIpLnN0cmlwKCksDQogICAgfQ0KICAgIHJldHVybiBjb25maWcNCg0KDQpkZWYgX3BhcnNlX2V4cGVyaW1lbnRfdGltZXMoKSAtPiB0dXBsZVtPcHRpb25hbFtkYXRldGltZV0sIE9wdGlvbmFsW2RhdGV0aW1lXV06DQogICAgIiIi6Kej5p6Q5a6e6aqM5pe26Ze0IiIiDQogICAgc3RhcnRfc3RyID0gb3MuZW52aXJvbi5nZXQoIkVYUEVSSU1FTlRfU1RBUlQiLCAiIikuc3RyaXAoKQ0KICAgIGVuZF9zdHIgPSBvcy5lbnZpcm9uLmdldCgiRVhQRVJJTUVOVF9FTkQiLCAiIikuc3RyaXAoKQ0KICAgIA0KICAgIHN0YXJ0X3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSA9IE5vbmUNCiAgICBlbmRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdID0gTm9uZQ0KICAgIA0KICAgIGlmIHN0YXJ0X3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTWiIsICIlWS0lbS0lZFQlSDolTTolUyV6Il06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBzdGFydF90aW1lID0gZGF0ZXRpbWUuc3RycHRpbWUoc3RhcnRfc3RyLCBmbXQpDQogICAgICAgICAgICAgICAgICAgIGlmIHN0YXJ0X3RpbWUudHppbmZvIGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgICAgICAgICAgIyDovazmjaLkuLrmnKzlnLDml7bpl7TlubbljrvpmaTml7bljLrkv6Hmga8NCiAgICAgICAgICAgICAgICAgICAgICAgIHN0YXJ0X3RpbWUgPSBzdGFydF90aW1lLmFzdGltZXpvbmUodHo9Tm9uZSkucmVwbGFjZSh0emluZm89Tm9uZSkNCiAgICAgICAgICAgICAgICAgICAgYnJlYWsNCiAgICAgICAgICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoNCiAgICAgICAgICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOg0KICAgICAgICAgICAgcHJpbnQoZiJXYXJuaW5nOiBGYWlsZWQgdG8gcGFyc2UgRVhQRVJJTUVOVF9TVEFSVCAne3N0YXJ0X3N0cn0nOiB7ZX0iLCBmaWxlPXN5cy5zdGRlcnIpDQogICAgDQogICAgaWYgZW5kX3N0cjoNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgZm9yIGZtdCBpbiBbIiVZLSVtLSVkVCVIOiVNOiVTWiIsICIlWS0lbS0lZFQlSDolTTolUyV6Il06DQogICAgICAgICAgICAgICAgdHJ5Og0KICAgICAgICAgICAgICAgICAgICBlbmRfdGltZSA9IGRhdGV0aW1lLnN0cnB0aW1lKGVuZF9zdHIsIGZtdCkNCiAgICAgICAgICAgICAgICAgICAgaWYgZW5kX3RpbWUudHppbmZvIGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgICAgICAgICAgIyDovazmjaLkuLrmnKzlnLDml7bpl7TlubbljrvpmaTml7bljLrkv6Hmga8NCiAgICAgICAgICAgICAgICAgICAgICAgIGVuZF90aW1lID0gZW5kX3RpbWUuYXN0aW1lem9uZSh0ej1Ob25lKS5yZXBsYWNlKHR6aW5mbz1Ob25lKQ0KICAgICAgICAgICAgICAgICAgICBicmVhaw0KICAgICAgICAgICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOg0KICAgICAgICAgICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBwcmludChmIldhcm5pbmc6IEZhaWxlZCB0byBwYXJzZSBFWFBFUklNRU5UX0VORCAne2VuZF9zdHJ9Jzoge2V9IiwgZmlsZT1zeXMuc3RkZXJyKQ0KICAgIA0KICAgIHJldHVybiBzdGFydF90aW1lLCBlbmRfdGltZQ0KDQoNCmRlZiBfcGFyc2VfdGltZV9zbG90KHNsb3Rfc3RyOiBzdHIpIC0+IGZsb2F0Og0KICAgICIiIuino+aekOaXtumXtOanveWtl+espuS4suS4uuWwj+aXtuaVsCIiIg0KICAgIGlmIG5vdCBzbG90X3N0cjoNCiAgICAgICAgcmV0dXJuIDAuMA0KICAgIA0KICAgIHNsb3Rfc3RyID0gc2xvdF9zdHIuc3RyaXAoKS5sb3dlcigpDQogICAgDQogICAgaWYgc2xvdF9zdHIuZW5kc3dpdGgoJ2gnKToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgcmV0dXJuIGZsb2F0KHNsb3Rfc3RyWzotMV0pDQogICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOg0KICAgICAgICAgICAgcGFzcw0KICAgIA0KICAgIHRyeToNCiAgICAgICAgcmV0dXJuIGZsb2F0KHNsb3Rfc3RyKQ0KICAgIGV4Y2VwdCBWYWx1ZUVycm9yOg0KICAgICAgICBwYXNzDQogICAgDQogICAgcmV0dXJuIDAuMA0KDQoNCmRlZiBfdGltZV9zbG90cygpIC0+IExpc3Rbc3RyXToNCiAgICByYXcgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfVElNRV9TTE9UUyIsICIiKS5zdHJpcCgpDQogICAgaWYgbm90IHJhdzoNCiAgICAgICAgIyDmoLnmja7lm77niYfvvIzml7bpl7TliLvluqbmmK/vvJowLjVoLCAxaCwgMS41aCwgMmgsIDIuNWgsIDNoLCAzLjVo77yIN+WIl++8iQ0KICAgICAgICByZXR1cm4gWyIwLjVoIiwgIjFoIiwgIjEuNWgiLCAiMmgiLCAiMi41aCIsICIzaCIsICIzLjVoIl0NCiAgICBzbG90cyA9IFtzbG90LnN0cmlwKCkgZm9yIHNsb3QgaW4gcmF3LnNwbGl0KCIsIildDQogICAgcmV0dXJuIFtzbG90IGZvciBzbG90IGluIHNsb3RzIGlmIHNsb3RdDQoNCg0KZGVmIF9kZWZhdWx0X3NlY3Rpb25zKCkgLT4gTGlzdFtEaWN0W3N0ciwgQW55XV06DQogICAgIyBuYW1lIC0+IHJvd3MgdW5kZXJuZWF0aO+8iGVudHJpZXPvvIkNCiAgICAjIOavj+S4qiBlbnRyeSDlr7nlupTkuIDkuKrmtYvor5Xpg6jkvY3vvIzpnIDopoHmmKDlsITliLAgSW5mbHV4REIg55qEIGZpZWxkIOaIliB0YWcNCiAgICByZXR1cm4gWw0KICAgICAgICB7Im5hbWUiOiAi5Li76L205om/IiwgImVudHJpZXMiOiBbDQogICAgICAgICAgICB7ImxhYmVsIjogIiMxIiwgImZpZWxkIjogIuS4u+i9tOaJvyMxIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLkuLvovbTmib8jMSJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMiIsICJmaWVsZCI6ICLkuLvovbTmib8jMiIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzIifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzMiLCAiZmllbGQiOiAi5Li76L205om/IzMiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuS4u+i9tOaJvyMzIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiM0IiwgImZpZWxkIjogIuS4u+i9tOaJvyM0IiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLkuLvovbTmib8jNCJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNSIsICJmaWVsZCI6ICLkuLvovbTmib8jNSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Li76L205om/IzUifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzYiLCAiZmllbGQiOiAi5Li76L205om/IzYiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuS4u+i9tOaJvyM2In0sDQogICAgICAgIF19LA0KICAgICAgICB7Im5hbWUiOiAi5Y2B5a2X5aS0IiwgImVudHJpZXMiOiBbDQogICAgICAgICAgICB7ImxhYmVsIjogIiMxIiwgImZpZWxkIjogIuWNgeWtl+WktCMxIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLljYHlrZflpLQjMSJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjMiIsICJmaWVsZCI6ICLljYHlrZflpLQjMiIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Y2B5a2X5aS0IzIifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzMiLCAiZmllbGQiOiAi5Y2B5a2X5aS0IzMiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWNgeWtl+WktCMzIn0sDQogICAgICAgICAgICB7ImxhYmVsIjogIiM0IiwgImZpZWxkIjogIuWNgeWtl+WktCM0IiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLljYHlrZflpLQjNCJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNSIsICJmaWVsZCI6ICLljYHlrZflpLQjNSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5Y2B5a2X5aS0IzUifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLlh4/pgJ/nrrHlsI/ovbTmib8iLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzHvvIjovpPlhaXms5XlhbDnq6/vvIkiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/MSIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5bCP6L205om/IzEifSwNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIzIiLCAiZmllbGQiOiAi5YeP6YCf566x5bCP6L205om/IzIiLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIkxTREFRIn0sICJyZXN1bHRfa2V5IjogIuWHj+mAn+euseWwj+i9tOaJvyMyIn0sDQogICAgICAgIF19LA0KICAgICAgICB7Im5hbWUiOiAi5YeP6YCf566x5aSn6L205om/IiwgImVudHJpZXMiOiBbDQogICAgICAgICAgICB7ImxhYmVsIjogIiMz77yI5aSn56uv55uW56uv77yJIiwgImZpZWxkIjogIuWHj+mAn+euseWkp+i9tOaJvyMzIiwgImZpbHRlcnMiOiB7ImRhdGFfdHlwZSI6ICJMU0RBUSJ9LCAicmVzdWx0X2tleSI6ICLlh4/pgJ/nrrHlpKfovbTmib8jMyJ9LA0KICAgICAgICAgICAgeyJsYWJlbCI6ICIjNCIsICJmaWVsZCI6ICLlh4/pgJ/nrrHlpKfovbTmib8jNCIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAiTFNEQVEifSwgInJlc3VsdF9rZXkiOiAi5YeP6YCf566x5aSn6L205om/IzQifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnmuKkiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiIiwgImZpZWxkIjogIm1lYW4iLCAiZmlsdGVycyI6IHsiZGF0YV90eXBlIjogIua2pua7keayuea4qSJ9LCAicmVzdWx0X2tleSI6ICLmtqbmu5HmsrnmuKkifSwNCiAgICAgICAgXX0sDQogICAgICAgIHsibmFtZSI6ICLmtqbmu5HmsrnljosiLCAiZW50cmllcyI6IFsNCiAgICAgICAgICAgIHsibGFiZWwiOiAiKFBzaSkiLCAiZmllbGQiOiAibWVhbiIsICJmaWx0ZXJzIjogeyJkYXRhX3R5cGUiOiAi5ram5ruR5rK55Y6LIn0sICJyZXN1bHRfa2V5IjogIua2pua7keayueWOiyJ9LA0KICAgICAgICBdfSwNCiAgICBdDQoNCmRlZiBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIGluZmx1eF91cmw6IHN0ciwNCiAgICBpbmZsdXhfb3JnOiBzdHIsDQogICAgaW5mbHV4X3Rva2VuOiBzdHIsDQogICAgaW5mbHV4X2J1Y2tldDogc3RyLA0KICAgIGluZmx1eF9tZWFzdXJlbWVudDogc3RyLA0KKSAtPiBMaXN0W0RpY3Rbc3RyLCBBbnldXToNCiAgICAiIiLmn6Xor6LmlbTkuKrlrp7pqozmnJ/pl7TnmoRsb2FkX3N0YXR1c+aXtumXtOe6v+aVsOaNriIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgIHBhc3MNCiAgICAgICAgcmV0dXJuIFtdDQoNCiAgICB0cnk6DQogICAgICAgIGNsaWVudCA9IEluZmx1eERCQ2xpZW50KHVybD1pbmZsdXhfdXJsLCBvcmc9aW5mbHV4X29yZywgdG9rZW49aW5mbHV4X3Rva2VuKQ0KICAgICAgICBxdWVyeV9hcGkgPSBjbGllbnQucXVlcnlfYXBpKCkNCg0KICAgICAgICBzdGFydF9yZmMgPSBzdGFydF90aW1lLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KICAgICAgICBlbmRfcmZjID0gZW5kX3RpbWUuc3RyZnRpbWUoJyVZLSVtLSVkVCVIOiVNOiVTWicpDQoNCiAgICAgICAgIyDmn6Xor6Jsb2FkX3N0YXR1c+Wtl+auteeahOaJgOacieaVsOaNrueCue+8iOWcqEJyZWFrZXLmlbDmja7nsbvlnovkuK3vvIkNCiAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtzdGFydF9yZmN9LCBzdG9wOiB7ZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbImRhdGFfdHlwZSJdID09ICJCcmVha2VyIikNCiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsiX2ZpZWxkIl0gPT0gImxvYWRfc3RhdHVzIikNCiAgfD4gc29ydChjb2x1bW5zOiBbIl90aW1lIl0pDQogIHw+IHlpZWxkKG5hbWU6ICJsb2FkX3N0YXR1c190aW1lbGluZSIpDQonJycuc3RyaXAoKQ0KDQogICAgICAgIHBhc3MNCg0KICAgICAgICB3aXRoIHdhcm5pbmdzLmNhdGNoX3dhcm5pbmdzKCk6DQogICAgICAgICAgICB3YXJuaW5ncy5zaW1wbGVmaWx0ZXIoImlnbm9yZSIsIE1pc3NpbmdQaXZvdEZ1bmN0aW9uKQ0KICAgICAgICAgICAgZnJhbWVzID0gcXVlcnlfYXBpLnF1ZXJ5X2RhdGFfZnJhbWUoZmx1eCkNCiAgICAgICAgDQogICAgICAgIGlmIGlzaW5zdGFuY2UoZnJhbWVzLCBsaXN0KToNCiAgICAgICAgICAgIGRmID0gcGQuY29uY2F0KGZyYW1lcywgaWdub3JlX2luZGV4PVRydWUpIGlmIGZyYW1lcyBlbHNlIHBkLkRhdGFGcmFtZSgpDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBkZiA9IGZyYW1lcw0KDQogICAgICAgIGlmIGRmLmVtcHR5IG9yICdfdmFsdWUnIG5vdCBpbiBkZi5jb2x1bW5zIG9yICdfdGltZScgbm90IGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBwYXNzDQogICAgICAgICAgICByZXR1cm4gW10NCg0KICAgICAgICAjIOi9rOaNouS4uuaXtumXtOe6v+aVsOaNru+8jOehruS/neaXtuWMuuS4gOiHtOaApw0KICAgICAgICB0aW1lbGluZSA9IFtdDQogICAgICAgIGZvciBfLCByb3cgaW4gZGYuaXRlcnJvd3MoKToNCiAgICAgICAgICAgIHRpbWVfb2JqID0gcGQudG9fZGF0ZXRpbWUocm93WydfdGltZSddKQ0KICAgICAgICAgICAgIyDovazmjaLkuLrmnKzlnLDml7bpl7TvvIzljrvpmaTml7bljLrkv6Hmga/vvIzkuI5zdGFydF90aW1lL2VuZF90aW1l5L+d5oyB5LiA6Ie0DQogICAgICAgICAgICBpZiBoYXNhdHRyKHRpbWVfb2JqLCAndHonKSBhbmQgdGltZV9vYmoudHogaXMgbm90IE5vbmU6DQogICAgICAgICAgICAgICAgIyDlr7nkuo5wYW5kYXMgVGltZXN0YW1w77yM5YWI6L2s5o2i5Li65pys5Zyw5pe25Yy65YaN6L2s5Li6UHl0aG9uIGRhdGV0aW1lDQogICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50el9jb252ZXJ0KE5vbmUpLnRvX3B5ZGF0ZXRpbWUoKQ0KICAgICAgICAgICAgZWxpZiBoYXNhdHRyKHRpbWVfb2JqLCAndG9fcHlkYXRldGltZScpOg0KICAgICAgICAgICAgICAgICMg6L2s5o2i5Li6UHl0aG9uIGRhdGV0aW1l5a+56LGhDQogICAgICAgICAgICAgICAgdGltZV9vYmogPSB0aW1lX29iai50b19weWRhdGV0aW1lKCkNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgIyDnoa7kv53msqHmnInml7bljLrkv6Hmga8NCiAgICAgICAgICAgIGlmIGhhc2F0dHIodGltZV9vYmosICd0emluZm8nKSBhbmQgdGltZV9vYmoudHppbmZvIGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgIHRpbWVfb2JqID0gdGltZV9vYmoucmVwbGFjZSh0emluZm89Tm9uZSkNCiAgICAgICAgICAgICAgICANCiAgICAgICAgICAgIHRpbWVsaW5lLmFwcGVuZCh7DQogICAgICAgICAgICAgICAgJ3RpbWUnOiB0aW1lX29iaiwNCiAgICAgICAgICAgICAgICAnbG9hZF9zdGF0dXMnOiBmbG9hdChyb3dbJ192YWx1ZSddKQ0KICAgICAgICAgICAgfSkNCg0KICAgICAgICBwYXNzDQogICAgICAgIA0KICAgICAgICAjIOiwg+ivle+8muajgOafpeaXtumXtOWvueixoeexu+Weiw0KICAgICAgICBpZiB0aW1lbGluZToNCiAgICAgICAgICAgIGZpcnN0X3RpbWUgPSB0aW1lbGluZVswXVsndGltZSddDQogICAgICAgICAgICBwYXNzDQogICAgICAgIHBhc3MNCiAgICAgICAgcGFzcw0KICAgICAgICANCiAgICAgICAgcmV0dXJuIHRpbWVsaW5lDQoNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIHBhc3MNCiAgICAgICAgcmV0dXJuIFtdDQogICAgZmluYWxseToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgY2xpZW50LmNsb3NlKCkNCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCg0KDQpkZWYgX2NhbGN1bGF0ZV9lZmZlY3RpdmVfdGltZV9wb2ludHMoDQogICAgc3RhcnRfdGltZTogZGF0ZXRpbWUsDQogICAgZW5kX3RpbWU6IGRhdGV0aW1lLA0KICAgIHRpbWVfc2xvdHM6IExpc3Rbc3RyXSwNCiAgICBpbmZsdXhfY29uZmlnOiBEaWN0W3N0ciwgc3RyXQ0KKSAtPiBEaWN0W3N0ciwgT3B0aW9uYWxbZGF0ZXRpbWVdXToNCiAgICAiIiLorqHnrpfln7rkuo7mnInmlYjov5DooYzml7bpl7TntK/orqHnmoTnnJ/lrp7ml7bpl7TngrkiIiINCiAgICANCiAgICAjIDEuIOiOt+WPlmxvYWRfc3RhdHVz5pe26Ze057q/DQogICAgdGltZWxpbmUgPSBfcXVlcnlfbG9hZF9zdGF0dXNfdGltZWxpbmUoDQogICAgICAgIHN0YXJ0X3RpbWUsIGVuZF90aW1lLA0KICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSwgaW5mbHV4X2NvbmZpZ1snb3JnJ10sIGluZmx1eF9jb25maWdbJ3Rva2VuJ10sDQogICAgICAgIGluZmx1eF9jb25maWdbJ2J1Y2tldCddLCBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddDQogICAgKQ0KICAgIA0KICAgIGlmIG5vdCB0aW1lbGluZToNCiAgICAgICAgcGFzcw0KICAgICAgICAjIOWbnumAgOWIsOWOn+Wni+aXtumXtOiuoeeulw0KICAgICAgICByZXN1bHQgPSB7fQ0KICAgICAgICBmb3Igc2xvdF9zdHIgaW4gdGltZV9zbG90czoNCiAgICAgICAgICAgIHNsb3RfaG91cnMgPSBfcGFyc2VfdGltZV9zbG90KHNsb3Rfc3RyKQ0KICAgICAgICAgICAgcmVzdWx0W3Nsb3Rfc3RyXSA9IHN0YXJ0X3RpbWUgKyB0aW1lZGVsdGEoaG91cnM9c2xvdF9ob3VycykNCiAgICAgICAgcmV0dXJuIHJlc3VsdA0KICAgIA0KICAgICMgMi4g6K6h566X5pyJ5pWI6L+Q6KGM5pe26Ze05q61DQogICAgZWZmZWN0aXZlX3BlcmlvZHMgPSBbXQ0KICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gTm9uZQ0KICAgIA0KICAgIGZvciBpLCBwb2ludCBpbiBlbnVtZXJhdGUodGltZWxpbmUpOg0KICAgICAgICBpZiBwb2ludFsnbG9hZF9zdGF0dXMnXSA9PSAxLjA6DQogICAgICAgICAgICBpZiBjdXJyZW50X3BlcmlvZF9zdGFydCBpcyBOb25lOg0KICAgICAgICAgICAgICAgIGN1cnJlbnRfcGVyaW9kX3N0YXJ0ID0gcG9pbnRbJ3RpbWUnXQ0KICAgICAgICBlbHNlOiAgIyBsb2FkX3N0YXR1cyAhPSAxLjANCiAgICAgICAgICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgICAgIGVmZmVjdGl2ZV9wZXJpb2RzLmFwcGVuZCh7DQogICAgICAgICAgICAgICAgICAgICdzdGFydCc6IGN1cnJlbnRfcGVyaW9kX3N0YXJ0LA0KICAgICAgICAgICAgICAgICAgICAnZW5kJzogcG9pbnRbJ3RpbWUnXSwNCiAgICAgICAgICAgICAgICAgICAgJ2R1cmF0aW9uX2hvdXJzJzogKHBvaW50Wyd0aW1lJ10gLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgICAgICAgICAgfSkNCiAgICAgICAgICAgICAgICBjdXJyZW50X3BlcmlvZF9zdGFydCA9IE5vbmUNCiAgICANCiAgICAjIOWkhOeQhuacgOWQjuS4gOS4quWRqOacn++8iOWmguaenOWunumqjOe7k+adn+aXtuS7jeWcqOi/kOihjO+8iQ0KICAgIGlmIGN1cnJlbnRfcGVyaW9kX3N0YXJ0IGlzIG5vdCBOb25lOg0KICAgICAgICBlZmZlY3RpdmVfcGVyaW9kcy5hcHBlbmQoew0KICAgICAgICAgICAgJ3N0YXJ0JzogY3VycmVudF9wZXJpb2Rfc3RhcnQsDQogICAgICAgICAgICAnZW5kJzogZW5kX3RpbWUsDQogICAgICAgICAgICAnZHVyYXRpb25faG91cnMnOiAoZW5kX3RpbWUgLSBjdXJyZW50X3BlcmlvZF9zdGFydCkudG90YWxfc2Vjb25kcygpIC8gMzYwMC4wDQogICAgICAgIH0pDQogICAgDQogICAgdG90YWxfZWZmZWN0aXZlX2hvdXJzID0gc3VtKHBlcmlvZFsnZHVyYXRpb25faG91cnMnXSBmb3IgcGVyaW9kIGluIGVmZmVjdGl2ZV9wZXJpb2RzKQ0KICAgIA0KICAgIGZvciBwZXJpb2QgaW4gZWZmZWN0aXZlX3BlcmlvZHM6DQogICAgICAgIHBhc3MNCiAgICANCiAgICAjIDMuIOiuoeeul+avj+S4quaXtumXtOanveWvueW6lOeahOecn+WunuaXtumXtOeCuQ0KICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50cyA9IHt9DQogICAgDQogICAgZm9yIHNsb3Rfc3RyIGluIHRpbWVfc2xvdHM6DQogICAgICAgIHRhcmdldF9lZmZlY3RpdmVfaG91cnMgPSBfcGFyc2VfdGltZV9zbG90KHNsb3Rfc3RyKQ0KICAgICAgICANCiAgICAgICAgaWYgdGFyZ2V0X2VmZmVjdGl2ZV9ob3VycyA8PSAwOg0KICAgICAgICAgICAgZWZmZWN0aXZlX3RpbWVfcG9pbnRzW3Nsb3Rfc3RyXSA9IE5vbmUNCiAgICAgICAgICAgIGNvbnRpbnVlDQogICAgICAgIA0KICAgICAgICAjIOWmguaenOebruagh+aXtumXtCA+PSDmgLvmnInmlYjml7bpl7TvvIjlhYHorrjlsI/nmoTmta7ngrnor6/lt67vvInvvIzkvb/nlKjmnIDlkI7kuIDkuKrmnInmlYjml7bpl7TmrrXnmoTnu5PmnZ/ml7bpl7QNCiAgICAgICAgIyDov5nmoLflj6/ku6XlpITnkIbovrnnlYzmg4XlhrXvvJrlrp7pqozmraPlpb3ov5DooYzkuobnm67moIfml7bplb/vvIzkvYbnlLHkuo7mta7ngrnnsr7luqblj6/og73nlaXlsI/kuo7nm67moIflgLwNCiAgICAgICAgdG9sZXJhbmNlID0gMC4wMSAgIyDlhYHorrggMC4wMSDlsI/ml7bnmoTlrrnlt64NCiAgICAgICAgaWYgdGFyZ2V0X2VmZmVjdGl2ZV9ob3VycyA+PSB0b3RhbF9lZmZlY3RpdmVfaG91cnMgLSB0b2xlcmFuY2U6DQogICAgICAgICAgICBpZiBlZmZlY3RpdmVfcGVyaW9kczoNCiAgICAgICAgICAgICAgICAjIOS9v+eUqOacgOWQjuS4gOS4quacieaViOaXtumXtOauteeahOe7k+adn+aXtumXtA0KICAgICAgICAgICAgICAgIGxhc3RfcGVyaW9kID0gZWZmZWN0aXZlX3BlcmlvZHNbLTFdDQogICAgICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQgPSBsYXN0X3BlcmlvZFsnZW5kJ10NCiAgICAgICAgICAgICAgICBlZmZlY3RpdmVfdGltZV9wb2ludHNbc2xvdF9zdHJdID0gdGFyZ2V0X3RpbWVfcG9pbnQNCiAgICAgICAgICAgICAgICBwYXNzDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgICMg5aaC5p6c5rKh5pyJ5pyJ5pWI5pe26Ze05q6177yM5L2/55So5a6e6aqM57uT5p2f5pe26Ze0DQogICAgICAgICAgICAgICAgZWZmZWN0aXZlX3RpbWVfcG9pbnRzW3Nsb3Rfc3RyXSA9IGVuZF90aW1lDQogICAgICAgICAgICAgICAgcGFzcw0KICAgICAgICAgICAgY29udGludWUKICAgICAgICANCiAgICAgICAgIyDlnKjmnInmlYjml7bpl7TmrrXkuK3mn6Xmib7ntK/orqHov5DooYx0YXJnZXRfZWZmZWN0aXZlX2hvdXJz5bCP5pe255qE5pe26Ze054K5DQogICAgICAgIGN1bXVsYXRpdmVfaG91cnMgPSAwLjANCiAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQgPSBOb25lDQogICAgICAgIA0KICAgICAgICBmb3IgcGVyaW9kIGluIGVmZmVjdGl2ZV9wZXJpb2RzOg0KICAgICAgICAgICAgcGVyaW9kX2R1cmF0aW9uID0gcGVyaW9kWydkdXJhdGlvbl9ob3VycyddDQogICAgICAgICAgICANCiAgICAgICAgICAgIGlmIGN1bXVsYXRpdmVfaG91cnMgKyBwZXJpb2RfZHVyYXRpb24gPj0gdGFyZ2V0X2VmZmVjdGl2ZV9ob3VyczoNCiAgICAgICAgICAgICAgICAjIOebruagh+aXtumXtOeCueWcqOi/meS4quWRqOacn+WGhQ0KICAgICAgICAgICAgICAgIHJlbWFpbmluZ19ob3VycyA9IHRhcmdldF9lZmZlY3RpdmVfaG91cnMgLSBjdW11bGF0aXZlX2hvdXJzDQogICAgICAgICAgICAgICAgdGFyZ2V0X3RpbWVfcG9pbnQgPSBwZXJpb2RbJ3N0YXJ0J10gKyB0aW1lZGVsdGEoaG91cnM9cmVtYWluaW5nX2hvdXJzKQ0KICAgICAgICAgICAgICAgIGJyZWFrDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgIGN1bXVsYXRpdmVfaG91cnMgKz0gcGVyaW9kX2R1cmF0aW9uDQogICAgICAgIA0KICAgICAgICBlZmZlY3RpdmVfdGltZV9wb2ludHNbc2xvdF9zdHJdID0gdGFyZ2V0X3RpbWVfcG9pbnQNCiAgICAgICAgDQogICAgICAgIGlmIHRhcmdldF90aW1lX3BvaW50Og0KICAgICAgICAgICAgcGFzcw0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgcGFzcw0KICAgIA0KICAgIHJldHVybiBlZmZlY3RpdmVfdGltZV9wb2ludHMNCg0KDQpkZWYgX3F1ZXJ5X2luZmx1eGRiX3JhbmdlX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgZmllbGRfbmFtZTogc3RyLA0KICAgIHN0YXJ0X3RpbWU6IGRhdGV0aW1lLA0KICAgIGVuZF90aW1lOiBkYXRldGltZSwNCiAgICBpbmZsdXhfdXJsOiBzdHIsDQogICAgaW5mbHV4X29yZzogc3RyLA0KICAgIGluZmx1eF90b2tlbjogc3RyLA0KICAgIGluZmx1eF9idWNrZXQ6IHN0ciwNCiAgICBpbmZsdXhfbWVhc3VyZW1lbnQ6IHN0ciwNCiAgICBmaWx0ZXJzOiBPcHRpb25hbFtEaWN0W3N0ciwgc3RyXV0gPSBOb25lLA0KKSAtPiBPcHRpb25hbFtmbG9hdF06DQogICAgIiIi5p+l6K+iIEluZmx1eERCIOiOt+WPluaMh+WumuWtl+auteWcqOaXtumXtOiMg+WbtOWGheeahOW5s+Wdh+WAvO+8iOS7heW9kyBsb2FkX3N0YXR1cyA9IDEg5pe277yJIiIiDQogICAgdHJ5Og0KICAgICAgICBmcm9tIGluZmx1eGRiX2NsaWVudCBpbXBvcnQgSW5mbHV4REJDbGllbnQNCiAgICAgICAgaW1wb3J0IHBhbmRhcyBhcyBwZA0KICAgICAgICBpbXBvcnQgd2FybmluZ3MNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQuY2xpZW50Lndhcm5pbmdzIGltcG9ydCBNaXNzaW5nUGl2b3RGdW5jdGlvbg0KICAgIGV4Y2VwdCBJbXBvcnRFcnJvcjoNCiAgICAgICAgcGFzcw0KICAgICAgICByZXR1cm4gTm9uZQ0KDQogICAgdHJ5Og0KICAgICAgICBjbGllbnQgPSBJbmZsdXhEQkNsaWVudCh1cmw9aW5mbHV4X3VybCwgb3JnPWluZmx1eF9vcmcsIHRva2VuPWluZmx1eF90b2tlbikNCiAgICAgICAgcXVlcnlfYXBpID0gY2xpZW50LnF1ZXJ5X2FwaSgpDQoNCiAgICAgICAgc3RhcnRfcmZjID0gc3RhcnRfdGltZS5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgZW5kX3JmYyA9IGVuZF90aW1lLnN0cmZ0aW1lKCclWS0lbS0lZFQlSDolTTolU1onKQ0KDQogICAgICAgICMg5p6E5bu66L+H5ruk5p2h5Lu2DQogICAgICAgIHRhZ19maWx0ZXJzID0gIiINCiAgICAgICAgaWYgZmlsdGVyczoNCiAgICAgICAgICAgIGZvciBrZXksIHZhbHVlIGluIGZpbHRlcnMuaXRlbXMoKToNCiAgICAgICAgICAgICAgICB0YWdfZmlsdGVycyArPSBmJ1xuICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJ7a2V5fSJdID09ICJ7dmFsdWV9IiknDQoNCiAgICAgICAgIyDlr7nkuo7njq/looPmuKnluqbvvIzlj5blhajpg6jpnZ4w5pWw5o2u55qE5Z2H5YC877yb5YW25LuW5a2X5q615LuN6ZyAbG9hZF9zdGF0dXM9Meetm+mAiQ0KICAgICAgICBpZiBmaWVsZF9uYW1lID09ICLnjq/looPmuKnluqYiOg0KICAgICAgICAgICAgZmx1eCA9IGYnJycNCmZyb20oYnVja2V0OiAie2luZmx1eF9idWNrZXR9IikNCiAgfD4gcmFuZ2Uoc3RhcnQ6IHtzdGFydF9yZmN9LCBzdG9wOiB7ZW5kX3JmY30pDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9tZWFzdXJlbWVudCJdID09ICJ7aW5mbHV4X21lYXN1cmVtZW50fSIpDQogIHw+IGZpbHRlcihmbjogKHIpID0+IHJbIl9maWVsZCJdID09ICJ7ZmllbGRfbmFtZX0iKQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfdmFsdWUiXSAhPSAwLjApe3RhZ19maWx0ZXJzfQ0KICB8PiBtZWFuKCkNCiAgfD4geWllbGQobmFtZTogIm1lYW5fbm9uX3plcm8iKQ0KJycnLnN0cmlwKCkNCiAgICAgICAgZWxzZToNCiAgICAgICAgICAgIGZsdXggPSBmJycnDQpmcm9tKGJ1Y2tldDogIntpbmZsdXhfYnVja2V0fSIpDQogIHw+IHJhbmdlKHN0YXJ0OiB7c3RhcnRfcmZjfSwgc3RvcDoge2VuZF9yZmN9KQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfbWVhc3VyZW1lbnQiXSA9PSAie2luZmx1eF9tZWFzdXJlbWVudH0iKQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfZmllbGQiXSA9PSAie2ZpZWxkX25hbWV9Iil7dGFnX2ZpbHRlcnN9DQogIHw+IG1lYW4oKQ0KICB8PiB5aWVsZChuYW1lOiAibWVhbl90ZW1wZXJhdHVyZV9kYXRhIikNCicnJy5zdHJpcCgpDQoNCiAgICAgICAgcGFzcw0KDQogICAgICAgIHdpdGggd2FybmluZ3MuY2F0Y2hfd2FybmluZ3MoKToNCiAgICAgICAgICAgIHdhcm5pbmdzLnNpbXBsZWZpbHRlcigiaWdub3JlIiwgTWlzc2luZ1Bpdm90RnVuY3Rpb24pDQogICAgICAgICAgICBmcmFtZXMgPSBxdWVyeV9hcGkucXVlcnlfZGF0YV9mcmFtZShmbHV4KQ0KICAgICAgICANCiAgICAgICAgaWYgaXNpbnN0YW5jZShmcmFtZXMsIGxpc3QpOg0KICAgICAgICAgICAgZGYgPSBwZC5jb25jYXQoZnJhbWVzLCBpZ25vcmVfaW5kZXg9VHJ1ZSkgaWYgZnJhbWVzIGVsc2UgcGQuRGF0YUZyYW1lKCkNCiAgICAgICAgZWxzZToNCiAgICAgICAgICAgIGRmID0gZnJhbWVzDQoNCiAgICAgICAgIyDojrflj5bnnqzml7blgLzvvIjmnIDov5HnmoTkuIDkuKrmnInmlYjmlbDmja7ngrnvvIkNCiAgICAgICAgaWYgZGYuZW1wdHkgb3IgJ192YWx1ZScgbm90IGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBwYXNzDQogICAgICAgICAgICByZXR1cm4gTm9uZQ0KICAgICAgICAgICAgDQogICAgICAgICMg5Y+W56ys5LiA6KGM55qE5YC877yI5Zug5Li65p+l6K+i5bey57uP5o6S5bqP5bm25Y+W5LqGbGFzdCgp77yJDQogICAgICAgIGluc3RhbnRfdmFsdWUgPSBkZlsnX3ZhbHVlJ10uaWxvY1swXQ0KICAgICAgICBpZiBwZC5pc25hKGluc3RhbnRfdmFsdWUpOg0KICAgICAgICAgICAgcGFzcw0KICAgICAgICAgICAgcmV0dXJuIE5vbmUNCg0KICAgICAgICB2YWx1ZSA9IGZsb2F0KGluc3RhbnRfdmFsdWUpDQogICAgICAgIA0KICAgICAgICAjIOWmguaenOacieaXtumXtOS/oeaBr++8jOiusOW9leWunumZheeahOaVsOaNruaXtumXtOeCuQ0KICAgICAgICBpZiAnX3RpbWUnIGluIGRmLmNvbHVtbnM6DQogICAgICAgICAgICBwYXNzDQogICAgICAgIGVsc2U6DQogICAgICAgICAgICBwYXNzDQogICAgICAgICAgICANCiAgICAgICAgcmV0dXJuIHZhbHVlDQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOg0KICAgICAgICBwYXNzCiAgICAgICAgcGFzcw0KICAgICAgICByZXR1cm4gTm9uZQ0KICAgIGZpbmFsbHk6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgIGNsaWVudC5jbG9zZSgpDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246DQogICAgICAgICAgICBwYXNzDQoNCg0KZGVmIF9xdWVyeV9pbmZsdXhkYl93aXRoX2xvYWRfc3RhdHVzKA0KICAgIGZpZWxkX25hbWU6IHN0ciwNCiAgICB0YXJnZXRfdGltZTogZGF0ZXRpbWUsDQogICAgaW5mbHV4X3VybDogc3RyLA0KICAgIGluZmx1eF9vcmc6IHN0ciwNCiAgICBpbmZsdXhfdG9rZW46IHN0ciwNCiAgICBpbmZsdXhfYnVja2V0OiBzdHIsDQogICAgaW5mbHV4X21lYXN1cmVtZW50OiBzdHIsDQogICAgZmlsdGVyczogT3B0aW9uYWxbRGljdFtzdHIsIHN0cl1dID0gTm9uZSwNCikgLT4gT3B0aW9uYWxbZmxvYXRdOg0KICAgICIiIuafpeivoiBJbmZsdXhEQiDojrflj5bmjIflrprlrZfmrrXlnKjmjIflrprml7bpl7TngrnnmoTnnqzml7blgLzvvIjku4XlvZMgbG9hZF9zdGF0dXMgPSAxIOaXtu+8iSIiIg0KICAgIHRyeToNCiAgICAgICAgZnJvbSBpbmZsdXhkYl9jbGllbnQgaW1wb3J0IEluZmx1eERCQ2xpZW50DQogICAgICAgIGltcG9ydCBwYW5kYXMgYXMgcGQNCiAgICAgICAgaW1wb3J0IHdhcm5pbmdzDQogICAgICAgIGZyb20gaW5mbHV4ZGJfY2xpZW50LmNsaWVudC53YXJuaW5ncyBpbXBvcnQgTWlzc2luZ1Bpdm90RnVuY3Rpb24NCiAgICBleGNlcHQgSW1wb3J0RXJyb3I6DQogICAgICAgIHBhc3MNCiAgICAgICAgcmV0dXJuIE5vbmUNCg0KICAgIHRyeToNCiAgICAgICAgY2xpZW50ID0gSW5mbHV4REJDbGllbnQodXJsPWluZmx1eF91cmwsIG9yZz1pbmZsdXhfb3JnLCB0b2tlbj1pbmZsdXhfdG9rZW4pDQogICAgICAgIHF1ZXJ5X2FwaSA9IGNsaWVudC5xdWVyeV9hcGkoKQ0KDQogICAgICAgICMg5p+l6K+i6YC76L6R77ya5p+l6K+i55uu5qCH5pe26Ze054K55LmL5YmN77yI5YyF5ZCr55uu5qCH5pe26Ze054K577yJ55qE5pWw5o2u77yM6I635Y+W5pyA5o6l6L+R55uu5qCH5pe26Ze054K555qE556s5pe25YC8DQogICAgICAgICMg5L2/55So5a6e6aqM5byA5aeL5pe26Ze05L2c5Li65p+l6K+i6LW354K577yM55uu5qCH5pe26Ze054K55L2c5Li65p+l6K+i57uI54K577yM56Gu5L+d6I635Y+W6K+l5pe26Ze054K555qE556s5pe25pWw5YC8DQogICAgICAgICMg6ZyA6KaB5LuO5a6e6aqM5byA5aeL5pe26Ze05p+l6K+i77yM5Zug5Li65pyJ5pWI5pe26Ze054K55piv5Z+65LqO57Sv6K6h6L+Q6KGM5pe26Ze06K6h566X55qEDQogICAgICAgIA0KICAgICAgICAjIOiOt+WPluWunumqjOW8gOWni+aXtumXtO+8iOmcgOimgeS7jueOr+Wig+WPmOmHj+aIluS8oOWFpeWPguaVsOiOt+WPlu+8iQ0KICAgICAgICAjIOS4uuS6hueugOWMlu+8jOaIkeS7rOS9v+eUqOS4gOS4quWQiOeQhueahOaXtumXtOeql+WPo++8muS7juebruagh+aXtumXtOeCueW+gOWJjeaOqOi2s+Wkn+mVv+eahOaXtumXtA0KICAgICAgICAjIOS9huS4uuS6hueyvuehru+8jOaIkeS7rOW6lOivpeafpeivouWIsOebruagh+aXtumXtOeCueS4uuatou+8jOWPluacgOWQjuS4gOadoQ0KICAgICAgICB3aW5kb3dfbWludXRlcyA9IDYwICAjIOW+gOWJjeafpeivojYw5YiG6ZKf77yM56Gu5L+d6IO96KaG55uW5Yiw5pWw5o2uDQogICAgICAgIA0KICAgICAgICBxdWVyeV9zdGFydCA9IHRhcmdldF90aW1lIC0gdGltZWRlbHRhKG1pbnV0ZXM9d2luZG93X21pbnV0ZXMpDQogICAgICAgICMg5p+l6K+i57uI54K56K6+572u5Li655uu5qCH5pe26Ze054K577yM56Gu5L+d6I635Y+W55qE5piv6K+l5pe26Ze054K55oiW5LmL5YmN55qE5pWw5o2uDQogICAgICAgIHF1ZXJ5X2VuZCA9IHRhcmdldF90aW1lDQogICAgICAgIA0KICAgICAgICBxdWVyeV9zdGFydF9yZmMgPSBxdWVyeV9zdGFydC5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCiAgICAgICAgcXVlcnlfZW5kX3JmYyA9IHF1ZXJ5X2VuZC5zdHJmdGltZSgnJVktJW0tJWRUJUg6JU06JVNaJykNCg0KICAgICAgICAjIOaehOW7uui/h+a7pOadoeS7tg0KICAgICAgICB0YWdfZmlsdGVycyA9ICIiDQogICAgICAgIGlmIGZpbHRlcnM6DQogICAgICAgICAgICBmb3Iga2V5LCB2YWx1ZSBpbiBmaWx0ZXJzLml0ZW1zKCk6DQogICAgICAgICAgICAgICAgdGFnX2ZpbHRlcnMgKz0gZidcbiAgfD4gZmlsdGVyKGZuOiAocikgPT4gclsie2tleX0iXSA9PSAie3ZhbHVlfSIpJw0KDQogICAgICAgICMg5p+l6K+i5rip5bqm5pWw5o2u77ya5p+l6K+i5Yiw55uu5qCH5pe26Ze054K55Li65q2i77yM5Y+W5pyA5ZCO5LiA5p2h77yI5pyA5o6l6L+R55uu5qCH5pe26Ze054K555qE556s5pe25YC877yJDQogICAgICAgIGZsdXggPSBmJycnDQpmcm9tKGJ1Y2tldDogIntpbmZsdXhfYnVja2V0fSIpDQogIHw+IHJhbmdlKHN0YXJ0OiB7cXVlcnlfc3RhcnRfcmZjfSwgc3RvcDoge3F1ZXJ5X2VuZF9yZmN9KQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfbWVhc3VyZW1lbnQiXSA9PSAie2luZmx1eF9tZWFzdXJlbWVudH0iKQ0KICB8PiBmaWx0ZXIoZm46IChyKSA9PiByWyJfZmllbGQiXSA9PSAie2ZpZWxkX25hbWV9Iil7dGFnX2ZpbHRlcnN9DQogIHw+IHNvcnQoY29sdW1uczogWyJfdGltZSJdKQ0KICB8PiBsYXN0KCkNCiAgfD4geWllbGQobmFtZTogImluc3RhbnRhbmVvdXNfYXRfZWZmZWN0aXZlX3RpbWUiKQ0KJycnLnN0cmlwKCkNCg0KICAgICAgICBwYXNzDQoNCiAgICAgICAgd2l0aCB3YXJuaW5ncy5jYXRjaF93YXJuaW5ncygpOg0KICAgICAgICAgICAgd2FybmluZ3Muc2ltcGxlZmlsdGVyKCJpZ25vcmUiLCBNaXNzaW5nUGl2b3RGdW5jdGlvbikNCiAgICAgICAgICAgIGZyYW1lcyA9IHF1ZXJ5X2FwaS5xdWVyeV9kYXRhX2ZyYW1lKGZsdXgpDQogICAgICAgIA0KICAgICAgICBpZiBpc2luc3RhbmNlKGZyYW1lcywgbGlzdCk6DQogICAgICAgICAgICBkZiA9IHBkLmNvbmNhdChmcmFtZXMsIGlnbm9yZV9pbmRleD1UcnVlKSBpZiBmcmFtZXMgZWxzZSBwZC5EYXRhRnJhbWUoKQ0KICAgICAgICBlbHNlOg0KICAgICAgICAgICAgZGYgPSBmcmFtZXMNCg0KICAgICAgICAjIOiOt+WPlueerOaXtuWAvO+8iOacgOi/keeahOS4gOS4quacieaViOaVsOaNrueCue+8iQ0KICAgICAgICBpZiBkZi5lbXB0eSBvciAnX3ZhbHVlJyBub3QgaW4gZGYuY29sdW1uczoNCiAgICAgICAgICAgIHBhc3MNCiAgICAgICAgICAgIHJldHVybiBOb25lDQogICAgICAgICAgICANCiAgICAgICAgIyDlj5bnrKzkuIDooYznmoTlgLzvvIjlm6DkuLrmn6Xor6Llt7Lnu4/mjpLluo/lubblj5bkuoZsYXN0KCnvvIkNCiAgICAgICAgaW5zdGFudF92YWx1ZSA9IGRmWydfdmFsdWUnXS5pbG9jWzBdDQogICAgICAgIGlmIHBkLmlzbmEoaW5zdGFudF92YWx1ZSk6DQogICAgICAgICAgICBwYXNzDQogICAgICAgICAgICByZXR1cm4gTm9uZQ0KDQogICAgICAgIHZhbHVlID0gZmxvYXQoaW5zdGFudF92YWx1ZSkNCiAgICAgICAgDQogICAgICAgICMg5aaC5p6c5pyJ5pe26Ze05L+h5oGv77yM6K6w5b2V5a6e6ZmF55qE5pWw5o2u5pe26Ze054K5DQogICAgICAgIGlmICdfdGltZScgaW4gZGYuY29sdW1uczoNCiAgICAgICAgICAgIHBhc3MNCiAgICAgICAgZWxzZToNCiAgICAgICAgICAgIHBhc3MNCiAgICAgICAgICAgIA0KICAgICAgICByZXR1cm4gdmFsdWUNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIHBhc3MNCiAgICAgICAgcmV0dXJuIE5vbmUNCiAgICBmaW5hbGx5Og0KICAgICAgICB0cnk6DQogICAgICAgICAgICBjbGllbnQuY2xvc2UoKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uOg0KICAgICAgICAgICAgcGFzcw0KDQoNCmRlZiBfbG9hZF90ZW1wZXJhdHVyZV9kYXRhX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgdGltZV9zbG90czogTGlzdFtzdHJdLA0KICAgIHNlY3Rpb25zOiBMaXN0W0RpY3Rbc3RyLCBBbnldXSwNCiAgICBzdGFydF90aW1lOiBPcHRpb25hbFtkYXRldGltZV0sDQogICAgZW5kX3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSwNCikgLT4gRGljdFtzdHIsIERpY3Rbc3RyLCBmbG9hdF1dOg0KICAgICIiIuS7jiBJbmZsdXhEQiDmn6Xor6LmiYDmnInmtYvor5Xpg6jkvY3lnKjlkITml7bpl7TngrnnmoTnnqzml7bmuKnluqblgLzvvIjku4XlvZMgbG9hZF9zdGF0dXMgPSAxIOaXtu+8iSIiIg0KICAgIGlmIG5vdCBzdGFydF90aW1lIG9yIG5vdCBlbmRfdGltZToNCiAgICAgICAgcGFzcw0KICAgICAgICByZXR1cm4ge30NCiAgICANCiAgICBpbmZsdXhfY29uZmlnID0gX2dldF9pbmZsdXhfY29uZmlnKCkNCiAgICANCiAgICBpZiBub3QgYWxsKFtpbmZsdXhfY29uZmlnWyd1cmwnXSwgaW5mbHV4X2NvbmZpZ1snb3JnJ10sIGluZmx1eF9jb25maWdbJ3Rva2VuJ10sIA0KICAgICAgICAgICAgICAgIGluZmx1eF9jb25maWdbJ2J1Y2tldCddLCBpbmZsdXhfY29uZmlnWydtZWFzdXJlbWVudCddXSk6DQogICAgICAgIHBhc3MNCiAgICAgICAgcmV0dXJuIHt9DQogICAgDQogICAgIyDorqHnrpfmgLvml7bplb/vvIjlsI/ml7bvvIkNCiAgICB0b3RhbF9kdXJhdGlvbiA9IChlbmRfdGltZSAtIHN0YXJ0X3RpbWUpLnRvdGFsX3NlY29uZHMoKSAvIDM2MDAuMA0KICAgIA0KICAgICMg5pS26ZuG5omA5pyJ6ZyA6KaB5p+l6K+i55qE5a2X5q61DQogICAgcXVlcnlfdGFyZ2V0czogTGlzdFt0dXBsZVtzdHIsIERpY3Rbc3RyLCBBbnldXV0gPSBbXQ0KICAgIGZvciBzZWN0aW9uIGluIHNlY3Rpb25zOg0KICAgICAgICBlbnRyaWVzID0gc2VjdGlvbi5nZXQoImVudHJpZXMiKSBvciBbXQ0KICAgICAgICBmb3IgZW50cnkgaW4gZW50cmllczoNCiAgICAgICAgICAgIGlmIGlzaW5zdGFuY2UoZW50cnksIGRpY3QpOg0KICAgICAgICAgICAgICAgIGZpZWxkX25hbWUgPSBlbnRyeS5nZXQoImZpZWxkIiwgIiIpDQogICAgICAgICAgICAgICAgaWYgZmllbGRfbmFtZToNCiAgICAgICAgICAgICAgICAgICAgcXVlcnlfdGFyZ2V0cy5hcHBlbmQoKGZpZWxkX25hbWUsIGVudHJ5KSkNCg0KICAgIGlmIG5vdCBxdWVyeV90YXJnZXRzOg0KICAgICAgICByZXR1cm4ge30NCiAgICANCiAgICAjIOiuoeeul+WfuuS6juacieaViOi/kOihjOaXtumXtOe0r+iuoeeahOecn+WunuaXtumXtOeCuQ0KICAgIGVmZmVjdGl2ZV90aW1lX3BvaW50cyA9IF9jYWxjdWxhdGVfZWZmZWN0aXZlX3RpbWVfcG9pbnRzKA0KICAgICAgICBzdGFydF90aW1lLCBlbmRfdGltZSwgdGltZV9zbG90cywgaW5mbHV4X2NvbmZpZw0KICAgICkNCiAgICANCiAgICAjIOS4uuavj+S4quacieaViOaXtumXtOeCueafpeivoua4qeW6puaVsOaNrg0KICAgIHRlbXBlcmF0dXJlX2RhdGE6IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXSA9IHt9DQogICAgDQogICAgZm9yIGlkeCwgc2xvdF9zdHIgaW4gZW51bWVyYXRlKHRpbWVfc2xvdHMpOg0KICAgICAgICB0YXJnZXRfdGltZV9wb2ludCA9IGVmZmVjdGl2ZV90aW1lX3BvaW50cy5nZXQoc2xvdF9zdHIpDQogICAgICAgIA0KICAgICAgICBpZiB0YXJnZXRfdGltZV9wb2ludCBpcyBOb25lOg0KICAgICAgICAgICAgcGFzcw0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgDQogICAgICAgIHBhc3MNCiAgICAgICAgDQogICAgICAgIGZvciBmaWVsZF9uYW1lLCBlbnRyeSBpbiBxdWVyeV90YXJnZXRzOg0KICAgICAgICAgICAgcmVzdWx0X2tleSA9IGVudHJ5LmdldCgicmVzdWx0X2tleSIpIG9yIGZpZWxkX25hbWUNCiAgICAgICAgICAgIGlmIG5vdCByZXN1bHRfa2V5Og0KICAgICAgICAgICAgICAgIHJlc3VsdF9rZXkgPSBmaWVsZF9uYW1lDQogICAgICAgICAgICBlbnRyeV9maWx0ZXJzID0gZW50cnkuZ2V0KCJmaWx0ZXJzIikgaWYgaXNpbnN0YW5jZShlbnRyeSwgZGljdCkgZWxzZSBOb25lDQogICAgICAgICAgICBpZiByZXN1bHRfa2V5IG5vdCBpbiB0ZW1wZXJhdHVyZV9kYXRhOg0KICAgICAgICAgICAgICAgIHRlbXBlcmF0dXJlX2RhdGFbcmVzdWx0X2tleV0gPSB7fQ0KDQogICAgICAgICAgICAjIOS9v+eUqOe0ouW8leS9nOS4umtlee+8jOWboOS4uuWPr+iDveaciemHjeWkjeeahOaXtumXtOWIu+W6pg0KICAgICAgICAgICAgc2xvdF9rZXkgPSBmIntpZHh9X3tzbG90X3N0cn0iICAjIOS9v+eUqOe0ouW8lSvml7bpl7TliLvluqbkvZzkuLrllK/kuIBrZXkNCg0KICAgICAgICAgICAgIyDmn6Xor6Lnnqzml7blgLzvvIjlnKjmnInmlYjml7bpl7TngrnvvIkNCiAgICAgICAgICAgIHZhbHVlID0gX3F1ZXJ5X2luZmx1eGRiX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgICAgICAgICAgICAgZmllbGRfbmFtZSwNCiAgICAgICAgICAgICAgICB0YXJnZXRfdGltZV9wb2ludCwNCiAgICAgICAgICAgICAgICBpbmZsdXhfY29uZmlnWyd1cmwnXSwNCiAgICAgICAgICAgICAgICBpbmZsdXhfY29uZmlnWydvcmcnXSwNCiAgICAgICAgICAgICAgICBpbmZsdXhfY29uZmlnWyd0b2tlbiddLA0KICAgICAgICAgICAgICAgIGluZmx1eF9jb25maWdbJ2J1Y2tldCddLA0KICAgICAgICAgICAgICAgIGluZmx1eF9jb25maWdbJ21lYXN1cmVtZW50J10sDQogICAgICAgICAgICAgICAgZmlsdGVycz1lbnRyeV9maWx0ZXJzIGlmIGVudHJ5X2ZpbHRlcnMgZWxzZSBOb25lLA0KICAgICAgICAgICAgKQ0KDQogICAgICAgICAgICBpZiB2YWx1ZSBpcyBub3QgTm9uZToNCiAgICAgICAgICAgICAgICB0ZW1wZXJhdHVyZV9kYXRhW3Jlc3VsdF9rZXldW3Nsb3Rfa2V5XSA9IHZhbHVlDQogICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKA0KICAgICAgICAgICAgICAgICAgICAiU2xvdD0lcyBmaWVsZD0lcyB2YWx1ZT0lLjNmIGF0IGVmZmVjdGl2ZV90aW1lPSVzIiwNCiAgICAgICAgICAgICAgICAgICAgc2xvdF9rZXksDQogICAgICAgICAgICAgICAgICAgIHJlc3VsdF9rZXksDQogICAgICAgICAgICAgICAgICAgIHZhbHVlLA0KICAgICAgICAgICAgICAgICAgICB0YXJnZXRfdGltZV9wb2ludC5zdHJmdGltZSgnJUg6JU06JVMnKQ0KICAgICAgICAgICAgICAgICkNCiAgICAgICAgICAgIGVsc2U6DQogICAgICAgICAgICAgICAgTE9HR0VSLmRlYnVnKA0KICAgICAgICAgICAgICAgICAgICAiU2xvdD0lcyBmaWVsZD0lcyBub19kYXRhIGF0IGVmZmVjdGl2ZV90aW1lPSVzIiwNCiAgICAgICAgICAgICAgICAgICAgc2xvdF9rZXksDQogICAgICAgICAgICAgICAgICAgIHJlc3VsdF9rZXksDQogICAgICAgICAgICAgICAgICAgIHRhcmdldF90aW1lX3BvaW50LnN0cmZ0aW1lKCclSDolTTolUycpDQogICAgICAgICAgICAgICAgKQ0KDQogICAgcmV0dXJuIHRlbXBlcmF0dXJlX2RhdGENCg0KDQpkZWYgX2J1aWxkX2NlbGxzX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgdGltZV9zbG90czogTGlzdFtzdHJdLA0KICAgIHNlY3Rpb25zOiBMaXN0W0RpY3Rbc3RyLCBBbnldXSwNCiAgICBtb3Rvcl9zcGVlZDogc3RyLA0KICAgIHN0YXJ0X3RpbWU6IE9wdGlvbmFsW2RhdGV0aW1lXSwNCiAgICBlbmRfdGltZTogT3B0aW9uYWxbZGF0ZXRpbWVdLA0KICAgIHRlbXBlcmF0dXJlX2RhdGE6IERpY3Rbc3RyLCBEaWN0W3N0ciwgZmxvYXRdXSwNCiAgICB1c2VfZGVmYXVsdHM6IGJvb2wgPSBGYWxzZSwNCikgLT4gTGlzdFtEaWN0W3N0ciwgQW55XV06DQogICAgIiIi5p6E5bu65Y2V5YWD5qC85pWw5o2u77yI5Z+65LqOIGxvYWRfc3RhdHVzID0gMSDnmoTmnInmlYjmlbDmja7vvIktIOS4juWOn+Wni+iEmuacrOe7k+aehOWujOWFqOS4gOiHtCIiIg0KICAgIGNlbGxzOiBMaXN0W0RpY3Rbc3RyLCBBbnldXSA9IFtdDQoNCiAgICBkZWYgYWRkX2NlbGwocm93OiBpbnQsIGNvbDogaW50LCB2YWx1ZTogc3RyID0gIiIsIHJvd3NwYW46IGludCA9IDEsIGNvbHNwYW46IGludCA9IDEpIC0+IE5vbmU6DQogICAgICAgIHBheWxvYWQ6IERpY3Rbc3RyLCBBbnldID0geyJyb3ciOiByb3csICJjb2wiOiBjb2wsICJ2YWx1ZSI6IHZhbHVlfQ0KICAgICAgICBpZiByb3dzcGFuID4gMToNCiAgICAgICAgICAgIHBheWxvYWRbInJvd3NwYW4iXSA9IHJvd3NwYW4NCiAgICAgICAgaWYgY29sc3BhbiA+IDE6DQogICAgICAgICAgICBwYXlsb2FkWyJjb2xzcGFuIl0gPSBjb2xzcGFuDQogICAgICAgIGNlbGxzLmFwcGVuZChwYXlsb2FkKQ0KDQogICAgIyDmqKHmnb/lt6bkvqfmoIfpopjliJflt7Lnu4/ljrvpmaTvvIzov5nph4zku4XnlJ/miJDnuq/mlbDmja7ljLrvvIzku44gKDAsMCkg5byA5aeL5aGr5YWl5pWw5YC844CCDQogICAgIyBjdXJyZW50X3JvdyDlr7nlupTmqKHmnb/kuK3nmoTlrp7pmYXmlbDmja7ooYzntKLlvJXjgIINCiAgICBjdXJyZW50X3JvdyA9IDANCiAgICBmb3Igc2VjdGlvbiBpbiBzZWN0aW9uczoNCiAgICAgICAgZW50cmllcyA9IHNlY3Rpb24uZ2V0KCJlbnRyaWVzIikgb3IgW10NCiAgICAgICAgaWYgbm90IGVudHJpZXM6DQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICAjIOavj+S4qua1i+ivlemDqOS9jeWtkOmhueWvueW6lOaooeadv+S4reeahOS4gOihjA0KICAgICAgICBmb3IgZW50cnkgaW4gZW50cmllczoNCiAgICAgICAgICAgICMg5pSv5oyB5paw5qC85byP77yI5bimIGZpZWxkIOaYoOWwhO+8ieWSjOaXp+agvOW8j++8iOe6r+Wtl+espuS4su+8iQ0KICAgICAgICAgICAgaWYgaXNpbnN0YW5jZShlbnRyeSwgZGljdCk6DQogICAgICAgICAgICAgICAgZmllbGRfbmFtZSA9IGVudHJ5LmdldCgiZmllbGQiLCAiIikNCiAgICAgICAgICAgICAgICBlbnRyeV9maWx0ZXJzID0gZW50cnkuZ2V0KCJmaWx0ZXJzIikNCiAgICAgICAgICAgICAgICBlbnRyeV9rZXkgPSBlbnRyeS5nZXQoInJlc3VsdF9rZXkiKSBvciBmaWVsZF9uYW1lDQogICAgICAgICAgICBlbHNlOg0KICAgICAgICAgICAgICAgIGZpZWxkX25hbWUgPSAiIg0KICAgICAgICAgICAgICAgIGVudHJ5X2ZpbHRlcnMgPSBOb25lDQogICAgICAgICAgICAgICAgZW50cnlfa2V5ID0gIiINCg0KICAgICAgICAgICAgIyDku4XovpPlh7rmlbDlgLzliJfvvJrliJfntKLlvJXnm7TmjqXlr7nlupTml7bpl7TmrrUNCiAgICAgICAgICAgICMg5by65Yi25aGr5YWF5omA5pyJ5YiX77yM5LyY5YWI5L2/55So5p+l6K+i5pWw5o2u77yM5ZCm5YiZ5L2/55So6buY6K6k5YC8DQogICAgICAgICAgICBpZiBmaWVsZF9uYW1lOg0KICAgICAgICAgICAgICAgIHRhcmdldF9rZXkgPSBlbnRyeV9rZXkgb3IgZmllbGRfbmFtZQ0KDQogICAgICAgICAgICAgICAgIyDpgY3ljobmiYDmnInml7bpl7TmrrXliJfvvIznoa7kv53mr4/kuIDliJfpg73mnInmlbDmja4NCiAgICAgICAgICAgICAgICBmb3IgY29sX2lkeCwgc2xvdCBpbiBlbnVtZXJhdGUodGltZV9zbG90cyk6DQogICAgICAgICAgICAgICAgICAgIHZhbHVlID0gTm9uZQ0KDQogICAgICAgICAgICAgICAgICAgICMg5LyY5YWI5L2/55So5p+l6K+i5Yiw55qE5pWw5o2uDQogICAgICAgICAgICAgICAgICAgIGlmIHRlbXBlcmF0dXJlX2RhdGE6DQogICAgICAgICAgICAgICAgICAgICAgICBzbG90X2RhdGEgPSB0ZW1wZXJhdHVyZV9kYXRhLmdldCh0YXJnZXRfa2V5LCB7fSkNCiAgICAgICAgICAgICAgICAgICAgICAgIGlmIHNsb3RfZGF0YToNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzbG90X2tleSA9IGYie2NvbF9pZHh9X3tzbG90fSINCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IHNsb3RfZGF0YS5nZXQoc2xvdF9rZXkpDQoNCiAgICAgICAgICAgICAgICAgICAgaWYgdmFsdWUgaXMgTm9uZSBhbmQgdXNlX2RlZmF1bHRzOg0KICAgICAgICAgICAgICAgICAgICAgICAgIyDkvb/nlKjln7rnoYDpu5jorqTlgLwgKyDml7bpl7TmrrXlgY/np7vvvIjmr4/kuKrml7bpl7TmrrXlop7liqAwLjHluqbvvIkNCiAgICAgICAgICAgICAgICAgICAgICAgIGRlZmF1bHRfYmFzZV92YWx1ZSA9IDI1LjAgICMg566A5YyW55qE6buY6K6k5YC8DQogICAgICAgICAgICAgICAgICAgICAgICB0aW1lX29mZnNldCA9IGNvbF9pZHggKiAwLjENCiAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlID0gZGVmYXVsdF9iYXNlX3ZhbHVlICsgdGltZV9vZmZzZXQNCg0KICAgICAgICAgICAgICAgICAgICBpZiB2YWx1ZSBpcyBOb25lOg0KICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVfc3RyID0gIiINCiAgICAgICAgICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICAgICAgICAgICMg5qC85byP5YyW5Li65a2X56ym5Liy77yI5L+d55WZMeS9jeWwj+aVsO+8iQ0KICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVfc3RyID0gZiJ7dmFsdWU6LjFmfSINCg0KICAgICAgICAgICAgICAgICAgICBhZGRfY2VsbChjdXJyZW50X3JvdywgY29sX2lkeCwgdmFsdWVfc3RyKQ0KICAgICAgICAgICAgZWxzZToNCiAgICAgICAgICAgICAgICAjIOWmguaenOayoeacieWtl+auteWQje+8jOWhq+WFheepuuWtl+espuS4sg0KICAgICAgICAgICAgICAgIGZvciBjb2xfaWR4IGluIHJhbmdlKGxlbih0aW1lX3Nsb3RzKSk6DQogICAgICAgICAgICAgICAgICAgIGFkZF9jZWxsKGN1cnJlbnRfcm93LCBjb2xfaWR4LCAiIikNCiAgICAgICAgICAgIGN1cnJlbnRfcm93ICs9IDENCg0KICAgIHJldHVybiBjZWxscw0KDQoNCmRlZiBidWlsZF90ZW1wZXJhdHVyZV90YWJsZV93aXRoX2xvYWRfc3RhdHVzKF86IERpY3Rbc3RyLCBBbnldKSAtPiBEaWN0W3N0ciwgQW55XToNCiAgICAiIiLmnoTlu7rmuKnluqbooajmoLzmlbDmja7vvIjku4Xkvb/nlKggbG9hZF9zdGF0dXMgPSAxIOeahOacieaViOaVsOaNru+8iSIiIg0KICAgIF9zZXR1cF9sb2dnaW5nKCkNCiAgICANCiAgICB0b2tlbiA9IG9zLmVudmlyb24uZ2V0KCJUQUJMRV9UT0tFTiIsICJzY3JpcHRUYWJsZTEiKQ0KICAgIHJvd19vZmZzZXQgPSBpbnQob3MuZW52aXJvbi5nZXQoIlRBQkxFX1NUQVJUX1JPVyIsICIwIikgb3IgMCkNCiAgICBjb2xfb2Zmc2V0ID0gaW50KG9zLmVudmlyb24uZ2V0KCJUQUJMRV9TVEFSVF9DT0wiLCAiMCIpIG9yIDApDQogICAgbW90b3Jfc3BlZWQgPSBvcy5lbnZpcm9uLmdldCgiVEFCTEVfTU9UT1JfU1BFRUQiLCAiOTgwUlBNIikNCiAgICANCiAgICAjIOino+aekOWunumqjOaXtumXtOiMg+WbtA0KICAgIHN0YXJ0X3RpbWUsIGVuZF90aW1lID0gX3BhcnNlX2V4cGVyaW1lbnRfdGltZXMoKQ0KICAgIA0KICAgIHRpbWVfc2xvdHMgPSBfdGltZV9zbG90cygpDQogICAgc2VjdGlvbnMgPSBfZGVmYXVsdF9zZWN0aW9ucygpDQogICAgDQogICAgIyDmn6Xor6LmuKnluqbmlbDmja7vvIjku4XlvZMgbG9hZF9zdGF0dXMgPSAxIOaXtu+8iQ0KICAgIHRlbXBlcmF0dXJlX2RhdGEgPSBfbG9hZF90ZW1wZXJhdHVyZV9kYXRhX3dpdGhfbG9hZF9zdGF0dXModGltZV9zbG90cywgc2VjdGlvbnMsIHN0YXJ0X3RpbWUsIGVuZF90aW1lKQ0KICAgIA0KICAgICMg5aeL57uI56aB5q2i6buY6K6k5pWw5o2u77yM5L+d6K+B5p+l6K+i5LiN5Yiw5YC85pe25L+d5oyB56m655m9DQogICAgdXNlX2RlZmF1bHRzID0gRmFsc2UNCiAgICANCiAgICBjZWxscyA9IF9idWlsZF9jZWxsc193aXRoX2xvYWRfc3RhdHVzKA0KICAgICAgICB0aW1lX3Nsb3RzLCANCiAgICAgICAgc2VjdGlvbnMsIA0KICAgICAgICBtb3Rvcl9zcGVlZCwgDQogICAgICAgIHN0YXJ0X3RpbWUsIA0KICAgICAgICBlbmRfdGltZSwgDQogICAgICAgIHRlbXBlcmF0dXJlX2RhdGEsDQogICAgICAgIHVzZV9kZWZhdWx0cz11c2VfZGVmYXVsdHMNCiAgICApDQogICAgDQogICAgIyDlupTnlKjooYzlgY/np7sNCiAgICBmb3IgY2VsbCBpbiBjZWxsczoNCiAgICAgICAgY2VsbFsicm93Il0gKz0gNA0KICAgIA0KICAgICMg5re75Yqg5a6e6aqM5pe26Ze05L+h5oGv77yI5LiO5Y6f5aeL6ISa5pys5a6M5YWo5LiA6Ie055qE6YC76L6R77yJDQogICAgc3RhcnRfdGltZV9yb3cgPSAxDQogICAgc3RhcnRfdGltZV92YWx1ZV9jb2wgPSAxDQogICAgZW5kX3RpbWVfdmFsdWVfY29sID0gMw0KICAgIA0KICAgICMg6I635Y+W5Y6f5aeL5pe26Ze05a2X56ym5Liy6L+b6KGM5aSE55CG77yI5LiO5Y6f5aeL6ISa5pys5L+d5oyB5LiA6Ie077yJDQogICAgc3RhcnRfc3RyID0gb3MuZW52aXJvbi5nZXQoIkVYUEVSSU1FTlRfU1RBUlQiLCAiIikuc3RyaXAoKQ0KICAgIGlmIHN0YXJ0X3N0ciBhbmQgc3RhcnRfdGltZToNCiAgICAgICAgdHJ5Og0KICAgICAgICAgICAgIyDkvb/nlKjkuI7ljp/lp4vohJrmnKznm7jlkIznmoTml7bpl7TlpITnkIbpgLvovpENCiAgICAgICAgICAgIHV0Y19hd2FyZV9kdCA9IGRhdGV0aW1lLnN0cnB0aW1lKHN0YXJ0X3N0ciwgIiVZLSVtLSVkVCVIOiVNOiVTJXoiKQ0KICAgICAgICAgICAgbG9jYWxfZHQxID0gdXRjX2F3YXJlX2R0LmFzdGltZXpvbmUodHo9Tm9uZSkNCiAgICAgICAgICAgIGxvY2FsX2R0MiA9IHV0Y19hd2FyZV9kdC5hc3RpbWV6b25lKHR6PU5vbmUpICsgdGltZWRlbHRhKGhvdXJzPTMuNSkNCiAgICAgICAgICAgIHN0YXJ0X3RpbWVfdmFsdWUgPSBsb2NhbF9kdDEuc3RyZnRpbWUoIiVZLSVtLSVkICVIOiVNOiVTIikNCiAgICAgICAgICAgIGVuZF90aW1lX3ZhbHVlID0gbG9jYWxfZHQyLnN0cmZ0aW1lKCIlWS0lbS0lZCAlSDolTTolUyIpDQogICAgICAgICAgICBjZWxscy5hcHBlbmQoeyJyb3ciOiBzdGFydF90aW1lX3JvdywgImNvbCI6IHN0YXJ0X3RpbWVfdmFsdWVfY29sLCAidmFsdWUiOiBzdGFydF90aW1lX3ZhbHVlfSkNCiAgICAgICAgICAgIGNlbGxzLmFwcGVuZCh7InJvdyI6IHN0YXJ0X3RpbWVfcm93LCAiY29sIjogZW5kX3RpbWVfdmFsdWVfY29sLCAidmFsdWUiOiBlbmRfdGltZV92YWx1ZX0pDQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgICAgIExPR0dFUi53YXJuaW5nKCJGYWlsZWQgdG8gcHJvY2VzcyBleHBlcmltZW50IHRpbWUgc3RyaW5nczogJXMiLCBlKQ0KICAgIA0KICAgICMg5p+l6K+i546v5aKD5rip5bqm77yI5LiO5Y6f5aeL6ISa5pys5a6M5YWo5LiA6Ie055qE6YC76L6R77yJDQogICAgaW5mbHV4X3VybCA9IG9zLmVudmlyb24uZ2V0KCJJTkZMVVhfVVJMIiwgIiIpLnN0cmlwKCkNCiAgICBpbmZsdXhfb3JnID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9PUkciLCAiIikuc3RyaXAoKQ0KICAgIGluZmx1eF90b2tlbiA9IG9zLmVudmlyb24uZ2V0KCJJTkZMVVhfVE9LRU4iLCAiIikuc3RyaXAoKQ0KICAgIGluZmx1eF9idWNrZXQgPSBvcy5lbnZpcm9uLmdldCgiSU5GTFVYX0JVQ0tFVCIsICJQQ00iKS5zdHJpcCgpDQogICAgaW5mbHV4X21lYXN1cmVtZW50ID0gb3MuZW52aXJvbi5nZXQoIklORkxVWF9NRUFTVVJFTUVOVCIsICJQQ01fTWVhc3VyZW1lbnQiKS5zdHJpcCgpDQogICAgDQogICAgaWYgc3RhcnRfdGltZSBhbmQgZW5kX3RpbWU6DQogICAgICAgICMg5a+55LqO546v5aKD5rip5bqm77yM5L2/55So5pe26Ze06IyD5Zu05p+l6K+i77yI5LiO5Y6f5aeL6ISa5pys6YC76L6R5LiA6Ie077yJDQogICAgICAgIHZhbHVlID0gX3F1ZXJ5X2luZmx1eGRiX3JhbmdlX3dpdGhfbG9hZF9zdGF0dXMoDQogICAgICAgICAgICAi546v5aKD5rip5bqmIiwNCiAgICAgICAgICAgIHN0YXJ0X3RpbWUsDQogICAgICAgICAgICBlbmRfdGltZSwNCiAgICAgICAgICAgIGluZmx1eF91cmwsDQogICAgICAgICAgICBpbmZsdXhfb3JnLA0KICAgICAgICAgICAgaW5mbHV4X3Rva2VuLA0KICAgICAgICAgICAgaW5mbHV4X2J1Y2tldCwNCiAgICAgICAgICAgIGluZmx1eF9tZWFzdXJlbWVudCwNCiAgICAgICAgICAgIGZpbHRlcnM9eyJkYXRhX3R5cGUiOiAiTFNEQVEifSwNCiAgICAgICAgKQ0KICAgICAgICAjIOehruS/nXZhbHVl5LiN5pivTm9uZe+8jOmBv+WFjVdvcmQgQ09N5pON5L2c5byC5bi477yI5LiO5Y6f5aeL6ISa5pys5LiA6Ie077yJDQogICAgICAgIGlmIHZhbHVlIGlzIG5vdCBOb25lOg0KICAgICAgICAgICAgY2VsbHMuYXBwZW5kKHsicm93IjogMCwgImNvbCI6IDEsICJ2YWx1ZSI6IGYie3ZhbHVlOi4xZn0ifSkNCiAgICAgICAgZWxzZToNCiAgICAgICAgICAgIGNlbGxzLmFwcGVuZCh7InJvdyI6IDAsICJjb2wiOiAxLCAidmFsdWUiOiAiIn0pDQogICAgDQogICAgTE9HR0VSLmluZm8oDQogICAgICAgICJUZW1wZXJhdHVyZSB0YWJsZSBidWlsdCB3aXRoIGxvYWRfc3RhdHVzPTEgZmlsdGVyOiB0b2tlbj0lcyBjZWxscz0lZCB0aW1lX3Nsb3RzPSVzIiwNCiAgICAgICAgdG9rZW4sDQogICAgICAgIGxlbihjZWxscyksDQogICAgICAgICIsIi5qb2luKHRpbWVfc2xvdHMpLA0KICAgICkNCiAgICANCiAgICByZXR1cm4gew0KICAgICAgICAidG9rZW4iOiB0b2tlbiwNCiAgICAgICAgInN0YXJ0Um93Ijogcm93X29mZnNldCwNCiAgICAgICAgInN0YXJ0Q29sIjogY29sX29mZnNldCwNCiAgICAgICAgImNlbGxzIjogY2VsbHMsDQogICAgfQ0KDQoNCmRlZiBfbG9hZF9wYXlsb2FkKCkgLT4gRGljdFtzdHIsIEFueV06DQogICAgIiIi5LuO5qCH5YeG6L6T5YWl5oiW546v5aKD5Y+Y6YeP5Yqg6L29cGF5bG9hZOaVsOaNriIiIg0KICAgIHRyeToNCiAgICAgICAgIyDlsJ3or5Xku47moIflh4bovpPlhaXor7vlj5ZKU09ODQogICAgICAgIHRyeToNCiAgICAgICAgICAgIGltcG9ydCBzZWxlY3QNCiAgICAgICAgICAgIGlmIHNlbGVjdC5zZWxlY3QoW3N5cy5zdGRpbl0sIFtdLCBbXSwgMC4wKVswXToNCiAgICAgICAgICAgICAgICBwYXlsb2FkX3N0ciA9IHN5cy5zdGRpbi5yZWFkKCkuc3RyaXAoKQ0KICAgICAgICAgICAgICAgIGlmIHBheWxvYWRfc3RyOg0KICAgICAgICAgICAgICAgICAgICByZXR1cm4ganNvbi5sb2FkcyhwYXlsb2FkX3N0cikNCiAgICAgICAgZXhjZXB0IEltcG9ydEVycm9yOg0KICAgICAgICAgICAgIyBXaW5kb3dz5LiKc2VsZWN05Y+v6IO95LiN5Y+v55So77yM5bCd6K+V55u05o6l6K+75Y+WDQogICAgICAgICAgICBpbXBvcnQgbXN2Y3J0DQogICAgICAgICAgICBpZiBtc3ZjcnQua2JoaXQoKToNCiAgICAgICAgICAgICAgICBwYXlsb2FkX3N0ciA9IHN5cy5zdGRpbi5yZWFkKCkuc3RyaXAoKQ0KICAgICAgICAgICAgICAgIGlmIHBheWxvYWRfc3RyOg0KICAgICAgICAgICAgICAgICAgICByZXR1cm4ganNvbi5sb2FkcyhwYXlsb2FkX3N0cikNCiAgICBleGNlcHQgRXhjZXB0aW9uOg0KICAgICAgICBwYXNzDQogICAgDQogICAgIyDlpoLmnpzmsqHmnInmoIflh4bovpPlhaXvvIzov5Tlm57nqbrlrZflhbgNCiAgICByZXR1cm4ge30NCg0KDQpkZWYgX2xvZ19lbnZpcm9ubWVudF92YXJpYWJsZXMoKSAtPiBOb25lOg0KICAgICIiIuiusOW9leebuOWFs+eOr+Wig+WPmOmHjyIiIg0KICAgIGVudl92YXJzID0gWw0KICAgICAgICAiVEFCTEVfVE9LRU4iLCAiVEFCTEVfU1RBUlRfUk9XIiwgIlRBQkxFX1NUQVJUX0NPTCIsICJUQUJMRV9USU1FX1NMT1RTIiwgIlRBQkxFX01PVE9SX1NQRUVEIiwNCiAgICAgICAgIkVYUEVSSU1FTlRfU1RBUlQiLCAiRVhQRVJJTUVOVF9FTkQiLA0KICAgICAgICAiSU5GTFVYX1VSTCIsICJJTkZMVVhfT1JHIiwgIklORkxVWF9UT0tFTiIsICJJTkZMVVhfQlVDS0VUIiwgIklORkxVWF9NRUFTVVJFTUVOVCINCiAgICBdDQogICAgDQogICAgZm9yIHZhciBpbiBlbnZfdmFyczoNCiAgICAgICAgdmFsdWUgPSBvcy5lbnZpcm9uLmdldCh2YXIsICIiKQ0KICAgICAgICBpZiAiVE9LRU4iIGluIHZhciBhbmQgdmFsdWU6DQogICAgICAgICAgICB2YWx1ZSA9IF9tYXNrX3NlY3JldCh2YWx1ZSkNCiAgICAgICAgTE9HR0VSLmRlYnVnKCJFTlYgJXM9JXMiLCB2YXIsIHZhbHVlIG9yICI8ZW1wdHk+IikNCg0KDQpkZWYgbWFpbigpIC0+IGludDoNCiAgICB0cnk6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgIHN5cy5zdGRvdXQucmVjb25maWd1cmUoZW5jb2Rpbmc9InV0Zi04IikgICMgdHlwZTogaWdub3JlW2F0dHItZGVmaW5lZF0NCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoNCiAgICAgICAgICAgIHBhc3MNCiAgICAgICAgDQogICAgICAgIHBheWxvYWQgPSBfbG9hZF9wYXlsb2FkKCkNCiAgICAgICAgdGFibGVfc3BlYyA9IGJ1aWxkX3RlbXBlcmF0dXJlX3RhYmxlX3dpdGhfbG9hZF9zdGF0dXMocGF5bG9hZCkNCiAgICAgICAgcmVzdWx0ID0geyJ0YWJsZXMiOiBbdGFibGVfc3BlY119DQogICAgICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0LCBlbnN1cmVfYXNjaWk9RmFsc2UpKQ0KICAgICAgICByZXR1cm4gMA0KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZXhjOg0KICAgICAgICBwcmludChmImVycm9yOiB7ZXhjfSIsIGZpbGU9c3lzLnN0ZGVycikNCiAgICAgICAgcmV0dXJuIDENCg0KDQppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOg0KICAgIHN5cy5leGl0KG1haW4oKSkNCg==", "scriptName": "table.py", "remark": "PCM性能测试实验" }, diff --git a/configs/600泵/table.py b/configs/600泵/table.py index bcbc588..40a2bfe 100644 --- a/configs/600泵/table.py +++ b/configs/600泵/table.py @@ -98,40 +98,50 @@ def _get_influx_config() -> Dict[str, str]: def _parse_experiment_times() -> tuple[Optional[datetime], Optional[datetime]]: - """解析实验时间""" + """解析实验时间,前端传入本地时间,转换为UTC用于InfluxDB查询""" + from datetime import timezone, timedelta + start_str = os.environ.get("EXPERIMENT_START", "").strip() end_str = os.environ.get("EXPERIMENT_END", "").strip() + # LOGGER.debug("原始时间字符串: START=%s, END=%s", start_str, end_str) + start_time: Optional[datetime] = None end_time: Optional[datetime] = None if start_str: try: - for fmt in ["%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%z"]: + for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"]: try: start_time = datetime.strptime(start_str, fmt) - if start_time.tzinfo is not None: - # 转换为本地时间并去除时区信息 - start_time = start_time.astimezone(tz=None).replace(tzinfo=None) + # 本地时间-8小时=UTC + start_time = start_time - timedelta(hours=8) + start_time = start_time.replace(tzinfo=timezone.utc) + # LOGGER.debug("解析START: 本地=%s → UTC=%s", start_str, start_time) break except ValueError: continue + if start_time is None: + LOGGER.warning("无法解析EXPERIMENT_START: %s", start_str) except Exception as e: - print(f"Warning: Failed to parse EXPERIMENT_START '{start_str}': {e}", file=sys.stderr) + LOGGER.error("解析EXPERIMENT_START失败 '%s': %s", start_str, e) if end_str: try: - for fmt in ["%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%z"]: + for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"]: try: end_time = datetime.strptime(end_str, fmt) - if end_time.tzinfo is not None: - # 转换为本地时间并去除时区信息 - end_time = end_time.astimezone(tz=None).replace(tzinfo=None) + # 本地时间-8小时=UTC + end_time = end_time - timedelta(hours=8) + end_time = end_time.replace(tzinfo=timezone.utc) + # LOGGER.debug("解析END: 本地=%s → UTC=%s", end_str, end_time) break except ValueError: continue + if end_time is None: + LOGGER.warning("无法解析EXPERIMENT_END: %s", end_str) except Exception as e: - print(f"Warning: Failed to parse EXPERIMENT_END '{end_str}': {e}", file=sys.stderr) + LOGGER.error("解析EXPERIMENT_END失败 '%s': %s", end_str, e) return start_time, end_time @@ -160,8 +170,8 @@ def _parse_time_slot(slot_str: str) -> float: def _time_slots() -> List[str]: raw = os.environ.get("TABLE_TIME_SLOTS", "").strip() if not raw: - # 根据图片,时间刻度是:0.5h, 1h, 1.5h, 2h, 2.5h, 3h, 3.5h(7列) - return ["0.5h", "1h", "1.5h", "2h", "2.5h", "3h", "3.5h"] + # 时间刻度:0.5h, 1h, 1.5h, 2h, 2.5h, 3h, 3.4h(7列) + return ["0.5h", "1h", "1.5h", "2h", "2.5h", "3h", "3.4h"] slots = [slot.strip() for slot in raw.split(",")] return [slot for slot in slots if slot] @@ -175,15 +185,11 @@ def _default_sections() -> List[Dict[str, Any]]: {"label": "#2", "field": "主轴承#2", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#2"}, {"label": "#3", "field": "主轴承#3", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#3"}, {"label": "#4", "field": "主轴承#4", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#4"}, - {"label": "#5", "field": "主轴承#5", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#5"}, - {"label": "#6", "field": "主轴承#6", "filters": {"data_type": "LSDAQ"}, "result_key": "主轴承#6"}, ]}, {"name": "十字头", "entries": [ {"label": "#1", "field": "十字头#1", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#1"}, {"label": "#2", "field": "十字头#2", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#2"}, {"label": "#3", "field": "十字头#3", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#3"}, - {"label": "#4", "field": "十字头#4", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#4"}, - {"label": "#5", "field": "十字头#5", "filters": {"data_type": "LSDAQ"}, "result_key": "十字头#5"}, ]}, {"name": "减速箱小轴承", "entries": [ {"label": "#1(输入法兰端)", "field": "减速箱小轴承1", "filters": {"data_type": "LSDAQ"}, "result_key": "减速箱小轴承#1"}, @@ -216,18 +222,20 @@ def _query_load_status_timeline( import pandas as pd import warnings from influxdb_client.client.warnings import MissingPivotFunction - except ImportError: - LOGGER.warning("InfluxDB client not available, skip load_status timeline query") + except ImportError as e: + LOGGER.error("InfluxDB客户端导入失败: %s,请安装: pip install influxdb-client pandas", e) return [] try: client = InfluxDBClient(url=influx_url, org=influx_org, token=influx_token) query_api = client.query_api() + # 确保使用UTC时间格式查询 start_rfc = start_time.strftime('%Y-%m-%dT%H:%M:%SZ') end_rfc = end_time.strftime('%Y-%m-%dT%H:%M:%SZ') + + LOGGER.debug("查询load_status时间范围: %s 到 %s", start_rfc, end_rfc) - # 查询load_status字段的所有数据点(在Breaker数据类型中) flux = f''' from(bucket: "{influx_bucket}") |> range(start: {start_rfc}, stop: {end_rfc}) @@ -253,21 +261,20 @@ from(bucket: "{influx_bucket}") LOGGER.warning("No load_status timeline data found") return [] - # 转换为时间线数据,确保时区一致性 + # 转换为时间线数据,保持UTC时区 + from datetime import timezone timeline = [] for _, row in df.iterrows(): time_obj = pd.to_datetime(row['_time']) - # 转换为本地时间,去除时区信息,与start_time/end_time保持一致 - if hasattr(time_obj, 'tz') and time_obj.tz is not None: - # 对于pandas Timestamp,先转换为本地时区再转为Python datetime - time_obj = time_obj.tz_convert(None).to_pydatetime() - elif hasattr(time_obj, 'to_pydatetime'): - # 转换为Python datetime对象 - time_obj = time_obj.to_pydatetime() + # 确保转换为UTC时区的datetime对象 + if hasattr(time_obj, 'tz_localize'): + if time_obj.tz is None: + time_obj = time_obj.tz_localize(timezone.utc) + else: + time_obj = time_obj.tz_convert(timezone.utc) - # 确保没有时区信息 - if hasattr(time_obj, 'tzinfo') and time_obj.tzinfo is not None: - time_obj = time_obj.replace(tzinfo=None) + if hasattr(time_obj, 'to_pydatetime'): + time_obj = time_obj.to_pydatetime() timeline.append({ 'time': time_obj, @@ -352,11 +359,11 @@ def _calculate_effective_time_points( LOGGER.info("Effective running periods: %d periods, total %.3f hours", len(effective_periods), total_effective_hours) - for period in effective_periods: - LOGGER.debug("Effective period: %s → %s (%.3f hours)", - period['start'].strftime('%H:%M:%S'), - period['end'].strftime('%H:%M:%S'), - period['duration_hours']) + # for period in effective_periods: + # LOGGER.debug("Effective period: %s → %s (%.3f hours)", + # period['start'].strftime('%H:%M:%S'), + # period['end'].strftime('%H:%M:%S'), + # period['duration_hours']) # 3. 计算每个时间槽对应的真实时间点 effective_time_points = {} @@ -368,24 +375,10 @@ def _calculate_effective_time_points( effective_time_points[slot_str] = None continue - # 如果目标时间 >= 总有效时间(允许小的浮点误差),使用最后一个有效时间段的结束时间 - # 这样可以处理边界情况:实验正好运行了目标时长,但由于浮点精度可能略小于目标值 - tolerance = 0.01 # 允许 0.01 小时的容差 - if target_effective_hours >= total_effective_hours - tolerance: - if effective_periods: - # 使用最后一个有效时间段的结束时间 - last_period = effective_periods[-1] - target_time_point = last_period['end'] - effective_time_points[slot_str] = target_time_point - LOGGER.info("Slot %s: effective %.3fh >= total %.3fh, using last period end time %s", - slot_str, target_effective_hours, total_effective_hours, - target_time_point.strftime('%H:%M:%S')) - else: - # 如果没有有效时间段,使用实验结束时间 - effective_time_points[slot_str] = end_time - LOGGER.info("Slot %s: effective %.3fh >= total %.3fh, using experiment end time %s", - slot_str, target_effective_hours, total_effective_hours, - end_time.strftime('%H:%M:%S') if end_time else "N/A") + if target_effective_hours > total_effective_hours: + # LOGGER.warning("Target effective time %.3fh exceeds total effective time %.3fh for slot %s", + # target_effective_hours, total_effective_hours, slot_str) + effective_time_points[slot_str] = None continue # 在有效时间段中查找累计运行target_effective_hours小时的时间点 @@ -439,8 +432,11 @@ def _query_influxdb_range_with_load_status( client = InfluxDBClient(url=influx_url, org=influx_org, token=influx_token) query_api = client.query_api() + # 确保使用UTC时间格式 start_rfc = start_time.strftime('%Y-%m-%dT%H:%M:%SZ') end_rfc = end_time.strftime('%Y-%m-%dT%H:%M:%SZ') + + LOGGER.debug("查询字段 %s 时间范围: %s 到 %s", field_name, start_rfc, end_rfc) # 构建过滤条件 tag_filters = "" @@ -518,102 +514,151 @@ def _query_influxdb_with_load_status( influx_measurement: str, filters: Optional[Dict[str, str]] = None, ) -> Optional[float]: - """查询 InfluxDB 获取指定字段在指定时间点的瞬时值(仅当 load_status = 1 时)""" + """查询 InfluxDB 获取指定字段在指定时间点的瞬时值(仅当 load_status = 1 时) + + 逻辑: + 1. 在 ±window 内同时查询温度数据和 load_status 数据 + 2. 对每个温度数据点,查找其最近的前一个 load_status 读数,判断是否为 1 + 3. 仅保留 load_status=1 期间的温度数据点 + 4. 在有效数据点中选取最接近 target_time 的瞬时值 + 5. 如果当前窗口无有效数据,逐步扩大窗口重试(10→20→30min) + """ try: from influxdb_client import InfluxDBClient import pandas as pd + import numpy as np import warnings from influxdb_client.client.warnings import MissingPivotFunction except ImportError: LOGGER.warning("InfluxDB client not available, skip query for field=%s", field_name) return None + client = None try: client = InfluxDBClient(url=influx_url, org=influx_org, token=influx_token) query_api = client.query_api() LOGGER.debug( - "Querying field=%s measurement=%s target_time=%s filters=%s (with load_status=1)", + "查询字段=%s 目标时间=%s (UTC) 过滤器=%s", field_name, - influx_measurement, target_time.strftime('%Y-%m-%dT%H:%M:%SZ'), filters or {}, ) - # 查询逻辑:查询目标时间点之前(包含目标时间点)的数据,获取最接近目标时间点的瞬时值 - # 使用实验开始时间作为查询起点,目标时间点作为查询终点,确保获取该时间点的瞬时数值 - # 需要从实验开始时间查询,因为有效时间点是基于累计运行时间计算的 - - # 获取实验开始时间(需要从环境变量或传入参数获取) - # 为了简化,我们使用一个合理的时间窗口:从目标时间点往前推足够长的时间 - # 但为了精确,我们应该查询到目标时间点为止,取最后一条 - window_minutes = 60 # 往前查询60分钟,确保能覆盖到数据 - - query_start = target_time - timedelta(minutes=window_minutes) - # 查询终点设置为目标时间点,确保获取的是该时间点或之前的数据 - query_end = target_time - - query_start_rfc = query_start.strftime('%Y-%m-%dT%H:%M:%SZ') - query_end_rfc = query_end.strftime('%Y-%m-%dT%H:%M:%SZ') - - # 构建过滤条件 + # 构建 tag 过滤条件 tag_filters = "" if filters: for key, value in filters.items(): tag_filters += f'\n |> filter(fn: (r) => r["{key}"] == "{value}")' - # 查询温度数据:查询到目标时间点为止,取最后一条(最接近目标时间点的瞬时值) - flux = f''' + # 逐步扩大窗口查找 load_status=1 的有效数据 + for window_minutes in [10, 20, 30]: + query_start = target_time - timedelta(minutes=window_minutes) + query_end = target_time + timedelta(minutes=window_minutes) + start_rfc = query_start.strftime('%Y-%m-%dT%H:%M:%SZ') + end_rfc = query_end.strftime('%Y-%m-%dT%H:%M:%SZ') + + LOGGER.debug("查询窗口 ±%dmin: %s 到 %s", window_minutes, start_rfc, end_rfc) + + # 查询温度数据(全部点位,按时间排序) + temp_flux = f''' from(bucket: "{influx_bucket}") - |> range(start: {query_start_rfc}, stop: {query_end_rfc}) + |> range(start: {start_rfc}, stop: {end_rfc}) |> filter(fn: (r) => r["_measurement"] == "{influx_measurement}") |> filter(fn: (r) => r["_field"] == "{field_name}"){tag_filters} |> sort(columns: ["_time"]) - |> last() - |> yield(name: "instantaneous_at_effective_time") '''.strip() - LOGGER.debug("Flux查询语句:\n%s", flux) + # 查询同窗口内的 load_status 时间线 + status_flux = f''' +from(bucket: "{influx_bucket}") + |> range(start: {start_rfc}, stop: {end_rfc}) + |> filter(fn: (r) => r["_measurement"] == "{influx_measurement}") + |> filter(fn: (r) => r["data_type"] == "Breaker") + |> filter(fn: (r) => r["_field"] == "load_status") + |> sort(columns: ["_time"]) +'''.strip() - with warnings.catch_warnings(): - warnings.simplefilter("ignore", MissingPivotFunction) - frames = query_api.query_data_frame(flux) - - if isinstance(frames, list): - df = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame() - else: - df = frames + LOGGER.debug("温度Flux:\n%s", temp_flux) + LOGGER.debug("状态Flux:\n%s", status_flux) - # 获取瞬时值(最近的一个有效数据点) - if df.empty or '_value' not in df.columns: - LOGGER.debug("No valid data found for field=%s at effective time point", field_name) - return None - - # 取第一行的值(因为查询已经排序并取了last()) - instant_value = df['_value'].iloc[0] - if pd.isna(instant_value): - LOGGER.debug("Instantaneous value is NaN for field=%s", field_name) - return None + with warnings.catch_warnings(): + warnings.simplefilter("ignore", MissingPivotFunction) + temp_frames = query_api.query_data_frame(temp_flux) + status_frames = query_api.query_data_frame(status_flux) - value = float(instant_value) - - # 如果有时间信息,记录实际的数据时间点 - if '_time' in df.columns: - actual_time = df['_time'].iloc[0] - LOGGER.debug("Field=%s instantaneous_value=%.3f actual_time=%s (at effective time)", - field_name, value, actual_time) - else: - LOGGER.debug("Field=%s instantaneous_value=%.3f (at effective time)", field_name, value) - - return value + # 合并结果 + if isinstance(temp_frames, list): + temp_df = pd.concat(temp_frames, ignore_index=True) if temp_frames else pd.DataFrame() + else: + temp_df = temp_frames + if isinstance(status_frames, list): + status_df = pd.concat(status_frames, ignore_index=True) if status_frames else pd.DataFrame() + else: + status_df = status_frames + + if temp_df.empty or '_value' not in temp_df.columns or '_time' not in temp_df.columns: + LOGGER.debug("±%dmin 窗口无温度数据 field=%s", window_minutes, field_name) + continue + if status_df.empty or '_value' not in status_df.columns or '_time' not in status_df.columns: + LOGGER.debug("±%dmin 窗口无 load_status 数据 field=%s", window_minutes, field_name) + continue + + # 构建 load_status 时间线(转换为int64纳秒时间戳,避免类型问题) + status_times = pd.to_datetime(status_df['_time']).values.astype('datetime64[ns]').astype(np.int64) + status_values = status_df['_value'].values.astype(float) + + # 对每个温度数据点,用最近的前一个 load_status 判断是否有效 + temp_df = temp_df.copy() + temp_df['_time_ns'] = pd.to_datetime(temp_df['_time']).values.astype('datetime64[ns]').astype(np.int64) + valid_mask = [] + + for t_ns in temp_df['_time_ns']: + # 找 <= t_ns 的最后一个 load_status 读数 + prior_idx = np.searchsorted(status_times, t_ns, side='right') - 1 + if prior_idx >= 0: + valid_mask.append(status_values[prior_idx] == 1.0) + else: + # 没有更早的读数,用最近的一个 + nearest_idx = np.argmin(np.abs(status_times - t_ns)) + valid_mask.append(status_values[nearest_idx] == 1.0) + + valid_df = temp_df[valid_mask] + + if valid_df.empty: + LOGGER.debug("±%dmin 窗口内无 load_status=1 的温度数据 field=%s", window_minutes, field_name) + continue + + # 在有效点中选取最接近 target_time 的瞬时值 + target_ns = np.datetime64(target_time, 'ns').astype(np.int64) + diffs = np.abs(valid_df['_time_ns'].values - target_ns) + closest_idx = np.argmin(diffs) + instant_value = valid_df.iloc[closest_idx]['_value'] + + if pd.isna(instant_value): + LOGGER.debug("最近有效点值为 NaN field=%s", field_name) + continue + + value = float(instant_value) + actual_time = valid_df.iloc[closest_idx]['_time'] + LOGGER.debug( + "Field=%s value=%.3f actual_time=%s (load_status=1, ±%dmin窗口, 有效点%d/%d)", + field_name, value, actual_time, window_minutes, + len(valid_df), len(temp_df), + ) + return value + + LOGGER.warning("扩窗到±30min仍无 load_status=1 数据 field=%s", field_name) + return None except Exception as e: LOGGER.error("Error querying InfluxDB for field=%s: %s", field_name, e) return None finally: - try: - client.close() - except Exception: - pass + if client: + try: + client.close() + except Exception: + pass def _load_temperature_data_with_load_status( @@ -675,8 +720,12 @@ def _load_temperature_data_with_load_status( target_time_point = effective_time_points.get(slot_str) if target_time_point is None: - LOGGER.warning("No effective time point calculated for slot %s, skipping", slot_str) - continue + LOGGER.warning("No effective time point for slot %s, using simple offset", slot_str) + slot_hours = _parse_time_slot(slot_str) + target_time_point = start_time + timedelta(hours=slot_hours) + if target_time_point > end_time: + LOGGER.warning("Time point %s exceeds end time, skipping", slot_str) + continue LOGGER.debug("Processing slot %s at effective time point %s", slot_str, target_time_point.strftime('%Y-%m-%d %H:%M:%S')) @@ -845,10 +894,15 @@ def build_temperature_table_with_load_status(_: Dict[str, Any]) -> Dict[str, Any start_str = os.environ.get("EXPERIMENT_START", "").strip() if start_str and start_time: try: - # 使用与原始脚本相同的时间处理逻辑 - utc_aware_dt = datetime.strptime(start_str, "%Y-%m-%dT%H:%M:%S%z") - local_dt1 = utc_aware_dt.astimezone(tz=None) - local_dt2 = utc_aware_dt.astimezone(tz=None) + timedelta(hours=3.5) + # 尝试带时区和不带时区两种格式 + try: + utc_aware_dt = datetime.strptime(start_str, "%Y-%m-%dT%H:%M:%S%z") + local_dt1 = utc_aware_dt.astimezone(tz=None) + except ValueError: + # 不带时区,直接解析为本地时间 + local_dt1 = datetime.strptime(start_str, "%Y-%m-%dT%H:%M:%S") + + local_dt2 = local_dt1 + timedelta(hours=3.5) start_time_value = local_dt1.strftime("%Y-%m-%d %H:%M:%S") end_time_value = local_dt2.strftime("%Y-%m-%d %H:%M:%S") cells.append({"row": start_time_row, "col": start_time_value_col, "value": start_time_value}) @@ -878,7 +932,7 @@ def build_temperature_table_with_load_status(_: Dict[str, Any]) -> Dict[str, Any ) # 确保value不是None,避免Word COM操作异常(与原始脚本一致) if value is not None: - cells.append({"row": 0, "col": 1, "value": f"{value:.1f}"}) + cells.append({"row": 0, "col": 1, "value": f"{value:.1f}℃"}) else: cells.append({"row": 0, "col": 1, "value": ""}) diff --git a/pcm_influxdb/config-1.2-debug.yaml b/pcm_influxdb/config-1.2-debug.yaml new file mode 100644 index 0000000..8fc8d20 --- /dev/null +++ b/pcm_influxdb/config-1.2-debug.yaml @@ -0,0 +1,300 @@ +# 任务配置 +task: + period: 210 # 单位分钟 + control_reg_addr: 1200 +# gps 配置 +gps: + port: /dev/ttyLP4 + baudrate: 9600 + timeout: 50 + +# breaker(断路器)配置 +breaker: + indicator: + port: /dev/ttyUSB_LIGHT + baudrate: 9600 + timeout: 50 + port: /dev/ttyUSB_BREAKER + baudrate: 9600 + timeout: 10 + task_start_threshold: 5000 #int, 判定任务开始的有功功率阈值/w + task_stop_threshold: 3000 #int, 判定任务结束的有功功率阈值/w + duration: 3 #int,电流持续时间/s + OVV: 300 #int, 过压值/V + UVV: 150 #int,欠压值/V + OCV: 10000 #int, 过流值/0.01A + LCV: 90 #int,漏电值/1mA + OTV: 150 #int,过温值/℃ + OPV: 13000 #int,过载有功功率/W + OVT: 4000 #int,过压动作时间/0.1s + UVT: 4000 #int,欠压动作时间/0.1s + LCT: 4000 #int,漏电动作时间/0.1s + OCT: 4000 #int,过流动作时间/0.1s + OPT: 4000 #int,有功过载动作时间/0.1s + OTT: 4000 #int,过温动作时间/0.1s +# lsdaq 配置 +lsdaq: + # Modbus-RTU 配置 + port: /dev/ttyLP3 # 串口号,如COM3或'/dev/ttyLP3' + baudrate: 115200 # 波特率 + timeout: 1 # 超时时间(秒) + mode: 0 # 工作模式,0-work或1-calib + # 配置采集通道传感器类型 + # 用1位标识传感器类型,16通道16位组成1个uint16数据。CH1->CH16 + # 0:PT100; 1: 4-20mA电流型传感器; + sensor_type: '0000 0000 0000 0000' + sensor_Tmp_CalibParam: + # PT100传感器对应的K值和T值 + CH1: {K2: 0, K: 0.0311314349267159, B: -536.209396150856} + CH2: {K2: 0, K: 0.0311931732683759, B: -538.060652944714} + CH3: {K2: 0, K: 0.0310081286336704, B: -533.809292492742} + CH4: {K2: 0, K: 0.0312335701465264, B: -538.760137686226} + CH5: {K2: 0, K: 0.0311077027362215, B: -536.798383628092} + CH6: {K2: 0, K: 0.0311166612575919, B: -536.877851004226} + CH7: {K2: 0, K: 0.0309995873314733, B: -534.326957548871} + CH8: {K2: 0, K: 0.031204654529397, B: -539.09245272611} + CH9: {K2: 0, K: 0.0311948194176528, B: -538.034587271267} + CH10: {K2: 0, K: 0.030996596669504, B: -534.701789148401} + CH11: {K2: 0, K: 0.0310055444481469, B: -535.504834782268} + CH12: {K2: 0, K: 0.0310551132865325, B: -536.24377288418} + CH13: {K2: 0, K: 0.0310239427307571, B: -535.807161638956} + CH14: {K2: 0, K: 0.0313454593571509, B: -541.451349369065} + CH15: {K2: 0, K: 0.0313738619404098, B: -541.684813710032} + CH16: {K2: 0, K: 0.0311081891835453, B: -536.64715117882} + sensor_Cur_CalibParam: + # 4~20mA传感器对应的K值和T值 + CH1: {K2: 0.0, K: 1.0, B: 0.0} + CH2: {K2: 0.0, K: 1.0, B: 0.0} + CH3: {K2: 0.0, K: 1.0, B: 0.0} + CH4: {K2: 0.0, K: 1.0, B: 0.0} + CH5: {K2: 0.0, K: 1.0, B: 0.0} + CH6: {K2: 0.0, K: 1.0, B: 0.0} + CH7: {K2: 0.0, K: 1.0, B: 0.0} + CH8: {K2: 0.0, K: 1.0, B: 0.0} + CH9: {K2: 0.0, K: 1.0, B: 0.0} + CH10: {K2: 0.0, K: 1.0, B: 0.0} + CH11: {K2: 0.0, K: 1.0, B: 0.0} + CH12: {K2: 0.0, K: 1.0, B: 0.0} + CH13: {K2: 0.0, K: 1.0, B: 0.0} + CH14: {K2: 0.0, K: 1.0, B: 0.0} + CH15: {K2: 0.0, K: 1.0, B: 0.0} + CH16: {K2: 0.0, K: 1.0, B: 0.0} + sensor_Pres_CalibParam: + # mA->PSI转换对应的K值和T值 + CH1: {K2: 0.0, K: 1.0, B: 0.0} + CH2: {K2: 0.0, K: 1.0, B: 0.0} + CH3: {K2: 0.0, K: 1.0, B: 0.0} + CH4: {K2: 0.0, K: 1.0, B: 0.0} + CH5: {K2: 0.0, K: 1.0, B: 0.0} + CH6: {K2: 0.0, K: 1.0, B: 0.0} + CH7: {K2: 0.0, K: 1.0, B: 0.0} + CH8: {K2: 0.0, K: 1.0, B: 0.0} + CH9: {K2: 0.0, K: 1.0, B: 0.0} + CH10: {K2: 0.0, K: 1.0, B: 0.0} + CH11: {K2: 0.0, K: 1.0, B: 0.0} + CH12: {K2: 0.0, K: 1.0, B: 0.0} + CH13: {K2: 0.0, K: 1.0, B: 0.0} + CH14: {K2: 0.0, K: 1.0, B: 0.0} + CH15: {K2: 0.0, K: 1.0, B: 0.0} + CH16: {K2: 0.0, K: 1.0, B: 0.0} + # 报警参数设置 + warning_param: + # 是否启用报警 + enable: '1111 0111 1111 1111' #1-启用,0-禁用 + CH1: {lower: -20.0, upper: 110.0} + CH2: {lower: -20.0, upper: 110.0} + CH3: {lower: -20.0, upper: 110.0} + CH4: {lower: -20.0, upper: 110.0} + CH5: {lower: -20.0, upper: 110.0} + CH6: {lower: -20.0, upper: 110.0} + CH7: {lower: -20.0, upper: 110.0} + CH8: {lower: -20.0, upper: 110.0} + CH9: {lower: -20.0, upper: 110.0} + CH10: {lower: -20.0, upper: 110.0} + CH11: {lower: -20.0, upper: 110.0} + CH12: {lower: -20.0, upper: 110.0} + CH13: {lower: -20.0, upper: 110.0} + CH14: {lower: -20.0, upper: 110.0} + CH15: {lower: -20.0, upper: 110.0} + CH16: {lower: -20.0, upper: 50.0} + # 通道别名设置 + alias: + CH1: '主轴承#1' + CH2: '主轴承#2' + CH3: '主轴承#3' + CH4: '主轴承#4' + CH5: '主轴承#5' + CH6: '主轴承#6' + CH7: '十字头#1' + CH8: '十字头#2' + CH9: '十字头#3' + CH10: '十字头#4' + CH11: '十字头#5' + CH12: '减速箱小轴承1' + CH13: '减速箱小轴承#2' + CH14: '减速箱大轴承#3' + CH15: '减速箱大轴承#4' + CH16: '环境温度' +# hsdaq 配置 +hsdaq: + host: 192.168.0.2 + port: 8080 + local_host: 192.168.0.3 + local_port: 8080 + timeout: 50 # 单位ms + channels: 16 + sample_time: 8 # 单位ms + sample_period: 1000 # 单位ms + one_sample_time: 100 # 单位us + # 配置高频采集通道传感器类型 + # 用2位标识传感器类型,16通道32位组成1个uint32数据。CH1->CH16 + # 00:NPN或PNP型开关量; 01:电压型传感器; 10:4-20mA电流型传感器; 11:振动传感器 + sensor_type: '1010 1011 1011 1011 1011 1010 1010 0100' + frame_size_max: 1464 # 最大包长 + file_size: 32000000 # 最大文件大小 + file_type: 0 # 0-csv或1-bin + output_dir: data # 文件保存目录 + min_free_gb: 1 # 最小剩余磁盘空间,单位GB + # 选择保存数据的通道,1-保存数据, 0-不保存,CH1->CH16 + save_flag: '0000 0000 0000 0000' + daq_board_no: '2504210002' + feature_type: "rms" + min_vol_cur_phy_value: 0.0 + max_vol_cur_phy_value: 160.0 + vol_cur_phy_scale: 1 + mode: 0 # 工作模式,0-'work'或1-'calib' + # 4~20mA传感器对应的K值和T值 + sensor_Cur_CalibParam: + CH1: {K2: 0.0, K: 0.00258263, B: 0.001601482} + CH2: {K2: 0.0, K: 0.002572228, B: 0.000180365} + CH3: {K2: 0.0, K: 0.002577854, B: 0.003481423} + CH4: {K2: 0.0, K: 0.002574779, B: 0.001034803} + CH5: {K2: 0.0, K: 0.002563052, B: 0.000328752} + CH6: {K2: 0.0, K: 0.002573372, B: -5.67175e-06} + CH7: {K2: 0.0, K: 0.002580244, B: 0.001414032} + CH8: {K2: 0.0, K: 0.002578148, B: 0.001619703} + CH9: {K2: 0.0, K: 0.002582191, B: 0.000300086} + CH10: {K2: 0.0, K: 0.002572029, B: 0.000148142} + CH11: {K2: 0.0, K: 0.002576609, B: 0.001021399} + # CH12: {K2: 0.0, K: 0.001928556, B: 0.003335270} + CH12: {K2: 0.0, K: 0.01903484772, B: -78.80708088} + CH13: {K2: 0.0, K: 1.0, B: 0.0} + # CH14: {K2: 0.0, K: 0.001923312, B: -0.001293004} + CH14: {K2: 0.0, K: 0.0435167228946, B: -90.5329674119268} + CH15: {K2: 0.0, K: 0.002580323, B: 0.000544915} + CH16: {K2: 0.0, K: 0.002573487, B: 0.000873064} + # 电压传感器对应的K值和T值 + sensor_Vol_CalibParam: + CH1: {K2: 0.0, K: 1.0, B: 0.0} + CH2: {K2: 0.0, K: 1.0, B: 0.0} + CH3: {K2: 0.0, K: 1.0, B: 0.0} + CH4: {K2: 0.0, K: 1.0, B: 0.0} + CH5: {K2: 0.0, K: 1.0, B: 0.0} + CH6: {K2: 0.0, K: 1.0, B: 0.0} + CH7: {K2: 0.0, K: 1.0, B: 0.0} + CH8: {K2: 0.0, K: 1.0, B: 0.0} + CH9: {K2: 0.0, K: 1.0, B: 0.0} + CH10: {K2: 0.0, K: 1.0, B: 0.0} + CH11: {K2: 0.0, K: 1.0, B: 0.0} + CH12: {K2: 0.0, K: 1.0, B: 0.0} + CH13: {K2: 0.0, K: 1.0, B: 0.0} + CH14: {K2: 0.0, K: 1.0, B: 0.0} + CH15: {K2: 0.0, K: 1.0, B: 0.0} + CH16: {K2: 0.0, K: 1.0, B: 0.0} + # 振动传感器对应的K值和T值 + sensor_Vib_CalibParam: + CH1: {K2: 0.0, K: 0.000980181688598713, B: 0.784199472182921} + CH2: {K2: 0.0, K: 0.000979536991333191, B: 0.758179588312897} + CH3: {K2: 0.0, K: 0.000980321826962675, B: 0.747037511177572} + CH4: {K2: 0.0, K: 0.000980792974240141, B: 0.757538218907948} + CH5: {K2: 0.0, K: 0.000980973262504023, B: 0.806926311144011} + CH6: {K2: 0.0, K: 0.000982175606057935, B: 0.785563011832194} + CH7: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH8: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH9: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH10: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH11: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH12: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH13: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH14: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH15: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + CH16: {K2: 0.0, K: 0.000980663833333333, B: 0.773234764062384} + # 报警参数设置 + warning_param: + # 是否启用报警 + enable: '0000000000010100' # 1-启用,0-禁用 + CH1: {lower: 0.0, upper: 1.0} + CH2: {lower: 0.0, upper: 1.0} + CH3: {lower: 0.0, upper: 1.0} + CH4: {lower: 0.0, upper: 1.0} + CH5: {lower: 0.0, upper: 1.0} + CH6: {lower: 0.0, upper: 1.0} + CH7: {lower: 0.0, upper: 1.0} + CH8: {lower: 0.0, upper: 1.0} + CH9: {lower: 0.0, upper: 1.0} + CH10: {lower: 0.0, upper: 1.0} + CH11: {lower: 0.0, upper: 1.0} + CH12: {lower: -20.0, upper: 80.0} + CH13: {lower: 0.0, upper: 1.0} + CH14: {lower: 10.0, upper: 100.0} + CH15: {lower: 0.0, upper: 1.0} + CH16: {lower: 0.0, upper: 1.0} + calib_params: + vibration: + frequency: 500 # 标定时振动频率,单位Hz + alias: + # CH1: '' + # CH2: '' + # CH3: '' + CH4: '振动1' + # CH5: '' + CH6: '振动2' + # CH7: '' + CH8: '振动3' + # CH9: '' + CH10: '振动4' + # CH11: '' + CH12: '润滑油温' + # CH13: '' + CH14: '润滑油压' + # CH15: '' + # CH16: '' +modbus-server: + host: 10.0.21.88 + port: 5020 + timeout: 50 +# 通过Modbus TCP协议提供gps、lsdaq和hsdaq特征值数据 +plc-server: + host: 192.168.1.200 + port: 5020 + timeout: 500 + slave_id: 1 + # 从PLC读取的物理量 + measurements: + pressure: + address: 100 + type: float32 + value: 0.6914023756980896 + warning_param: + lower: 0.0 + upper: 1.0 + enable: 1 + warning: 0 + flow: + address: 104 + type: float32 + value: 0.2740088403224945 + warning_param: + lower: 0.0 + upper: 1.0 + enable: 1 + warning: 0 +influxdb: + url: http://10.0.21.88:8086 + token: 4nOdMJpKXQXAGoLDYYdRYDMxoKaEpqchzkqCQnYmgMqkQVDO3zRfaO5ifaCx90HbIRRuMZtgaUKWKNqyUD1hEg== + org: MEASCON + active: true + bucket: PCM +config-server: + host: 0.0.0.0 + port: 5000 diff --git a/pcm_influxdb/docker-compose.yml b/pcm_influxdb/docker-compose.yml new file mode 100644 index 0000000..746b338 --- /dev/null +++ b/pcm_influxdb/docker-compose.yml @@ -0,0 +1,101 @@ +# version: '1.0' +services: + pcmv1: + image: pcmv1:v1.0 + container_name: pcmv1 + command: ["/bin/bash", "-c", " source .venv/bin/activate && stty -F /dev/ttyUSB_LIGHT raw && stty -F /dev/ttyUSB_BREAKER raw && python3 src/pcm-influxdb-debug.py"] + network_mode: host + depends_on: + - influxdb + privileged: true + restart: unless-stopped + mem_limit: 256M + cpuset: "0" + # ports: + # - "0.0.0.0:5000:5000" + volumes: + - /home/torizon/src:/pcmv1/src + # - /mnt/ssd_data/pcmv1:/pcmv1/data + - /home/torizon/data:/pcmv1/data + - /dev:/dev:ro + tty: true + stdin_open: true + environment: + - TZ=Asia/Shanghai + deploy: + mode: replicated + replicas: 1 + group_add: + - dialout + + influxdb: + image: influxdb:v1.0 + container_name: influxdb + restart: unless-stopped + # depends_on: + # - alpine + ports: + - "8086:8086" + mem_limit: 256M + cpuset: "1" + environment: + DOCKER_INFLUXDB_INIT_MODE: "setup" + DOCKER_INFLUXDB_INIT_USERNAME: "PCM" + DOCKER_INFLUXDB_INIT_PASSWORD: "1842moon" # 请修改密码 + DOCKER_INFLUXDB_INIT_ORG: "MEASCON" + DOCKER_INFLUXDB_INIT_BUCKET: "PCM" + volumes: + - "/mnt/ssd_data/influxdb:/var/lib/influxdb2" # 数据持久化 + - "/home/torizon/src/influxdb/config:/etc/influxdb2" # 配置持久化(可选) + + # hdtestor: + # image: hdtestor:V0.1 + # container_name: hdtestor + # command: ["/bin/bash", "-c", "/hdtestor/scripts/auto_partition_sda.sh"] + # network_mode: host + # privileged: true + # devices: + # - "/dev:/dev" + # restart: "no" + # mem_limit: 256M + # cpuset: "2" + # volumes: + # - /home/torizon/src/pcmv1/bash_scripts:/hdtestor/scripts + # environment: + # - TZ=Asia/Shanghai + + alpine: + image: alpine:v1.0 + container_name: alpine + command: ["sh", "-c", "ls /app -la && /app/auto_partition_sda.sh"] + network_mode: host + privileged: true + devices: + - "/dev:/dev" + restart: no + mem_limit: 256M + cpuset: "2" + volumes: + - /home/torizon/bash_scripts:/app + environment: + - TZ=Asia/Shanghai + + # pcmv1_flask: + # image: pcm_flask_v1:latest + # container_name: pcm_flask_v1 + # command: python src/app.pyc + # network_mode: host + # privileged: true + # restart: unless-stopped + # mem_limit: 512M + # cpuset: "2-3" + # volumes: + # - /home/torizon/app:/app + # - /mnt/ssd_data/pcmv1:/app/data + # tty: true + # stdin_open: true + # environment: + # - TZ=Asia/Shanghai + # deploy: + # mode: replicated + # replicas: 1 diff --git a/pcm_influxdb/pcm-influxdb-debug.py b/pcm_influxdb/pcm-influxdb-debug.py index 61cb73c..9945ba5 100644 --- a/pcm_influxdb/pcm-influxdb-debug.py +++ b/pcm_influxdb/pcm-influxdb-debug.py @@ -733,8 +733,15 @@ class SerialClient: self.serial = None def open(self): - self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) - return self.serial.is_open + try: + self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + if self.logger: + self.logger.info(f"[{nowStr()}] Serial opened: {self.port}, baudrate={self.baudrate}, timeout={self.timeout}") + return self.serial.is_open + except Exception as e: + if self.logger: + self.logger.error(f"[{nowStr()}] Serial open failed: {self.port}, err={e}") + return False def close(self): if self.serial and self.serial.is_open: @@ -793,10 +800,19 @@ class IndicatorController: 'turnOffAlarm': ['', "0105 00A1 0000", 8, 100, 1, 1, 3], } self.alarm = 0 - self.client.open() + opened = self.client.open() + if not opened and self.logger: + self.logger.warning(f"[{nowStr()}] Indicator serial not open, port={config.get('port', '/dev/ttyUSB1')}") def exe(self, name): - return self.client.exeCmd(self.cmdList[name]) + ret = self.client.exeCmd(self.cmdList[name]) + if self.logger: + if ret[0]: + self.logger.info(f"[{nowStr()}] Indicator cmd ok: {name}") + self.logger.debug(ret[2]) + else: + self.logger.warning(f"[{nowStr()}] Indicator cmd failed: {name}; {ret[2]}") + return ret def alarming(self, closed): """报警时:红灯亮+蜂鸣器响,绿灯灭""" diff --git a/pcm_influxdb/pcm-influxdb-debug0402 copy.py b/pcm_influxdb/pcm-influxdb-debug0402 copy.py new file mode 100644 index 0000000..5a2236d --- /dev/null +++ b/pcm_influxdb/pcm-influxdb-debug0402 copy.py @@ -0,0 +1,2148 @@ +import threading, pynmea2, time, struct, serial, socket, yaml, os, logging.config, json, subprocess, shutil, time, copy, gc, glob +from pymodbus.server.sync import StartTcpServer +from pymodbus.client.sync import ModbusTcpClient +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from threading import Lock +import numpy as np +from datetime import datetime +from pathlib import Path +from influxdb_client import InfluxDBClient, Point +from influxdb_client.client.write_api import SYNCHRONOUS +from config_service import ConfigService + +def checkValue(data, little_endian=True): + """ + 计算Modbus CRC16校验和 + 参数: + data: 字节串或字节数组 + little_endian: 是否使用小端字节序,默认为False(大端) + 返回: + CRC16值 (2字节,小端字节序) + """ + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc = crc >> 1 + if little_endian: + # 小端字节序:低位在前,高位在后 + low_byte = crc & 0xFF + high_byte = (crc >> 8) & 0xFF + return (low_byte << 8) | high_byte + else: + # 大端字节序:高位在前,低位在后 + return crc & 0xFFFF + +def nowStr(): + now = datetime.now() + ret = now.strftime('%Y/%m/%d %H:%M:%S.') + f"{now.microsecond // 1000:03d}" + return ret + +def wordData2HexStr(data): + if data: + ret = ' '.join(data[i:i+2].hex() for i in range(0, len(data), 2)) + else: + ret = '' + return ret.upper() + +def float_to_registers(value): + packed = struct.pack('>f', value) + return [struct.unpack('>H', packed[0:2])[0], struct.unpack('>H', packed[2:4])[0]] + +def registers_to_float(registers, byte_order='ABCD'): + """ + 将两个寄存器转换为浮点数 + Args: + registers (list): 两个寄存器的值 [reg1, reg2] + byte_order (str): 字节顺序 + Returns: + float: 转换后的浮点数 + """ + if len(registers) != 2: + return None + + # 将寄存器拆分为字节 + # 每个寄存器是16位,拆分为2个字节 + reg1_bytes = registers[0].to_bytes(2, byteorder='big') # 高地址寄存器 + reg2_bytes = registers[1].to_bytes(2, byteorder='big') # 低地址寄存器 + + # 根据字节顺序组合字节 + if byte_order == 'ABCD': # 标准Modbus (大端序) + byte_array = reg1_bytes + reg2_bytes + elif byte_order == 'CDAB': # 字交换 + byte_array = reg2_bytes + reg1_bytes + elif byte_order == 'BADC': # 字节交换 + byte_array = bytes(reversed(reg1_bytes)) + bytes(reversed(reg2_bytes)) + elif byte_order == 'DCBA': # 字节和字都交换 + byte_array = bytes(reversed(reg2_bytes)) + bytes(reversed(reg1_bytes)) + else: + return None + float_value = struct.unpack('>f', byte_array)[0] # '>f' 表示大端序浮点数 + # 检查是否为NaN或无穷大 + if abs(float_value) == float('inf'): + return None + return float_value + +class ConfigManager: + def __init__(self, regs_config_file, config_file, logger): + self.config_file = Path(config_file) + self.regs_config_file = Path(regs_config_file) + self.lock = threading.Lock() + self.config = {} + self.regs_config = {} + self.logger = logger + self.mapping = BidirectionalMap() + + self.load_all_configs() + + # 设置文件监视器 + # self.observer = Observer() + # self.event_handler = ConfigFileHandler(self) + # self.observer.schedule(self.event_handler, path=str(self.config_file.parent)) + # self.observer.start() + + def load_all_configs(self): + """加载主配置和寄存器配置""" + with self.lock: + if not os.path.exists(self.config_file): + self.logger.warning(f"Config file {self.config_file} not found") + + if not os.path.exists(self.regs_config_file): + self.logger.warning(f"Regsister mapping file {self.regs_config_file} not found") + + # 加载主配置 + with open(self.config_file, 'r') as f: + self.config = yaml.safe_load(f) + # 低速采集sensor_type处理 + self.config['lsdaq']['sensor_type'] = self.config['lsdaq'].get('sensor_type').replace(' ', '') + if len(self.config['lsdaq']['sensor_type']) != 16 or not all(c in '01' for c in self.config['lsdaq']['sensor_type']): + self.config['lsdaq']['sensor_type'] = '1111111111111111' + self.config['lsdaq']['sensor_type'] = int(self.config['lsdaq']['sensor_type'][::-1], 2) + + # 低速采集 warning_param enable 处理 + self.config['lsdaq']['warning_param']['enable'] = self.config['lsdaq']['warning_param'].get('enable').replace(' ', '') + if len(self.config['lsdaq']['warning_param']['enable']) != 16 or not all(c in '01' for c in self.config['lsdaq']['warning_param']['enable']): + self.config['lsdaq']['warning_param']['enable'] = '0000000000000000' + self.config['lsdaq']['warning_param']['enable'] = int(self.config['lsdaq']['warning_param']['enable'][::-1], 2) + + # 高速采集sensor_type处理 + self.config['hsdaq']['sensor_type'] = self.config['hsdaq'].get('sensor_type').replace(' ', '') + if len(self.config['hsdaq']['sensor_type']) != 32 or not all(c in '01' for c in self.config['hsdaq']['sensor_type']): + self.config['hsdaq']['sensor_type'] = '11111111111111111111111111111111' + _s = self.config['hsdaq']['sensor_type'] + self.config['hsdaq']['sensor_type'] = int(''.join([_s[2*i:2*i+2] for i in range(len(_s)//2-1, -1, -1)]), 2) + + # 高速采集save_flag处理 + self.config['hsdaq']['save_flag'] = self.config['hsdaq'].get('save_flag').replace(' ', '') + if len(self.config['hsdaq']['save_flag']) != 16 or not all(c in '01' for c in self.config['hsdaq']['save_flag']): + self.config['hsdaq']['save_flag'] = '1111111111111111' + self.config['hsdaq']['save_flag'] = int(self.config['hsdaq']['save_flag'][::-1], 2) + + # 高速采集 warning_param enable 处理 + self.config['hsdaq']['warning_param']['enable'] = self.config['hsdaq']['warning_param'].get('enable').replace(' ', '') + if len(self.config['hsdaq']['warning_param']['enable']) != 16 or not all(c in '01' for c in self.config['hsdaq']['warning_param']['enable']): + self.config['hsdaq']['warning_param']['enable'] = '0000000000000000' + self.config['hsdaq']['warning_param']['enable'] = int(self.config['hsdaq']['warning_param']['enable'][::-1], 2) + + with open(self.regs_config_file, 'r') as f: + self.regs_config = yaml.safe_load(f) + + # 构建映射关系 + self._build_mappings() + + def _build_mappings(self): + """构建配置键到地址的双向映射""" + # 处理value_regs + # if 'value_regs' in self.regs_config: + # self._process_registers_section(self.regs_config['value_regs'], '', 'value') + + # 处理control_regs + if 'control_regs' in self.regs_config: + self._process_registers_section(self.regs_config['control_regs'], '', 'control') + + def _process_registers_section(self, section, current_path, reg_type): + """处理寄存器配置部分""" + def traverse(node, current_path=""): + # print(f"node={node}, current_path={current_path}") + for key, value in node.items(): + new_path = f"{current_path}.{key}" if current_path else key + if isinstance(value, dict): + if all(isinstance(k, str) and isinstance(v, int) for k, v in value.items()): + # 这是叶子节点,包含寄存器地址 + for sub_key, address in value.items(): + full_path = f"{new_path}.{sub_key}" + self.mapping.add_mapping(full_path, address, reg_type) + else: + traverse(value, new_path) + else: + # 直接映射 + self.mapping.add_mapping(new_path, value[0], value[1]) + + traverse(section, current_path) + # print(f"key_to_address={self.mapping.key_to_address}") + # print(f"address_to_keys={self.mapping.address_to_keys}") + # print(f"key_to_data_type={self.mapping.key_to_data_type}") + # print(f"address_to_data_type={self.mapping.address_to_data_type}") + + def get_config_value(self, config_path): + """通过配置路径获取配置值""" + keys = config_path.split('.') + node = self.config + for key in keys: + if isinstance(node, dict) and key in node: + node = node[key] + else: + return None + return node + + def update_config_value(self, config_path, value): + """更新配置值并保存""" + with self.lock: + # print(config_path) + keys = config_path.split('.') + node = self.config + for key in keys[:-1]: + if key not in node: + node[key] = {} + node = node[key] + node[keys[-1]] = value + + # 保存到文件 + # self._save_config() + return True + + def _save_config(self): + """保存配置到文件""" + _config = copy.deepcopy(self.config) + _config['lsdaq']['sensor_type'] = f"{_config['lsdaq']['sensor_type']:016b}"[::-1] + _config['lsdaq']['warning_param']['enable'] = f"{_config['lsdaq']['warning_param']['enable']:016b}"[::-1] + _s = f"{_config['hsdaq']['sensor_type']:032b}" + _config['hsdaq']['sensor_type'] = ''.join([_s[2*i:2*i+2] for i in range(len(_s)//2-1, -1, -1)]) + _config['hsdaq']['save_flag'] = f"{_config['hsdaq']['save_flag']:016b}"[::-1] + _config['hsdaq']['warning_param']['enable'] = f"{_config['hsdaq']['warning_param']['enable']:016b}"[::-1] + + with open(self.config_file, 'w') as f: + yaml.dump(_config, f, sort_keys=False, default_flow_style=False) + + # def close(self): + # self.observer.stop() + # self.observer.join() + +class BidirectionalMap: + def __init__(self): + self.key_to_address = {} # 配置键 -> (地址, 类型) + self.address_to_keys = {} # 地址 -> [配置键] + self.key_to_data_type = {} # 配置键 -> 数据类型 + self.address_to_data_type = {} # 地址 -> 数据类型 + + def add_mapping(self, config_key, address, reg_type, data_type='uint16'): + """添加映射关系""" + self.key_to_address[config_key] = (address, reg_type) + self.address_to_keys.setdefault(address, []).append(config_key) + self.key_to_data_type[config_key] = data_type + self.address_to_data_type[address] = data_type + + def get_address(self, config_key): + """通过配置键获取地址和类型""" + print(self.key_to_address) + return self.key_to_address.get(config_key, (None, None)) + + def get_config_keys(self, address): + """通过地址获取配置键列表""" + # print(self.address_to_keys) + return self.address_to_keys.get(address, []) + + def get_data_type(self, identifier): + """获取数据类型,identifier可以是地址或配置键""" + if isinstance(identifier, int): + return self.address_to_data_type.get(identifier) + else: + return self.key_to_data_type.get(identifier) + +class DataTypeValidator: + @staticmethod + def validate(value, data_type): + try: + if data_type == 'float32': + return float(value) + elif data_type in ('uint16', 'uint32'): + val = int(value) + if data_type == 'uint16' and not (0 <= val <= 65535): + raise ValueError("Value out of range for uint16") + elif data_type == 'uint32' and not (0 <= val <= 4294967295): + raise ValueError("Value out of range for uint32") + return val + elif data_type == 'int32': + val = int(value) + if not (-2147483648 <= val <= 2147483647): + raise ValueError("Value out of range for int32") + return val + elif data_type == 'string': + return str(value) + else: + return int(value) # 默认处理为uint16 + except (ValueError, TypeError) as e: + logging.error(f"Validation failed for {value} as {data_type}: {str(e)}") + return None + +class RegisterConfigEnhancer: + def __init__(self, register_config): + self.register_config = register_config + self.data_type_mapping = self._create_data_type_mapping() + + def _create_data_type_mapping(self): + """为寄存器分配适当的数据类型""" + mapping = {} + + # GPS数据通常需要浮点数 + if 'value_regs' in self.register_config and 'gps' in self.register_config['value_regs']: + for field in ['latitude', 'longitude', 'altitude', 'speed']: + if field in self.register_config['value_regs']['gps']: + addr = self.register_config['value_regs']['gps'][field] + mapping[addr] = 'float32' + + # 传感器校准参数需要浮点数 + for dev in ['lsdaq', 'hsdaq']: + if dev in self.register_config.get('control_regs', {}): + for param_type in ['sensor_Tmp_CalibParam', 'sensor_Cur_CalibParam', + 'sensor_Vol_CalibParam', 'sensor_Vib_CalibParam']: + if param_type in self.register_config['control_regs'][dev]: + for ch in self.register_config['control_regs'][dev][param_type]: + for param in ['K2', 'K', 'B']: + addr = self.register_config['control_regs'][dev][param_type][ch][param] + mapping[addr] = 'float32' + + return mapping + + def get_data_type(self, address): + return self.data_type_mapping.get(address, 'uint16') + +class ModbusSequentialDataBlockForPCM(ModbusSequentialDataBlock): + def __init__(self, config_manager, logger, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config_manager = config_manager + self._is_client_write = True + self.logger = logger + self._initialize_registers() + + def _initialize_registers(self): + """Initialize register values from configuration""" + for key, value in self.config_manager.regs_config['control_regs'].items(): + config_value = self.config_manager.get_config_value(key) + # print(f"{key}:{value[0]}:{config_value}") + if config_value is not None and ('w' in value[2] or 'W' in value[2]): + match value[1]: + case 'float32': + config_value = float(config_value) + self.server_set_values(value[0]+1, float_to_registers(config_value)) + case 'uint32': + config_value = int(config_value) + # self.server_set_values(value[0]+1, [config_value & 0xFFFF, (config_value >> 16) & 0xFFFF]) + self.server_set_values(value[0]+1, [(config_value >> 16) & 0xFFFF, config_value & 0xFFFF]) + case 'int32': + config_value = int(config_value) + # self.server_set_values(value[0]+1, [config_value & 0xFFFF, (config_value >> 16) & 0xFFFF]) + self.server_set_values(value[0]+1, [(config_value >> 16) & 0xFFFF, config_value & 0xFFFF]) + case 'uint16': + config_value = int(config_value) + # print(f"{key}:{value[0]}:{config_value}:{[struct.pack('>H', config_value)[0]]}") + self.server_set_values(value[0]+1, [config_value & 0xFFFF]) + case 'int16': + config_value = int(config_value) + self.server_set_values(value[0]+1, [config_value & 0xFFFF]) + case _: + pass + self.logger.info("Register initialization completed") + + def setValues(self, address, values): + """Override setValues method""" + if not self._is_client_write: + super().setValues(address, values) + return + + super().setValues(address, values) + + # Handle client writes + updated = False + print(f"*************************address={address}, values={values}*************************") + reg_addr = address - 1 + # print(f"values = {values}") + # path = self.config_manager.mapping.get_config_keys(reg_addr) + # print(f"*************************{path}:{reg_addr}:{values}********************") + # if self.config_manager.update_config_value(path[0], value[0]): + # updated = True + + regCount = len(values) + while(regCount > 0): + path = self.config_manager.mapping.get_config_keys(reg_addr) + print(f"*************************{path}, {reg_addr}, {regCount}*************************") + dataType = self.config_manager.mapping.key_to_address[path[0]][1] + print(f"*************************{path}, {dataType}, {regCount}*************************") + if len(path) > 0: + if '16' in dataType: + print(f"*************************{path}:{reg_addr}:{values[0]}:{regCount}********************") + if dataType in ['int16', 'uint16']: + self.config_manager.update_config_value(path[0], int(values[0])) + regCount -= 1 + reg_addr += 1 + values = values[1:] + elif '32' in dataType: + print(f"*************************{path}:{reg_addr}:{values[0:2]}:{regCount}********************") + if dataType in ['int32', 'uint32']: + self.config_manager.update_config_value(path[0], (values[0]<<16)+values[1]) + elif dataType == 'float32': + self.config_manager.update_config_value(path[0], registers_to_float(values)) + regCount -= 2 + reg_addr += 2 + values = values[2:] + else: + regCount -= 1 + reg_addr += 1 + + if updated: + self.config_manager.save_config() + self.logger.debug(f"Register {address} update triggered configuration change") + + def server_set_values(self, address, values): + """Server-only write method that won't trigger YAML update""" + # self._is_client_write = False + # self.setValues(address, values) + # self._is_client_write = True + super().setValues(address, values) + +class LSDAQ: + def __init__(self, config:dict, logger): + # 加载配置参数 + ''' self.status 码表 + -200: 配置信息错误 + -201: 串口号错误 + -202: 传感器类型错误 + -203: 工作模式错误 + -100: 设备关闭 + -101: 设备未连接 + -1: 多次执行指令失败 + 0: 正常 + 100: 连接失败 + 200: 命令执行失败 + 202: 读取命令错误 + 203: 响应超时 + 204: 报头错误 + 205: 校验错误 + 206: 数据解析错误 + ''' + self.status = -1 + self.config = config + self.logger = logger + self.port = config.get('port', '/dev/ttyLP3') + if self.port != '/dev/ttyLP3': + self.status = -201 + self.baudrate = config.get('baudrate', 115200) + self.timeout = config.get('timeout', 50)/1000.0 + self.mode = config.get('mode', 0) + self.channels = config.get('channels', 16) + if self.mode not in [0, 1]: + self.mode = 0 + self.status = -203 + self.frameNo = 0 + self.sensor_type = config.get('sensor_type', 0xffff) + self.alias = config.get('alias', {}) + for i in range(16): + if f'CH{i+1}' not in self.alias: + self.alias[f'CH{i+1}'] = '' + self.reg_values = { + 'CH1': 0.0, + 'CH2': 0.0, + 'CH3': 0.0, + 'CH4': 0.0, + 'CH5': 0.0, + 'CH6': 0.0, + 'CH7': 0.0, + 'CH8': 0.0, + 'CH9': 0.0, + 'CH10': 0.0, + 'CH11': 0.0, + 'CH12': 0.0, + 'CH13': 0.0, + 'CH14': 0.0, + 'CH15': 0.0, + 'CH16': 0.0, + 'OFFSET': 0.0, + 'POWERVOL': 0.0, + 'TEMP': 0.0, + 'GAIN': 0.0, + 'REF': 0.0, + 'STATUS': 0.0 + } + self.warning_values = { + 'CH1': 0, + 'CH2': 0, + 'CH3': 0, + 'CH4': 0, + 'CH5': 0, + 'CH6': 0, + 'CH7': 0, + 'CH8': 0, + 'CH9': 0, + 'CH10': 0, + 'CH11': 0, + 'CH12': 0, + 'CH13': 0, + 'CH14': 0, + 'CH15': 0, + 'CH16': 0 + } + _sensor_Tmp_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + _sensor_Cur_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + + _sensor_Pres_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + + self.sensor_Tmp_CalibParam = config.get('sensor_Tmp_CalibParam', _sensor_Tmp_CalibParam) + self.sensor_Cur_CalibParam = config.get('sensor_Cur_CalibParam', _sensor_Cur_CalibParam) + self.sensor_Pres_CalibParam = config.get('sensor_Cur_CalibParam', _sensor_Pres_CalibParam) + + # 构建指令集 + self.cmdList = { + # 查询所有通道采集数据 + # 指令格式:指令字符串,回复长度,超时时间,发送校验标志,接收校验标志,指令描述,重试次数 + 'readAllADs': ['', f"0000 0000 0006 0103 0008 0017", 55, 200, 0, 0, 5] + } + self.optFlag = 0 + + def update_config(self): + self.mode = self.config.get('mode') + if self.mode not in [0, 1]: + self.mode = 0 + self.sensor_type = self.config.get('sensor_type', 0xffff) + self.sensor_Tmp_CalibParam = self.config.get('sensor_Tmp_CalibParam') + self.sensor_Cur_CalibParam = self.config.get('sensor_Cur_CalibParam') + self.sensor_Pres_CalibParam = self.config.get('sensor_Pres_CalibParam') + + def exeCmd(self, cmdName:str='readAllADs') -> list: # type: ignore + try: + info = '' + cmd = self.cmdList.get(cmdName, None) + self.status = 0 + if cmd is None: + self.status = 202 + return [False, None, f"Command {cmdName} not found in cmdList."] + retry = 0 + data = bytearray().fromhex(cmd[1]) + + if (cmd[4] == 1): + data += bytearray(checkValue(data[2:]).to_bytes(2, 'big')) + if len(cmd) >= 7: + RETRYTIMES = int(cmd[6]) + else: + RETRYTIMES = 1 + while (retry < RETRYTIMES): + info += f"[{nowStr()}] Sent:{wordData2HexStr(data)}\n" + recvData = bytearray() + self.serial.write(data) + time.sleep(int(cmd[3])/1000.0) + recvData = self.serial.read(int(cmd[2])) + info += (f"[{nowStr()}] Echo:{wordData2HexStr(recvData)}\n") + rspLen = int(cmd[2]) + if len(recvData) >= rspLen: + if recvData[0:2] == bytearray().fromhex(f"0000"): + # info += f"[{nowStr()}] Echo:{wordData2HexStr(recvData[0:rspLen])}\n" + rspLen = len(recvData) + if (cmd[5] == 1): + crc = int.from_bytes(recvData[rspLen-2:rspLen], byteorder='big') + calc_value = checkValue(recvData[0:rspLen-2]) + # info += f"{crc:04X}, {calc_value:04X}\n" + if crc == calc_value: + # self.logger.info(info) + return [True, recvData, info] + else: + self.status = 205 + else: + self.logger.info(info) + return [True, recvData, info] + else: + self.status = 204 + recvData = recvData[1:] + else: + self.status = 203 + retry += 1 + if retry == RETRYTIMES: + self.status = -1 + # self.logger.info(info) + return [False, None, info] + except Exception as e: + info += f"[{nowStr()}] Error in exeCmd({cmd}): {str(e)}\n" # type: ignore + # self.logger.info(info) + return [False, None, info] + + def parseData(self, cmdName, rawData): + _sensor_type = f"{self.sensor_type:016b}"[::-1] + match cmdName: + case 'readAllADs': + datas = struct.unpack('>23H', rawData[9:55]) + if self.mode == 1: + # 校准模式下,直接返回原始数据 + for i in range(self.channels): + self.reg_values[f'CH{i+1}'] = datas[i] + else: + # 工作模式下,进行数据转换 + for i in range(self.channels): + j = i + 1 + if _sensor_type[i] == '0': + # 温度传感器 + # self.logger.info(str(self.reg_values)) + self.reg_values[f'CH{j}'] = (datas[i]**2*self.sensor_Tmp_CalibParam[f'CH{j}']['K2'] + datas[i]*self.sensor_Tmp_CalibParam[f'CH{j}']['K'] + self.sensor_Tmp_CalibParam[f'CH{j}']['B']) + elif _sensor_type[i] == '1': + # 电流传感器 + self.reg_values[f'CH{j}'] = (datas[i]**2*self.sensor_Cur_CalibParam[f'CH{j}']['K2'] + datas[i]*self.sensor_Cur_CalibParam[f'CH{j}']['K'] + self.sensor_Cur_CalibParam[f'CH{j}']['B']) + # 转换为物理量 + self.reg_values[f'CH{j}'] = (self.reg_values[f'CH{j}']**2*self.sensor_Pres_CalibParam[f'CH{j}']['K2'] + self.reg_values[f'CH{j}']*self.sensor_Pres_CalibParam[f'CH{j}']['K'] + self.sensor_Pres_CalibParam[f'CH{j}']['B']) + + self.reg_values['OFFSET'] = datas[16]*256/786432 + self.reg_values['POWERVOL'] = datas[18]*256/786432 + self.reg_values['TEMP'] = (datas[19]*4500000*256/7864320-168000)/563 + 25 #7864320*256/4500000-168000)/563 + 25 + self.reg_values['GAIN'] = datas[20]*256/7864320 + self.reg_values['REF'] = datas[21]*256/786432 + self.reg_values['STATUS'] = self.status + + self.warning_check() + case _: + self.status = 206 + + def warning_check(self): + """检查是否有报警条件""" + for i in range(self.channels): + ch = f'CH{i+1}' + val = self.reg_values[ch] + wp = self.config.get('warning_param', {}) + enable_bits = f"{wp.get('enable', 0):016b}"[::-1] + if enable_bits[i] == '1': + low_limit = wp.get(ch, {}).get('lower', float('-inf')) + high_limit = wp.get(ch, {}).get('upper', float('inf')) + if val < low_limit or val > high_limit: + # self.logger.warning(f"Warning: {ch} value {val} out of limits ({low_limit}, {high_limit})") + self.warning_values[ch] = 1 + else: + self.warning_values[ch] = 0 + else: + self.warning_values[ch] = 0 + + def open(self): + """打开串口连接""" + self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + if not self.serial.is_open: + self.status = -101 + return -1 + else: + self.status = 0 + return 0 + + def close(self): + self.serial.close() + self.status = -100 + + def run(self): + """主运行循环""" + try: + while True: + match self.optFlag: + case 0: + if self.open() == 0: + self.optFlag = 1 + else: + self.optFlag = -1 + case 1: + ret = self.exeCmd('readAllADs') + if ret[0]: + self.parseData('readAllADs', ret[1]) + # self.logger.info(str(self.reg_values)) + self.frameNo += 1 + if self.frameNo > 0xFFFF: + self.frameNo = 0 + time.sleep(1) + if self.status == -1: + self.optFlag = -1 + case _: + time.sleep(5) + self.close() + self.optFlag = 0 + except KeyboardInterrupt: + self.close() + self.logger.info("Modbus Serial TCP Client stopped.") +class SerialClient: + """串口通信基础类""" + def __init__(self, port, baudrate, timeout, logger): + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.logger = logger + self.serial = None + + def open(self): + try: + self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + if self.logger: + self.logger.info(f"[{nowStr()}] Serial opened: {self.port}, baudrate={self.baudrate}, timeout={self.timeout}") + return self.serial.is_open + except Exception as e: + if self.logger: + self.logger.error(f"[{nowStr()}] Serial open failed: {self.port}, err={e}") + return False + + def close(self): + if self.serial and self.serial.is_open: + self.serial.close() + + def exeCmd(self, cmd): + try: + info = '' + data = bytearray().fromhex(cmd[1]) + if cmd[4] == 1: + data += bytearray(checkValue(data).to_bytes(2, 'big')) + + retry = 0 + RETRYTIMES = int(cmd[6]) if len(cmd) >= 7 else 1 + + while retry < RETRYTIMES: + info += f"[{nowStr()}] Sent:{wordData2HexStr(data)}\n" + self.serial.write(data) + time.sleep(int(cmd[3])/1000.0) + recvData = self.serial.read(int(cmd[2])) + info += f"[{nowStr()}] Echo:{wordData2HexStr(recvData)}\n" + + rspLen = int(cmd[2]) + if len(recvData) >= rspLen: + if recvData[0:2] == bytearray().fromhex(cmd[1][0:4]): + recvData = recvData[0:rspLen] + if cmd[5] == 1: + crc = int.from_bytes(recvData[rspLen-2:rspLen], byteorder='big') + calc_value = checkValue(recvData[0:rspLen-2]) + if crc == calc_value: + return [True, recvData, info] + else: + return [True, recvData, info] + retry += 1 + return [False, None, info] + except Exception as e: + info += f"[{nowStr()}] Error in exeCmd: {str(e)}\n" + return [False, None, info] +class IndicatorController: + """指示灯和蜂鸣器控制器(独立串口)""" + def __init__(self, config, logger): + self.logger = logger + self.client = SerialClient( + config.get('port', '/dev/ttyUSB_LIGHT'), + config.get('baudrate', 9600), + config.get('timeout', 50)/1000.0, + logger + ) + self.cmdList = { + 'turnOnGreen': ['', "0105 0002 FF00", 8, 200, 1, 1, 3], + 'turnOffGreen': ['', "0105 0002 0000", 8, 200, 1, 1, 3], + 'turnOnRed': ['', "0105 0008 FF00", 8, 200, 1, 1, 3], + 'turnOffRed': ['', "0105 0000 0000", 8, 200, 1, 1, 3], + 'turnOnAlarm': ['', "0105 00A1 FF00", 8, 200, 1, 1, 3], + 'turnOffAlarm': ['', "0105 00A1 0000", 8, 200, 1, 1, 3], + } + self.alarm = 0 + opened = self.client.open() + if not opened and self.logger: + self.logger.warning(f"[{nowStr()}] Indicator serial not open, port={config.get('port', '/dev/ttyUSB1')}") + + def exe(self, name): + ret = self.client.exeCmd(self.cmdList[name]) + if self.logger: + if ret[0]: + self.logger.info(f"[{nowStr()}] Indicator cmd ok: {name}") + self.logger.debug(ret[2]) + else: + self.logger.warning(f"[{nowStr()}] Indicator cmd failed: {name}; {ret[2]}") + return ret + + def alarming(self, closed): + """报警时:红灯亮+蜂鸣器响,绿灯灭""" + if not self.alarm and closed == 0xF0: + self.exe('turnOffGreen') + self.exe('turnOnRed') + self.exe('turnOnAlarm') + self.alarm = 1 + + def unalarming(self, closed): + """解除报警:根据合闸状态控制指示灯""" + if self.alarm: + self.exe('turnOffRed') + self.exe('turnOffAlarm') + if closed == 0xF0: + self.exe('turnOnGreen') + else: + self.exe('turnOffGreen') + self.alarm = 0 + + def turnOffAll(self): + """关闭所有指示灯和蜂鸣器""" + self.exe('turnOffGreen') + self.logger.error("***********---turnOffGreen") + self.exe('turnOffRed') + self.logger.error("***********---turnOffRed") + self.exe('turnOffAlarm') + self.logger.error("***********---turnOffAlarm") + self.alarm = 0 + + def turnOnGreen(self): + self.exe('turnOnGreen') +class Breaker: + def __init__(self, config:dict, logger): + # 加载配置参数 + ''' self.errorCode 码表 + 0x0001 打开/dev/ttyUSB0设备失败 + 0x0101 与断路器通讯失败 + ''' + self.errorCode = 0 + ''' self.load_status 码表 + 0x00 负载不在线 + 0x0101 负载在线 + ''' + self.load_status = 0 + self.config = config + self.logger = logger + self.port = config.get('port', '/dev/ttyUSB0') + self.baudrate = config.get('baudrate', 9600) + self.timeout = config.get('timeout', 50)/1000.0 + self.task_start_threshold = config.get('task_start_threshold', 2000) + self.task_stop_threshold = config.get('task_stop_threshold', 2000) + self.locked = 0 + self.closed = 0x0F + self.reasonForLastOpen = 15 + self.active_powers = [] + self.duration = config.get('duration', 5) + self.active_power = 0 + + # 创建独立的串口客户端 + self.client = SerialClient(self.port, self.baudrate, self.timeout, logger) + + # 从配置中创建指示灯控制器(如果配置存在) + indicator_config = config.get('indicator', None) + if indicator_config: + self.indicator = IndicatorController(indicator_config, logger) + else: + self.indicator = None + + OVV = config.get('OVV', 275) + UVV = config.get('UVV', 150) + OCV = config.get('OCV', 10000) + LCV = config.get('LCV', 30) + OTV = config.get('OTV', 80) + OPV = config.get('OPV', 13000) + OVT = config.get('OVT', 0) + UVT = config.get('UVT', 0) + OCT = config.get('OCT', 0) + LCT = config.get('LCT', 200) + OTT = config.get('OTT', 200) + OPT = config.get('OPT', 100) + + self.reg_values = { + 'locked': 0, + 'closed': 0x0F, + 'reasonForLastOpen': 0x0F, + 'alarm': 0, + 'active_power': 0, + 'load_status': 0 + } + # 构建指令集(仅断路器指令) + self.cmdList = { + 'readAllDatas': ['', f"0204 0000 0027", 83, 300, 1, 1, 3], + 'readOverLimitValues': ['', f"0203 0002 0006", 17, 200, 1, 1, 3], + 'readOverLimitActionTime': ['', f"0203 0010 0006", 17, 200, 1, 1, 3], + 'setOverLimitValues': ['', f"0210 0002 0006 0C {OVV:04X} {UVV:04X} {OCV:04X} {LCV:04X} {OTV:04X} {OPV:04X}", 8, 100, 1, 1, 3], + 'setOverLimitActionTime': ['', f"0210 0010 0006 0C {OVT:04X} {UVT:04X} {OCT:04X} {LCT:04X} {OTT:04X} {OPT:04X}", 8, 100, 1, 1, 3], + 'closeBreaker': ['', f"0205 0001 ff00", 8, 100, 1, 1, 3], + 'openBreaker': ['', f"0205 0001 0000", 8, 100, 1, 1, 3] + } + + self.optFlag = 0 + self.logger.info(f"Breader routine inspection started.") + + def update_config(self): + pass + + def exeCmd(self, cmdName) -> list: + cmd = self.cmdList.get(cmdName, None) + if cmd is None: + return [False, None, f"Command {cmdName} not found in cmdList."] + # self.logger.info(f"==-=={cmdName}") + return self.client.exeCmd(cmd) + + def parseData(self, cmdName, rawData): + try: + match cmdName: + case 'readAllDatas': + rawData = rawData[3:-2] + self.locked = rawData[0] + self.closed = rawData[1] + self.reasonForLastOpen = (rawData[6]&0xF0)>>4 + self.active_power = int.from_bytes(rawData[68:70], byteorder='big') + self.active_powers.append(self.active_power) + + if len(self.active_powers) > self.duration * 2: + self.active_powers = self.active_powers[1:] + if np.mean(self.active_powers) > self.task_start_threshold: + self.load_status = 1 + if np.mean(self.active_powers) < self.task_stop_threshold: + self.load_status = 0 + + + self.reg_values['locked'] = self.locked + self.reg_values['closed'] = self.closed + self.reg_values['reasonForLastOpen'] = self.reasonForLastOpen + self.reg_values['alarm'] = self.indicator.alarm if self.indicator else 0 + self.reg_values['active_power'] = self.active_power + self.reg_values['load_status'] = self.load_status + + print(f"breaker: {self.reg_values}") + + case 'closeBreaker': + pass + case 'openBreaker': + pass + case _: + pass + except Exception as e: + pass + # self.logger.error(f"[{nowStr()}] Error in Breaker: parseData({cmdName}): {str(e)}\n") + + def openBreaker(self): + # self.logger.info(f"[{nowStr()}] openBreaker called - current closed value: 0x{self.reg_values['closed']:02X}") + if self.reg_values['closed'] == 0xF0: + # self.logger.info(f"[{nowStr()}] openBreaker condition met (closed == 0xF0), setting optFlag to 2") + self.optFlag = 2 + else: + pass + # self.logger.warning(f"[{nowStr()}] openBreaker condition NOT met (closed: 0x{self.reg_values['closed']:02X} != 0xF0), optFlag unchanged") + + def closeBreaker(self): + # self.logger.info(f"[{nowStr()}] closeBreaker called - current closed value: 0x{self.reg_values['closed']:02X}") + if self.reg_values['closed'] == 0x0F: + # self.logger.info(f"[{nowStr()}] closeBreaker condition met (closed == 0x0F), setting optFlag to 3") + self.optFlag = 3 + else: + pass + # self.logger.warning(f"[{nowStr()}] closeBreaker condition NOT met (closed: 0x{self.reg_values['closed']:02X} != 0x0F), optFlag unchanged") + + def alarming(self): + """报警时:红灯亮+蜂鸣器响,绿灯灭""" + if self.indicator: + self.indicator.alarming(self.closed & 0xF0) + + def unalarming(self): + """解除报警:根据合闸状态控制指示灯""" + if self.indicator: + self.indicator.unalarming(self.closed & 0xFF) + + def open(self): + """打开串口连接""" + if self.client.open(): + self.errorCode = 0 + return 0 + else: + self.errorCode = 0x0001 + return -1 + + def close(self): + self.client.close() + + def run(self): + """主运行循环""" + try: + while True: + match self.optFlag: + case 0: + if self.open() == 0: + ret0 = self.exeCmd('openBreaker') + # 初始化时关闭所有指示灯 + if self.indicator: + self.indicator.turnOffAll() + ret1 = self.exeCmd('setOverLimitValues') + ret2 = self.exeCmd('readOverLimitValues') + self.logger.info(f"readOverLimitValues ret: {ret2}") + ret3 = self.exeCmd('setOverLimitActionTime') + ret4 = self.exeCmd('readOverLimitActionTime') + self.logger.info(f"readOverLimitActionTime ret: {ret4}") + if ret0[0] and ret1[0]: + self.optFlag = 1 + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case 1: + time.sleep(0.2) + ret = self.exeCmd('readAllDatas') + self.logger.info(f"readAllDatas ret: {wordData2HexStr(ret[1])}") + if ret[0]: + self.parseData('readAllDatas', ret[1]) + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case 2: + ret = self.exeCmd('openBreaker') + if ret[0]: + # # 分闸成功后,关闭所有指示灯 + # if self.indicator: + # self.indicator.turnOffAll() + self.optFlag = 1 + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case 3: + ret = self.exeCmd('closeBreaker') + if ret[0]: + # 合闸成功后,点亮绿灯 + if self.indicator: + self.indicator.turnOnGreen() + self.optFlag = 1 + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case _: + time.sleep(1) + self.close() + self.optFlag = 0 + except Exception as e: + self.close() + self.logger.info(f"Error in Breader: run(), {e}") + +class GPS: + def __init__(self, config:dict, logger): + self.status = -1 + self.logger = logger + self.config = config + self.port = config.get('port', '/dev/ttyLP4') + if self.port != '/dev/ttyLP4': + self.status = -201 + self.baudrate = config.get('baudrate', 9600) + self.timeout = config.get('timeout', 1) + self.optFlag = 0 + self.gps_data = {'latitude': 0.0, 'longitude': 0.0, 'altitude': 0.0, 'speed': 0.0} + + def read_data(self): + """从串口读取GPS数据""" + if not self.serial or not self.serial.is_open: + return -1 + try: + # 读取NMEA数据 (简化示例,实际需要解析NMEA语句) + line = self.serial.readline().decode('ascii', errors='ignore').strip() + if line.startswith('$GNGGA') or line.startswith('$GPGGA') or line.startswith('$BDGGA'): + # 示例解析GPGGA语句 (实际应用中需要更健壮的解析) + parts = line.split(',') + if len(parts) > 9: + try: + # 纬度格式转换: ddmm.mmmm -> 十进制 + lat = (float(parts[2][:2]) if parts[2] else 0.0) + (float(parts[2][2:]) if parts[2] else 0.0)/60.0 + if parts[3] == 'S': + lat = -lat + + # 经度格式转换: dddmm.mmmm -> 十进制 + lon = (float(parts[4][:3]) if parts[4] else 0.0) + (float(parts[4][3:]) if parts[4] else 0.0)/60.0 + if parts[5] == 'W': + lon = -lon + + # 海拔高度 + alt = float(parts[9]) if parts[9] else 0.0 + + self.gps_data = { + 'latitude': lat, + 'longitude': lon, + 'altitude': alt, + 'speed': 0.0 # GPGGA不包含速度,需要从GPRMC获取 + } + return 0 + except (ValueError, IndexError) as e: + raise Exception(f"Error in parse GPS data: {e}") + except Exception as e: + self.logger.error(f"Error in read_gps_data(): {e}") + + def open(self): + """打开串口连接""" + self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + if not self.serial.is_open: + self.status = -101 + return -1 + else: + self.status = 0 + return 0 + + def close(self): + self.serial.close() + self.status = -100 + + def run(self): + """主运行循环""" + try: + while True: + match self.optFlag: + case 0: + if self.open() == 0: + self.optFlag = 1 + else: + self.optFlag = -1 + case 1: + ret = self.read_data() + if ret != 0: + self.optFlag = -1 + continue + self.logger.info(str(self.gps_data)) + case _: + time.sleep(5) + self.close() + self.open() + self.optFlag = 0 + except KeyboardInterrupt: + self.close() + self.logger.info("Modbus Serial TCP Client stopped.") + +class HSDAQ: + def __init__(self, config:dict, logger): + try: + self.config = config + self.logger = logger + result = subprocess.run(["ip","neigh","add", "192.168.0.2", "lladdr","00:0A:35:01:FE:C0", "dev", "ethernet0"], capture_output=True, text=True, encoding="utf-8") + if result.returncode != 0: + self.logger.info(result.stderr) + # result = subprocess.run(["sudo","ethtool","-s", "ethernet0", "speed", "100", "duplex", "full", "autoneg", "off"], capture_output=True, text=True, encoding="utf-8") + # if result.returncode != 0: + # self.logger.info(result.stderr) + + # 设置允许强制修改缓存区大小 + self.sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003)) + SO_RCVBUFORCE = 33 + self.sock.setsockopt(socket.SOL_SOCKET, SO_RCVBUFORCE, 1024 * 1024 * 25) + # 设置 SO_NO_CHECK 选项,使用整数值 11 + SO_NO_CHECK = 11 + self.sock.setsockopt(socket.SOL_SOCKET, SO_NO_CHECK, 0) + actual_buf_size = self.sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) + self.logger.info(f"Requested UDP buffer: 50MB, Actual UDP buffer: {actual_buf_size/1024/1024:.2f}MB") + self.sock.bind(('ethernet0', 0)) + self.dataFileDir = self.config['output_dir'] + self.file_type = self.config.get('file_type', 1) + if self.file_type not in [0, 1]: + self.file_type = 1 + + self.save_flag = self.config.get('save_flag', 0xffff) + self.channels = self.config.get('channels', 16) + + if not os.path.exists(self.dataFileDir): + os.makedirs(self.dataFileDir) + for i in range(self.channels): + os.makedirs(os.path.join(self.config['output_dir'], f"{i+1:02}"), exist_ok=True) + + + self.buffer = b'' + self.feature_data = {} + self.frequency = [0]*16 + self.reg_values = [] + + self.daqBoardNo = self.config.get('daq_board_no', 'XXXXXXXXXX') + self.sensor_type = self.config.get('sensor_type', 0xffffffff) + + self.feature_type = self.config.get('feature_type', '加速度rms') + self.min_vol_cur_phy_value = self.config.get('min_vol_cur_phy_value', 0.0) + self.max_vol_cur_phy_value = self.config.get('max_vol_cur_phy_value', 160.0) + self.scale = self.config.get('vol_cur_phy_scale', 1) + + self.sample_time = self.config.get('sample_time', 1000) + self.sample_period = self.config.get('sample_period', 4000) + self.one_sample_time = self.config.get('one_sample_time', 10) + self.sample_rate = int(1000000/self.one_sample_time) + + self.sample_points = int(self.sample_time*1000/self.one_sample_time) + self.mode = self.config.get('mode', 0) + self.alias = config.get('alias', {}) + for i in range(16): + if f'CH{i+1}' not in self.alias: + self.alias[f'CH{i+1}'] = '' + + self.cmdList = {'startDAQ': bytes.fromhex(f"DDDD 0001 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}"), + 'stopDAQ': bytes.fromhex(f"DDDD 0000 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}")} + + _sensor_Vol_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + _sensor_Cur_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + _sensor_Vib_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + self.sensor_Vol_CalibParam = self.config.get('sensor_Vol_CalibParam', _sensor_Vol_CalibParam) + self.sensor_Cur_CalibParam = self.config.get('sensor_Cur_CalibParam', _sensor_Cur_CalibParam) + self.sensor_Vib_CalibParam = self.config.get('sensor_Vib_CalibParam', _sensor_Vib_CalibParam) + self.warning_values = { + 'CH1': 0, + 'CH2': 0, + 'CH3': 0, + 'CH4': 0, + 'CH5': 0, + 'CH6': 0, + 'CH7': 0, + 'CH8': 0, + 'CH9': 0, + 'CH10': 0, + 'CH11': 0, + 'CH12': 0, + 'CH13': 0, + 'CH14': 0, + 'CH15': 0, + 'CH16': 0 + } + self.logger.info(f"DAQ thread starts. Address of DAQ board: IP={self.config['host']}, port={self.config['port']}") + except Exception as e: + self.logger.error(f"Error in __init__(): {e}") + time.sleep(5) + self.__init__() + + def update_config(self): + self.file_type = self.config.get('file_type') + if self.file_type not in [0, 1]: + self.file_type = 1 + self.save_flag = self.config.get('save_flag') + self.sensor_type = self.config.get('sensor_type') + self.sample_time = self.config.get('sample_time') + self.sample_period = self.config.get('sample_period') + self.one_sample_time = self.config.get('one_sample_time') + self.sample_points = int(self.sample_time*1000/self.one_sample_time) + self.mode = self.config.get('mode') + if self.mode not in [0, 1]: + self.mode = 1 + + self.cmdList = {'startDAQ': bytes.fromhex(f"DDDD 0001 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}"), + 'stopDAQ': bytes.fromhex(f"DDDD 0000 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}")} + self.sensor_Vol_CalibParam = self.config.get('sensor_Vol_CalibParam') + self.sensor_Cur_CalibParam = self.config.get('sensor_Cur_CalibParam') + self.sensor_Vib_CalibParam = self.config.get('sensor_Vib_CalibParam') + + def start_DAQ(self): + """发送启动采集指令""" + try: + self.buffer = bytearray() + if hasattr(self, 'udpsock') and self.udpsock: + self.udpsock.close() + self.udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # self.udpsock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 25) + self.udpsock.bind((self.config['local_host'], self.config['local_port'])) + self.udpsock.sendto(self.cmdList['startDAQ'], (self.config['host'], self.config['port'])) + self.udpsock.close() + self.logger.info(f"Send start command to DAQ board. {self.cmdList['startDAQ'].hex()}") + except Exception as e: + self.logger.error(f"Error in start_DAQ(): {e}") + + def stop_DAQ(self): + """发送停止采集指令""" + try: + self.buffer = bytearray() + if hasattr(self, 'udpsock') and self.udpsock: + self.udpsock.close() + self.udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # self.udpsock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 25) + self.udpsock.bind((self.config['local_host'], self.config['local_port'])) + self.udpsock.sendto(self.cmdList['stopDAQ'], (self.config['host'], self.config['port'])) + self.udpsock.close() + self.logger.info(f"Send stop command to DAQ board. {self.cmdList['stopDAQ'].hex()}") + except Exception as e: + self.logger.error(f"Error in stop_DAQ(): {e}") + + def _get_dir_size(self, path: Path) -> int: + """利用 Linux 的 du,返回目录本身已占用字节数,毫秒级""" + return int(subprocess.check_output( + ['du', '-sb', str(path)], text=True).split()[0]) + + def _oldest_file(self, path: Path): + """返回目录中最旧的普通文件 Path 对象,没有则返回 None""" + with os.scandir(path) as it: + files = [e for e in it if e.is_file()] + if not files: + return None + # 按修改时间升序 + return Path(min(files, key=lambda e: e.stat().st_mtime).path) + + def save_data(self): + """保存数据到文件""" + try: + #判断磁盘剩余空间是否小于1G,如果是从16通道的旧文件目录中删除文件 + channels = self.channels + # usage = shutil.disk_usage("C:/") + # while usage.free < self.config['daq']['min_free_gb']*1024*1024*1024: + # #获取目录下文件列表,并按照降序排序,如果硬盘空间小于阈值,删除旧的文件 + # for i in range(channels): + # os.makedirs(os.path.join(self.config['daq']['output_dir'], f"{i+1:02}"), exist_ok=True) + # fileList = os.listdir(f"C:/users/Administrator/PCM/data/{i+1:02}") + # fileList.sort(reverse=False) + # if os.path.exists(f"C:/users/Administrator/PCM/data/{i+1:02}/{fileList[0]}"): + # os.remove(f"C:/users/Administrator/PCM/data/{i+1:02}/{fileList[0]}") + # # os.remove(f"C:/users/Administrator/PCM/data/{fileList[1]}") + # usage = shutil.disk_usage("C:/") + + target_dir = Path(self.dataFileDir) + max_usage_gb = 5 + max_usage_bytes = max_usage_gb * 1024**3 + channels = self.channels + while True: + if self._get_dir_size(target_dir) <= max_usage_bytes: + break + for ch in range(channels): + ch_path = target_dir / f'{ch+1:02d}' + ch_path.mkdir(parents=True, exist_ok=True) + victim = self._oldest_file(ch_path) + if victim: + victim.unlink() + self.reg_values = [] + timestamp = time.time() + timeStr = datetime.fromtimestamp(timestamp).strftime("%Y%m%d%H%M%S") + # with open(filename, 'wb') as f: + # f.write(self.buffer) + # f.close() + print(f"Length of buffer: {len(self.buffer)}") + datas = np.frombuffer(self.buffer, dtype='>h') + print(f"Length of datas: {len(datas)}") + datas = datas[:int(len(datas)/channels)*channels] + datas = datas.reshape(-1, channels) + data = None + _s = f"{self.sensor_type:032b}" + _sensor_type = ''.join([_s[2*i:2*i+2] for i in range(channels-1, -1, -1)]) + print(f"save_flag = {self.save_flag}") + _save_flag = f"{self.save_flag:016b}"[::-1] + if 'calib_params' in self.config and 'vibration' in self.config['calib_params']: + _fre = self.config['calib_params']['vibration'].get('frequency', -1) + else: + _fre = -1 + + for i in range(channels): + j = i + 1 + if _fre != -1 and _fre > 0 and self.config['mode'] == 1: + _len = len(datas[:,i])//_fre*_fre + _data = datas[0:_len,i] + else: + _data = datas[:,i] + + _data = _data[0:20] + log_data = np.log(np.abs(_data) + 1e-300) + log_mean_squared = 2 * np.mean(log_data) + np.log(len(_data)) + _rms = np.exp(0.5 * log_mean_squared) / self.scale + + # _rms = np.sqrt(np.mean(_data**2))/self.scale + _min = np.min(_data) + _max = np.max(_data) + _mean = np.mean(_data) + + # if (_max - _mean) * 5 < (_mean - _min): + # _mean = np.mean(_data[0:20]) + # _min = np.min(_data[0:20]) + # _max = np.max(_data[0:20]) + # _rms = np.sqrt(np.mean(np.square(_data[0:20]))) + + self.feature_data[f'CH{j}'] = { + 'min': _min/self.scale, + 'max': _max/self.scale, + 'mean': _mean/self.scale, + 'std': np.std(_data)/self.scale, + 'rms': _rms, + 'sr0': 0.0, + 'sr1': 0.0, + 'sr2': 0.0, + 'sr3': 0.0, + 'sr4': 0.0 + } + rms = self.feature_data[f'CH{j}']['rms'] + mean = self.feature_data[f'CH{j}']['mean'] + + filename = '' + match _sensor_type[2*i:2*i+2]: + case '00': + # 计算频率,以Hz为单位 + self.feature_data[f'CH{j}']['sr0'] = self.calculateFrequency(datas[:, i], self.one_sample_time) + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + data = datas[:, i] + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case '01': + # 计算声音大小 + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + data = datas[:, i] + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case '10': + # 计算电流大小 + data = datas[:, i] + if self.mode != 1: + for k, v in self.feature_data[f'CH{j}'].items(): + self.feature_data[f'CH{j}'][k] = v**2*self.sensor_Cur_CalibParam[f'CH{j}']['K2'] + v*self.sensor_Cur_CalibParam[f'CH{j}']['K'] + self.sensor_Cur_CalibParam[f'CH{j}']['B'] + data = (datas[:, i]**2*self.sensor_Cur_CalibParam[f'CH{j}']['K2'] + datas[:, i]*self.sensor_Cur_CalibParam[f'CH{j}']['K']+self.sensor_Cur_CalibParam[f'CH{j}']['B']).astype(np.float32) + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case '11': + # 计算振动大小 + data = datas[:, i] + if self.mode != 1: + for k, v in self.feature_data[f'CH{j}'].items(): + self.feature_data[f'CH{j}'][k] = v**2*self.sensor_Vib_CalibParam[f'CH{j}']['K2'] + v*self.sensor_Vib_CalibParam[f'CH{j}']['K'] + self.sensor_Vib_CalibParam[f'CH{j}']['B'] + data = (datas[:, i]**2*self.sensor_Vib_CalibParam[f'CH{j}']['K2'] + datas[:, i]*self.sensor_Vib_CalibParam[f'CH{j}']['K']+self.sensor_Vib_CalibParam[f'CH{j}']['B']).astype(np.float32) + self.feature_data[f'CH{j}']['rms'] = np.sqrt(np.mean(data**2)) + self.feature_data[f'CH{j}']['sr0'] = self.feature_data[f'CH{j}']['rms']*np.sqrt(2) + else: + self.feature_data[f'CH{j}']['sr0'] = self.feature_data[f'CH{j}']['std']*np.sqrt(2) + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case _: + pass + + # 将数据写入文件 + if self.file_type == 1: + filename += '.bin' + if _save_flag[i] == '1': + if isinstance(data, np.ndarray): + try: + temp_data = data.astype('>f4') + bytes_data = temp_data.tobytes() + with open(filename, 'wb', buffering=0) as f: + # 1. 正常写 + f.write(bytes_data) + + # 2. 告诉内核:整个文件以后大概率不读,页 cache 可以立即回收 + fd = os.open(filename, os.O_RDONLY) + try: + # POSIX_FADV_DONTNEED = 4 + os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) + finally: + os.close(fd) + del temp_data, bytes_data, data + finally: + if 'temp_data' in locals(): + del temp_data + if 'bytes_data' in locals(): + del bytes_data + self.logger.debug(f"Success to save data to {filename}.") + else: + filename += '.csv' + if _save_flag[i] == '1': + if isinstance(data, np.ndarray): + with open(filename, 'w') as f: + # 使用生成器表达式避免创建巨大列表 + lines = (f"{num:.4f}\n" for num in data) + f.writelines(lines) + self.logger.debug(f"Saved data to {filename}.") + del lines + self.warning_check() + data = None + datas = None + self.buffer = bytearray() + self._force_memory_cleanup() + except Exception as e: + self.logger.error(f"Error in save_data(): {e}") + + def warning_check(self): + """检查是否有报警条件""" + for i in range(self.channels): + ch = f'CH{i+1}' + val = self.feature_data[ch]['mean'] + + wp = self.config.get('warning_param', {}) + enable_bits = f"{wp.get('enable', 0):016b}"[::-1] + if enable_bits[i] == '1': + low_limit = wp.get(ch, {}).get('lower', float('-inf')) + high_limit = wp.get(ch, {}).get('upper', float('inf')) + if val < low_limit or val > high_limit: + # self.logger.warning(f"Warning: {ch} value {val} out of limits ({low_limit}, {high_limit})") + self.warning_values[ch] = 1 + else: + self.warning_values[ch] = 0 + + def _force_memory_cleanup(self): + """强制内存清理""" + import gc + # 清除各种缓存 + if hasattr(np, 'getbufsize'): + np.setbufsize(32768) + + # 强制垃圾回收 + gc.collect() + gc.collect() # 两次确保回收 + + # 稍微等待让系统处理 + import time + time.sleep(0.01) + + def calculateFrequency(self, signal, oneSampleTime): + '''计算0-1变换的数组中0-1变化的次数, 并计算其频率''' + # oneSampleTime 单位为us + if len(signal) < 2: + return 0.0 # 信号太短无法计算频率 + transitions = 0 # 跳变次数计数器 + # 遍历数组计算跳变次数 + for i in range(1, len(signal)): + if signal[i] != signal[i-1]: + transitions += 1 + # 计算频率: + # 每个周期有2次跳变(0→1和1→0) + # 总时间 = 采样点数 / 采样率 + # 频率 = (跳变次数 / 2) / (总时间) + total_time = len(signal) * oneSampleTime / 1000000 + frequency = (transitions // 2) / total_time if total_time > 0 else 0.0 + return frequency + + def run(self): + """主运行循环""" + self.logger.info(f"Start DAQ thread.") + frame_size = self.config.get('frame_size_max', 1464) + FILESIZE = self.config.get('file_size', 32000000) + DATA_DIR = self.config.get('output_dir', 'data') + optFlag = 0 + # 清空接收缓存,并向DAQ模块发送启动采集指令 + self.buffer = b'' + self.sampleNum = 0 + self.stop_DAQ() + time.sleep(0.01) + self.start_DAQ() + lastFrameNo = 0 + cycles = 0 + while(True): + try: + data, addr = self.sock.recvfrom(frame_size+42) + # 如何返回了数据,数据起始符正确,包号正确,则存储数据 + if data: + data = data[42:] + nowFrameNo = int.from_bytes(data[4:8], 'big') + if nowFrameNo != lastFrameNo + 1: + print(f"Received data: len={len(data)}, lastFrameNo={lastFrameNo}, nowFrameNo={nowFrameNo}") + # self.logger.info(f"Received data: len={len(data)}, frame NO.={int.from_bytes(data[4:8], 'big')}") + else: + continue + # self.logger.debug(f"Head Data:(20 byte) ={' '.join(data[i:i+2].hex() for i in range(0, 20, 2))}") + + if optFlag == 0: + if data and data[0:4] == bytearray([0xa5, 0x5a, 0xa5, 0x5a]) and len(data) == frame_size and data[4:8] == bytearray([0x00, 0x00, 0x00, 0x01]): + self.buffer += data[24:] + self.sampleNum += ((len(data)-24)/32) + # if self.sensorType != int.from_bytes(data[8:12], 'big'): + # self.logger.error(f"In daq_thread(): SensorType in return data doesn't match with config.") + optFlag = 1 + elif optFlag == 1: + if data and data[0:4] == bytearray([0xa5, 0x5a, 0xa5, 0x5a]): + if len(data) != frame_size or data[5:8] == bytearray([0x00, 0x00, 0x01]): + if data[4:8] != bytearray([0x00, 0x00, 0x00, 0x01]): + self.buffer += data[24:] + self.sampleNum += ((len(data)-24)/32) + + cycles += 1 + self.save_data() + self.buffer = bytearray() + self.sampleNum = 0 + optFlag = 0 + else: + self.buffer += data[24:] + self.sampleNum += ((len(data)-24)/32) + + if nowFrameNo - lastFrameNo != 1 and lastFrameNo < nowFrameNo: + if lastFrameNo != 0: + pass + self.logger.warning(f"cycles= {cycles}, lastFrameNo={lastFrameNo}, nowFrameNo={nowFrameNo}") + # raise Exception() + if nowFrameNo != 0: + lastFrameNo = nowFrameNo + except Exception as e: + self.stop_DAQ() + self.sock.close() + self.running = False + self.buffer = bytearray() + self.logger.error(f"Error in daq_thread(): {e}") + self.logger.info(f"Stop DAQ thread.") + break + +class InfluxDBWriter: + def __init__(self, url="http://localhost:8086", token="", org="my-org", bucket="my-bucket"): + """ + 初始化 InfluxDB 客户端 + + 参数: + url: InfluxDB 地址,host模式下使用 http://localhost:8086 + token: API token,格式为 "username:password" 或 token字符串 + org: 组织名称 + bucket: 存储桶名称 + """ + self.client = InfluxDBClient(url=url, token=token, org=org) + self.write_api = self.client.write_api(write_options=SYNCHRONOUS) + self.bucket = bucket + self.org = org + + def write_sensor_data(self, measurement, tags, fields): + """ + 写入传感器数据到 InfluxDB + + 参数: + measurement: 测量名称 (类似表名) + tags: 标签字典,用于索引和分组 (如: {"device": "sensor1", "location": "factory"}) + fields: 字段字典,存储实际数据 (如: {"temperature": 25.6, "humidity": 60.2}) + """ + try: + # 创建数据点 + point = Point(measurement) + + # 添加标签 + for tag_key, tag_value in tags.items(): + point = point.tag(tag_key, tag_value) + + # 添加字段 + for field_key, field_value in fields.items(): + point = point.field(field_key, field_value) + + # 写入数据 + self.write_api.write(bucket=self.bucket, record=point) + print(f"[{datetime.now()}]数据写入成功: {point.to_line_protocol()}") + + except Exception as e: + print(f"写入数据时出错: {e}") + + def write_batch_data(self, points): + """ + 批量写入多个数据点 + """ + try: + self.write_api.write(bucket=self.bucket, record=points) + print(f"[{datetime.now()}]批量写入成功,共 {len(points)} 个数据点") + except Exception as e: + print(f"批量写入时出错: {e}") + + def close(self): + """关闭连接""" + self.client.close() + +class ModbusGateway: + def __init__(self): + # 初始化logger + config_file = 'src/config-1.2-debug.yaml' + config_file_temp = 'config-1.2-debugcopy.yaml' + with open('src/logging-config.json', 'r') as f: + logging.config.dictConfig(json.load(f)) + self.logger = logging.getLogger('PCM') + + self.config_manager = ConfigManager( + regs_config_file='src/regs-mapping-1.2-debug.yaml', + config_file=config_file, + logger=self.logger) + + self.taskInfo = {'status':0x0000, 'running_time':0, 'period':0} + self.taskInfo['period'] = self.config_manager.config['task']['period']*60 + + # 初始化influxdb client + self.influx_client = InfluxDBWriter( + url=self.config_manager.config['influxdb'].get('url', 'http://localhost:8086'), + token=self.config_manager.config['influxdb'].get('token', 'PCM:1842moon'), + org=self.config_manager.config['influxdb'].get('org', 'MEASCON'), + bucket=self.config_manager.config['influxdb'].get('bucket', 'PCM') + ) + + # 连接plc server + self.plc_host = self.config_manager.config['plc-server'].get('host', '172.22.0.3') + self.plc_port = self.config_manager.config['plc-server'].get('port', 5020) + self.plc_client = ModbusTcpClient(self.plc_host, port=self.plc_port) + self.plc_measurements = self.config_manager.config['plc-server'].get('measurements', {}) + + # 创建本地modbus tcp服务器 + self.max_address = 1300 + self.host = self.config_manager.config['modbus-server']['host'] + self.port = self.config_manager.config['modbus-server']['port'] + + self.holding_registers = ModbusSequentialDataBlockForPCM(self.config_manager, self.logger, 0x00, [0]*(self.max_address+1)) + + # 创建数据存储 + store = ModbusSlaveContext( + di=ModbusSequentialDataBlock(0, [0]*1), + co=ModbusSequentialDataBlock(0, [0]*1), + # hr=ModbusSequentialDataBlock(0, [0]*1000), + hr=self.holding_registers, + ir=ModbusSequentialDataBlock(0, [0]*1)) + + self.context = ModbusServerContext(slaves={1:store}, single=False) + + # 启动服务器线程 + self.modbus_sever = threading.Thread( + target=StartTcpServer, + # kwargs={"context": self.context, "address": (self.host, self.port)}) + kwargs={"context": self.context, "address": ('0.0.0.0', self.port)}) + self.modbus_sever.daemon = True + self.modbus_sever.start() + self.logger.info(f"Local modbusTCP service starts, IP={self.host}, port={self.port}") + + self.gps = GPS(self.config_manager.config['gps'], self.logger) + self.breaker = Breaker(self.config_manager.config['breaker'], self.logger) + self.lsdaq = LSDAQ(self.config_manager.config['lsdaq'], self.logger) + self.hsdaq = HSDAQ(self.config_manager.config['hsdaq'], self.logger) + + self.gps_thread = threading.Thread(target=self.gps.run) + self.gps_thread.daemon = True + self.gps_thread.start() + + self.breaker_thread = threading.Thread(target=self.breaker.run) + self.breaker_thread.daemon = True + self.breaker_thread.start() + + self.lsdaq_thread = threading.Thread(target=self.lsdaq.run) + self.lsdaq_thread.daemon = True + self.lsdaq_thread.start() + + self.hsdaq_thread = threading.Thread(target=self.hsdaq.run) + self.hsdaq_thread.daemon = True + self.hsdaq_thread.start() + + # 启动配置服务(HTTP API) + config_service_port = self.config_manager.config.get('config-server', {}).get('port', 5000) + config_service_host = self.config_manager.config.get('config-server', {}).get('host', '127.0.0.1') + + # 如果配置文件中指定了配置文件路径,使用它;否则使用默认的YAML配置文件 + config_service_config_path = self.config_manager.config.get('config-server', {}).get('config_path', config_file_temp) + + self.config_service = ConfigService( + default_config_path=config_service_config_path, + host=config_service_host, + port=config_service_port, + debug=False, + logger=self.logger, + ) + self.config_service.start() + self.logger.info(f"Config service started on {config_service_host}:{config_service_port}") + + # 任务状态持久化文件路径(独立文件,不会被外部覆盖) + self.task_state_file = '.task_state.json' + + def _load_task_running_time(self) -> float: + """ + 从独立的持久化文件加载任务累计运行时间 + + Returns: + float: 累计运行时间(秒),文件不存在时返回0 + """ + try: + if not os.path.exists(self.task_state_file): + self.logger.info(f"Task state file not found: {self.task_state_file}, starting from 0") + return 0 + + with open(self.task_state_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + running_time = data.get('running_time', 0) + if running_time > 0: + self.logger.info(f"Loaded task running_time from {self.task_state_file}: {running_time:.2f}s") + return float(running_time) + + except json.JSONDecodeError as e: + self.logger.error(f"Task state file corrupted: {e}, starting from 0") + return 0 + except Exception as e: + self.logger.error(f"Error loading task running_time: {e}, starting from 0") + return 0 + + def _save_task_running_time(self, running_time: float) -> bool: + """ + 保存任务累计运行时间到独立的持久化文件(原子写入) + + Args: + running_time: 累计运行时间(秒) + + Returns: + bool: 保存成功返回 True,失败返回 False + """ + try: + data = { + 'running_time': running_time, + 'last_update': datetime.now().isoformat(), + 'version': '1.0' + } + + # 原子写入:先写临时文件,再重命名 + temp_file = self.task_state_file + '.tmp' + with open(temp_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + # 重命名(原子操作) + os.replace(temp_file, self.task_state_file) + return True + + except Exception as e: + self.logger.error(f"Error saving task running_time: {e}") + # 清理临时文件 + try: + temp_file = self.task_state_file + '.tmp' + if os.path.exists(temp_file): + os.remove(temp_file) + except: + pass + return False + + def _reset_task_running_time(self) -> bool: + """ + 重置任务累计运行时间为0 + + Returns: + bool: 重置成功返回 True,失败返回 False + """ + self.logger.info("Resetting task running_time to 0") + return self._save_task_running_time(0) + + def _update_modbus_datas(self): + """更新本地Modbus服务器的寄存器""" + if not hasattr(self, 'context'): + self.logger.error("Local modbus tcp service isn't initilized.") + return + + try: + # 获取本地服务器的slave上下文 + slave = self.context[1] + holding_registers = self.holding_registers + + gps_reg_values = list(self.gps.gps_data.values()) + # print(f"gps_reg_values:{gps_reg_values}") + for i in range(len(gps_reg_values)): + holding_registers.server_set_values(2*i+1, float_to_registers(gps_reg_values[i])) # type: ignore + + breaker_reg_values = list(self.breaker.reg_values.values()) + # print(f"breaker_reg_values:{breaker_reg_values}") + for i in range(len(breaker_reg_values)): + holding_registers.server_set_values(1220+i+1, int(breaker_reg_values[i])) # type: ignore + + lsdaq_reg_values = list(self.lsdaq.reg_values.values()) + # print(f"lsdaq_reg_values:{lsdaq_reg_values}") + for i in range(len(lsdaq_reg_values)): + holding_registers.server_set_values(8+2*i+1, float_to_registers(lsdaq_reg_values[i])) # type: ignore + + lsdaq_warning_values = list(self.lsdaq.warning_values.values()) + # print(f"lsdaq_warning_values:{lsdaq_warning_values}") + for i in range(len(lsdaq_warning_values)): + holding_registers.server_set_values(1129+i+1, int(lsdaq_warning_values[i])) # type: ignore + + hsdaq_reg_values = self.hsdaq.reg_values + # print(f"hsdaq_reg_values:{hsdaq_reg_values}") + # print(f"len = {len(hsdaq_reg_values)}") + for i in range(len(hsdaq_reg_values)): + holding_registers.server_set_values(60+2*i+1, float_to_registers(hsdaq_reg_values[i])) # type: ignore + + hsdaq_warning_values = list(self.hsdaq.warning_values.values()) + # print(f"hsdaq_warning_values:{hsdaq_warning_values}") + for i in range(len(hsdaq_warning_values)): + holding_registers.server_set_values(1145+i+1, int(hsdaq_warning_values[i])) # type: ignore + + # 更新从plc采集到的数据 + # packed_data = bytearray() + # for k, v in self.plc_measurements.items(): + # packed_data.extend(struct.pack('>fHffH', v['value'], v['warning_param']['enable'], v['warning_param']['lower'], v['warning_param']['upper'], v['warning'])) + # register_values = [] + # for i in range(0, len(packed_data), 2): + # if i + 1 < len(packed_data): + # # 组合两个字节为一个16位整数 + # value = (packed_data[i] << 8) | packed_data[i + 1] + # else: + # # 如果字节数为奇数,最后一个字节补0 + # value = packed_data[i] << 8 + # register_values.append(value) + # holding_registers.server_set_values(1161+1, register_values) + + # 读取CPU各温区温度 + thermal_zones = 5 + self.cpu_temperatures = {} + for i in range(thermal_zones): + with open(f"/sys/class/thermal/thermal_zone{i}/temp", 'r') as f: + # 读取温度值(毫摄氏度) + temp_millic = int(f.read().strip()) + # 转换为摄氏度 + temp_c = temp_millic / 1000.0 + self.cpu_temperatures[f'zone{i}'] = temp_c + + holding_registers.server_set_values(50+2*i+1, float_to_registers(temp_c)) # type: ignore + + except Exception as e: + self.logger.error(f"Error in _update_modbus_datas(): {e}") + + def _write_to_influxdb(self): + if not self.influx_client: + self.logger.error("Influxdb isn't initilized. Try to initilize after 1 seconds.") + time.sleep(1) + if hasattr(self, 'influx_client'): + self.influx_client.close() + self.influx_client = InfluxDBClient( + url=self.config_manager.config['influxdb'].get('url', 'http://localhost:8086'), + token=self.config_manager.config['influxdb'].get('token', 'PCM:1842moon'), + org=self.config_manager.config['influxdb'].get('org', 'MEASCON'), + ) + self.write_api = self.influx_client.write_api(write_options=SYNCHRONOUS) + try: + points = [] + # 构建toradex核心板CPU温度 + point = Point("PCM_Measurement") + point.tag("data_type", 'cpu_temperatures') + for field_name, field_value in self.cpu_temperatures.items(): + point.field(field_name, float(field_value)) + points.append(point) + + # 构建GPS采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'GPS') + for field_name, field_value in self.gps.gps_data.items(): + point.field(field_name, float(field_value)) + points.append(point) + + # 构建Breaker采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'Breaker') + for field_name, field_value in self.breaker.reg_values.items(): + point.field(field_name, float(field_value)) + points.append(point) + + # 构建TaskInfo采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'TaskInfo') + print(f"taskInfo = {self.taskInfo}") + for field_name, field_value in self.taskInfo.items(): + if field_value == None: + field_value = 0 + point.field(field_name, float(field_value)) + points.append(point) + + # 构建低速采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'LSDAQ') + # for field_name, field_value in self.lsdaq.reg_values.items(): + # point.field(field_name, float(field_value)) + # for field_name, field_value in self.lsdaq.warning_values.items(): + # point.field(field_name+'.warning', field_value) + for field_name, field_value in self.lsdaq.reg_values.items(): + if field_name in self.lsdaq.alias and self.lsdaq.alias.get(field_name) != '': + point.field(self.lsdaq.alias[field_name], float(field_value)) + for field_name, field_value in self.lsdaq.warning_values.items(): + if field_name in self.lsdaq.alias and self.lsdaq.alias.get(field_name) != '': + point.field(self.lsdaq.alias[field_name]+'.warning', field_value) + points.append(point) + + # 构建高速采集数据 + for i in range(16): + # print(str(self.hsdaq.feature_data[f'CH{i}'])) + j = i + 1 + point = Point("PCM_Measurement") + # point.tag("data_type", f'HSDAQ_CH{j}') + if self.hsdaq.alias.get(f'CH{j}') != '': + point.tag("data_type", self.hsdaq.alias[f'CH{j}']) + for field_name, field_value in self.hsdaq.feature_data[f'CH{j}'].items(): + point.field(field_name, float(field_value)) + point.field('warning', self.hsdaq.warning_values[f'CH{j}']) + points.append(point) + + # 构建从PLC采集到的数据 + point = Point("PCM_Measurement") + point.tag("data_type", f'PLC') + for k, v in self.plc_measurements.items(): + point.field(k, float(v['value'])) + points.append(point) + + # 将数据点写入influxdb + self.influx_client.write_batch_data(points) + + except Exception as e: + self.logger.error(f"Error in _write_to_influxdb(): {e}") + + def _read_plc_datas(self): + if not self.plc_client: + self.plc_client = ModbusTcpClient(self.plc_host, port=self.plc_port) + if self.plc_client.connect() and self.plc_measurements: + for k, v in self.plc_measurements.items(): + ret = self.plc_client.read_holding_registers(address=v['address'], count=2, slave=1) + print(f"{ret}") + if (not ret.isError()) and len(ret.registers) == 2: + self.plc_measurements[k]['value'] = registers_to_float(ret.registers, byte_order='ABCD') + if v['warning_param']['enable'] == 1: + low_limit = v['warning_param']['lower'] + high_limit = v['warning_param']['upper'] + val = self.plc_measurements[k]['value'] + if val < low_limit or val > high_limit: + self.plc_measurements[k]['warning'] = 1 + else: + self.plc_measurements[k]['warning'] = 0 + else: + self.plc_measurements[k]['warning'] = 0 + + def run(self): + """主运行循环""" + timestamp = time.time() + task_control_reg_addr = self.config_manager.config['task']['control_reg_addr'] + task_control_reg_value = 0x0000 + self.taskInfo['start_time'] = None + + # 从配置文件恢复运行时间(如果有) + saved_running_time = self._load_task_running_time() + if saved_running_time > 0: + self.taskInfo['running_time'] = saved_running_time + self.logger.info(f"Restored running_time from config: {self.taskInfo['running_time']:.2f}s") + else: + self.taskInfo['running_time'] = 0 + + # 持久化保存相关变量 + last_save_time = time.time() + SAVE_INTERVAL = 5 # 每5秒保存一次 + last_running_time = self.taskInfo['running_time'] # 用于检测变化 + + while True: + self.gps.config = self.config_manager.config['gps'] + self.lsdaq.config = self.config_manager.config['lsdaq'] + self.lsdaq.update_config() + self.hsdaq.config = self.config_manager.config['hsdaq'] + self.hsdaq.update_config() + self.taskInfo['status'] = self.holding_registers.values[task_control_reg_addr+1] + match self.taskInfo['status']: + case 0x0000: + self.breaker.openBreaker() + # 实验停止时重置运行时间 + if self.taskInfo['running_time'] > 0: + self._reset_task_running_time() + self.taskInfo['start_time'] = None + self.taskInfo['running_time'] = 0 + case 0x5555: + self.breaker.closeBreaker() + if self.breaker.reg_values['load_status'] == 1: + current_time = time.time() + self.taskInfo['running_time'] += ( current_time - self.taskInfo['start_time'] ) + self.taskInfo['start_time'] = current_time + else: + self.taskInfo['start_time'] = time.time() + + if self.taskInfo['running_time'] > self.taskInfo['period']: + # 实验完成时重置运行时间 + self._reset_task_running_time() + self.holding_registers.values[task_control_reg_addr+1] = 0xFFFF + continue + + if 1 in self.lsdaq.warning_values.values() or ( self.hsdaq.warning_values['CH12'] == 1 ) or ( self.hsdaq.warning_values['CH14'] == 1 and self.breaker.reg_values['load_status'] == 1): + # self.breaker.alarming() + self.holding_registers.values[task_control_reg_addr+1] = 0xFFFF + continue + # else: + # self.breaker.unalarming() + case 0xAAAA: + self.logger.error(f"AAAA{self.taskInfo}") + self.breaker.openBreaker() + self.taskInfo['start_time'] = time.time() + case 0xFFFF: + self.holding_registers.values[task_control_reg_addr+1] = 0x0000 + + # time.sleep(0.01) + nowtime = time.time() + # print(f"{timestamp}, {nowtime}") + if (nowtime-timestamp) > 1: + timestamp = nowtime + #将数据写入influxdb + self._write_to_influxdb() + # self._read_plc_datas() + self._update_modbus_datas() + + # 周期性保存运行时间(仅在运行状态且时间有变化时保存) + if self.taskInfo['status'] == 0x5555: + if (nowtime - last_save_time) >= SAVE_INTERVAL: + if self.taskInfo['running_time'] != last_running_time: + self._save_task_running_time(self.taskInfo['running_time']) + last_running_time = self.taskInfo['running_time'] + last_save_time = nowtime + + # 控制寄存器 + + +if __name__ == "__main__": + gateway = ModbusGateway() + gateway.run() \ No newline at end of file diff --git a/pcm_influxdb/pcm-influxdb-debug0402.py b/pcm_influxdb/pcm-influxdb-debug0402.py new file mode 100644 index 0000000..ac67aab --- /dev/null +++ b/pcm_influxdb/pcm-influxdb-debug0402.py @@ -0,0 +1,2148 @@ +import threading, pynmea2, time, struct, serial, socket, yaml, os, logging.config, json, subprocess, shutil, time, copy, gc, glob +from pymodbus.server.sync import StartTcpServer +from pymodbus.client.sync import ModbusTcpClient +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from threading import Lock +import numpy as np +from datetime import datetime +from pathlib import Path +from influxdb_client import InfluxDBClient, Point +from influxdb_client.client.write_api import SYNCHRONOUS +from config_service import ConfigService + +def checkValue(data, little_endian=True): + """ + 计算Modbus CRC16校验和 + 参数: + data: 字节串或字节数组 + little_endian: 是否使用小端字节序,默认为False(大端) + 返回: + CRC16值 (2字节,小端字节序) + """ + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc = crc >> 1 + if little_endian: + # 小端字节序:低位在前,高位在后 + low_byte = crc & 0xFF + high_byte = (crc >> 8) & 0xFF + return (low_byte << 8) | high_byte + else: + # 大端字节序:高位在前,低位在后 + return crc & 0xFFFF + +def nowStr(): + now = datetime.now() + ret = now.strftime('%Y/%m/%d %H:%M:%S.') + f"{now.microsecond // 1000:03d}" + return ret + +def wordData2HexStr(data): + if data: + ret = ' '.join(data[i:i+2].hex() for i in range(0, len(data), 2)) + else: + ret = '' + return ret.upper() + +def float_to_registers(value): + packed = struct.pack('>f', value) + return [struct.unpack('>H', packed[0:2])[0], struct.unpack('>H', packed[2:4])[0]] + +def registers_to_float(registers, byte_order='ABCD'): + """ + 将两个寄存器转换为浮点数 + Args: + registers (list): 两个寄存器的值 [reg1, reg2] + byte_order (str): 字节顺序 + Returns: + float: 转换后的浮点数 + """ + if len(registers) != 2: + return None + + # 将寄存器拆分为字节 + # 每个寄存器是16位,拆分为2个字节 + reg1_bytes = registers[0].to_bytes(2, byteorder='big') # 高地址寄存器 + reg2_bytes = registers[1].to_bytes(2, byteorder='big') # 低地址寄存器 + + # 根据字节顺序组合字节 + if byte_order == 'ABCD': # 标准Modbus (大端序) + byte_array = reg1_bytes + reg2_bytes + elif byte_order == 'CDAB': # 字交换 + byte_array = reg2_bytes + reg1_bytes + elif byte_order == 'BADC': # 字节交换 + byte_array = bytes(reversed(reg1_bytes)) + bytes(reversed(reg2_bytes)) + elif byte_order == 'DCBA': # 字节和字都交换 + byte_array = bytes(reversed(reg2_bytes)) + bytes(reversed(reg1_bytes)) + else: + return None + float_value = struct.unpack('>f', byte_array)[0] # '>f' 表示大端序浮点数 + # 检查是否为NaN或无穷大 + if abs(float_value) == float('inf'): + return None + return float_value + +class ConfigManager: + def __init__(self, regs_config_file, config_file, logger): + self.config_file = Path(config_file) + self.regs_config_file = Path(regs_config_file) + self.lock = threading.Lock() + self.config = {} + self.regs_config = {} + self.logger = logger + self.mapping = BidirectionalMap() + + self.load_all_configs() + + # 设置文件监视器 + # self.observer = Observer() + # self.event_handler = ConfigFileHandler(self) + # self.observer.schedule(self.event_handler, path=str(self.config_file.parent)) + # self.observer.start() + + def load_all_configs(self): + """加载主配置和寄存器配置""" + with self.lock: + if not os.path.exists(self.config_file): + self.logger.warning(f"Config file {self.config_file} not found") + + if not os.path.exists(self.regs_config_file): + self.logger.warning(f"Regsister mapping file {self.regs_config_file} not found") + + # 加载主配置 + with open(self.config_file, 'r') as f: + self.config = yaml.safe_load(f) + # 低速采集sensor_type处理 + self.config['lsdaq']['sensor_type'] = self.config['lsdaq'].get('sensor_type').replace(' ', '') + if len(self.config['lsdaq']['sensor_type']) != 16 or not all(c in '01' for c in self.config['lsdaq']['sensor_type']): + self.config['lsdaq']['sensor_type'] = '1111111111111111' + self.config['lsdaq']['sensor_type'] = int(self.config['lsdaq']['sensor_type'][::-1], 2) + + # 低速采集 warning_param enable 处理 + self.config['lsdaq']['warning_param']['enable'] = self.config['lsdaq']['warning_param'].get('enable').replace(' ', '') + if len(self.config['lsdaq']['warning_param']['enable']) != 16 or not all(c in '01' for c in self.config['lsdaq']['warning_param']['enable']): + self.config['lsdaq']['warning_param']['enable'] = '0000000000000000' + self.config['lsdaq']['warning_param']['enable'] = int(self.config['lsdaq']['warning_param']['enable'][::-1], 2) + + # 高速采集sensor_type处理 + self.config['hsdaq']['sensor_type'] = self.config['hsdaq'].get('sensor_type').replace(' ', '') + if len(self.config['hsdaq']['sensor_type']) != 32 or not all(c in '01' for c in self.config['hsdaq']['sensor_type']): + self.config['hsdaq']['sensor_type'] = '11111111111111111111111111111111' + _s = self.config['hsdaq']['sensor_type'] + self.config['hsdaq']['sensor_type'] = int(''.join([_s[2*i:2*i+2] for i in range(len(_s)//2-1, -1, -1)]), 2) + + # 高速采集save_flag处理 + self.config['hsdaq']['save_flag'] = self.config['hsdaq'].get('save_flag').replace(' ', '') + if len(self.config['hsdaq']['save_flag']) != 16 or not all(c in '01' for c in self.config['hsdaq']['save_flag']): + self.config['hsdaq']['save_flag'] = '1111111111111111' + self.config['hsdaq']['save_flag'] = int(self.config['hsdaq']['save_flag'][::-1], 2) + + # 高速采集 warning_param enable 处理 + self.config['hsdaq']['warning_param']['enable'] = self.config['hsdaq']['warning_param'].get('enable').replace(' ', '') + if len(self.config['hsdaq']['warning_param']['enable']) != 16 or not all(c in '01' for c in self.config['hsdaq']['warning_param']['enable']): + self.config['hsdaq']['warning_param']['enable'] = '0000000000000000' + self.config['hsdaq']['warning_param']['enable'] = int(self.config['hsdaq']['warning_param']['enable'][::-1], 2) + + with open(self.regs_config_file, 'r') as f: + self.regs_config = yaml.safe_load(f) + + # 构建映射关系 + self._build_mappings() + + def _build_mappings(self): + """构建配置键到地址的双向映射""" + # 处理value_regs + # if 'value_regs' in self.regs_config: + # self._process_registers_section(self.regs_config['value_regs'], '', 'value') + + # 处理control_regs + if 'control_regs' in self.regs_config: + self._process_registers_section(self.regs_config['control_regs'], '', 'control') + + def _process_registers_section(self, section, current_path, reg_type): + """处理寄存器配置部分""" + def traverse(node, current_path=""): + # print(f"node={node}, current_path={current_path}") + for key, value in node.items(): + new_path = f"{current_path}.{key}" if current_path else key + if isinstance(value, dict): + if all(isinstance(k, str) and isinstance(v, int) for k, v in value.items()): + # 这是叶子节点,包含寄存器地址 + for sub_key, address in value.items(): + full_path = f"{new_path}.{sub_key}" + self.mapping.add_mapping(full_path, address, reg_type) + else: + traverse(value, new_path) + else: + # 直接映射 + self.mapping.add_mapping(new_path, value[0], value[1]) + + traverse(section, current_path) + # print(f"key_to_address={self.mapping.key_to_address}") + # print(f"address_to_keys={self.mapping.address_to_keys}") + # print(f"key_to_data_type={self.mapping.key_to_data_type}") + # print(f"address_to_data_type={self.mapping.address_to_data_type}") + + def get_config_value(self, config_path): + """通过配置路径获取配置值""" + keys = config_path.split('.') + node = self.config + for key in keys: + if isinstance(node, dict) and key in node: + node = node[key] + else: + return None + return node + + def update_config_value(self, config_path, value): + """更新配置值并保存""" + with self.lock: + # print(config_path) + keys = config_path.split('.') + node = self.config + for key in keys[:-1]: + if key not in node: + node[key] = {} + node = node[key] + node[keys[-1]] = value + + # 保存到文件 + # self._save_config() + return True + + def _save_config(self): + """保存配置到文件""" + _config = copy.deepcopy(self.config) + _config['lsdaq']['sensor_type'] = f"{_config['lsdaq']['sensor_type']:016b}"[::-1] + _config['lsdaq']['warning_param']['enable'] = f"{_config['lsdaq']['warning_param']['enable']:016b}"[::-1] + _s = f"{_config['hsdaq']['sensor_type']:032b}" + _config['hsdaq']['sensor_type'] = ''.join([_s[2*i:2*i+2] for i in range(len(_s)//2-1, -1, -1)]) + _config['hsdaq']['save_flag'] = f"{_config['hsdaq']['save_flag']:016b}"[::-1] + _config['hsdaq']['warning_param']['enable'] = f"{_config['hsdaq']['warning_param']['enable']:016b}"[::-1] + + with open(self.config_file, 'w') as f: + yaml.dump(_config, f, sort_keys=False, default_flow_style=False) + + # def close(self): + # self.observer.stop() + # self.observer.join() + +class BidirectionalMap: + def __init__(self): + self.key_to_address = {} # 配置键 -> (地址, 类型) + self.address_to_keys = {} # 地址 -> [配置键] + self.key_to_data_type = {} # 配置键 -> 数据类型 + self.address_to_data_type = {} # 地址 -> 数据类型 + + def add_mapping(self, config_key, address, reg_type, data_type='uint16'): + """添加映射关系""" + self.key_to_address[config_key] = (address, reg_type) + self.address_to_keys.setdefault(address, []).append(config_key) + self.key_to_data_type[config_key] = data_type + self.address_to_data_type[address] = data_type + + def get_address(self, config_key): + """通过配置键获取地址和类型""" + print(self.key_to_address) + return self.key_to_address.get(config_key, (None, None)) + + def get_config_keys(self, address): + """通过地址获取配置键列表""" + # print(self.address_to_keys) + return self.address_to_keys.get(address, []) + + def get_data_type(self, identifier): + """获取数据类型,identifier可以是地址或配置键""" + if isinstance(identifier, int): + return self.address_to_data_type.get(identifier) + else: + return self.key_to_data_type.get(identifier) + +class DataTypeValidator: + @staticmethod + def validate(value, data_type): + try: + if data_type == 'float32': + return float(value) + elif data_type in ('uint16', 'uint32'): + val = int(value) + if data_type == 'uint16' and not (0 <= val <= 65535): + raise ValueError("Value out of range for uint16") + elif data_type == 'uint32' and not (0 <= val <= 4294967295): + raise ValueError("Value out of range for uint32") + return val + elif data_type == 'int32': + val = int(value) + if not (-2147483648 <= val <= 2147483647): + raise ValueError("Value out of range for int32") + return val + elif data_type == 'string': + return str(value) + else: + return int(value) # 默认处理为uint16 + except (ValueError, TypeError) as e: + logging.error(f"Validation failed for {value} as {data_type}: {str(e)}") + return None + +class RegisterConfigEnhancer: + def __init__(self, register_config): + self.register_config = register_config + self.data_type_mapping = self._create_data_type_mapping() + + def _create_data_type_mapping(self): + """为寄存器分配适当的数据类型""" + mapping = {} + + # GPS数据通常需要浮点数 + if 'value_regs' in self.register_config and 'gps' in self.register_config['value_regs']: + for field in ['latitude', 'longitude', 'altitude', 'speed']: + if field in self.register_config['value_regs']['gps']: + addr = self.register_config['value_regs']['gps'][field] + mapping[addr] = 'float32' + + # 传感器校准参数需要浮点数 + for dev in ['lsdaq', 'hsdaq']: + if dev in self.register_config.get('control_regs', {}): + for param_type in ['sensor_Tmp_CalibParam', 'sensor_Cur_CalibParam', + 'sensor_Vol_CalibParam', 'sensor_Vib_CalibParam']: + if param_type in self.register_config['control_regs'][dev]: + for ch in self.register_config['control_regs'][dev][param_type]: + for param in ['K2', 'K', 'B']: + addr = self.register_config['control_regs'][dev][param_type][ch][param] + mapping[addr] = 'float32' + + return mapping + + def get_data_type(self, address): + return self.data_type_mapping.get(address, 'uint16') + +class ModbusSequentialDataBlockForPCM(ModbusSequentialDataBlock): + def __init__(self, config_manager, logger, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config_manager = config_manager + self._is_client_write = True + self.logger = logger + self._initialize_registers() + + def _initialize_registers(self): + """Initialize register values from configuration""" + for key, value in self.config_manager.regs_config['control_regs'].items(): + config_value = self.config_manager.get_config_value(key) + # print(f"{key}:{value[0]}:{config_value}") + if config_value is not None and ('w' in value[2] or 'W' in value[2]): + match value[1]: + case 'float32': + config_value = float(config_value) + self.server_set_values(value[0]+1, float_to_registers(config_value)) + case 'uint32': + config_value = int(config_value) + # self.server_set_values(value[0]+1, [config_value & 0xFFFF, (config_value >> 16) & 0xFFFF]) + self.server_set_values(value[0]+1, [(config_value >> 16) & 0xFFFF, config_value & 0xFFFF]) + case 'int32': + config_value = int(config_value) + # self.server_set_values(value[0]+1, [config_value & 0xFFFF, (config_value >> 16) & 0xFFFF]) + self.server_set_values(value[0]+1, [(config_value >> 16) & 0xFFFF, config_value & 0xFFFF]) + case 'uint16': + config_value = int(config_value) + # print(f"{key}:{value[0]}:{config_value}:{[struct.pack('>H', config_value)[0]]}") + self.server_set_values(value[0]+1, [config_value & 0xFFFF]) + case 'int16': + config_value = int(config_value) + self.server_set_values(value[0]+1, [config_value & 0xFFFF]) + case _: + pass + self.logger.info("Register initialization completed") + + def setValues(self, address, values): + """Override setValues method""" + if not self._is_client_write: + super().setValues(address, values) + return + + super().setValues(address, values) + + # Handle client writes + updated = False + print(f"*************************address={address}, values={values}*************************") + reg_addr = address - 1 + # print(f"values = {values}") + # path = self.config_manager.mapping.get_config_keys(reg_addr) + # print(f"*************************{path}:{reg_addr}:{values}********************") + # if self.config_manager.update_config_value(path[0], value[0]): + # updated = True + + regCount = len(values) + while(regCount > 0): + path = self.config_manager.mapping.get_config_keys(reg_addr) + print(f"*************************{path}, {reg_addr}, {regCount}*************************") + dataType = self.config_manager.mapping.key_to_address[path[0]][1] + print(f"*************************{path}, {dataType}, {regCount}*************************") + if len(path) > 0: + if '16' in dataType: + print(f"*************************{path}:{reg_addr}:{values[0]}:{regCount}********************") + if dataType in ['int16', 'uint16']: + self.config_manager.update_config_value(path[0], int(values[0])) + regCount -= 1 + reg_addr += 1 + values = values[1:] + elif '32' in dataType: + print(f"*************************{path}:{reg_addr}:{values[0:2]}:{regCount}********************") + if dataType in ['int32', 'uint32']: + self.config_manager.update_config_value(path[0], (values[0]<<16)+values[1]) + elif dataType == 'float32': + self.config_manager.update_config_value(path[0], registers_to_float(values)) + regCount -= 2 + reg_addr += 2 + values = values[2:] + else: + regCount -= 1 + reg_addr += 1 + + if updated: + self.config_manager.save_config() + self.logger.debug(f"Register {address} update triggered configuration change") + + def server_set_values(self, address, values): + """Server-only write method that won't trigger YAML update""" + # self._is_client_write = False + # self.setValues(address, values) + # self._is_client_write = True + super().setValues(address, values) + +class LSDAQ: + def __init__(self, config:dict, logger): + # 加载配置参数 + ''' self.status 码表 + -200: 配置信息错误 + -201: 串口号错误 + -202: 传感器类型错误 + -203: 工作模式错误 + -100: 设备关闭 + -101: 设备未连接 + -1: 多次执行指令失败 + 0: 正常 + 100: 连接失败 + 200: 命令执行失败 + 202: 读取命令错误 + 203: 响应超时 + 204: 报头错误 + 205: 校验错误 + 206: 数据解析错误 + ''' + self.status = -1 + self.config = config + self.logger = logger + self.port = config.get('port', '/dev/ttyLP3') + if self.port != '/dev/ttyLP3': + self.status = -201 + self.baudrate = config.get('baudrate', 115200) + self.timeout = config.get('timeout', 50)/1000.0 + self.mode = config.get('mode', 0) + self.channels = config.get('channels', 16) + if self.mode not in [0, 1]: + self.mode = 0 + self.status = -203 + self.frameNo = 0 + self.sensor_type = config.get('sensor_type', 0xffff) + self.alias = config.get('alias', {}) + for i in range(16): + if f'CH{i+1}' not in self.alias: + self.alias[f'CH{i+1}'] = '' + self.reg_values = { + 'CH1': 0.0, + 'CH2': 0.0, + 'CH3': 0.0, + 'CH4': 0.0, + 'CH5': 0.0, + 'CH6': 0.0, + 'CH7': 0.0, + 'CH8': 0.0, + 'CH9': 0.0, + 'CH10': 0.0, + 'CH11': 0.0, + 'CH12': 0.0, + 'CH13': 0.0, + 'CH14': 0.0, + 'CH15': 0.0, + 'CH16': 0.0, + 'OFFSET': 0.0, + 'POWERVOL': 0.0, + 'TEMP': 0.0, + 'GAIN': 0.0, + 'REF': 0.0, + 'STATUS': 0.0 + } + self.warning_values = { + 'CH1': 0, + 'CH2': 0, + 'CH3': 0, + 'CH4': 0, + 'CH5': 0, + 'CH6': 0, + 'CH7': 0, + 'CH8': 0, + 'CH9': 0, + 'CH10': 0, + 'CH11': 0, + 'CH12': 0, + 'CH13': 0, + 'CH14': 0, + 'CH15': 0, + 'CH16': 0 + } + _sensor_Tmp_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + _sensor_Cur_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + + _sensor_Pres_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + + self.sensor_Tmp_CalibParam = config.get('sensor_Tmp_CalibParam', _sensor_Tmp_CalibParam) + self.sensor_Cur_CalibParam = config.get('sensor_Cur_CalibParam', _sensor_Cur_CalibParam) + self.sensor_Pres_CalibParam = config.get('sensor_Cur_CalibParam', _sensor_Pres_CalibParam) + + # 构建指令集 + self.cmdList = { + # 查询所有通道采集数据 + # 指令格式:指令字符串,回复长度,超时时间,发送校验标志,接收校验标志,指令描述,重试次数 + 'readAllADs': ['', f"0000 0000 0006 0103 0008 0017", 55, 200, 0, 0, 5] + } + self.optFlag = 0 + + def update_config(self): + self.mode = self.config.get('mode') + if self.mode not in [0, 1]: + self.mode = 0 + self.sensor_type = self.config.get('sensor_type', 0xffff) + self.sensor_Tmp_CalibParam = self.config.get('sensor_Tmp_CalibParam') + self.sensor_Cur_CalibParam = self.config.get('sensor_Cur_CalibParam') + self.sensor_Pres_CalibParam = self.config.get('sensor_Pres_CalibParam') + + def exeCmd(self, cmdName:str='readAllADs') -> list: # type: ignore + try: + info = '' + cmd = self.cmdList.get(cmdName, None) + self.status = 0 + if cmd is None: + self.status = 202 + return [False, None, f"Command {cmdName} not found in cmdList."] + retry = 0 + data = bytearray().fromhex(cmd[1]) + + if (cmd[4] == 1): + data += bytearray(checkValue(data[2:]).to_bytes(2, 'big')) + if len(cmd) >= 7: + RETRYTIMES = int(cmd[6]) + else: + RETRYTIMES = 1 + while (retry < RETRYTIMES): + info += f"[{nowStr()}] Sent:{wordData2HexStr(data)}\n" + recvData = bytearray() + self.serial.write(data) + time.sleep(int(cmd[3])/1000.0) + recvData = self.serial.read(int(cmd[2])) + info += (f"[{nowStr()}] Echo:{wordData2HexStr(recvData)}\n") + rspLen = int(cmd[2]) + if len(recvData) >= rspLen: + if recvData[0:2] == bytearray().fromhex(f"0000"): + # info += f"[{nowStr()}] Echo:{wordData2HexStr(recvData[0:rspLen])}\n" + rspLen = len(recvData) + if (cmd[5] == 1): + crc = int.from_bytes(recvData[rspLen-2:rspLen], byteorder='big') + calc_value = checkValue(recvData[0:rspLen-2]) + # info += f"{crc:04X}, {calc_value:04X}\n" + if crc == calc_value: + # self.logger.info(info) + return [True, recvData, info] + else: + self.status = 205 + else: + self.logger.info(info) + return [True, recvData, info] + else: + self.status = 204 + recvData = recvData[1:] + else: + self.status = 203 + retry += 1 + if retry == RETRYTIMES: + self.status = -1 + # self.logger.info(info) + return [False, None, info] + except Exception as e: + info += f"[{nowStr()}] Error in exeCmd({cmd}): {str(e)}\n" # type: ignore + # self.logger.info(info) + return [False, None, info] + + def parseData(self, cmdName, rawData): + _sensor_type = f"{self.sensor_type:016b}"[::-1] + match cmdName: + case 'readAllADs': + datas = struct.unpack('>23H', rawData[9:55]) + if self.mode == 1: + # 校准模式下,直接返回原始数据 + for i in range(self.channels): + self.reg_values[f'CH{i+1}'] = datas[i] + else: + # 工作模式下,进行数据转换 + for i in range(self.channels): + j = i + 1 + if _sensor_type[i] == '0': + # 温度传感器 + # self.logger.info(str(self.reg_values)) + self.reg_values[f'CH{j}'] = (datas[i]**2*self.sensor_Tmp_CalibParam[f'CH{j}']['K2'] + datas[i]*self.sensor_Tmp_CalibParam[f'CH{j}']['K'] + self.sensor_Tmp_CalibParam[f'CH{j}']['B']) + elif _sensor_type[i] == '1': + # 电流传感器 + self.reg_values[f'CH{j}'] = (datas[i]**2*self.sensor_Cur_CalibParam[f'CH{j}']['K2'] + datas[i]*self.sensor_Cur_CalibParam[f'CH{j}']['K'] + self.sensor_Cur_CalibParam[f'CH{j}']['B']) + # 转换为物理量 + self.reg_values[f'CH{j}'] = (self.reg_values[f'CH{j}']**2*self.sensor_Pres_CalibParam[f'CH{j}']['K2'] + self.reg_values[f'CH{j}']*self.sensor_Pres_CalibParam[f'CH{j}']['K'] + self.sensor_Pres_CalibParam[f'CH{j}']['B']) + + self.reg_values['OFFSET'] = datas[16]*256/786432 + self.reg_values['POWERVOL'] = datas[18]*256/786432 + self.reg_values['TEMP'] = (datas[19]*4500000*256/7864320-168000)/563 + 25 #7864320*256/4500000-168000)/563 + 25 + self.reg_values['GAIN'] = datas[20]*256/7864320 + self.reg_values['REF'] = datas[21]*256/786432 + self.reg_values['STATUS'] = self.status + + self.warning_check() + case _: + self.status = 206 + + def warning_check(self): + """检查是否有报警条件""" + for i in range(self.channels): + ch = f'CH{i+1}' + val = self.reg_values[ch] + wp = self.config.get('warning_param', {}) + enable_bits = f"{wp.get('enable', 0):016b}"[::-1] + if enable_bits[i] == '1': + low_limit = wp.get(ch, {}).get('lower', float('-inf')) + high_limit = wp.get(ch, {}).get('upper', float('inf')) + if val < low_limit or val > high_limit: + # self.logger.warning(f"Warning: {ch} value {val} out of limits ({low_limit}, {high_limit})") + self.warning_values[ch] = 1 + else: + self.warning_values[ch] = 0 + else: + self.warning_values[ch] = 0 + + def open(self): + """打开串口连接""" + self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + if not self.serial.is_open: + self.status = -101 + return -1 + else: + self.status = 0 + return 0 + + def close(self): + self.serial.close() + self.status = -100 + + def run(self): + """主运行循环""" + try: + while True: + match self.optFlag: + case 0: + if self.open() == 0: + self.optFlag = 1 + else: + self.optFlag = -1 + case 1: + ret = self.exeCmd('readAllADs') + if ret[0]: + self.parseData('readAllADs', ret[1]) + # self.logger.info(str(self.reg_values)) + self.frameNo += 1 + if self.frameNo > 0xFFFF: + self.frameNo = 0 + time.sleep(1) + if self.status == -1: + self.optFlag = -1 + case _: + time.sleep(5) + self.close() + self.optFlag = 0 + except KeyboardInterrupt: + self.close() + self.logger.info("Modbus Serial TCP Client stopped.") +class SerialClient: + """串口通信基础类""" + def __init__(self, port, baudrate, timeout, logger): + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.logger = logger + self.serial = None + + def open(self): + try: + self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + if self.logger: + self.logger.info(f"[{nowStr()}] Serial opened: {self.port}, baudrate={self.baudrate}, timeout={self.timeout}") + return self.serial.is_open + except Exception as e: + if self.logger: + self.logger.error(f"[{nowStr()}] Serial open failed: {self.port}, err={e}") + return False + + def close(self): + if self.serial and self.serial.is_open: + self.serial.close() + + def exeCmd(self, cmd): + try: + info = '' + data = bytearray().fromhex(cmd[1]) + if cmd[4] == 1: + data += bytearray(checkValue(data).to_bytes(2, 'big')) + + retry = 0 + RETRYTIMES = int(cmd[6]) if len(cmd) >= 7 else 1 + + while retry < RETRYTIMES: + info += f"[{nowStr()}] Sent:{wordData2HexStr(data)}\n" + self.serial.write(data) + time.sleep(int(cmd[3])/1000.0) + recvData = self.serial.read(int(cmd[2])) + info += f"[{nowStr()}] Echo:{wordData2HexStr(recvData)}\n" + + rspLen = int(cmd[2]) + if len(recvData) >= rspLen: + if recvData[0:2] == bytearray().fromhex(cmd[1][0:4]): + recvData = recvData[0:rspLen] + if cmd[5] == 1: + crc = int.from_bytes(recvData[rspLen-2:rspLen], byteorder='big') + calc_value = checkValue(recvData[0:rspLen-2]) + if crc == calc_value: + return [True, recvData, info] + else: + return [True, recvData, info] + retry += 1 + return [False, None, info] + except Exception as e: + info += f"[{nowStr()}] Error in exeCmd: {str(e)}\n" + return [False, None, info] +class IndicatorController: + """指示灯和蜂鸣器控制器(独立串口)""" + def __init__(self, config, logger): + self.logger = logger + self.client = SerialClient( + config.get('port', '/dev/ttyUSB_LIGHT'), + config.get('baudrate', 9600), + config.get('timeout', 50)/1000.0, + logger + ) + self.cmdList = { + 'turnOnGreen': ['', "0105 0002 FF00", 8, 200, 1, 1, 3], + 'turnOffGreen': ['', "0105 0002 0000", 8, 200, 1, 1, 3], + 'turnOnRed': ['', "0105 0008 FF00", 8, 200, 1, 1, 3], + 'turnOffRed': ['', "0105 0000 0000", 8, 200, 1, 1, 3], + 'turnOnAlarm': ['', "0105 00A1 FF00", 8, 200, 1, 1, 3], + 'turnOffAlarm': ['', "0105 00A1 0000", 8, 200, 1, 1, 3], + } + self.alarm = 0 + opened = self.client.open() + if not opened and self.logger: + self.logger.warning(f"[{nowStr()}] Indicator serial not open, port={config.get('port', '/dev/ttyUSB1')}") + + def exe(self, name): + ret = self.client.exeCmd(self.cmdList[name]) + if self.logger: + if ret[0]: + self.logger.info(f"[{nowStr()}] Indicator cmd ok: {name}") + self.logger.debug(ret[2]) + else: + self.logger.warning(f"[{nowStr()}] Indicator cmd failed: {name}; {ret[2]}") + return ret + + def alarming(self, closed): + """报警时:红灯亮+蜂鸣器响,绿灯灭""" + if not self.alarm and closed == 0xF0: + self.exe('turnOffGreen') + self.exe('turnOnRed') + self.exe('turnOnAlarm') + self.alarm = 1 + + def unalarming(self, closed): + """解除报警:根据合闸状态控制指示灯""" + if self.alarm: + self.exe('turnOffRed') + self.exe('turnOffAlarm') + if closed == 0xF0: + self.exe('turnOnGreen') + else: + self.exe('turnOffGreen') + self.alarm = 0 + + def turnOffAll(self): + """关闭所有指示灯和蜂鸣器""" + self.exe('turnOffGreen') + self.logger.error("***********---turnOffGreen") + self.exe('turnOffRed') + self.logger.error("***********---turnOffRed") + self.exe('turnOffAlarm') + self.logger.error("***********---turnOffAlarm") + self.alarm = 0 + + def turnOnGreen(self): + self.exe('turnOnGreen') +class Breaker: + def __init__(self, config:dict, logger): + # 加载配置参数 + ''' self.errorCode 码表 + 0x0001 打开/dev/ttyUSB0设备失败 + 0x0101 与断路器通讯失败 + ''' + self.errorCode = 0 + ''' self.load_status 码表 + 0x00 负载不在线 + 0x0101 负载在线 + ''' + self.load_status = 0 + self.config = config + self.logger = logger + self.port = config.get('port', '/dev/ttyUSB0') + self.baudrate = config.get('baudrate', 9600) + self.timeout = config.get('timeout', 50)/1000.0 + self.task_start_threshold = config.get('task_start_threshold', 2000) + self.task_stop_threshold = config.get('task_stop_threshold', 2000) + self.locked = 0 + self.closed = 0x0F + self.reasonForLastOpen = 15 + self.active_powers = [] + self.duration = config.get('duration', 5) + self.active_power = 0 + + # 创建独立的串口客户端 + self.client = SerialClient(self.port, self.baudrate, self.timeout, logger) + + # 从配置中创建指示灯控制器(如果配置存在) + indicator_config = config.get('indicator', None) + if indicator_config: + self.indicator = IndicatorController(indicator_config, logger) + else: + self.indicator = None + + OVV = config.get('OVV', 275) + UVV = config.get('UVV', 150) + OCV = config.get('OCV', 10000) + LCV = config.get('LCV', 30) + OTV = config.get('OTV', 80) + OPV = config.get('OPV', 13000) + OVT = config.get('OVT', 0) + UVT = config.get('UVT', 0) + OCT = config.get('OCT', 0) + LCT = config.get('LCT', 200) + OTT = config.get('OTT', 200) + OPT = config.get('OPT', 100) + + self.reg_values = { + 'locked': 0, + 'closed': 0x0F, + 'reasonForLastOpen': 0x0F, + 'alarm': 0, + 'active_power': 0, + 'load_status': 0 + } + # 构建指令集(仅断路器指令) + self.cmdList = { + 'readAllDatas': ['', f"0204 0000 0027", 83, 300, 1, 1, 3], + 'readOverLimitValues': ['', f"0203 0002 0006", 17, 200, 1, 1, 3], + 'readOverLimitActionTime': ['', f"0203 0010 0006", 17, 200, 1, 1, 3], + 'setOverLimitValues': ['', f"0210 0002 0006 0C {OVV:04X} {UVV:04X} {OCV:04X} {LCV:04X} {OTV:04X} {OPV:04X}", 8, 100, 1, 1, 3], + 'setOverLimitActionTime': ['', f"0210 0010 0006 0C {OVT:04X} {UVT:04X} {OCT:04X} {LCT:04X} {OTT:04X} {OPT:04X}", 8, 100, 1, 1, 3], + 'closeBreaker': ['', f"0205 0001 ff00", 8, 100, 1, 1, 3], + 'openBreaker': ['', f"0205 0001 0000", 8, 100, 1, 1, 3] + } + + self.optFlag = 0 + self.logger.info(f"Breader routine inspection started.") + + def update_config(self): + pass + + def exeCmd(self, cmdName) -> list: + cmd = self.cmdList.get(cmdName, None) + if cmd is None: + return [False, None, f"Command {cmdName} not found in cmdList."] + # self.logger.info(f"==-=={cmdName}") + return self.client.exeCmd(cmd) + + def parseData(self, cmdName, rawData): + try: + match cmdName: + case 'readAllDatas': + rawData = rawData[3:-2] + self.locked = rawData[0] + self.closed = rawData[1] + self.reasonForLastOpen = (rawData[6]&0xF0)>>4 + self.active_power = int.from_bytes(rawData[68:70], byteorder='big') + self.active_powers.append(self.active_power) + + if len(self.active_powers) > self.duration * 2: + self.active_powers = self.active_powers[1:] + if np.mean(self.active_powers) > self.task_start_threshold: + self.load_status = 1 + if np.mean(self.active_powers) < self.task_stop_threshold: + self.load_status = 0 + + + self.reg_values['locked'] = self.locked + self.reg_values['closed'] = self.closed + self.reg_values['reasonForLastOpen'] = self.reasonForLastOpen + self.reg_values['alarm'] = self.indicator.alarm if self.indicator else 0 + self.reg_values['active_power'] = self.active_power + self.reg_values['load_status'] = self.load_status + + print(f"breaker: {self.reg_values}") + + case 'closeBreaker': + pass + case 'openBreaker': + pass + case _: + pass + except Exception as e: + pass + # self.logger.error(f"[{nowStr()}] Error in Breaker: parseData({cmdName}): {str(e)}\n") + + def openBreaker(self): + # self.logger.info(f"[{nowStr()}] openBreaker called - current closed value: 0x{self.reg_values['closed']:02X}") + if self.reg_values['closed'] == 0xF0: + # self.logger.info(f"[{nowStr()}] openBreaker condition met (closed == 0xF0), setting optFlag to 2") + self.optFlag = 2 + else: + pass + # self.logger.warning(f"[{nowStr()}] openBreaker condition NOT met (closed: 0x{self.reg_values['closed']:02X} != 0xF0), optFlag unchanged") + + def closeBreaker(self): + # self.logger.info(f"[{nowStr()}] closeBreaker called - current closed value: 0x{self.reg_values['closed']:02X}") + if self.reg_values['closed'] == 0x0F: + # self.logger.info(f"[{nowStr()}] closeBreaker condition met (closed == 0x0F), setting optFlag to 3") + self.optFlag = 3 + else: + pass + # self.logger.warning(f"[{nowStr()}] closeBreaker condition NOT met (closed: 0x{self.reg_values['closed']:02X} != 0x0F), optFlag unchanged") + + def alarming(self): + """报警时:红灯亮+蜂鸣器响,绿灯灭""" + if self.indicator: + self.indicator.alarming(self.closed & 0xFF) + + def unalarming(self): + """解除报警:根据合闸状态控制指示灯""" + if self.indicator: + self.indicator.unalarming(self.closed & 0xFF) + + def open(self): + """打开串口连接""" + if self.client.open(): + self.errorCode = 0 + return 0 + else: + self.errorCode = 0x0001 + return -1 + + def close(self): + self.client.close() + + def run(self): + """主运行循环""" + try: + while True: + match self.optFlag: + case 0: + if self.open() == 0: + ret0 = self.exeCmd('openBreaker') + # 初始化时关闭所有指示灯 + if self.indicator: + self.indicator.turnOffAll() + ret1 = self.exeCmd('setOverLimitValues') + ret2 = self.exeCmd('readOverLimitValues') + self.logger.info(f"readOverLimitValues ret: {ret2}") + ret3 = self.exeCmd('setOverLimitActionTime') + ret4 = self.exeCmd('readOverLimitActionTime') + self.logger.info(f"readOverLimitActionTime ret: {ret4}") + if ret0[0] and ret1[0]: + self.optFlag = 1 + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case 1: + time.sleep(0.2) + ret = self.exeCmd('readAllDatas') + self.logger.info(f"readAllDatas ret: {wordData2HexStr(ret[1])}") + if ret[0]: + self.parseData('readAllDatas', ret[1]) + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case 2: + ret = self.exeCmd('openBreaker') + if ret[0]: + # 分闸成功后,关闭所有指示灯 + if self.indicator: + self.indicator.turnOffAll() + self.optFlag = 1 + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case 3: + ret = self.exeCmd('closeBreaker') + if ret[0]: + # 合闸成功后,点亮绿灯 + if self.indicator: + self.indicator.turnOnGreen() + self.optFlag = 1 + continue + self.optFlag = -1 + self.errorCode = 0x0101 + case _: + time.sleep(1) + self.close() + self.optFlag = 0 + except Exception as e: + self.close() + self.logger.info(f"Error in Breader: run(), {e}") + +class GPS: + def __init__(self, config:dict, logger): + self.status = -1 + self.logger = logger + self.config = config + self.port = config.get('port', '/dev/ttyLP4') + if self.port != '/dev/ttyLP4': + self.status = -201 + self.baudrate = config.get('baudrate', 9600) + self.timeout = config.get('timeout', 1) + self.optFlag = 0 + self.gps_data = {'latitude': 0.0, 'longitude': 0.0, 'altitude': 0.0, 'speed': 0.0} + + def read_data(self): + """从串口读取GPS数据""" + if not self.serial or not self.serial.is_open: + return -1 + try: + # 读取NMEA数据 (简化示例,实际需要解析NMEA语句) + line = self.serial.readline().decode('ascii', errors='ignore').strip() + if line.startswith('$GNGGA') or line.startswith('$GPGGA') or line.startswith('$BDGGA'): + # 示例解析GPGGA语句 (实际应用中需要更健壮的解析) + parts = line.split(',') + if len(parts) > 9: + try: + # 纬度格式转换: ddmm.mmmm -> 十进制 + lat = (float(parts[2][:2]) if parts[2] else 0.0) + (float(parts[2][2:]) if parts[2] else 0.0)/60.0 + if parts[3] == 'S': + lat = -lat + + # 经度格式转换: dddmm.mmmm -> 十进制 + lon = (float(parts[4][:3]) if parts[4] else 0.0) + (float(parts[4][3:]) if parts[4] else 0.0)/60.0 + if parts[5] == 'W': + lon = -lon + + # 海拔高度 + alt = float(parts[9]) if parts[9] else 0.0 + + self.gps_data = { + 'latitude': lat, + 'longitude': lon, + 'altitude': alt, + 'speed': 0.0 # GPGGA不包含速度,需要从GPRMC获取 + } + return 0 + except (ValueError, IndexError) as e: + raise Exception(f"Error in parse GPS data: {e}") + except Exception as e: + self.logger.error(f"Error in read_gps_data(): {e}") + + def open(self): + """打开串口连接""" + self.serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + if not self.serial.is_open: + self.status = -101 + return -1 + else: + self.status = 0 + return 0 + + def close(self): + self.serial.close() + self.status = -100 + + def run(self): + """主运行循环""" + try: + while True: + match self.optFlag: + case 0: + if self.open() == 0: + self.optFlag = 1 + else: + self.optFlag = -1 + case 1: + ret = self.read_data() + if ret != 0: + self.optFlag = -1 + continue + self.logger.info(str(self.gps_data)) + case _: + time.sleep(5) + self.close() + self.open() + self.optFlag = 0 + except KeyboardInterrupt: + self.close() + self.logger.info("Modbus Serial TCP Client stopped.") + +class HSDAQ: + def __init__(self, config:dict, logger): + try: + self.config = config + self.logger = logger + result = subprocess.run(["ip","neigh","add", "192.168.0.2", "lladdr","00:0A:35:01:FE:C0", "dev", "ethernet0"], capture_output=True, text=True, encoding="utf-8") + if result.returncode != 0: + self.logger.info(result.stderr) + # result = subprocess.run(["sudo","ethtool","-s", "ethernet0", "speed", "100", "duplex", "full", "autoneg", "off"], capture_output=True, text=True, encoding="utf-8") + # if result.returncode != 0: + # self.logger.info(result.stderr) + + # 设置允许强制修改缓存区大小 + self.sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003)) + SO_RCVBUFORCE = 33 + self.sock.setsockopt(socket.SOL_SOCKET, SO_RCVBUFORCE, 1024 * 1024 * 25) + # 设置 SO_NO_CHECK 选项,使用整数值 11 + SO_NO_CHECK = 11 + self.sock.setsockopt(socket.SOL_SOCKET, SO_NO_CHECK, 0) + actual_buf_size = self.sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) + self.logger.info(f"Requested UDP buffer: 50MB, Actual UDP buffer: {actual_buf_size/1024/1024:.2f}MB") + self.sock.bind(('ethernet0', 0)) + self.dataFileDir = self.config['output_dir'] + self.file_type = self.config.get('file_type', 1) + if self.file_type not in [0, 1]: + self.file_type = 1 + + self.save_flag = self.config.get('save_flag', 0xffff) + self.channels = self.config.get('channels', 16) + + if not os.path.exists(self.dataFileDir): + os.makedirs(self.dataFileDir) + for i in range(self.channels): + os.makedirs(os.path.join(self.config['output_dir'], f"{i+1:02}"), exist_ok=True) + + + self.buffer = b'' + self.feature_data = {} + self.frequency = [0]*16 + self.reg_values = [] + + self.daqBoardNo = self.config.get('daq_board_no', 'XXXXXXXXXX') + self.sensor_type = self.config.get('sensor_type', 0xffffffff) + + self.feature_type = self.config.get('feature_type', '加速度rms') + self.min_vol_cur_phy_value = self.config.get('min_vol_cur_phy_value', 0.0) + self.max_vol_cur_phy_value = self.config.get('max_vol_cur_phy_value', 160.0) + self.scale = self.config.get('vol_cur_phy_scale', 1) + + self.sample_time = self.config.get('sample_time', 1000) + self.sample_period = self.config.get('sample_period', 4000) + self.one_sample_time = self.config.get('one_sample_time', 10) + self.sample_rate = int(1000000/self.one_sample_time) + + self.sample_points = int(self.sample_time*1000/self.one_sample_time) + self.mode = self.config.get('mode', 0) + self.alias = config.get('alias', {}) + for i in range(16): + if f'CH{i+1}' not in self.alias: + self.alias[f'CH{i+1}'] = '' + + self.cmdList = {'startDAQ': bytes.fromhex(f"DDDD 0001 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}"), + 'stopDAQ': bytes.fromhex(f"DDDD 0000 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}")} + + _sensor_Vol_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + _sensor_Cur_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + _sensor_Vib_CalibParam = { + 'CH1': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH2': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH3': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH4': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH5': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH6': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH7': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH8': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH9': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH10': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH11': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH12': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH13': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH14': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH15': {'K2':0.0, 'K': 1.0, 'B': 0.0}, + 'CH16': {'K2':0.0, 'K': 1.0, 'B': 0.0} + } + self.sensor_Vol_CalibParam = self.config.get('sensor_Vol_CalibParam', _sensor_Vol_CalibParam) + self.sensor_Cur_CalibParam = self.config.get('sensor_Cur_CalibParam', _sensor_Cur_CalibParam) + self.sensor_Vib_CalibParam = self.config.get('sensor_Vib_CalibParam', _sensor_Vib_CalibParam) + self.warning_values = { + 'CH1': 0, + 'CH2': 0, + 'CH3': 0, + 'CH4': 0, + 'CH5': 0, + 'CH6': 0, + 'CH7': 0, + 'CH8': 0, + 'CH9': 0, + 'CH10': 0, + 'CH11': 0, + 'CH12': 0, + 'CH13': 0, + 'CH14': 0, + 'CH15': 0, + 'CH16': 0 + } + self.logger.info(f"DAQ thread starts. Address of DAQ board: IP={self.config['host']}, port={self.config['port']}") + except Exception as e: + self.logger.error(f"Error in __init__(): {e}") + time.sleep(5) + self.__init__() + + def update_config(self): + self.file_type = self.config.get('file_type') + if self.file_type not in [0, 1]: + self.file_type = 1 + self.save_flag = self.config.get('save_flag') + self.sensor_type = self.config.get('sensor_type') + self.sample_time = self.config.get('sample_time') + self.sample_period = self.config.get('sample_period') + self.one_sample_time = self.config.get('one_sample_time') + self.sample_points = int(self.sample_time*1000/self.one_sample_time) + self.mode = self.config.get('mode') + if self.mode not in [0, 1]: + self.mode = 1 + + self.cmdList = {'startDAQ': bytes.fromhex(f"DDDD 0001 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}"), + 'stopDAQ': bytes.fromhex(f"DDDD 0000 {self.sample_time*1000:08X} {(self.sample_period-self.sample_time)*1000:08X} {self.one_sample_time:08X} {self.sensor_type:08X}")} + self.sensor_Vol_CalibParam = self.config.get('sensor_Vol_CalibParam') + self.sensor_Cur_CalibParam = self.config.get('sensor_Cur_CalibParam') + self.sensor_Vib_CalibParam = self.config.get('sensor_Vib_CalibParam') + + def start_DAQ(self): + """发送启动采集指令""" + try: + self.buffer = bytearray() + if hasattr(self, 'udpsock') and self.udpsock: + self.udpsock.close() + self.udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # self.udpsock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 25) + self.udpsock.bind((self.config['local_host'], self.config['local_port'])) + self.udpsock.sendto(self.cmdList['startDAQ'], (self.config['host'], self.config['port'])) + self.udpsock.close() + self.logger.info(f"Send start command to DAQ board. {self.cmdList['startDAQ'].hex()}") + except Exception as e: + self.logger.error(f"Error in start_DAQ(): {e}") + + def stop_DAQ(self): + """发送停止采集指令""" + try: + self.buffer = bytearray() + if hasattr(self, 'udpsock') and self.udpsock: + self.udpsock.close() + self.udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # self.udpsock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 25) + self.udpsock.bind((self.config['local_host'], self.config['local_port'])) + self.udpsock.sendto(self.cmdList['stopDAQ'], (self.config['host'], self.config['port'])) + self.udpsock.close() + self.logger.info(f"Send stop command to DAQ board. {self.cmdList['stopDAQ'].hex()}") + except Exception as e: + self.logger.error(f"Error in stop_DAQ(): {e}") + + def _get_dir_size(self, path: Path) -> int: + """利用 Linux 的 du,返回目录本身已占用字节数,毫秒级""" + return int(subprocess.check_output( + ['du', '-sb', str(path)], text=True).split()[0]) + + def _oldest_file(self, path: Path): + """返回目录中最旧的普通文件 Path 对象,没有则返回 None""" + with os.scandir(path) as it: + files = [e for e in it if e.is_file()] + if not files: + return None + # 按修改时间升序 + return Path(min(files, key=lambda e: e.stat().st_mtime).path) + + def save_data(self): + """保存数据到文件""" + try: + #判断磁盘剩余空间是否小于1G,如果是从16通道的旧文件目录中删除文件 + channels = self.channels + # usage = shutil.disk_usage("C:/") + # while usage.free < self.config['daq']['min_free_gb']*1024*1024*1024: + # #获取目录下文件列表,并按照降序排序,如果硬盘空间小于阈值,删除旧的文件 + # for i in range(channels): + # os.makedirs(os.path.join(self.config['daq']['output_dir'], f"{i+1:02}"), exist_ok=True) + # fileList = os.listdir(f"C:/users/Administrator/PCM/data/{i+1:02}") + # fileList.sort(reverse=False) + # if os.path.exists(f"C:/users/Administrator/PCM/data/{i+1:02}/{fileList[0]}"): + # os.remove(f"C:/users/Administrator/PCM/data/{i+1:02}/{fileList[0]}") + # # os.remove(f"C:/users/Administrator/PCM/data/{fileList[1]}") + # usage = shutil.disk_usage("C:/") + + target_dir = Path(self.dataFileDir) + max_usage_gb = 5 + max_usage_bytes = max_usage_gb * 1024**3 + channels = self.channels + while True: + if self._get_dir_size(target_dir) <= max_usage_bytes: + break + for ch in range(channels): + ch_path = target_dir / f'{ch+1:02d}' + ch_path.mkdir(parents=True, exist_ok=True) + victim = self._oldest_file(ch_path) + if victim: + victim.unlink() + self.reg_values = [] + timestamp = time.time() + timeStr = datetime.fromtimestamp(timestamp).strftime("%Y%m%d%H%M%S") + # with open(filename, 'wb') as f: + # f.write(self.buffer) + # f.close() + print(f"Length of buffer: {len(self.buffer)}") + datas = np.frombuffer(self.buffer, dtype='>h') + print(f"Length of datas: {len(datas)}") + datas = datas[:int(len(datas)/channels)*channels] + datas = datas.reshape(-1, channels) + data = None + _s = f"{self.sensor_type:032b}" + _sensor_type = ''.join([_s[2*i:2*i+2] for i in range(channels-1, -1, -1)]) + print(f"save_flag = {self.save_flag}") + _save_flag = f"{self.save_flag:016b}"[::-1] + if 'calib_params' in self.config and 'vibration' in self.config['calib_params']: + _fre = self.config['calib_params']['vibration'].get('frequency', -1) + else: + _fre = -1 + + for i in range(channels): + j = i + 1 + if _fre != -1 and _fre > 0 and self.config['mode'] == 1: + _len = len(datas[:,i])//_fre*_fre + _data = datas[0:_len,i] + else: + _data = datas[:,i] + + _data = _data[0:20] + log_data = np.log(np.abs(_data) + 1e-300) + log_mean_squared = 2 * np.mean(log_data) + np.log(len(_data)) + _rms = np.exp(0.5 * log_mean_squared) / self.scale + + # _rms = np.sqrt(np.mean(_data**2))/self.scale + _min = np.min(_data) + _max = np.max(_data) + _mean = np.mean(_data) + + # if (_max - _mean) * 5 < (_mean - _min): + # _mean = np.mean(_data[0:20]) + # _min = np.min(_data[0:20]) + # _max = np.max(_data[0:20]) + # _rms = np.sqrt(np.mean(np.square(_data[0:20]))) + + self.feature_data[f'CH{j}'] = { + 'min': _min/self.scale, + 'max': _max/self.scale, + 'mean': _mean/self.scale, + 'std': np.std(_data)/self.scale, + 'rms': _rms, + 'sr0': 0.0, + 'sr1': 0.0, + 'sr2': 0.0, + 'sr3': 0.0, + 'sr4': 0.0 + } + rms = self.feature_data[f'CH{j}']['rms'] + mean = self.feature_data[f'CH{j}']['mean'] + + filename = '' + match _sensor_type[2*i:2*i+2]: + case '00': + # 计算频率,以Hz为单位 + self.feature_data[f'CH{j}']['sr0'] = self.calculateFrequency(datas[:, i], self.one_sample_time) + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + data = datas[:, i] + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case '01': + # 计算声音大小 + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + data = datas[:, i] + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case '10': + # 计算电流大小 + data = datas[:, i] + if self.mode != 1: + for k, v in self.feature_data[f'CH{j}'].items(): + self.feature_data[f'CH{j}'][k] = v**2*self.sensor_Cur_CalibParam[f'CH{j}']['K2'] + v*self.sensor_Cur_CalibParam[f'CH{j}']['K'] + self.sensor_Cur_CalibParam[f'CH{j}']['B'] + data = (datas[:, i]**2*self.sensor_Cur_CalibParam[f'CH{j}']['K2'] + datas[:, i]*self.sensor_Cur_CalibParam[f'CH{j}']['K']+self.sensor_Cur_CalibParam[f'CH{j}']['B']).astype(np.float32) + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case '11': + # 计算振动大小 + data = datas[:, i] + if self.mode != 1: + for k, v in self.feature_data[f'CH{j}'].items(): + self.feature_data[f'CH{j}'][k] = v**2*self.sensor_Vib_CalibParam[f'CH{j}']['K2'] + v*self.sensor_Vib_CalibParam[f'CH{j}']['K'] + self.sensor_Vib_CalibParam[f'CH{j}']['B'] + data = (datas[:, i]**2*self.sensor_Vib_CalibParam[f'CH{j}']['K2'] + datas[:, i]*self.sensor_Vib_CalibParam[f'CH{j}']['K']+self.sensor_Vib_CalibParam[f'CH{j}']['B']).astype(np.float32) + self.feature_data[f'CH{j}']['rms'] = np.sqrt(np.mean(data**2)) + self.feature_data[f'CH{j}']['sr0'] = self.feature_data[f'CH{j}']['rms']*np.sqrt(2) + else: + self.feature_data[f'CH{j}']['sr0'] = self.feature_data[f'CH{j}']['std']*np.sqrt(2) + self.reg_values.extend(list(self.feature_data[f'CH{j}'].values())) + filename = os.path.join(self.dataFileDir, f"{j:02}/{timeStr}_{rms:.3f}_{self.feature_type}_{self.daqBoardNo}_{j:02}_N_{self.sample_rate}_{self.sample_points}_0.000_{mean:.3f}_{self.min_vol_cur_phy_value}_{self.max_vol_cur_phy_value}_{self.scale}") + case _: + pass + + # 将数据写入文件 + if self.file_type == 1: + filename += '.bin' + if _save_flag[i] == '1': + if isinstance(data, np.ndarray): + try: + temp_data = data.astype('>f4') + bytes_data = temp_data.tobytes() + with open(filename, 'wb', buffering=0) as f: + # 1. 正常写 + f.write(bytes_data) + + # 2. 告诉内核:整个文件以后大概率不读,页 cache 可以立即回收 + fd = os.open(filename, os.O_RDONLY) + try: + # POSIX_FADV_DONTNEED = 4 + os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) + finally: + os.close(fd) + del temp_data, bytes_data, data + finally: + if 'temp_data' in locals(): + del temp_data + if 'bytes_data' in locals(): + del bytes_data + self.logger.debug(f"Success to save data to {filename}.") + else: + filename += '.csv' + if _save_flag[i] == '1': + if isinstance(data, np.ndarray): + with open(filename, 'w') as f: + # 使用生成器表达式避免创建巨大列表 + lines = (f"{num:.4f}\n" for num in data) + f.writelines(lines) + self.logger.debug(f"Saved data to {filename}.") + del lines + self.warning_check() + data = None + datas = None + self.buffer = bytearray() + self._force_memory_cleanup() + except Exception as e: + self.logger.error(f"Error in save_data(): {e}") + + def warning_check(self): + """检查是否有报警条件""" + for i in range(self.channels): + ch = f'CH{i+1}' + val = self.feature_data[ch]['mean'] + + wp = self.config.get('warning_param', {}) + enable_bits = f"{wp.get('enable', 0):016b}"[::-1] + if enable_bits[i] == '1': + low_limit = wp.get(ch, {}).get('lower', float('-inf')) + high_limit = wp.get(ch, {}).get('upper', float('inf')) + if val < low_limit or val > high_limit: + # self.logger.warning(f"Warning: {ch} value {val} out of limits ({low_limit}, {high_limit})") + self.warning_values[ch] = 1 + else: + self.warning_values[ch] = 0 + + def _force_memory_cleanup(self): + """强制内存清理""" + import gc + # 清除各种缓存 + if hasattr(np, 'getbufsize'): + np.setbufsize(32768) + + # 强制垃圾回收 + gc.collect() + gc.collect() # 两次确保回收 + + # 稍微等待让系统处理 + import time + time.sleep(0.01) + + def calculateFrequency(self, signal, oneSampleTime): + '''计算0-1变换的数组中0-1变化的次数, 并计算其频率''' + # oneSampleTime 单位为us + if len(signal) < 2: + return 0.0 # 信号太短无法计算频率 + transitions = 0 # 跳变次数计数器 + # 遍历数组计算跳变次数 + for i in range(1, len(signal)): + if signal[i] != signal[i-1]: + transitions += 1 + # 计算频率: + # 每个周期有2次跳变(0→1和1→0) + # 总时间 = 采样点数 / 采样率 + # 频率 = (跳变次数 / 2) / (总时间) + total_time = len(signal) * oneSampleTime / 1000000 + frequency = (transitions // 2) / total_time if total_time > 0 else 0.0 + return frequency + + def run(self): + """主运行循环""" + self.logger.info(f"Start DAQ thread.") + frame_size = self.config.get('frame_size_max', 1464) + FILESIZE = self.config.get('file_size', 32000000) + DATA_DIR = self.config.get('output_dir', 'data') + optFlag = 0 + # 清空接收缓存,并向DAQ模块发送启动采集指令 + self.buffer = b'' + self.sampleNum = 0 + self.stop_DAQ() + time.sleep(0.01) + self.start_DAQ() + lastFrameNo = 0 + cycles = 0 + while(True): + try: + data, addr = self.sock.recvfrom(frame_size+42) + # 如何返回了数据,数据起始符正确,包号正确,则存储数据 + if data: + data = data[42:] + nowFrameNo = int.from_bytes(data[4:8], 'big') + if nowFrameNo != lastFrameNo + 1: + print(f"Received data: len={len(data)}, lastFrameNo={lastFrameNo}, nowFrameNo={nowFrameNo}") + # self.logger.info(f"Received data: len={len(data)}, frame NO.={int.from_bytes(data[4:8], 'big')}") + else: + continue + # self.logger.debug(f"Head Data:(20 byte) ={' '.join(data[i:i+2].hex() for i in range(0, 20, 2))}") + + if optFlag == 0: + if data and data[0:4] == bytearray([0xa5, 0x5a, 0xa5, 0x5a]) and len(data) == frame_size and data[4:8] == bytearray([0x00, 0x00, 0x00, 0x01]): + self.buffer += data[24:] + self.sampleNum += ((len(data)-24)/32) + # if self.sensorType != int.from_bytes(data[8:12], 'big'): + # self.logger.error(f"In daq_thread(): SensorType in return data doesn't match with config.") + optFlag = 1 + elif optFlag == 1: + if data and data[0:4] == bytearray([0xa5, 0x5a, 0xa5, 0x5a]): + if len(data) != frame_size or data[5:8] == bytearray([0x00, 0x00, 0x01]): + if data[4:8] != bytearray([0x00, 0x00, 0x00, 0x01]): + self.buffer += data[24:] + self.sampleNum += ((len(data)-24)/32) + + cycles += 1 + self.save_data() + self.buffer = bytearray() + self.sampleNum = 0 + optFlag = 0 + else: + self.buffer += data[24:] + self.sampleNum += ((len(data)-24)/32) + + if nowFrameNo - lastFrameNo != 1 and lastFrameNo < nowFrameNo: + if lastFrameNo != 0: + pass + self.logger.warning(f"cycles= {cycles}, lastFrameNo={lastFrameNo}, nowFrameNo={nowFrameNo}") + # raise Exception() + if nowFrameNo != 0: + lastFrameNo = nowFrameNo + except Exception as e: + self.stop_DAQ() + self.sock.close() + self.running = False + self.buffer = bytearray() + self.logger.error(f"Error in daq_thread(): {e}") + self.logger.info(f"Stop DAQ thread.") + break + +class InfluxDBWriter: + def __init__(self, url="http://localhost:8086", token="", org="my-org", bucket="my-bucket"): + """ + 初始化 InfluxDB 客户端 + + 参数: + url: InfluxDB 地址,host模式下使用 http://localhost:8086 + token: API token,格式为 "username:password" 或 token字符串 + org: 组织名称 + bucket: 存储桶名称 + """ + self.client = InfluxDBClient(url=url, token=token, org=org) + self.write_api = self.client.write_api(write_options=SYNCHRONOUS) + self.bucket = bucket + self.org = org + + def write_sensor_data(self, measurement, tags, fields): + """ + 写入传感器数据到 InfluxDB + + 参数: + measurement: 测量名称 (类似表名) + tags: 标签字典,用于索引和分组 (如: {"device": "sensor1", "location": "factory"}) + fields: 字段字典,存储实际数据 (如: {"temperature": 25.6, "humidity": 60.2}) + """ + try: + # 创建数据点 + point = Point(measurement) + + # 添加标签 + for tag_key, tag_value in tags.items(): + point = point.tag(tag_key, tag_value) + + # 添加字段 + for field_key, field_value in fields.items(): + point = point.field(field_key, field_value) + + # 写入数据 + self.write_api.write(bucket=self.bucket, record=point) + print(f"[{datetime.now()}]数据写入成功: {point.to_line_protocol()}") + + except Exception as e: + print(f"写入数据时出错: {e}") + + def write_batch_data(self, points): + """ + 批量写入多个数据点 + """ + try: + self.write_api.write(bucket=self.bucket, record=points) + print(f"[{datetime.now()}]批量写入成功,共 {len(points)} 个数据点") + except Exception as e: + print(f"批量写入时出错: {e}") + + def close(self): + """关闭连接""" + self.client.close() + +class ModbusGateway: + def __init__(self): + # 初始化logger + config_file = 'src/config-1.2-debug.yaml' + config_file_temp = 'config-1.2-debugcopy.yaml' + with open('src/logging-config.json', 'r') as f: + logging.config.dictConfig(json.load(f)) + self.logger = logging.getLogger('PCM') + + self.config_manager = ConfigManager( + regs_config_file='src/regs-mapping-1.2-debug.yaml', + config_file=config_file, + logger=self.logger) + + self.taskInfo = {'status':0x0000, 'running_time':0, 'period':0} + self.taskInfo['period'] = self.config_manager.config['task']['period']*60 + + # 初始化influxdb client + self.influx_client = InfluxDBWriter( + url=self.config_manager.config['influxdb'].get('url', 'http://localhost:8086'), + token=self.config_manager.config['influxdb'].get('token', 'PCM:1842moon'), + org=self.config_manager.config['influxdb'].get('org', 'MEASCON'), + bucket=self.config_manager.config['influxdb'].get('bucket', 'PCM') + ) + + # 连接plc server + self.plc_host = self.config_manager.config['plc-server'].get('host', '172.22.0.3') + self.plc_port = self.config_manager.config['plc-server'].get('port', 5020) + self.plc_client = ModbusTcpClient(self.plc_host, port=self.plc_port) + self.plc_measurements = self.config_manager.config['plc-server'].get('measurements', {}) + + # 创建本地modbus tcp服务器 + self.max_address = 1300 + self.host = self.config_manager.config['modbus-server']['host'] + self.port = self.config_manager.config['modbus-server']['port'] + + self.holding_registers = ModbusSequentialDataBlockForPCM(self.config_manager, self.logger, 0x00, [0]*(self.max_address+1)) + + # 创建数据存储 + store = ModbusSlaveContext( + di=ModbusSequentialDataBlock(0, [0]*1), + co=ModbusSequentialDataBlock(0, [0]*1), + # hr=ModbusSequentialDataBlock(0, [0]*1000), + hr=self.holding_registers, + ir=ModbusSequentialDataBlock(0, [0]*1)) + + self.context = ModbusServerContext(slaves={1:store}, single=False) + + # 启动服务器线程 + self.modbus_sever = threading.Thread( + target=StartTcpServer, + # kwargs={"context": self.context, "address": (self.host, self.port)}) + kwargs={"context": self.context, "address": ('0.0.0.0', self.port)}) + self.modbus_sever.daemon = True + self.modbus_sever.start() + self.logger.info(f"Local modbusTCP service starts, IP={self.host}, port={self.port}") + + self.gps = GPS(self.config_manager.config['gps'], self.logger) + self.breaker = Breaker(self.config_manager.config['breaker'], self.logger) + self.lsdaq = LSDAQ(self.config_manager.config['lsdaq'], self.logger) + self.hsdaq = HSDAQ(self.config_manager.config['hsdaq'], self.logger) + + self.gps_thread = threading.Thread(target=self.gps.run) + self.gps_thread.daemon = True + self.gps_thread.start() + + self.breaker_thread = threading.Thread(target=self.breaker.run) + self.breaker_thread.daemon = True + self.breaker_thread.start() + + self.lsdaq_thread = threading.Thread(target=self.lsdaq.run) + self.lsdaq_thread.daemon = True + self.lsdaq_thread.start() + + self.hsdaq_thread = threading.Thread(target=self.hsdaq.run) + self.hsdaq_thread.daemon = True + self.hsdaq_thread.start() + + # 启动配置服务(HTTP API) + config_service_port = self.config_manager.config.get('config-server', {}).get('port', 5000) + config_service_host = self.config_manager.config.get('config-server', {}).get('host', '127.0.0.1') + + # 如果配置文件中指定了配置文件路径,使用它;否则使用默认的YAML配置文件 + config_service_config_path = self.config_manager.config.get('config-server', {}).get('config_path', config_file_temp) + + self.config_service = ConfigService( + default_config_path=config_service_config_path, + host=config_service_host, + port=config_service_port, + debug=False, + logger=self.logger, + ) + self.config_service.start() + self.logger.info(f"Config service started on {config_service_host}:{config_service_port}") + + # 任务状态持久化文件路径(独立文件,不会被外部覆盖) + self.task_state_file = '.task_state.json' + + def _load_task_running_time(self) -> float: + """ + 从独立的持久化文件加载任务累计运行时间 + + Returns: + float: 累计运行时间(秒),文件不存在时返回0 + """ + try: + if not os.path.exists(self.task_state_file): + self.logger.info(f"Task state file not found: {self.task_state_file}, starting from 0") + return 0 + + with open(self.task_state_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + running_time = data.get('running_time', 0) + if running_time > 0: + self.logger.info(f"Loaded task running_time from {self.task_state_file}: {running_time:.2f}s") + return float(running_time) + + except json.JSONDecodeError as e: + self.logger.error(f"Task state file corrupted: {e}, starting from 0") + return 0 + except Exception as e: + self.logger.error(f"Error loading task running_time: {e}, starting from 0") + return 0 + + def _save_task_running_time(self, running_time: float) -> bool: + """ + 保存任务累计运行时间到独立的持久化文件(原子写入) + + Args: + running_time: 累计运行时间(秒) + + Returns: + bool: 保存成功返回 True,失败返回 False + """ + try: + data = { + 'running_time': running_time, + 'last_update': datetime.now().isoformat(), + 'version': '1.0' + } + + # 原子写入:先写临时文件,再重命名 + temp_file = self.task_state_file + '.tmp' + with open(temp_file, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + # 重命名(原子操作) + os.replace(temp_file, self.task_state_file) + return True + + except Exception as e: + self.logger.error(f"Error saving task running_time: {e}") + # 清理临时文件 + try: + temp_file = self.task_state_file + '.tmp' + if os.path.exists(temp_file): + os.remove(temp_file) + except: + pass + return False + + def _reset_task_running_time(self) -> bool: + """ + 重置任务累计运行时间为0 + + Returns: + bool: 重置成功返回 True,失败返回 False + """ + self.logger.info("Resetting task running_time to 0") + return self._save_task_running_time(0) + + def _update_modbus_datas(self): + """更新本地Modbus服务器的寄存器""" + if not hasattr(self, 'context'): + self.logger.error("Local modbus tcp service isn't initilized.") + return + + try: + # 获取本地服务器的slave上下文 + slave = self.context[1] + holding_registers = self.holding_registers + + gps_reg_values = list(self.gps.gps_data.values()) + # print(f"gps_reg_values:{gps_reg_values}") + for i in range(len(gps_reg_values)): + holding_registers.server_set_values(2*i+1, float_to_registers(gps_reg_values[i])) # type: ignore + + breaker_reg_values = list(self.breaker.reg_values.values()) + # print(f"breaker_reg_values:{breaker_reg_values}") + for i in range(len(breaker_reg_values)): + holding_registers.server_set_values(1220+i+1, int(breaker_reg_values[i])) # type: ignore + + lsdaq_reg_values = list(self.lsdaq.reg_values.values()) + # print(f"lsdaq_reg_values:{lsdaq_reg_values}") + for i in range(len(lsdaq_reg_values)): + holding_registers.server_set_values(8+2*i+1, float_to_registers(lsdaq_reg_values[i])) # type: ignore + + lsdaq_warning_values = list(self.lsdaq.warning_values.values()) + # print(f"lsdaq_warning_values:{lsdaq_warning_values}") + for i in range(len(lsdaq_warning_values)): + holding_registers.server_set_values(1129+i+1, int(lsdaq_warning_values[i])) # type: ignore + + hsdaq_reg_values = self.hsdaq.reg_values + # print(f"hsdaq_reg_values:{hsdaq_reg_values}") + # print(f"len = {len(hsdaq_reg_values)}") + for i in range(len(hsdaq_reg_values)): + holding_registers.server_set_values(60+2*i+1, float_to_registers(hsdaq_reg_values[i])) # type: ignore + + hsdaq_warning_values = list(self.hsdaq.warning_values.values()) + # print(f"hsdaq_warning_values:{hsdaq_warning_values}") + for i in range(len(hsdaq_warning_values)): + holding_registers.server_set_values(1145+i+1, int(hsdaq_warning_values[i])) # type: ignore + + # 更新从plc采集到的数据 + # packed_data = bytearray() + # for k, v in self.plc_measurements.items(): + # packed_data.extend(struct.pack('>fHffH', v['value'], v['warning_param']['enable'], v['warning_param']['lower'], v['warning_param']['upper'], v['warning'])) + # register_values = [] + # for i in range(0, len(packed_data), 2): + # if i + 1 < len(packed_data): + # # 组合两个字节为一个16位整数 + # value = (packed_data[i] << 8) | packed_data[i + 1] + # else: + # # 如果字节数为奇数,最后一个字节补0 + # value = packed_data[i] << 8 + # register_values.append(value) + # holding_registers.server_set_values(1161+1, register_values) + + # 读取CPU各温区温度 + thermal_zones = 5 + self.cpu_temperatures = {} + for i in range(thermal_zones): + with open(f"/sys/class/thermal/thermal_zone{i}/temp", 'r') as f: + # 读取温度值(毫摄氏度) + temp_millic = int(f.read().strip()) + # 转换为摄氏度 + temp_c = temp_millic / 1000.0 + self.cpu_temperatures[f'zone{i}'] = temp_c + + holding_registers.server_set_values(50+2*i+1, float_to_registers(temp_c)) # type: ignore + + except Exception as e: + self.logger.error(f"Error in _update_modbus_datas(): {e}") + + def _write_to_influxdb(self): + if not self.influx_client: + self.logger.error("Influxdb isn't initilized. Try to initilize after 1 seconds.") + time.sleep(1) + if hasattr(self, 'influx_client'): + self.influx_client.close() + self.influx_client = InfluxDBClient( + url=self.config_manager.config['influxdb'].get('url', 'http://localhost:8086'), + token=self.config_manager.config['influxdb'].get('token', 'PCM:1842moon'), + org=self.config_manager.config['influxdb'].get('org', 'MEASCON'), + ) + self.write_api = self.influx_client.write_api(write_options=SYNCHRONOUS) + try: + points = [] + # 构建toradex核心板CPU温度 + point = Point("PCM_Measurement") + point.tag("data_type", 'cpu_temperatures') + for field_name, field_value in self.cpu_temperatures.items(): + point.field(field_name, float(field_value)) + points.append(point) + + # 构建GPS采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'GPS') + for field_name, field_value in self.gps.gps_data.items(): + point.field(field_name, float(field_value)) + points.append(point) + + # 构建Breaker采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'Breaker') + for field_name, field_value in self.breaker.reg_values.items(): + point.field(field_name, float(field_value)) + points.append(point) + + # 构建TaskInfo采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'TaskInfo') + print(f"taskInfo = {self.taskInfo}") + for field_name, field_value in self.taskInfo.items(): + if field_value == None: + field_value = 0 + point.field(field_name, float(field_value)) + points.append(point) + + # 构建低速采集数据 + point = Point("PCM_Measurement") + point.tag("data_type", 'LSDAQ') + # for field_name, field_value in self.lsdaq.reg_values.items(): + # point.field(field_name, float(field_value)) + # for field_name, field_value in self.lsdaq.warning_values.items(): + # point.field(field_name+'.warning', field_value) + for field_name, field_value in self.lsdaq.reg_values.items(): + if field_name in self.lsdaq.alias and self.lsdaq.alias.get(field_name) != '': + point.field(self.lsdaq.alias[field_name], float(field_value)) + for field_name, field_value in self.lsdaq.warning_values.items(): + if field_name in self.lsdaq.alias and self.lsdaq.alias.get(field_name) != '': + point.field(self.lsdaq.alias[field_name]+'.warning', field_value) + points.append(point) + + # 构建高速采集数据 + for i in range(16): + # print(str(self.hsdaq.feature_data[f'CH{i}'])) + j = i + 1 + point = Point("PCM_Measurement") + # point.tag("data_type", f'HSDAQ_CH{j}') + if self.hsdaq.alias.get(f'CH{j}') != '': + point.tag("data_type", self.hsdaq.alias[f'CH{j}']) + for field_name, field_value in self.hsdaq.feature_data[f'CH{j}'].items(): + point.field(field_name, float(field_value)) + point.field('warning', self.hsdaq.warning_values[f'CH{j}']) + points.append(point) + + # 构建从PLC采集到的数据 + point = Point("PCM_Measurement") + point.tag("data_type", f'PLC') + for k, v in self.plc_measurements.items(): + point.field(k, float(v['value'])) + points.append(point) + + # 将数据点写入influxdb + self.influx_client.write_batch_data(points) + + except Exception as e: + self.logger.error(f"Error in _write_to_influxdb(): {e}") + + def _read_plc_datas(self): + if not self.plc_client: + self.plc_client = ModbusTcpClient(self.plc_host, port=self.plc_port) + if self.plc_client.connect() and self.plc_measurements: + for k, v in self.plc_measurements.items(): + ret = self.plc_client.read_holding_registers(address=v['address'], count=2, slave=1) + print(f"{ret}") + if (not ret.isError()) and len(ret.registers) == 2: + self.plc_measurements[k]['value'] = registers_to_float(ret.registers, byte_order='ABCD') + if v['warning_param']['enable'] == 1: + low_limit = v['warning_param']['lower'] + high_limit = v['warning_param']['upper'] + val = self.plc_measurements[k]['value'] + if val < low_limit or val > high_limit: + self.plc_measurements[k]['warning'] = 1 + else: + self.plc_measurements[k]['warning'] = 0 + else: + self.plc_measurements[k]['warning'] = 0 + + def run(self): + """主运行循环""" + timestamp = time.time() + task_control_reg_addr = self.config_manager.config['task']['control_reg_addr'] + task_control_reg_value = 0x0000 + self.taskInfo['start_time'] = None + + # 从配置文件恢复运行时间(如果有) + saved_running_time = self._load_task_running_time() + if saved_running_time > 0: + self.taskInfo['running_time'] = saved_running_time + self.logger.info(f"Restored running_time from config: {self.taskInfo['running_time']:.2f}s") + else: + self.taskInfo['running_time'] = 0 + + # 持久化保存相关变量 + last_save_time = time.time() + SAVE_INTERVAL = 5 # 每5秒保存一次 + last_running_time = self.taskInfo['running_time'] # 用于检测变化 + + while True: + self.gps.config = self.config_manager.config['gps'] + self.lsdaq.config = self.config_manager.config['lsdaq'] + self.lsdaq.update_config() + self.hsdaq.config = self.config_manager.config['hsdaq'] + self.hsdaq.update_config() + self.taskInfo['status'] = self.holding_registers.values[task_control_reg_addr+1] + match self.taskInfo['status']: + case 0x0000: + self.breaker.openBreaker() + # 实验停止时重置运行时间 + if self.taskInfo['running_time'] > 0: + self._reset_task_running_time() + self.taskInfo['start_time'] = None + self.taskInfo['running_time'] = 0 + case 0x5555: + self.breaker.closeBreaker() + if self.breaker.reg_values['load_status'] == 1: + current_time = time.time() + self.taskInfo['running_time'] += ( current_time - self.taskInfo['start_time'] ) + self.taskInfo['start_time'] = current_time + else: + self.taskInfo['start_time'] = time.time() + + if self.taskInfo['running_time'] > self.taskInfo['period']: + # 实验完成时重置运行时间 + self._reset_task_running_time() + self.holding_registers.values[task_control_reg_addr+1] = 0xFFFF + continue + + if 1 in self.lsdaq.warning_values.values() or ( self.hsdaq.warning_values['CH12'] == 1 ) or ( self.hsdaq.warning_values['CH14'] == 1 and self.breaker.reg_values['load_status'] == 1): + # self.breaker.alarming() + self.holding_registers.values[task_control_reg_addr+1] = 0xFFFF + continue + # else: + # self.breaker.unalarming() + case 0xAAAA: + self.logger.error(f"AAAA{self.taskInfo}") + self.breaker.openBreaker() + self.taskInfo['start_time'] = time.time() + case 0xFFFF: + self.holding_registers.values[task_control_reg_addr+1] = 0x0000 + + # time.sleep(0.01) + nowtime = time.time() + # print(f"{timestamp}, {nowtime}") + if (nowtime-timestamp) > 1: + timestamp = nowtime + #将数据写入influxdb + self._write_to_influxdb() + # self._read_plc_datas() + self._update_modbus_datas() + + # 周期性保存运行时间(仅在运行状态且时间有变化时保存) + if self.taskInfo['status'] == 0x5555: + if (nowtime - last_save_time) >= SAVE_INTERVAL: + if self.taskInfo['running_time'] != last_running_time: + self._save_task_running_time(self.taskInfo['running_time']) + last_running_time = self.taskInfo['running_time'] + last_save_time = nowtime + + # 控制寄存器 + + +if __name__ == "__main__": + gateway = ModbusGateway() + gateway.run() \ No newline at end of file