Bläddra i källkod

Merge branch 'dev' of http://124.70.149.18:10880/lianghj/queenshow into dev

qinyan 1 år sedan
förälder
incheckning
a1180b9f57

+ 3 - 1
.eslintrc.js

@@ -22,7 +22,9 @@ module.exports = {
     "@typescript-eslint/no-namespace": "off",
     "@typescript-eslint/no-this-alias": "off",
     "prefer-const": "off",
-    "@typescript-eslint/adjacent-overload-signatures":"off"
+    "@typescript-eslint/adjacent-overload-signatures":"off",
+    "no-empty": "off",
+    "no-fallthrough": "off"
   },
   overrides: [
     {

+ 1 - 0
package.json

@@ -61,6 +61,7 @@
     "file-saver": "^2.0.5",
     "hotkeys-js": "^3.10.2",
     "howler": "^2.2.3",
+    "js-base64": "^3.7.5",
     "jszip": "^3.10.1",
     "load-asset": "^1.2.0",
     "lodash": "^4.17.21",

+ 77 - 0
src/comm/controllers/appMsgQueen5Ctrl.ts

@@ -0,0 +1,77 @@
+/**
+ * 
+ * 当前应用queen5接受的消息处理。
+ *   目前文件依赖 设计为 当前文件 依赖于modules/ediotor/controllers下的文件,这样避免share页面打包
+ *   当前模块,导致nats.ws文件被引用,引发 部分手机 share页面打不开的bug
+ */
+
+
+import { EditorModule } from "@/modules/editor/module";
+import { Controller } from "../core/controller";
+import { useCtx } from "../ctx";
+import { queenApi } from "queenjs";
+import { CompImageObj } from "@/modules/editor/components/CompUI/basicUI/Image2";
+export class AppMsgQueen5Controller extends Controller {
+
+    editorModule?:EditorModule;
+
+    get editor() {
+        //使用的时候确定当前模块内容已经被设定
+        return this.editorModule as EditorModule;
+    }
+
+    async onReady() {
+        this.ResetEditor();
+    }
+
+    Clear() {
+        const { recvCtrl } = useCtx();
+        recvCtrl.clearListeners()
+        recvCtrl.emitChange();
+    }
+
+    async uploadImage(url:string) {
+
+        const response = await fetch(url)
+        const blob = await response.blob();
+        const file = await this.editor.controls.uploader.uploadFile(blob, "queenshow");
+        return file.url;
+    }
+
+    //重置状态    
+    ResetEditor() {
+        const { recvCtrl } = useCtx();
+        recvCtrl.clearListeners()
+        recvCtrl.addImageListener("添加图片", async (uri:string)=>{
+            console.log("添加图片=>", uri);
+
+            queenApi.showLoading("添加图片中")
+            const url = await this.uploadImage(uri)
+            await this.editor.actions.addImageToDesign(url);
+            queenApi.hideLoading();
+
+            return true;
+        })
+        recvCtrl.addImageListener("替换当前图片", async (uri:string)=>{
+            console.log("替换当前图片=>", uri);
+
+            if (this.editor.controls.selectCtrl.gizmo.selectedIds.length != 1) {
+                queenApi.messageError("当前没有选中的图片");
+                return true;
+            }
+
+            queenApi.showLoading("图片替换中")
+            const url = await this.uploadImage(uri) as string;
+
+            const comp = this.editor.controls.pageCtrl.currComp  as CompImageObj;
+            comp.value.url = url;
+            comp.value.x = 0;
+            comp.value.y = 0;
+            comp.value.s = 1;
+            comp.value.matrix = "";
+            queenApi.hideLoading();
+            return true;
+        })
+        recvCtrl.emitChange();
+    }
+}

+ 83 - 0
src/comm/controllers/appMsgRecvCtrl.ts

@@ -0,0 +1,83 @@
+/**
+ * 负责app之间的消息的接收模块
+ */
+import { nanoid } from "nanoid";
+import { Controller } from "../core/controller";
+import { useCtx } from "../ctx";
+
+export type AssetSendedCallback = (assetUri:string)=>Promise<boolean>;
+export type AssetType = "empty" | "image"
+const RevcChangeEvent = "app.recv.change"
+class AssetListener {
+    id = "";
+    type = "empty" as AssetType;
+    constructor(public actionName:string, public callback: AssetSendedCallback){
+        this.id = actionName;
+    }
+    
+    toJson() {
+        return {
+            id: this.id,
+            type: this.type,
+            action: this.actionName,
+        }
+    }
+}
+
+
+export class AppMsgRecvController extends Controller {
+
+    listeners = [] as AssetListener[];
+    appGuid = "";
+
+    async onReady() {
+        const {deviceCtrl, natsCtrl} = useCtx();
+
+        this.appGuid = deviceCtrl.profile.appGuid;
+
+        natsCtrl.subscribe(`send.${this.appGuid}`, async (msg:{id:string,fromKey:string, uri:string,name?:string, thumbnail?:string})=>{
+            const listen = this.listeners.find(item=>item.id == msg.id)
+            if (!listen) return {
+                isOk: false,
+                error: "nolistener"
+            }
+            const ok = await listen.callback(msg.uri);
+            return JSON.stringify({ isOk: ok})
+        })
+
+        natsCtrl.subscribe(`recv.actions.${this.appGuid}`, async ()=>{
+            return JSON.stringify(this.listeners.map(item=>item.toJson()))
+        })
+    }
+
+
+    //清除所有监听者
+    clearListeners() {
+        this.listeners = [];
+    }
+
+    emitChange() {
+        const {natsCtrl} = useCtx();
+        natsCtrl.publish(RevcChangeEvent, JSON.stringify({Guid: this.appGuid}))
+    }
+
+    //添加图片监听者
+    addImageListener(actionName:string, handle: AssetSendedCallback) {
+        let listen = this.listeners.find(item=>item.actionName == actionName);
+        if (listen)  {
+            listen.callback = handle;
+            return listen.id;
+        }
+        listen = new AssetListener(actionName, handle)
+        listen.type = "image";
+        this.listeners.push(listen);
+    }
+
+    removeListener(id:string) {
+        let listen = this.listeners.find(item=>item.id == id);
+        if (!listen)  {
+            return;
+        }
+        this.listeners.splice(this.listeners.indexOf(listen), 1);
+    }
+}

+ 9 - 0
src/comm/controllers/appMsgSendCtrl.ts

@@ -0,0 +1,9 @@
+/**
+ * 应用间消息发送模块
+ */
+
+import { Controller } from "../core/controller";
+
+export class AppMsgSendController extends Controller {
+
+}

+ 25 - 0
src/comm/controllers/cmdsvcCtrl.ts

@@ -0,0 +1,25 @@
+import { queenApi } from "queenjs";
+import { connect, StringCodec, Empty, ErrorCode } from "nats.ws";
+import { Controller } from "../core/controller";
+import { useCtx } from "../ctx";
+import { reactive } from "vue";
+
+export class CmdSvcController extends Controller {
+  
+    state = reactive({
+        list: [] as {f:string,m:string}[]
+    })
+
+    async onReady() {
+      const {natsCtrl} = useCtx();
+      const params = new URLSearchParams(decodeURIComponent(location.search));
+      const guid = params.get("guid");
+
+      const state = this.state;
+      console.log("substring", `${guid}.cmdmsg`)
+      natsCtrl.subscribe(`${guid}.cmdmsg`, (msg:any)=>{
+        console.log("cmdMsg=>", msg);
+        state.list.push(msg);
+      }, {queue: "cmdsvc"})
+    }
+}

+ 605 - 0
src/comm/controllers/deviceCtrl.ts

@@ -0,0 +1,605 @@
+import {NormMsg, decodeNormMsg, encodeNormMsg} from "./entity/message";
+import { Controller } from "../core/controller";
+//@ts-ignore
+import { saveAs } from "file-saver";
+
+import {Base64 } from "js-base64"
+type UploadItem = {
+  id: string;
+  fpath: string;
+  dir: string;
+  name?: string;
+  url?: string;
+  size?: number;
+};
+
+export type FileInfo = {
+  Fpath: string;
+  Size: number;
+  CreateAt: number; //info.ModTime().Unix()
+};
+
+export class DeviceController extends Controller {
+  ipc: any;
+
+  profile = {wsPort: "", appGuid: ""}
+
+  isEnvOk = false;
+
+  constructor() {
+    super();
+
+    //@ts-ignore
+    if (window.ipc) {
+      //@ts-ignore
+      this.ipc = window.ipc;
+      this.isEnvOk = true;
+
+    } else {
+      this.ipc = {
+        emit(name: string, ...args: any[]) {
+          console.error("emit msg=>", name, args);
+        },
+        on(name: string) {
+          console.error("on message", name);
+        },
+      };
+    }
+    this.initEvent();
+  }
+  _conn?: WebSocket;
+
+  initEvent() {
+    const scope = this;
+    this.ipc.on("OnDragEnter", function (files: string[]) {
+      scope.emit("onDragEnter", files);
+    });
+    this.ipc.on("downloadsucc", (fpath:string, size:number)=>{
+       scope.emit("downloadsucc", fpath, size);
+    })
+
+    this.ipc.emit("GetSocketUri", [], (uri:string) => {
+      let conn = new WebSocket(uri);
+      conn.onopen = (e)=>{
+        console.log("bus socket connected!!!")
+      }
+      conn.onclose =  (evt)=>{
+        console.log("app bus socket conn closed", evt);
+
+        setTimeout(() => {
+          this._conn = new WebSocket(uri);
+        }, 1000);
+      };
+      conn.onmessage = function (evt) {
+        if (!evt.data || evt.data == "") return;
+        
+        try {
+          const blob = evt.data as Blob;
+          blob.arrayBuffer().then(buff=>{
+            const msg = decodeNormMsg(new Uint8Array(buff))
+            scope.emit("on" + msg.sub, msg);
+          })
+        } catch (error) {
+          console.error(evt.data, typeof evt.data,  error);
+        }
+      };
+      this._conn = conn;
+    });
+
+  }
+
+  async onReady() {
+     if (!this.isEnvOk) return;
+
+     this.profile = await this.GetLocalAppProfile() ||  {wsPort:"", appGuid: ""};
+
+     console.log("current app profile=>", this.profile);
+  }
+  
+  //选择磁盘文件夹
+  SelectDir(): Promise<string> {
+    const sid = Date.now();
+    const cbname = "c" + sid;
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("SelectDir", [sid], function (ok: boolean) {
+        console.log("call=>", ok);
+        if (!ok) {
+          r("");
+        }
+        ipc.on(cbname, function (dir: string) {
+          r(dir);
+        });
+      });
+    });
+  }
+  IsDirEmpty(dir: string): Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("IsDirEmpty", [dir], function (ok: boolean) {
+        r(ok);
+      });
+    });
+  }
+  IsFileExit(fpath: string): Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("IsFileExit", [fpath], function (ok: boolean) {
+        r(ok);
+      });
+    });
+  }
+
+  WriteFileText(dir: string, content: string): Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("WriteFileText", [dir, content], function (ok: boolean) {
+        r(ok);
+      });
+    });
+  }
+
+  ReadFileText(dir: string): Promise<{ error: string; text: string }> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("ReadFileText", [dir], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  //拷贝文件
+  CopyFile(srcFile: string, targetFile: string): Promise<boolean> {
+    const sid = Date.now();
+    const cbname = "copy" + sid;
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.on(cbname, function (ok:boolean) {
+        r(ok);
+      });
+      ipc.emit("CopyFile", [cbname, srcFile, targetFile]);
+    });
+  }
+
+  //保存应用级的配置
+  SaveSysConfigItem(key: string, value: string): Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("SaveAppConfigItem", [key, value], function (data: any) {
+        r(data);
+      });
+    });
+  }
+  GetSysConfigItem(key: string): Promise<{ error: string; text: string }> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("GetAppConfigItem", [key], function (data: any) {
+        r(data);
+      });
+    });
+  }
+  RemoveSysConfigItem(key: string): Promise<{ error: string; text: string }> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("RemoveSysConfigItem", [key], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  //获取应用的baseUrl
+  GetSysDataBaseUrl() {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("GetAppDataBaseUrl", [], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  //获取应用的baseUrl
+  CopyFileToAppData(src: string, targetRelativePath: string): Promise<string> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit(
+        "CopyFileToAppData",
+        [src, targetRelativePath],
+        function (data: any) {
+          r(data);
+        }
+      );
+    });
+  }
+
+  RemoveAppData(targetRelativePath: string): Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("RemoveAppData", [targetRelativePath], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  //打开文件所在的目录
+  OpenDir(dir: string): Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("OpenDir", [dir], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  SelectOneFilePath(title: string, filters: string): Promise<string> {
+    const sid = Date.now();
+
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      const cbname = "c" + sid;
+      ipc.emit("SelectOneFilePath", [sid, title, filters], function (ok:boolean) {
+        if (!ok) {
+          r("");
+        }
+        ipc.on(cbname, function (dir: string) {
+          r(dir);
+        });
+      });
+    });
+  }
+
+  //开启projects 的httpserver
+  StartHttpServer(prjFile: string): Promise<string> {
+    const sid = Date.now();
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("StartProjectHttpServer", [prjFile], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  //下在线文件 默认超时时间20分钟
+  DownloadFile(
+    url: string,
+    fpath: string,
+    timeoutSecend: number = 60 * 20,
+    cb?: (event: "start" | "progress" | "error" | "succ", p1: NormMsg) => void
+  ): Promise<boolean> {
+    const sid = "d" + Date.now();
+    return new Promise((r) => {
+
+        console.log(url,  "==>", fpath)
+      const cancel = this.OnMsg(sid, (data) => {
+       
+        cb && cb(data.type as any,  data);
+        if (data.type == "succ") {
+          r(true);
+          cancel.unbind();
+          return;
+        }
+        if (data.type == "error") {
+          r(false);
+          cancel.unbind();
+        }
+      });
+
+      const ipc = this.ipc;
+      ipc.emit(
+        "DownloadFile",
+        [sid, fpath, url, timeoutSecend],
+        function (err: string) {
+          console.log("DownloadFile callback");
+          if (err) {
+            console.error(err);
+            r(false);
+          }
+        }
+      );
+    });
+  }
+
+  Unzip(fpath:string, distDir:string, cb?: (event: "progress" | "error" | "succ", p1: NormMsg) => void) {
+    const sid = "unzip" + Date.now();
+
+    return new Promise((r) => {
+
+      const cancel = this.OnMsg(sid, (data) => {
+        cb && cb(data.type as any,  data);
+        if (data.type == "succ") {
+          r(true);
+          cancel.unbind();
+          return;
+        }
+        if (data.type == "error") {
+          r(false);
+          cancel.unbind();
+        }
+      });
+      const ipc = this.ipc;
+      ipc.emit("Unzip", [sid, fpath, distDir]);
+    });
+  }
+
+  //获取文件夹的大小
+  //文件目录不要太大,不然会卡死程序
+  GetDirSize(dir: string): Promise<number> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("GetDirSize", [dir], function (size: number) {
+        r(size);
+      });
+    });
+  }
+
+  //获取系统数据目录
+  GetAppDataDir(): Promise<string> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("GetAppDataDir", [], function (dir: string) {
+        r(dir);
+      });
+    });
+  }
+
+  OpenAssetWindow(projectPath: string) {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit(
+        "openAssetsWindow",
+        [
+          `${location.host}/library.html?path=${projectPath}`,
+          "资源管理库",
+          1280,
+          960,
+        ],
+        function (ok:boolean) {
+          r(ok);
+        }
+      );
+    });
+  }
+
+  SendMsg(subject: string, msg: string) {
+    const conn = this._conn;
+    if (!conn) {
+      console.error("应用socket异常");
+      return;
+    }
+    conn.send(encodeNormMsg({sub: subject, msg: msg}));
+  }
+
+  OnMsg(subject: string, cb: (msg: NormMsg) => any) {
+    return this.on("on" + subject, cb);
+  }
+
+  //同步阻塞当前进程
+  UploadSync(files: UploadItem[]): Promise<UploadItem[]> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("UploadSync", [files], function (ret:any) {
+        r(ret);
+      });
+    });
+  }
+
+  //异步上传不会阻塞当前进程
+  UploadASync(files: UploadItem[]): Promise<UploadItem[]> {
+    const sid = "u" + Date.now();
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("UploadASync", [sid, files], function (ok:boolean) {
+        if (!ok) {
+          r([]);
+          return;
+        }
+        ipc.on(sid, (items:any) => {
+          r(items);
+        });
+      });
+    });
+  }
+
+  //异步上传不会阻塞当前进程
+  UploadDir(dir: string, targetDir: string): Promise<UploadItem[]> {
+    const sid = "u" + Date.now();
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("UploadDir", [sid, dir, targetDir], function (ok:any) {
+        if (!ok) {
+          r([]);
+          return;
+        }
+        ipc.on(sid, (items:any) => {
+          r(items);
+        });
+      });
+    });
+  }
+
+  GetFilesInDir(dir:string, filter:string): Promise<FileInfo[]>{
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("GetFilesInDir", [ dir, filter], function (ret:any) {
+        r(ret);
+      });
+    });
+  }
+
+  GetSubDirNames(dir:string):Promise<string[]>{ 
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("GetSubDirNames", [ dir], function (ret:any) {
+        r(ret);
+      });
+    });
+  }
+
+  RemoveFile(fpath: string) : Promise<boolean>{
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("RemoveFile", [fpath], function (ok:boolean) {
+        r(ok);
+      });
+    });
+  }
+
+  GetSaveFile(title:string, filter= "图片(*.png;*.jpg)") :Promise<string> {
+    const sid = "sf"+Date.now();
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("GetSaveFile", [sid, title, filter], function (ok:boolean) {
+         if(ok) {
+          ipc.on(sid, (ret:any)=>{
+              r(ret);
+          })
+          return
+         }
+         r("");
+      });
+    });
+  }
+
+  OpenOneFile(title:string, filter= "图片(*.png;*.jpg)") :Promise<string> {
+    const sid = "sf"+Date.now();
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("OpenOneFile", [sid, title, filter], function (file:any) {
+        setTimeout(() => {
+          r(file);
+        }, 0);
+      });
+    });
+  }
+
+  GetImageMeta(fpath:string): Promise<{Width: number, Height: number, Size:number}> {
+
+   
+    return new Promise((r) => {
+
+      const img = new Image();
+      img.onload=()=>{
+        r({Width: img.width, Height: img.height, Size: 0})
+      }
+      img.src = fpath;
+
+      // const ipc = this.ipc;
+      // ipc.emit("GetImageMeta", [fpath], function (meta) {
+      //     r(meta);
+      // });
+    });
+  }
+  
+  SetMainTitle(title :string):Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("SetMainTitle", [title], function (ok:any) {
+         r(ok)
+      });
+    });
+  }
+
+  SaveFile(fpath: string, buff:any) :Promise<string>{
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("SaveFile", [fpath, Array.from(buff)], function (err:string) {
+         r(err)
+      });
+    });
+  }
+  
+  OpenQueen5(url: string, title:any) :Promise<boolean>{
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("OpenQueen5", [url, title], function (err:any) {
+         r(err)
+      });
+    });
+  }
+  OpenQueen5Play(url: string, title:any) :Promise<boolean>{
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("OpenQueen5Play", [url, title], function (err:any) {
+         r(err)
+      });
+    });
+  }
+
+  SaveBlobFile(url:string, fbase64path:string) :Promise<boolean> {
+    console.log("save blob to ", fbase64path);
+
+    return new Promise((r) => {
+      const cancel = this.on("downloadsucc", (dpath:string)=>{
+        console.log("fpath=>", dpath)
+        if (fbase64path ==  Base64.encode(dpath)) {
+          cancel.unbind();
+          r(true)
+        }
+      })
+      saveAs(url, fbase64path);
+    });
+  }
+
+  SaveClipboard(blob:Blob) {
+   return navigator.clipboard.write([
+      new ClipboardItem({
+        "image/png": blob
+      })
+    ])
+  }
+
+  GetNatsProfile(): Promise<{ apiPort: string, wsPort: string , ip:string}> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("NatsProfile", [], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  GetLocalAppProfile(): Promise<{wsPort:string, appGuid:string}> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("LocalAppProfile", [], function (data: any) {
+        r(data || {wsPort:"", appGuid: ""});
+      });
+    });
+  }
+
+  OpenWeb(url:string, title:string, width = 1280, height = 720) :Promise<boolean>{
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("OpenWeb", [url, title, width, height], function (data: any) {
+        r(data);
+      });
+    });
+  }
+
+  RunNativeApp(guid:string, entry:string, params:string[]) :Promise<string> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("RunNativeApp", [guid, entry, params], function (err: string) {
+        console.log("xxxx", err)
+        r(err);
+      });
+    });
+  }
+
+  RunCmdSvcApp(guid:string, webUrl:string, entry:string, params:string[]) :Promise<string> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("RunCmdSvcApp", [guid, webUrl, entry, params], function (err: string) {
+        console.log("xxxx", err)
+        r(err);
+      });
+    });
+  }
+
+  StopNativeApp(guid:string) :Promise<boolean> {
+    return new Promise((r) => {
+      const ipc = this.ipc;
+      ipc.emit("StopNativeApp", [guid], function (ok: boolean) {
+        r(ok);
+      });
+    });
+  }
+}

