diff --git a/docs/en/10-third-party/03-visual/02-perspective.md b/docs/en/10-third-party/03-visual/02-perspective.md index abb2baae23..056abed8f6 100644 --- a/docs/en/10-third-party/03-visual/02-perspective.md +++ b/docs/en/10-third-party/03-visual/02-perspective.md @@ -19,7 +19,7 @@ Perform the following installation operations in the Linux system: - Python version 3.10 or higher has been installed (if not installed, please refer to [Python Installation](https://docs.python.org/)). - Download or clone the [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) project. After entering the root directory of the project, run the "install.sh" script to download and install the TDengine client library and related dependencies locally. -## Data Analysis +## Visualize data **Step 1**, Run the "run.sh" script in the root directory of the [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) project to start the Perspective service. This service will retrieve data from the TDengine database every 300 milliseconds and transmit the data in a streaming form to the web-based `Perspective Viewer`. @@ -33,6 +33,8 @@ sh run.sh python -m http.server 8081 ``` +The effect presented after accessing the web page through the browser is shown in the following figure: + ![perspective-viewer](./perspective/prsp_view.webp) ## Instructions for use @@ -56,40 +58,9 @@ The `perspective_server.py` script in the root directory of the [perspective-con 3. Create a Perspective table (the table structure needs to match the type of the table in the TDengine database). 4. Call the `Tornado.PeriodicCallback` function to start a scheduled task, thereby achieving the update of the data in the Perspective table. The sample code is as follows: - ```python - def perspective_thread(perspective_server: perspective.Server, tdengine_conn: taosws.Connection): - """ - Create a new Perspective table and update it with new data every 50ms - """ - # create a new Perspective table - client = perspective_server.new_local_client() - schema = { - "timestamp": datetime, - "location": str, - "groupid": int, - "current": float, - "voltage": int, - "phase": float, - } - # define the table schema - table = client.table( - schema, - limit=1000, # maximum number of rows in the table - name=PERSPECTIVE_TABLE_NAME, # table name. Use this with perspective-viewer on the client side - ) - logger.info("Created new Perspective table") - - # update with new data - def updater(): - data = read_tdengine(tdengine_conn) - table.update(data) - logger.debug(f"Updated Perspective table: {len(data)} rows") - - logger.info(f"Starting tornado ioloop update loop every {PERSPECTIVE_REFRESH_RATE} milliseconds") - # start the periodic callback to update the table data - callback = tornado.ioloop.PeriodicCallback(callback=updater, callback_time=PERSPECTIVE_REFRESH_RATE) - callback.start() - ``` +```python +{{#include docs/examples/perspective/perspective_server.py:perspective_server}} +``` ### HTML Page Configuration @@ -100,77 +71,7 @@ The `prsp-viewer.html` file in the root directory of the [perspective-connect-de - Import the Perspective library, connect to the Perspective server via a WebSocket, and load the `meters_values` table to display dynamic data. ```html - - - -
-
- -
-
+{{#include docs/examples/perspective/prsp-viewer.html:perspective_viewer}} ``` ## Reference Materials diff --git a/docs/examples/perspective/perspective_server.py b/docs/examples/perspective/perspective_server.py new file mode 100644 index 0000000000..919828379e --- /dev/null +++ b/docs/examples/perspective/perspective_server.py @@ -0,0 +1,207 @@ +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +# ┃ Copyright (c) 2017, the Perspective Authors. ┃ +# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +# ┃ This file is part of the Perspective library, distributed under the terms ┃ +# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import logging +import tornado.websocket +import tornado.web +import tornado.ioloop +from datetime import date, datetime +import perspective +import perspective.handlers.tornado +import json +import taosws + + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger('main') + + +# ============================================================================= +# TDengine connection parameters +# ============================================================================= +TAOS_HOST = "localhost" # TDengine server host +TAOS_PORT = 6041 # TDengine server port +TAOS_USER = "root" # TDengine username +TAOS_PASSWORD = "taosdata" # TDengine password + +TAOS_DATABASE = "power" # TDengine database name +TAOS_TABLENAME = "meters" # TDengine table name + +# ============================================================================= +# Perspective server parameters +# ============================================================================= +PERSPECTIVE_TABLE_NAME = "meters_values" # name of the Perspective table +PERSPECTIVE_REFRESH_RATE = 250 # refresh rate in milliseconds + + +class CustomJSONEncoder(json.JSONEncoder): + """ + Custom JSON encoder that serializes datetime and date objects + """ + def default(self, obj): + if isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, date): + return obj.isoformat() + return super().default(obj) + + +json.JSONEncoder.default = CustomJSONEncoder().default + + +def convert_ts(ts) -> datetime: + """ + Convert a timestamp string to a datetime object + """ + for fmt in ('%Y-%m-%d %H:%M:%S.%f %z', '%Y-%m-%d %H:%M:%S %z'): + try: + return datetime.strptime(ts, fmt) + except ValueError: + continue + raise ValueError(f"Time data '{ts}' does not match any format") + + +def create_tdengine_connection( + host: str = TAOS_HOST, + port: int = TAOS_PORT, + user: str = TAOS_USER, + password: str = TAOS_PASSWORD, + ) -> taosws.Connection: + try: + # connect to the tdengine server + conn = taosws.connect( + user=user, + password=password, + host=host, + port=port, + ) + # switch to the right database + conn.execute(f"USE {TAOS_DATABASE}") + # connection successful + logger.info(f"Connected to tdengine successfully: {host}:{port}") + return conn + except Exception as err: + logger.error(f"Failed to connect to tdengine: {host}:{port} -- ErrMessage: {err}") + raise err + + +def read_tdengine( + conn: taosws.Connection, + ) -> list[dict]: + try: + # query the database + sql = f""" + SELECT `ts`, location, groupid, current, voltage, phase + FROM {TAOS_TABLENAME} + WHERE `ts` >= NOW() - 12h + ORDER BY `ts` DESC + LIMIT 1000 + """ + logger.debug(f"Executing query: {sql}") + res = conn.query(sql) + data = [ + { + "timestamp": convert_ts(row[0]), + "location": row[1], + "groupid": row[2], + "current": row[3], + "voltage": row[4], + "phase": row[5], + } + for row in res + ] + logger.info(f"select result: {data}") + return data + except Exception as err: + logger.error(f"Failed to query tdengine: {err}") + raise err + + +// ANCHOR: perspective_server +def perspective_thread(perspective_server: perspective.Server, tdengine_conn: taosws.Connection): + """ + Create a new Perspective table and update it with new data every 50ms + """ + # create a new Perspective table + client = perspective_server.new_local_client() + schema = { + "timestamp": datetime, + "location": str, + "groupid": int, + "current": float, + "voltage": int, + "phase": float, + } + # define the table schema + table = client.table( + schema, + limit=1000, # maximum number of rows in the table + name=PERSPECTIVE_TABLE_NAME, # table name. Use this with perspective-viewer on the client side + ) + logger.info("Created new Perspective table") + + # update with new data + def updater(): + data = read_tdengine(tdengine_conn) + table.update(data) + logger.debug(f"Updated Perspective table: {len(data)} rows") + + logger.info(f"Starting tornado ioloop update loop every {PERSPECTIVE_REFRESH_RATE} milliseconds") + # start the periodic callback to update the table data + callback = tornado.ioloop.PeriodicCallback(callback=updater, callback_time=PERSPECTIVE_REFRESH_RATE) + callback.start() + +// ANCHOR_END: perspective_server + +def make_app(perspective_server): + """ + Create a new Tornado application with a websocket handler that + serves a Perspective table. PerspectiveTornadoHandler handles + the websocket connection and streams the Perspective table changes + to the client. + """ + return tornado.web.Application([ + ( + r"/websocket", # websocket endpoint. Use this URL to configure the websocket client OR Prospective Server adapter + perspective.handlers.tornado.PerspectiveTornadoHandler, # PerspectiveTornadoHandler handles perspective table updates <-> websocket client + {"perspective_server": perspective_server}, # pass the perspective server to the handler + ), + ]) + + +if __name__ == "__main__": + logger.info("TDEngine <-> Perspective Demo") + + # create a new Perspective server + logger.info("Creating new Perspective server") + perspective_server = perspective.Server() + # create the tdengine connection + logger.info("Creating new TDEngine connection") + tdengine_conn = create_tdengine_connection() + + # setup and start the Tornado app + logger.info("Creating Tornado server") + app = make_app(perspective_server) + app.listen(8085, address='0.0.0.0') + logger.info("Listening on http://localhost:8080") + + try: + # start the io loop + logger.info("Starting ioloop to update Perspective table data via tornado websocket...") + loop = tornado.ioloop.IOLoop.current() + loop.call_later(0, perspective_thread, perspective_server, tdengine_conn) + loop.start() + except KeyboardInterrupt: + logger.warning("Keyboard interrupt detected. Shutting down tornado server...") + loop.stop() + loop.close() + logging.info("Shut down") diff --git a/docs/examples/perspective/prsp-viewer.html b/docs/examples/perspective/prsp-viewer.html new file mode 100644 index 0000000000..e6b1a6e734 --- /dev/null +++ b/docs/examples/perspective/prsp-viewer.html @@ -0,0 +1,135 @@ + + + + + + + Perspective Viewer Dashboard + + + + + + + + + + + +// ANCHOR: perspective_viewer + + + +
+
+ +
+
+// ANCHOR_END: perspective_viewer + + + \ No newline at end of file diff --git a/docs/zh/10-third-party/03-visual/02-perspective.mdx b/docs/zh/10-third-party/03-visual/02-perspective.mdx index 8e92e7209b..1db8474570 100644 --- a/docs/zh/10-third-party/03-visual/02-perspective.mdx +++ b/docs/zh/10-third-party/03-visual/02-perspective.mdx @@ -22,7 +22,7 @@ Perspective 是一款开源且强大的数据可视化库,由 [Prospective.co] - Python 3.10 及以上版本已安装(如未安装,可参考 [Python 安装](https://docs.python.org/)。 - 下载或克隆 [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) 项目,进入项目根目录后运行 “install.sh” 脚本,以便在本地下载并安装 TDengine 客户端库以及相关的依赖项。 -## 数据分析 +## 可视化数据 **第 1 步**,运行 [perspective-connect-demo](https://github.com/taosdata/perspective-connect-demo) 项目根目录中的 “run.sh” 脚本,以此启动 Perspective 服务。该服务会每隔 300 毫秒从 TDengine 数据库中获取一次数据,并将数据以流的形式传输至基于 Web 的 `Perspective Viewer` 。 @@ -36,6 +36,8 @@ sh run.sh python -m http.server 8081 ``` +通过浏览器访问该 Web 页面后所呈现出的效果如下图所示: + ![perspective-viewer](./perspective/prsp_view.webp) ## 使用说明 @@ -59,40 +61,9 @@ Python 连接器详细写入说明可参见 [Python 参数绑定](../../../refer 1. 创建一个 Perspective 表(表结构需要与 TDengine 数据库中表的类型保持匹配)。 1. 调用 `Tornado.PeriodicCallback` 函数来启动定时任务,进而实现对 Perspective 表数据的更新,示例代码如下: - ```python - def perspective_thread(perspective_server: perspective.Server, tdengine_conn: taosws.Connection): - """ - Create a new Perspective table and update it with new data every 50ms - """ - # create a new Perspective table - client = perspective_server.new_local_client() - schema = { - "timestamp": datetime, - "location": str, - "groupid": int, - "current": float, - "voltage": int, - "phase": float, - } - # define the table schema - table = client.table( - schema, - limit=1000, # maximum number of rows in the table - name=PERSPECTIVE_TABLE_NAME, # table name. Use this with perspective-viewer on the client side - ) - logger.info("Created new Perspective table") - - # update with new data - def updater(): - data = read_tdengine(tdengine_conn) - table.update(data) - logger.debug(f"Updated Perspective table: {len(data)} rows") - - logger.info(f"Starting tornado ioloop update loop every {PERSPECTIVE_REFRESH_RATE} milliseconds") - # start the periodic callback to update the table data - callback = tornado.ioloop.PeriodicCallback(callback=updater, callback_time=PERSPECTIVE_REFRESH_RATE) - callback.start() - ``` +```python +{{#include docs/examples/perspective/perspective_server.py:perspective_server}} +``` ### HTML 页面配置 @@ -103,80 +74,7 @@ Python 连接器详细写入说明可参见 [Python 参数绑定](../../../refer - 引入 Perspective 库,通过 WebSocket 连接到 Perspective 服务器,加载 meters_values 表来展示动态数据。 ```html - - - - -
-
- -
-
+{{#include docs/examples/perspective/prsp-viewer.html:perspective_viewer}} ``` ## 参考资料