+ 637 - 0
src/comm/controllers/entity/message.ts

@@ -0,0 +1,637 @@
+export interface NormMsg {
+  sub?: string;
+  msg?: string;
+  error?: string;
+  type?: string;
+  fva11?: number;
+  fva12?: number;
+  iva11?: number;
+  iva12?: number;
+}
+
+export function encodeNormMsg(message: NormMsg): Uint8Array {
+  let bb = popByteBuffer();
+  _encodeNormMsg(message, bb);
+  return toUint8Array(bb);
+}
+
+function _encodeNormMsg(message: NormMsg, bb: ByteBuffer): void {
+  // optional string sub = 1;
+  let $sub = message.sub;
+  if ($sub !== undefined) {
+    writeVarint32(bb, 10);
+    writeString(bb, $sub);
+  }
+
+  // optional string msg = 2;
+  let $msg = message.msg;
+  if ($msg !== undefined) {
+    writeVarint32(bb, 18);
+    writeString(bb, $msg);
+  }
+
+  // optional string error = 3;
+  let $error = message.error;
+  if ($error !== undefined) {
+    writeVarint32(bb, 26);
+    writeString(bb, $error);
+  }
+
+  // optional string type = 4;
+  let $type = message.type;
+  if ($type !== undefined) {
+    writeVarint32(bb, 34);
+    writeString(bb, $type);
+  }
+
+  // optional float fva11 = 5;
+  let $fva11 = message.fva11;
+  if ($fva11 !== undefined) {
+    writeVarint32(bb, 45);
+    writeFloat(bb, $fva11);
+  }
+
+  // optional float fva12 = 6;
+  let $fva12 = message.fva12;
+  if ($fva12 !== undefined) {
+    writeVarint32(bb, 53);
+    writeFloat(bb, $fva12);
+  }
+
+  // optional int32 iva11 = 7;
+  let $iva11 = message.iva11;
+  if ($iva11 !== undefined) {
+    writeVarint32(bb, 56);
+    writeVarint64(bb, intToLong($iva11));
+  }
+
+  // optional int32 iva12 = 8;
+  let $iva12 = message.iva12;
+  if ($iva12 !== undefined) {
+    writeVarint32(bb, 64);
+    writeVarint64(bb, intToLong($iva12));
+  }
+}
+
+export function decodeNormMsg(binary: Uint8Array): NormMsg {
+  return _decodeNormMsg(wrapByteBuffer(binary));
+}
+
+function _decodeNormMsg(bb: ByteBuffer): NormMsg {
+  let message: NormMsg = {} as any;
+
+  end_of_message: while (!isAtEnd(bb)) {
+    let tag = readVarint32(bb);
+
+    switch (tag >>> 3) {
+      case 0:
+        break end_of_message;
+
+      // optional string sub = 1;
+      case 1: {
+        message.sub = readString(bb, readVarint32(bb));
+        break;
+      }
+
+      // optional string msg = 2;
+      case 2: {
+        message.msg = readString(bb, readVarint32(bb));
+        break;
+      }
+
+      // optional string error = 3;
+      case 3: {
+        message.error = readString(bb, readVarint32(bb));
+        break;
+      }
+
+      // optional string type = 4;
+      case 4: {
+        message.type = readString(bb, readVarint32(bb));
+        break;
+      }
+
+      // optional float fva11 = 5;
+      case 5: {
+        message.fva11 = readFloat(bb);
+        break;
+      }
+
+      // optional float fva12 = 6;
+      case 6: {
+        message.fva12 = readFloat(bb);
+        break;
+      }
+
+      // optional int32 iva11 = 7;
+      case 7: {
+        message.iva11 = readVarint32(bb);
+        break;
+      }
+
+      // optional int32 iva12 = 8;
+      case 8: {
+        message.iva12 = readVarint32(bb);
+        break;
+      }
+
+      default:
+        skipUnknownField(bb, tag & 7);
+    }
+  }
+
+  return message;
+}
+
+export interface Long {
+  low: number;
+  high: number;
+  unsigned: boolean;
+}
+
+interface ByteBuffer {
+  bytes: Uint8Array;
+  offset: number;
+  limit: number;
+}
+
+function pushTemporaryLength(bb: ByteBuffer): number {
+  let length = readVarint32(bb);
+  let limit = bb.limit;
+  bb.limit = bb.offset + length;
+  return limit;
+}
+
+function skipUnknownField(bb: ByteBuffer, type: number): void {
+  switch (type) {
+    case 0: while (readByte(bb) & 0x80) { } break;
+    case 2: skip(bb, readVarint32(bb)); break;
+    case 5: skip(bb, 4); break;
+    case 1: skip(bb, 8); break;
+    default: throw new Error("Unimplemented type: " + type);
+  }
+}
+
+function stringToLong(value: string): Long {
+  return {
+    low: value.charCodeAt(0) | (value.charCodeAt(1) << 16),
+    high: value.charCodeAt(2) | (value.charCodeAt(3) << 16),
+    unsigned: false,
+  };
+}
+
+function longToString(value: Long): string {
+  let low = value.low;
+  let high = value.high;
+  return String.fromCharCode(
+    low & 0xFFFF,
+    low >>> 16,
+    high & 0xFFFF,
+    high >>> 16);
+}
+
+// The code below was modified from https://github.com/protobufjs/bytebuffer.js
+// which is under the Apache License 2.0.
+
+let f32 = new Float32Array(1);
+let f32_u8 = new Uint8Array(f32.buffer);
+
+let f64 = new Float64Array(1);
+let f64_u8 = new Uint8Array(f64.buffer);
+
+function intToLong(value: number): Long {
+  value |= 0;
+  return {
+    low: value,
+    high: value >> 31,
+    unsigned: value >= 0,
+  };
+}
+
+let bbStack: ByteBuffer[] = [];
+
+function popByteBuffer(): ByteBuffer {
+  const bb = bbStack.pop();
+  if (!bb) return { bytes: new Uint8Array(64), offset: 0, limit: 0 };
+  bb.offset = bb.limit = 0;
+  return bb;
+}
+
+function pushByteBuffer(bb: ByteBuffer): void {
+  bbStack.push(bb);
+}
+
+function wrapByteBuffer(bytes: Uint8Array): ByteBuffer {
+  return { bytes, offset: 0, limit: bytes.length };
+}
+
+function toUint8Array(bb: ByteBuffer): Uint8Array {
+  let bytes = bb.bytes;
+  let limit = bb.limit;
+  return bytes.length === limit ? bytes : bytes.subarray(0, limit);
+}
+
+function skip(bb: ByteBuffer, offset: number): void {
+  if (bb.offset + offset > bb.limit) {
+    throw new Error('Skip past limit');
+  }
+  bb.offset += offset;
+}
+
+function isAtEnd(bb: ByteBuffer): boolean {
+  return bb.offset >= bb.limit;
+}
+
+function grow(bb: ByteBuffer, count: number): number {
+  let bytes = bb.bytes;
+  let offset = bb.offset;
+  let limit = bb.limit;
+  let finalOffset = offset + count;
+  if (finalOffset > bytes.length) {
+    let newBytes = new Uint8Array(finalOffset * 2);
+    newBytes.set(bytes);
+    bb.bytes = newBytes;
+  }
+  bb.offset = finalOffset;
+  if (finalOffset > limit) {
+    bb.limit = finalOffset;
+  }
+  return offset;
+}
+
+function advance(bb: ByteBuffer, count: number): number {
+  let offset = bb.offset;
+  if (offset + count > bb.limit) {
+    throw new Error('Read past limit');
+  }
+  bb.offset += count;
+  return offset;
+}
+
+function readBytes(bb: ByteBuffer, count: number): Uint8Array {
+  let offset = advance(bb, count);
+  return bb.bytes.subarray(offset, offset + count);
+}
+
+function writeBytes(bb: ByteBuffer, buffer: Uint8Array): void {
+  let offset = grow(bb, buffer.length);
+  bb.bytes.set(buffer, offset);
+}
+
+function readString(bb: ByteBuffer, count: number): string {
+  // Sadly a hand-coded UTF8 decoder is much faster than subarray+TextDecoder in V8
+  let offset = advance(bb, count);
+  let fromCharCode = String.fromCharCode;
+  let bytes = bb.bytes;
+  let invalid = '\uFFFD';
+  let text = '';
+
+  for (let i = 0; i < count; i++) {
+    let c1 = bytes[i + offset], c2: number, c3: number, c4: number, c: number;
+
+    // 1 byte
+    if ((c1 & 0x80) === 0) {
+      text += fromCharCode(c1);
+    }
+
+    // 2 bytes
+    else if ((c1 & 0xE0) === 0xC0) {
+      if (i + 1 >= count) text += invalid;
+      else {
+        c2 = bytes[i + offset + 1];
+        if ((c2 & 0xC0) !== 0x80) text += invalid;
+        else {
+          c = ((c1 & 0x1F) << 6) | (c2 & 0x3F);
+          if (c < 0x80) text += invalid;
+          else {
+            text += fromCharCode(c);
+            i++;
+          }
+        }
+      }
+    }
+
+    // 3 bytes
+    else if ((c1 & 0xF0) == 0xE0) {
+      if (i + 2 >= count) text += invalid;
+      else {
+        c2 = bytes[i + offset + 1];
+        c3 = bytes[i + offset + 2];
+        if (((c2 | (c3 << 8)) & 0xC0C0) !== 0x8080) text += invalid;
+        else {
+          c = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
+          if (c < 0x0800 || (c >= 0xD800 && c <= 0xDFFF)) text += invalid;
+          else {
+            text += fromCharCode(c);
+            i += 2;
+          }
+        }
+      }
+    }
+
+    // 4 bytes
+    else if ((c1 & 0xF8) == 0xF0) {
+      if (i + 3 >= count) text += invalid;
+      else {
+        c2 = bytes[i + offset + 1];
+        c3 = bytes[i + offset + 2];
+        c4 = bytes[i + offset + 3];
+        if (((c2 | (c3 << 8) | (c4 << 16)) & 0xC0C0C0) !== 0x808080) text += invalid;
+        else {
+          c = ((c1 & 0x07) << 0x12) | ((c2 & 0x3F) << 0x0C) | ((c3 & 0x3F) << 0x06) | (c4 & 0x3F);
+          if (c < 0x10000 || c > 0x10FFFF) text += invalid;
+          else {
+            c -= 0x10000;
+            text += fromCharCode((c >> 10) + 0xD800, (c & 0x3FF) + 0xDC00);
+            i += 3;
+          }
+        }
+      }
+    }
+
+    else text += invalid;
+  }
+
+  return text;
+}
+
+function writeString(bb: ByteBuffer, text: string): void {
+  // Sadly a hand-coded UTF8 encoder is much faster than TextEncoder+set in V8
+  let n = text.length;
+  let byteCount = 0;
+
+  // Write the byte count first
+  for (let i = 0; i < n; i++) {
+    let c = text.charCodeAt(i);
+    if (c >= 0xD800 && c <= 0xDBFF && i + 1 < n) {
+      c = (c << 10) + text.charCodeAt(++i) - 0x35FDC00;
+    }
+    byteCount += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4;
+  }
+  writeVarint32(bb, byteCount);
+
+  let offset = grow(bb, byteCount);
+  let bytes = bb.bytes;
+
+  // Then write the bytes
+  for (let i = 0; i < n; i++) {
+    let c = text.charCodeAt(i);
+    if (c >= 0xD800 && c <= 0xDBFF && i + 1 < n) {
+      c = (c << 10) + text.charCodeAt(++i) - 0x35FDC00;
+    }
+    if (c < 0x80) {
+      bytes[offset++] = c;
+    } else {
+      if (c < 0x800) {
+        bytes[offset++] = ((c >> 6) & 0x1F) | 0xC0;
+      } else {
+        if (c < 0x10000) {
+          bytes[offset++] = ((c >> 12) & 0x0F) | 0xE0;
+        } else {
+          bytes[offset++] = ((c >> 18) & 0x07) | 0xF0;
+          bytes[offset++] = ((c >> 12) & 0x3F) | 0x80;
+        }
+        bytes[offset++] = ((c >> 6) & 0x3F) | 0x80;
+      }
+      bytes[offset++] = (c & 0x3F) | 0x80;
+    }
+  }
+}
+
+function writeByteBuffer(bb: ByteBuffer, buffer: ByteBuffer): void {
+  let offset = grow(bb, buffer.limit);
+  let from = bb.bytes;
+  let to = buffer.bytes;
+
+  // This for loop is much faster than subarray+set on V8
+  for (let i = 0, n = buffer.limit; i < n; i++) {
+    from[i + offset] = to[i];
+  }
+}
+
+function readByte(bb: ByteBuffer): number {
+  return bb.bytes[advance(bb, 1)];
+}
+
+function writeByte(bb: ByteBuffer, value: number): void {
+  let offset = grow(bb, 1);
+  bb.bytes[offset] = value;
+}
+
+function readFloat(bb: ByteBuffer): number {
+  let offset = advance(bb, 4);
+  let bytes = bb.bytes;
+
+  // Manual copying is much faster than subarray+set in V8
+  f32_u8[0] = bytes[offset++];
+  f32_u8[1] = bytes[offset++];
+  f32_u8[2] = bytes[offset++];
+  f32_u8[3] = bytes[offset++];
+  return f32[0];
+}
+
+function writeFloat(bb: ByteBuffer, value: number): void {
+  let offset = grow(bb, 4);
+  let bytes = bb.bytes;
+  f32[0] = value;
+
+  // Manual copying is much faster than subarray+set in V8
+  bytes[offset++] = f32_u8[0];
+  bytes[offset++] = f32_u8[1];
+  bytes[offset++] = f32_u8[2];
+  bytes[offset++] = f32_u8[3];
+}
+
+function readDouble(bb: ByteBuffer): number {
+  let offset = advance(bb, 8);
+  let bytes = bb.bytes;
+
+  // Manual copying is much faster than subarray+set in V8
+  f64_u8[0] = bytes[offset++];
+  f64_u8[1] = bytes[offset++];
+  f64_u8[2] = bytes[offset++];
+  f64_u8[3] = bytes[offset++];
+  f64_u8[4] = bytes[offset++];
+  f64_u8[5] = bytes[offset++];
+  f64_u8[6] = bytes[offset++];
+  f64_u8[7] = bytes[offset++];
+  return f64[0];
+}
+
+function writeDouble(bb: ByteBuffer, value: number): void {
+  let offset = grow(bb, 8);
+  let bytes = bb.bytes;
+  f64[0] = value;
+
+  // Manual copying is much faster than subarray+set in V8
+  bytes[offset++] = f64_u8[0];
+  bytes[offset++] = f64_u8[1];
+  bytes[offset++] = f64_u8[2];
+  bytes[offset++] = f64_u8[3];
+  bytes[offset++] = f64_u8[4];
+  bytes[offset++] = f64_u8[5];
+  bytes[offset++] = f64_u8[6];
+  bytes[offset++] = f64_u8[7];
+}
+
+function readInt32(bb: ByteBuffer): number {
+  let offset = advance(bb, 4);
+  let bytes = bb.bytes;
+  return (
+    bytes[offset] |
+    (bytes[offset + 1] << 8) |
+    (bytes[offset + 2] << 16) |
+    (bytes[offset + 3] << 24)
+  );
+}
+
+function writeInt32(bb: ByteBuffer, value: number): void {
+  let offset = grow(bb, 4);
+  let bytes = bb.bytes;
+  bytes[offset] = value;
+  bytes[offset + 1] = value >> 8;
+  bytes[offset + 2] = value >> 16;
+  bytes[offset + 3] = value >> 24;
+}
+
+function readInt64(bb: ByteBuffer, unsigned: boolean): Long {
+  return {
+    low: readInt32(bb),
+    high: readInt32(bb),
+    unsigned,
+  };
+}
+
+function writeInt64(bb: ByteBuffer, value: Long): void {
+  writeInt32(bb, value.low);
+  writeInt32(bb, value.high);
+}
+
+function readVarint32(bb: ByteBuffer): number {
+  let c = 0;
+  let value = 0;
+  let b: number;
+  do {
+    b = readByte(bb);
+    if (c < 32) value |= (b & 0x7F) << c;
+    c += 7;
+  } while (b & 0x80);
+  return value;
+}
+
+function writeVarint32(bb: ByteBuffer, value: number): void {
+  value >>>= 0;
+  while (value >= 0x80) {
+    writeByte(bb, (value & 0x7f) | 0x80);
+    value >>>= 7;
+  }
+  writeByte(bb, value);
+}
+
+function readVarint64(bb: ByteBuffer, unsigned: boolean): Long {
+  let part0 = 0;
+  let part1 = 0;
+  let part2 = 0;
+  let b: number;
+
+  b = readByte(bb); part0 = (b & 0x7F); if (b & 0x80) {
+    b = readByte(bb); part0 |= (b & 0x7F) << 7; if (b & 0x80) {
+      b = readByte(bb); part0 |= (b & 0x7F) << 14; if (b & 0x80) {
+        b = readByte(bb); part0 |= (b & 0x7F) << 21; if (b & 0x80) {
+
+          b = readByte(bb); part1 = (b & 0x7F); if (b & 0x80) {
+            b = readByte(bb); part1 |= (b & 0x7F) << 7; if (b & 0x80) {
+              b = readByte(bb); part1 |= (b & 0x7F) << 14; if (b & 0x80) {
+                b = readByte(bb); part1 |= (b & 0x7F) << 21; if (b & 0x80) {
+
+                  b = readByte(bb); part2 = (b & 0x7F); if (b & 0x80) {
+                    b = readByte(bb); part2 |= (b & 0x7F) << 7;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return {
+    low: part0 | (part1 << 28),
+    high: (part1 >>> 4) | (part2 << 24),
+    unsigned,
+  };
+}
+
+function writeVarint64(bb: ByteBuffer, value: Long): void {
+  let part0 = value.low >>> 0;
+  let part1 = ((value.low >>> 28) | (value.high << 4)) >>> 0;
+  let part2 = value.high >>> 24;
+
+  // ref: src/google/protobuf/io/coded_stream.cc
+  let size =
+    part2 === 0 ?
+      part1 === 0 ?
+        part0 < 1 << 14 ?
+          part0 < 1 << 7 ? 1 : 2 :
+          part0 < 1 << 21 ? 3 : 4 :
+        part1 < 1 << 14 ?
+          part1 < 1 << 7 ? 5 : 6 :
+          part1 < 1 << 21 ? 7 : 8 :
+      part2 < 1 << 7 ? 9 : 10;
+
+  let offset = grow(bb, size);
+  let bytes = bb.bytes;
+
+  switch (size) {
+    case 10: bytes[offset + 9] = (part2 >>> 7) & 0x01;
+    case 9: bytes[offset + 8] = size !== 9 ? part2 | 0x80 : part2 & 0x7F;
+    case 8: bytes[offset + 7] = size !== 8 ? (part1 >>> 21) | 0x80 : (part1 >>> 21) & 0x7F;
+    case 7: bytes[offset + 6] = size !== 7 ? (part1 >>> 14) | 0x80 : (part1 >>> 14) & 0x7F;
+    case 6: bytes[offset + 5] = size !== 6 ? (part1 >>> 7) | 0x80 : (part1 >>> 7) & 0x7F;
+    case 5: bytes[offset + 4] = size !== 5 ? part1 | 0x80 : part1 & 0x7F;
+    case 4: bytes[offset + 3] = size !== 4 ? (part0 >>> 21) | 0x80 : (part0 >>> 21) & 0x7F;
+    case 3: bytes[offset + 2] = size !== 3 ? (part0 >>> 14) | 0x80 : (part0 >>> 14) & 0x7F;
+    case 2: bytes[offset + 1] = size !== 2 ? (part0 >>> 7) | 0x80 : (part0 >>> 7) & 0x7F;
+    case 1: bytes[offset] = size !== 1 ? part0 | 0x80 : part0 & 0x7F;
+  }
+}
+
+function readVarint32ZigZag(bb: ByteBuffer): number {
+  let value = readVarint32(bb);
+
+  // ref: src/google/protobuf/wire_format_lite.h
+  return (value >>> 1) ^ -(value & 1);
+}
+
+function writeVarint32ZigZag(bb: ByteBuffer, value: number): void {
+  // ref: src/google/protobuf/wire_format_lite.h
+  writeVarint32(bb, (value << 1) ^ (value >> 31));
+}
+
+function readVarint64ZigZag(bb: ByteBuffer): Long {
+  let value = readVarint64(bb, /* unsigned */ false);
+  let low = value.low;
+  let high = value.high;
+  let flip = -(low & 1);
+
+  // ref: src/google/protobuf/wire_format_lite.h
+  return {
+    low: ((low >>> 1) | (high << 31)) ^ flip,
+    high: (high >>> 1) ^ flip,
+    unsigned: false,
+  };
+}
+
+function writeVarint64ZigZag(bb: ByteBuffer, value: Long): void {
+  let low = value.low;
+  let high = value.high;
+  let flip = high >> 31;
+
+  // ref: src/google/protobuf/wire_format_lite.h
+  writeVarint64(bb, {
+    low: (low << 1) ^ flip,
+    high: ((high << 1) | (low >>> 31)) ^ flip,
+    unsigned: false,
+  });
+}

+ 13 - 0
src/comm/controllers/index.ts

@@ -0,0 +1,13 @@
+import { DeviceController } from "./deviceCtrl";
+import { NatsController } from "./natsCtrl";
+import { CmdSvcController } from "./cmdsvcCtrl";
+import { AppMsgRecvController } from "./appMsgRecvCtrl";
+import { AppMsgQueen5Controller } from "./appMsgQueen5Ctrl";
+
+export {
+  DeviceController,
+  NatsController,
+  CmdSvcController,
+  AppMsgRecvController,
+  AppMsgQueen5Controller
+};

+ 169 - 0
src/comm/controllers/natsCtrl.ts

@@ -0,0 +1,169 @@
+import { queenApi } from "queenjs";
+import { connect, StringCodec, Empty, ErrorCode } from "nats.ws";
+import { Controller } from "../core/controller";
+import { useCtx } from "../ctx";
+
+export class NatsController extends Controller {
+ 
+  _conn: any;
+  _isConned = false;
+  _startInit = false;
+
+  async onReady() {
+      await this.initConn();
+  }
+
+  async initConn() {
+    if (this._startInit) return;
+    this._startInit = true;
+
+    const {deviceCtrl} = useCtx()
+
+    const profile = deviceCtrl.profile;
+
+    if (!profile.wsPort) return;
+    
+    
+    const wsHost = `nats://localhost:${profile.wsPort}`
+    console.log("ws host=>", wsHost);
+    let ret = false;
+
+    try {
+      this._conn = await connect({ servers: wsHost });
+      this._isConned = !!this._conn;
+
+      ret = true;
+    } catch (error) {
+      console.log(error);
+    }
+
+    this._startInit = false;
+    return ret;
+  }
+  
+  async GetConn() {
+    return this._conn;
+  }
+
+  async subscribe(subject: string, callback: any, options:any = {}) {
+  
+    if (!this._isConned) {
+      console.error("建立连接失败");
+      return;
+    }
+
+    const sc = StringCodec();
+    const sub = this._conn.subscribe(subject, options);
+
+    (async () => {
+      for await (const m of sub) {
+        const ret = sc.decode(m.data);
+        console.log(subject, "=>recieved");
+        try {
+          if (ret) {
+            const msg = JSON.parse(ret);
+            const out = await callback(msg);
+            m.respond(sc.encode(out))
+
+          } else {
+            const out = await callback(ret);
+            m.respond(sc.encode(out))
+          }
+        } catch (error) {
+          console.log(subject, "=>recieved json parse eror", ret);
+          console.log(error);
+        }
+      }
+      console.log(subject, "subscription closed");
+    })();
+    return function () {
+      sub.unsubscribe();
+    };
+  }
+
+  async requestApi(subject: string, data?: any, timeout?: number) {
+    if (!this._isConned) return;
+
+    const ret = await this.request(subject, data, timeout);
+    console.log("request api=>", ret);
+
+    if (ret.error || (ret.result.ErrorNo && ret.result.ErrorNo != 200)) {
+      queenApi.messageError(ret.error || ret.result.ErrorDesc);
+      return;
+    }
+    try {
+      const retJson = ret.result.Result;
+      if (!retJson) return;
+      if (retJson[0] != "{" && retJson[0] != "[") return retJson;
+      return JSON.parse(retJson);
+    } catch (error) {
+      console.log(ret);
+      console.error(error);
+    }
+  }
+  
+  async publish(subject:string, value:string) {
+    
+    if (!this._isConned) {return}
+
+    const sc = StringCodec();
+
+    return await this._conn.publish(subject,  sc.encode(value));
+  }
+
+  async request(subject: string, data?: any, timeout?: number) {
+
+    const ret: { error: string; result: any } = { error: "", result: null };
+    if (!this._isConned) {
+      console.error("建立连接失败");
+      ret.error = "建立连接失败";
+
+      queenApi.showConfirm({
+        title: "数据请求失败",
+        content: "请求数据失败,请重新启动后再试",
+        type: "danger",
+      });
+      return ret;
+    }
+
+    const sc = StringCodec();
+    try {
+      let req = Empty;
+      if (data) {
+        if (typeof data != "string") {
+          req = sc.encode(JSON.stringify(data));
+        } else {
+          req = sc.encode(data);
+        }
+      }
+      const options = { timeout: 5000 };
+      if (timeout) {
+        options.timeout = timeout;
+      }
+      const m = await this._conn.request(subject, req, options);
+      let payload = sc.decode(m.data);
+      try {
+        payload = JSON.parse(payload);
+        console.log("m=>", payload);
+      } catch (error) {
+        console.log(error);
+      }
+      ret.result = payload;
+    } catch (error: any) {
+      console.error(error);
+      ret.error = error.message || "请求" + subject + "出错";
+      if (ret.error == "503") {
+        //NoResponders
+        ret.error = "网路异常,请重启服务";
+      }
+    }
+    return ret;
+  }
+
+  close() {
+    if (!this._isConned) {
+      return;
+    }
+    return this._conn.close();
+  }
+}

+ 15 - 0
src/comm/core/controller.ts

@@ -0,0 +1,15 @@
+import { Events } from "queenjs";
+
+export class Controller extends Events {
+    constructor(){
+        super();
+        this.init();
+    }
+    init() {
+        console.log("controller init");
+    }
+    
+    onReady() {
+        console.log("here can call other controller");
+    }
+}

+ 98 - 0
src/comm/core/historyCtrl.ts

@@ -0,0 +1,98 @@
+import {reactive,  computed} from "vue"
+import { ValueSnap } from "./rxValue";
+
+export class HistoryController {
+  enable = false;
+  state = reactive({
+        currLen: 0, //操作栈的长度
+        maxLen: 100, //操作栈总长度
+        opIndex: -1, //操作栈的指针
+  });
+
+  refCanUndo = computed(() => {
+    return this.state.opIndex >= 0;
+  });
+  refCanRedo = computed(() => {
+    return this.state.opIndex < this.state.currLen - 1;
+  });
+
+  queues: Map<string, ValueSnap>[] = [];
+  cacheSnapValues = new Map<string , ValueSnap>();
+
+  changeCbs:((flag:number)=>void)[]  = [];
+
+  // 添加缓存记录
+  record(snap: ValueSnap) {
+    if ( !this.enable ) return;
+
+    const first = this.cacheSnapValues.get(snap.Id)
+    if (first) {
+        snap.OldValue = first.OldValue;
+    }
+    this.cacheSnapValues.set(snap.Id, snap);
+  }
+
+  // 保存缓存记录到历史栈中
+  submit(change:(flag:number)=>void=(flag)=>{console.log("default history changed ", flag)}) {
+    if (this.cacheSnapValues.size < 1  || !this.enable) return;
+
+    console.log("submiting history=>", this.cacheSnapValues.size);
+    const state = this.state;
+    const queue = this.queues;
+
+    // 将缓存操作记录保存到当前指针的下一栈中
+    const index = ++state.opIndex;
+    queue[index] = this.cacheSnapValues;
+    this.changeCbs[index] = change;
+
+    // 重置缓存记录
+    this.cacheSnapValues = new Map<string, ValueSnap>();
+
+    // 设置栈的长度为指针的长度,舍弃后面的记录
+    queue.length = state.opIndex + 1;
+    // 若栈长度超过上限, 舍弃之前的记录
+    if (queue.length > state.maxLen) {
+      queue.splice(0, queue.length - state.maxLen);
+      state.opIndex = state.maxLen - 1;
+    }
+    // 更新当前长度状态
+    state.currLen = queue.length;
+  }
+
+  undo() {
+    if (!this.refCanUndo.value || !this.enable ) return;
+
+    this.cacheSnapValues = new Map<string, ValueSnap>();
+    const index = this.state.opIndex--;
+    const snaps = this.queues[index]
+
+    snaps.forEach((vn)=>vn.undo())
+
+    const cb = this.changeCbs[index];
+  
+    cb && cb(1);
+  }
+
+  redo() {
+    if (!this.refCanRedo.value  || !this.enable) return;
+
+    this.cacheSnapValues = new Map<string, ValueSnap>();
+    const index = ++this.state.opIndex;
+    const snaps = this.queues[index];
+    snaps.forEach(vn=>vn.redo());
+
+    const cb = this.changeCbs[index];
+    cb && cb(2);
+  }
+
+  //清除操作
+  clear() {
+    if ( !this.enable ) return;
+
+    this.queues = [];
+    this.changeCbs = [];
+    this.state.currLen = 0;
+    this.state.opIndex = -1;
+    this.cacheSnapValues = new Map<string, ValueSnap>();
+  }
+}

+ 171 - 0
src/comm/core/rxValue.ts

@@ -0,0 +1,171 @@
+import { cloneDeep } from "lodash";
+import { HistoryController } from "./historyCtrl";
+import {BehaviorSubject, Subscription} from "rxjs";
+import { reactive, toRaw } from  "vue";
+
+export class ValueSnap {
+    Id:string;
+    Value: any;
+    OldValue: any;
+
+    Rx: BehaviorSubject<any>;
+    constructor(id:string, value:any, oldValue:any, rx: BehaviorSubject<any>) {
+        this.Id = id;
+        this.Value = value;
+        this.OldValue = oldValue;
+        this.Rx = rx
+    }
+    redo() {
+        this.Rx.next({value: this.Value, _hstry:false});
+    }
+    undo() {
+        this.Rx.next({value: this.OldValue, _hstry:false});
+    }
+
+    clone() {
+        return new ValueSnap(this.Id, this.Value,this.OldValue, this.Rx);
+    }
+}
+
+export type RxValueType<T> = {
+    value: T,
+    _hstry?: boolean
+}
+
+
+function createRxValue<T>(value: T, histry:boolean) {
+    return new BehaviorSubject< RxValueType<T> >({value:value,  _hstry: histry})
+}
+
+let _valueIndex = 0;
+export function createValueSnap(value:any, oldValue:any, rx:BehaviorSubject<any>) {
+  let i = _valueIndex + 1;
+  _valueIndex +=1;
+  return new ValueSnap(i+"", value, oldValue, rx);
+}
+
+class RxValue {
+   static create<T extends {[key:string]: any}>(_fields:T, histroy?: HistoryController ) {
+        let obj = {__rx:true} as any;
+        
+        obj._historySnap = {} as any;
+        obj._historySub = {} as any;
+        obj._rxs = {} as any;
+        obj._fields = _fields;
+        obj._history = histroy;
+        obj._refs = {} as any;
+
+        const names = Object.keys(_fields);
+        
+        names.forEach(name=>{
+
+
+               const currName = name;
+               const initValue = _fields[currName]
+
+               const isRxField = typeof initValue == "object" && initValue.__rx
+               if (isRxField) {
+                    obj[currName] = initValue;
+                    return;
+               }
+
+                const f = createRxValue(initValue, !!histroy);
+                obj._rxs[name] = f;
+
+                const snap = createValueSnap(initValue, initValue, f);
+                obj._historySnap[name] = snap;
+                const rxc =  reactive({value: initValue});
+
+                Object.defineProperty(obj, currName, {
+                    get: function(){
+                        return rxc.value;
+                    },
+                    set: function(v) {
+                        f.next({value: v});
+                    },
+                    configurable: true,
+                    enumerable: true
+                })
+
+                const CamName = currName.slice(0,1).toUpperCase() +currName.slice(1);
+                
+                obj["set"+CamName] = function(value:T, nohistory = false){
+                    f.next({value, _hstry: !nohistory});
+                }
+        
+                obj["on"+CamName + "Changed"] = function(subscribe: (value:T, oldValue:T)=>void){
+                    return f.subscribe((v:any)=>{
+                        //if (CamName == "Transform") console.log("history 2222222222222222222222222222")
+                            subscribe(v.value, snap.OldValue)
+                        }
+                    )
+                }
+                
+                obj._historySub[name] = f.subscribe((v)=>{
+                    //if (CamName == "Transform") console.log("history 11111111111111111111111111")
+                    snap.OldValue = rxc.value;
+                    rxc.value = v.value;
+                    if (obj._history && obj._history.enable) {
+                        if (!v._hstry) return;
+                        const s = snap.clone();
+                        s.Value = v.value;
+                        obj._history.record(s);
+                    }
+                })
+        });
+
+
+        obj["setHistory"] = function(h: HistoryController){
+            obj._history = h;
+        }
+        obj["toJson"] = function() {
+            const out:any = {};
+            const names = Object.keys(_fields);
+
+            names.forEach(name=>{
+                const initV = _fields[name]
+                const isRxField = typeof initV == "object" && initV.__rx
+                if (isRxField) {
+                    out[name] = obj[name].toJson();
+                    return;
+                }
+                out[name] =cloneDeep(toRaw(obj._rxs[name].getValue().value));
+            })
+            return out;
+        }
+        obj["fromJson"] = function(json:any) {
+            const out:any = {};
+            const names = Object.keys(_fields);
+            names.forEach(name=>{
+                const initV = _fields[name]
+                const isRxField = typeof initV == "object" && initV.__rx
+                if (isRxField ) {
+                    if(json[name]) out[name] = obj[name].fromJson( json[name] );
+                    return;
+                }
+                obj._rxs[name].next({value: json[name] != undefined ? json[name] : cloneDeep(initV) , _hstry: false})
+            })
+            return out;
+        }
+        
+        type setHistoryType = (history: HistoryController)=>void;
+        type toJSONType = ()=>typeof _fields;
+        type fromJsonType = (json:typeof _fields)=>void;
+
+        return obj as typeof _fields & {
+            [K in keyof typeof _fields as `set${Capitalize<string & K>}`]: (value: typeof _fields[K], nohistory?:boolean) => void;
+        } & {
+            [K in keyof typeof _fields as `on${Capitalize<string & K>}Changed`]: (subscribe: (value: typeof _fields[K], oldValue:typeof _fields[K])=>void) => Subscription;
+        } & 
+        // {
+        //     [K in keyof typeof _fields as `ref${Capitalize<string & K>}`]: () => typeof _fields[K];
+        // } &
+        {
+            setHistory: setHistoryType
+            toJson:toJSONType
+            fromJson:fromJsonType
+        }
+    }
+}
+
+export {RxValue};

+ 9 - 0
src/comm/ctx/config.ts

@@ -0,0 +1,9 @@
+import * as Controls from "../controllers";
+const ctx = {
+  deviceCtrl: new Controls.DeviceController(),
+  natsCtrl: new Controls.NatsController(),
+  recvCtrl: new Controls.AppMsgRecvController(),
+  queen5MsgCtrl:new Controls.AppMsgQueen5Controller(),
+}
+
+export {ctx};

+ 53 - 0
src/comm/ctx/index.ts

@@ -0,0 +1,53 @@
+import {ctx} from "./config";
+
+let initOrder =  ["deviceCtrl", "natsCtrl", "recvCtrl", "queen5MsgCtrl"];
+export async function SetInitOrder<K extends keyof typeof ctx>(ctrls: K[]) {
+    initOrder = ctrls;
+}
+
+let OnReadyCtrls = Object.keys(ctx);
+export async function SetOnReadyCtrls<K extends keyof typeof ctx>(ctrls: K[]) {
+    OnReadyCtrls = ctrls;
+}
+
+let excludeCtrls = [] as string[];
+export async function SetExcludeCtrls<K extends keyof typeof ctx>(ctrls: K[]) {
+    excludeCtrls = ctrls;
+}
+
+export async function InitControllers() {
+    console.log("begin init ");
+    let n = initOrder.length;
+    for(let i= 0; i<n; i++) {
+
+        console.log("ready=>",i,  initOrder[i]);
+        
+        //@ts-ignore
+        await ctx[initOrder[i]].onReady();
+    }
+    const keys = OnReadyCtrls.slice(0)
+    while(n--) {
+        if (initOrder.indexOf(keys[n]) != -1 ) {
+            keys.splice(n, 1);
+        }
+    }
+    
+    if (excludeCtrls.length > 0 ) {
+        n = keys.length;
+        while(n--) {
+            if (excludeCtrls.indexOf(keys[n]) != -1 ) {
+                keys.splice(n, 1);
+            }
+        }
+    }
+
+    keys.forEach(k=>{
+        //@ts-ignore
+        ctx[k].onReady();
+    })
+    console.log("end init");
+}
+
+export function useCtx() {
+    return ctx;
+}

+ 2 - 1
src/modules/editor/components/CompUI/basicUI/Image2/component.tsx

@@ -37,7 +37,8 @@ export const Component = defineComponent({
       try {
         const url = await controls.pickCtrl.pickOneImage();
         if (!url) return;
-
+        console.log("xx->",url);
+        
         comp.value.url = url;
         comp.value.x = 0;
         comp.value.y = 0;

+ 0 - 1
src/modules/editor/controllers/PageCtrl/index.ts

@@ -10,7 +10,6 @@ import { createObj, history } from "../../objects/DesignTemp/factory";
 import { ICompKeys } from "../../typings";
 import { RxValue } from "../ReactCtrl/rxValue";
 
-
 export class PageCtrl extends ModuleControl<EditorModule> {
   
     state = RxValue.create({

+ 5 - 0
src/pages/editor/EditPage/index.tsx

@@ -1,3 +1,4 @@
+import { InitControllers, useCtx } from "@/comm/ctx";
 import { initEditor } from "@/modules/editor";
 import Viewport from "@/modules/editor/components/Viewport";
 import { EditorMode } from "@/modules/editor/typings";
@@ -50,5 +51,9 @@ export default defineComponent(() => {
     return resource.treeController.selectOnePackScene();
   };
 
+  InitControllers().then(()=>{
+     useCtx().queen5MsgCtrl.editorModule = editor;
+  })
+  
   return () => <Viewport class="!h-100vh" />;
 });

+ 5 - 0
yarn.lock

@@ -5748,6 +5748,11 @@ js-base64@^2.5.2:
   resolved "http://124.70.149.18:4873/js-base64/-/js-base64-2.6.4.tgz"
   integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
 
+js-base64@^3.7.5:
+  version "3.7.5"
+  resolved "http://124.70.149.18:4873/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca"
+  integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==
+
 js-message@1.0.7:
   version "1.0.7"
   resolved "http://124.70.149.18:4873/js-message/-/js-message-1.0.7.tgz"