├── .github └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── clibs ├── README.md └── linux64 │ ├── README.md │ ├── caRepeater │ ├── libCom.so │ └── libca.so ├── package.json ├── src ├── ca │ ├── __mocks__ │ │ └── channel.ts │ ├── channel.test.ts │ ├── channel.ts │ ├── connect.test.ts │ ├── connect.ts │ ├── enum.ts │ ├── error.ts │ ├── get.ts │ ├── index.ts │ └── put.ts ├── index.ts ├── init.ts └── types │ ├── ref-array-napi.d.ts │ ├── ref-napi.d.ts │ └── ref-struct-napi.d.ts ├── tsconfig.json └── yarn.lock /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Pubish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Prepare repo... 12 | uses: actions/checkout@v2 13 | 14 | - name: Use Node 12 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12.x 18 | - name: Install dependencies 19 | run: yarn install --frozen-lockfile 20 | 21 | - name: Publish 22 | uses: pascalgn/npm-publish-action@51fdb4531e99aac1873764ef7271af448dc42ab4 23 | with: # All of theses inputs are optional 24 | tag_name: "v%s" 25 | tag_message: "v%s" 26 | commit_pattern: "^(\\S+)" 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: Prepare repo... 9 | uses: actions/checkout@v2 10 | 11 | - name: Use Node 12 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 12.x 15 | 16 | - name: Use cached node_modules 17 | uses: actions/cache@v1 18 | with: 19 | path: node_modules 20 | key: nodeModules-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | nodeModules- 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | env: 27 | CI: true 28 | 29 | - name: Lint 30 | run: yarn lint 31 | env: 32 | CI: true 33 | 34 | - name: Test 35 | run: yarn test --ci --coverage --maxWorkers=2 36 | env: 37 | CI: true 38 | 39 | - name: Build 40 | run: yarn build 41 | env: 42 | CI: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | /test.js 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .github 3 | .storybook 4 | package.json 5 | package-lock.json 6 | dist 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 onichandame 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This package is not used by the author any more. Hence new features are not going to be added. Bug reports about the existing functions are welcome.** 2 | 3 | # EPICS IOC Connection 4 | 5 | Forked from [this repo](https://github.com/RobbieClarken/node-epics) but does not have the same signature with the original package. 6 | 7 | This package aims to provide a convenient interface for JS/TS codes to communicate with the EPICS IOCs. 8 | 9 | Shipped with types 10 | 11 | # Author 12 | 13 | [onichandame](https://github.com/onichandame) 14 | 15 | # Guide 16 | 17 | ## Pre-requisite 18 | 19 | You are assumed to be familiar with the EPICS framework. If not, check the official site and the [API docs](https://epics.anl.gov/base/R3-14/10-docs/CAref.html). 20 | 21 | This package is not a traditional JS package which only depends on the JS runtime due to the complexity of the ChannelAccess protocol. It requires the following conditions being met: 22 | 23 | 1. an installation of epics base. The development of this package is based on EPICS 3.14.12.8 24 | 2. one of the below env variables set(Check the meaning of them in the official installation guide of EPICS). If none is installed, it will fallback to the binaries shipped in `clibs` directory, which will not be guaranteed to work in your environment. 25 | - LIBCA_PATH 26 | - EPICS_BASE and EPICS_HOST_ARCH 27 | 28 | ## Installation 29 | 30 | ```bash 31 | yarn add epics-ioc-connection 32 | # or 33 | npm i epics-ioc-connection 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```typescript 39 | import { CA } from "epics-ioc-connection" 40 | 41 | ;(async () => { 42 | // self-managed channel 43 | const channel = await CA.connect("rootHost:ai1") 44 | // get once 45 | channel 46 | .get() 47 | .then(value => console.log(value)) 48 | // put once 49 | .then(channel.put(4)) 50 | .then(() => console.log("pushed value to channel")) 51 | // get new value when value changes 52 | .then(channel.monitor) 53 | .then(() => { 54 | channel.on("value", value => console.log(value)) 55 | }) 56 | // disconnect 57 | setTimeout(channel.disconnect, 5000) 58 | 59 | // managed methods 60 | // get once 61 | console.log(await CA.get("rootHost:ai1")) 62 | // put once 63 | try { 64 | await CA.put("rootHost:ai1", 3) 65 | } catch (e) { 66 | console.error(`put failed due to ${e}`) 67 | } 68 | })() 69 | ``` 70 | 71 | # LICENSE 72 | 73 | [MIT](https://opensource.org/licenses/MIT) 74 | 75 | # Roadmap 76 | 77 | - add counter to channel 78 | - separate types of dependencies to DefinitelyTyped 79 | - test caget/caput/camonitor 80 | - implement ca using napi 81 | - implement ca using pure ts 82 | - write unit test(after previous) 83 | -------------------------------------------------------------------------------- /clibs/README.md: -------------------------------------------------------------------------------- 1 | # C Library 2 | 3 | the CA library is shipped in this directory as a workaround while the CA protocol has not been implemented using pure JS. 4 | -------------------------------------------------------------------------------- /clibs/linux64/README.md: -------------------------------------------------------------------------------- 1 | These shared libraries were built using 2 | 3 | Epics Base 3.15.7 on Centos 7 with gcc 4.8.5, glibc 2.17 4 | 5 | Readline support was turned off by setting 6 | COMMANDLINE_LIBRARY = 7 | 8 | in 9 | CONFIG_SITE.Common.linux-x86 10 | 11 | 12 | The patchelf utility (from CentOS 7) was used to make the shared 13 | libraries portable with the following commands: 14 | 15 | mv libCom.so libCom.so 16 | patchelf --set-soname libca.so libca.so 17 | patchelf --set-soname libCom.so libCom.so 18 | patchelf --set-rpath '$ORIGIN' libca.so 19 | patchelf --set-rpath '$ORIGIN' libCom.so 20 | patchelf --replace-needed libCom.so.3.15.7 libCom.so libca.so 21 | -------------------------------------------------------------------------------- /clibs/linux64/caRepeater: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onichandame/epics-ioc-connection/78c6d868175694a11bfc093a892fad6800b3f5b6/clibs/linux64/caRepeater -------------------------------------------------------------------------------- /clibs/linux64/libCom.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onichandame/epics-ioc-connection/78c6d868175694a11bfc093a892fad6800b3f5b6/clibs/linux64/libCom.so -------------------------------------------------------------------------------- /clibs/linux64/libca.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onichandame/epics-ioc-connection/78c6d868175694a11bfc093a892fad6800b3f5b6/clibs/linux64/libca.so -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.10", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "/dist", 8 | "/clibs" 9 | ], 10 | "engines": { 11 | "node": ">10 <=14" 12 | }, 13 | "scripts": { 14 | "start": "tsdx watch", 15 | "build": "tsdx build", 16 | "test": "tsdx test", 17 | "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"", 18 | "lint": "yarn format", 19 | "prepare": "tsdx build" 20 | }, 21 | "peerDependencies": {}, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "yarn lint", 25 | "pre-push": "yarn test && yarn build" 26 | } 27 | }, 28 | "name": "epics-ioc-connection", 29 | "author": "onichandame", 30 | "module": "dist/epics-base.esm.js", 31 | "devDependencies": { 32 | "@types/ffi-napi": "^2.4.1", 33 | "@types/jest": "^25.2.1", 34 | "husky": "^4.2.3", 35 | "prettier": "^2.0.5", 36 | "tsdx": "^0.13.1", 37 | "tslib": "^1.11.1", 38 | "typescript": "^3.8.3" 39 | }, 40 | "dependencies": { 41 | "ffi-napi": "^2.4.7", 42 | "ref-struct-napi": "^1.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ca/__mocks__/channel.ts: -------------------------------------------------------------------------------- 1 | export const MockChannel = jest.fn() 2 | export const MockConnect = jest 3 | .fn() 4 | .mockImplementation(() => Promise.resolve(MockChannel)) 5 | export const MockDisconnect = jest 6 | .fn() 7 | .mockImplementation(() => Promise.resolve()) 8 | export const MockGet = jest.fn().mockImplementation(() => Promise.resolve()) 9 | export const MockPut = jest.fn().mockImplementation(() => Promise.resolve()) 10 | export const MockMonitor = jest.fn().mockImplementation(() => Promise.resolve()) 11 | 12 | export const Channel = jest.fn().mockImplementation(() => ({ 13 | new: MockChannel, 14 | connect: MockConnect, 15 | disconnect: MockDisconnect, 16 | get: MockGet, 17 | put: MockPut, 18 | monitor: MockMonitor, 19 | })) 20 | -------------------------------------------------------------------------------- /src/ca/channel.test.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from "./channel" 2 | 3 | jest.mock("./channel") 4 | 5 | const channel = new Channel("") 6 | channel.get() 7 | 8 | describe("channel", () => { 9 | test("cannot be unit tested as it depends on EPICS", () => { 10 | expect(true).toBeTruthy() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/ca/channel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | import { 3 | alloc, 4 | deref, 5 | refType, 6 | types, 7 | writeCString, 8 | readCString, 9 | reinterpret, 10 | } from "ref-napi" 11 | import Struct from "ref-struct-napi" 12 | import { Library, Callback } from "ffi-napi" 13 | import { EventEmitter } from "events" 14 | import { join } from "path" 15 | 16 | import { ConError, DepError, GetError, PutError } from "./error" 17 | import { 18 | Mask, 19 | DataType, 20 | ConState, 21 | CommonState, 22 | State, 23 | CAConState, 24 | ContextReturnState, 25 | PendIoReturnState, 26 | PendEventReturnState, 27 | CreateChannelReturnState, 28 | ClearSubscriptionReturnState, 29 | CreateSubscriptionReturnState, 30 | GetReturnState, 31 | ClearChannelState, 32 | } from "./enum" 33 | 34 | type CallbackArgs = { 35 | usr: Buffer 36 | chid: number 37 | type: number 38 | count: number 39 | dbr: Buffer 40 | status: State 41 | } 42 | 43 | export type Value = number | string | Array 44 | 45 | // use local epics installation 46 | let LIBCA_PATH = process.env.NODE_EPICS_LIBCA 47 | if (!LIBCA_PATH) { 48 | if (process.env.EPICS_BASE && process.env.EPICS_HOST_ARCH) { 49 | LIBCA_PATH = join( 50 | process.env.EPICS_BASE, 51 | "lib", 52 | process.env.EPICS_HOST_ARCH, 53 | "libca" 54 | ) 55 | } 56 | } 57 | // use shipped binary 58 | if (!LIBCA_PATH) { 59 | if (process.platform.includes("linux")) { 60 | if (process.arch.includes("64")) { 61 | LIBCA_PATH = join(global.epicsRootPath, "clibs", "linux64", "libca") 62 | } 63 | } 64 | } 65 | 66 | if (!LIBCA_PATH) { 67 | throw DepError 68 | } 69 | 70 | const MAX_STRING_SIZE = 40 71 | const pendDelay = 1e-5 72 | const size_tPtr = refType(types.size_t) 73 | // const dblPtr = refType(types.double) 74 | 75 | const chanId = types.long 76 | const evId = types.long 77 | const chtype = types.long 78 | 79 | const libca = Library(LIBCA_PATH, { 80 | ca_message: ["string", ["int"]], 81 | ca_context_create: ["int", ["int"]], 82 | ca_context_destroy: ["int", []], 83 | ca_current_context: ["int", []], 84 | ca_pend_event: ["int", ["float"]], 85 | ca_pend_io: ["int", ["float"]], 86 | ca_test_io: ["int", []], 87 | ca_create_channel: [ 88 | "int", 89 | ["string", "pointer", "pointer", "int", "pointer"], 90 | ], 91 | ca_host_name: ["string", ["long"]], 92 | ca_field_type: ["short", ["long"]], 93 | ca_state: ["short", [chanId]], 94 | ca_element_count: ["int", ["long"]], 95 | ca_name: ["string", ["long"]], 96 | ca_array_get: ["int", ["int", "ulong", chanId, "pointer"]], 97 | ca_array_get_callback: [ 98 | "int", 99 | ["int", "ulong", chanId, "pointer", "pointer"], 100 | ], 101 | ca_array_put_callback: [ 102 | "int", 103 | [chtype, "ulong", chanId, "pointer", "pointer", "pointer"], 104 | ], 105 | ca_create_subscription: [ 106 | "int", 107 | ["int", "ulong", chanId, "long", "pointer", "pointer", "pointer"], 108 | ], 109 | ca_clear_subscription: ["int", [evId]], 110 | ca_clear_channel: ["int", [chanId]], 111 | }) 112 | 113 | const message = (code: State): string => libca.ca_message(code) 114 | 115 | // create context here. need to destroy in the future versions 116 | const getContext = (): ContextReturnState => libca.ca_context_create(1) 117 | 118 | const ccCode = getContext() 119 | 120 | if (ccCode !== CommonState.ECA_NORMAL) { 121 | throw new Error(message(ccCode)) 122 | } 123 | 124 | const pend = (): void => { 125 | const eventCode: PendEventReturnState = libca.ca_pend_event(pendDelay) 126 | const ioCode: PendIoReturnState = libca.ca_pend_io(pendDelay) 127 | if (eventCode !== CommonState.ECA_TIMEOUT) { 128 | throw PutError 129 | } else if (ioCode !== CommonState.ECA_NORMAL) { 130 | throw GetError 131 | } 132 | } 133 | 134 | const stringArrayToBuffer = (raw: Value): Buffer => { 135 | const count = Array.isArray(raw) ? raw.length : 1 136 | const array: string[] = Array(count) 137 | if (Array.isArray(raw)) { 138 | raw.forEach((item, index) => { 139 | array[index] = item.toString() 140 | }) 141 | } else { 142 | array[0] = raw.toString() 143 | } 144 | const buf = Buffer.alloc(count * MAX_STRING_SIZE) 145 | for (let i = 0; i < count; i += 1) { 146 | writeCString(buf, i * MAX_STRING_SIZE, array[i]) 147 | } 148 | return buf 149 | } 150 | 151 | const evargs_t = Struct({ 152 | usr: size_tPtr, 153 | chid: chanId, 154 | type: types.long, 155 | count: types.long, 156 | dbr: size_tPtr, 157 | status: types.int, 158 | }) 159 | 160 | export interface Channel { 161 | on(event: "value", listener: (value: DataType) => void): this 162 | } 163 | 164 | export class Channel extends EventEmitter { 165 | private _count: number 166 | private _field_type: DataType 167 | private _monitor_event_id_ptr: Buffer | null 168 | private _monitor_callback_ptr: Buffer | undefined 169 | private _callback_ptrs: Buffer[] 170 | private _connection_state_change_ptr: Buffer | null 171 | private _chid: number | null 172 | 173 | constructor(private _pvname: string) { 174 | super() 175 | this._count = 0 176 | this._callback_ptrs = [] 177 | this._chid = null 178 | this._monitor_event_id_ptr = null 179 | this._field_type = DataType.NO_ACCESS 180 | this._connection_state_change_ptr = null 181 | } 182 | 183 | private parseValue(buf: Buffer, type: DataType, count: number): Value { 184 | const raw: string[] = [] 185 | const bufRef = reinterpret(buf, count * MAX_STRING_SIZE) 186 | for (let i = 0; i < count; i++) { 187 | raw.push(readCString(bufRef, i * MAX_STRING_SIZE)) 188 | } 189 | let result: string[] | number[] = Array(raw.length) 190 | switch (type) { 191 | case DataType.NO_ACCESS: 192 | return [] 193 | case DataType.INT: 194 | case DataType.ENUM: 195 | case DataType.SHORT: 196 | raw.forEach((item, index) => { 197 | result[index] = parseInt(item) 198 | }) 199 | break 200 | case DataType.STRING: 201 | case DataType.CHAR: 202 | default: 203 | result = raw 204 | } 205 | if (count === 1) { 206 | return result[0] 207 | } else { 208 | return result 209 | } 210 | } 211 | 212 | public get state(): State { 213 | if (this._chid === null) { 214 | return ConState.CS_CLOSED 215 | } 216 | return libca.ca_state(this._chid) 217 | } 218 | 219 | public get connected(): boolean { 220 | return this.state === ConState.CS_CONN 221 | } 222 | 223 | public connect({ timeout = 2000 } = {}): Promise { 224 | return new Promise((resolve, reject) => { 225 | const chidPtr: Buffer = Buffer.alloc(chanId.size) 226 | chidPtr.type = chanId 227 | chidPtr.writeBigInt64LE(BigInt(0), 0) 228 | 229 | let firstCallback = true 230 | const userDataPtr = null 231 | const priority = 0 232 | 233 | this._connection_state_change_ptr = new Callback( 234 | "void", 235 | ["pointer", "long"], 236 | (_: number, ev: CAConState) => { 237 | this._count = libca.ca_element_count(this._chid) 238 | this._field_type = libca.ca_field_type(this._chid) 239 | this.emit("connection", ev) 240 | if (firstCallback) { 241 | firstCallback = false 242 | if (this.connected) { 243 | resolve() 244 | } else { 245 | reject(ConError) 246 | } 247 | } 248 | } 249 | ) 250 | const caCode: CreateChannelReturnState = libca.ca_create_channel( 251 | this._pvname, 252 | this._connection_state_change_ptr, 253 | userDataPtr, 254 | priority, 255 | chidPtr 256 | ) 257 | pend() 258 | this._chid = deref(chidPtr) 259 | if (caCode !== CommonState.ECA_NORMAL) { 260 | firstCallback = false 261 | reject(ConError) 262 | } 263 | setTimeout(() => { 264 | if (this.state === ConState.CS_NEVER_CONN) { 265 | firstCallback = false 266 | reject(ConError) 267 | } 268 | }, timeout) 269 | }) 270 | } 271 | 272 | // a deadlock is seen when calling ca_clear_subscription or ca_clear_channel. Have to wait for a short time to bypass it. Intuitively it seems like a race condition 273 | // currently do not know what affects this behaviour, have to read the source code of EPICS which is not easy to do 274 | // increase the timeout if a deadlock is seen. 275 | public disconnect({ timeout = 10 } = {}): Promise { 276 | return new Promise((resolve, reject) => { 277 | setTimeout(() => { 278 | if (this._monitor_event_id_ptr !== null) { 279 | const csCode: ClearSubscriptionReturnState = libca.ca_clear_subscription( 280 | deref(this._monitor_event_id_ptr) 281 | ) 282 | pend() 283 | if (csCode !== CommonState.ECA_NORMAL) { 284 | reject(message(csCode)) 285 | } 286 | } 287 | if (this._chid) { 288 | const ccCode: ClearChannelState = libca.ca_clear_channel(this._chid) 289 | pend() 290 | if (ccCode !== CommonState.ECA_NORMAL) { 291 | reject(new Error(message(ccCode))) 292 | } 293 | } 294 | this._chid = null 295 | resolve() 296 | }, timeout) 297 | }) 298 | } 299 | 300 | public get({ type = this._field_type } = {}): Promise { 301 | return new Promise((resolve, reject) => { 302 | const getCallbackPtr = Callback( 303 | "void", 304 | [evargs_t], 305 | ({ status, dbr }: CallbackArgs) => { 306 | if (status !== CommonState.ECA_NORMAL) { 307 | return reject(new Error(message(status))) 308 | } 309 | resolve(this.parseValue(dbr, type, this._count)) 310 | this._callback_ptrs.splice( 311 | this._callback_ptrs.indexOf(getCallbackPtr), 312 | 1 313 | ) 314 | } 315 | ) 316 | this._callback_ptrs.push(getCallbackPtr) 317 | const usrArg = null 318 | const getCode: GetReturnState = libca.ca_array_get_callback( 319 | DataType.STRING, 320 | this._count, 321 | this._chid, 322 | getCallbackPtr, 323 | usrArg 324 | ) 325 | if (getCode !== CommonState.ECA_NORMAL) { 326 | return reject(new Error(message(getCode))) 327 | } 328 | pend() 329 | }) 330 | } 331 | 332 | public put(value: Value): Promise { 333 | return new Promise((resolve, reject) => { 334 | const putCallbackPtr = Callback( 335 | "void", 336 | [evargs_t], 337 | ({ status }: CallbackArgs) => { 338 | if (status !== CommonState.ECA_NORMAL) { 339 | reject(PutError) 340 | } else { 341 | resolve() 342 | } 343 | } 344 | ) 345 | const count = Array.isArray(value) ? value.length : 1 346 | const buf: Buffer = stringArrayToBuffer(value) 347 | const usrArg = null 348 | const apCode = libca.ca_array_put_callback( 349 | DataType.STRING, 350 | count, 351 | this._chid, 352 | buf, 353 | putCallbackPtr, 354 | usrArg 355 | ) 356 | if (apCode !== CommonState.ECA_NORMAL) { 357 | reject(PutError) 358 | } 359 | pend() 360 | }) 361 | } 362 | 363 | public monitor({ type = this._field_type } = {}): Promise { 364 | return new Promise((resolve, reject) => { 365 | this._monitor_event_id_ptr = alloc(types.size_t) 366 | this._monitor_callback_ptr = Callback( 367 | "void", 368 | [evargs_t], 369 | ({ dbr }: CallbackArgs) => { 370 | const value = this.parseValue(dbr, type, this._count) 371 | this.emit("value", value) 372 | } 373 | ) 374 | const usrArg = null 375 | const csCode: CreateSubscriptionReturnState = libca.ca_create_subscription( 376 | DataType.STRING, 377 | this._count, 378 | this._chid, 379 | Mask.DBE_VALUE, 380 | this._monitor_callback_ptr, 381 | usrArg, 382 | this._monitor_event_id_ptr 383 | ) 384 | if (csCode === CommonState.ECA_NORMAL) { 385 | resolve() 386 | } else { 387 | reject(new Error(message(csCode))) 388 | } 389 | pend() 390 | }) 391 | } 392 | } 393 | 394 | export { DataType } 395 | -------------------------------------------------------------------------------- /src/ca/connect.test.ts: -------------------------------------------------------------------------------- 1 | import { connect } from "./connect" 2 | 3 | jest.mock("./channel") 4 | 5 | describe("connect", () => { 6 | test("connect without throw", async () => { 7 | return expect(connect("")).resolves.toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/ca/connect.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from "./channel" 2 | 3 | export const connect = async ( 4 | pvname: string, 5 | { timeout = 2000 } = {} 6 | ): Promise => { 7 | const ca = new Channel(pvname) 8 | await ca.connect({ timeout }) 9 | return ca 10 | } 11 | -------------------------------------------------------------------------------- /src/ca/enum.ts: -------------------------------------------------------------------------------- 1 | // state code 2 | export enum CommonState { 3 | ECA_NORMAL = 1, 4 | ECA_TIMEOUT = 80, 5 | ECA_ALLOCMEM = 48, 6 | ECA_NOTTHREADED = 458, 7 | ECA_EVDISALLOW = 210, 8 | ECA_BADTYPE = 114, 9 | ECA_STRTOBIG = 96, 10 | ECA_BADCHID = 410, 11 | ECA_BADCOUNT = 196, 12 | ECA_GETFAIL = 152, 13 | ECA_NORDACCESS = 368, 14 | ECA_DISCONN = 192, 15 | ECA_ADDFAIL = 168, 16 | } 17 | 18 | // state codes returned by individual functions 19 | export type ContextReturnState = 20 | | CommonState.ECA_NORMAL 21 | | CommonState.ECA_ALLOCMEM 22 | | CommonState.ECA_NOTTHREADED 23 | export type PendIoReturnState = 24 | | CommonState.ECA_NORMAL 25 | | CommonState.ECA_TIMEOUT 26 | | CommonState.ECA_EVDISALLOW 27 | export type PendEventReturnState = 28 | | CommonState.ECA_TIMEOUT 29 | | CommonState.ECA_EVDISALLOW 30 | export type CreateChannelReturnState = 31 | | CommonState.ECA_NORMAL 32 | | CommonState.ECA_BADTYPE 33 | | CommonState.ECA_STRTOBIG 34 | | CommonState.ECA_ALLOCMEM 35 | export type ClearSubscriptionReturnState = 36 | | CommonState.ECA_NORMAL 37 | | CommonState.ECA_BADCHID 38 | export type GetReturnState = 39 | | CommonState.ECA_NORMAL 40 | | CommonState.ECA_BADTYPE 41 | | CommonState.ECA_BADCHID 42 | | CommonState.ECA_BADCOUNT 43 | | CommonState.ECA_GETFAIL 44 | | CommonState.ECA_NORDACCESS 45 | | CommonState.ECA_ALLOCMEM 46 | | CommonState.ECA_DISCONN 47 | export type ClearChannelState = CommonState.ECA_NORMAL | CommonState.ECA_BADCHID 48 | export type CreateSubscriptionReturnState = 49 | | CommonState.ECA_NORMAL 50 | | CommonState.ECA_BADCHID 51 | | CommonState.ECA_BADTYPE 52 | | CommonState.ECA_ALLOCMEM 53 | | CommonState.ECA_ADDFAIL 54 | 55 | export enum ConState { 56 | CS_NEVER_CONN, 57 | CS_PREV_CONN, 58 | CS_CONN, 59 | CS_CLOSED, 60 | } 61 | 62 | export enum CAConState { 63 | CA_OP_CONN_UP = 6, 64 | CA_OP_CONN_DOWN = 7, 65 | } 66 | 67 | export const state = { 68 | OP_CONN_UP: 6, 69 | OP_CONN_DOWN: 7, 70 | CS_NEVER_CONN: 0, 71 | CS_PREV_CONN: 1, 72 | CS_CONN: 2, 73 | CS_CLOSED: 3, 74 | CS_NEVER_SEARCH: 4, 75 | } 76 | export enum Mask { 77 | DBE_VALUE = 1, 78 | DBE_LOG = 2, 79 | DBE_ALARM = 4, 80 | DBE_PROPERTY = 8, 81 | } 82 | 83 | export enum DataType { 84 | STRING, 85 | INT, 86 | SHORT, 87 | FLOAT, 88 | ENUM, 89 | CHAR, 90 | LONG, 91 | DOUBLE, 92 | NO_ACCESS, 93 | } 94 | 95 | export type State = number 96 | -------------------------------------------------------------------------------- /src/ca/error.ts: -------------------------------------------------------------------------------- 1 | export const DepError = new Error("Cannot find epics installation") 2 | export const ConError = new Error("Connection not established") 3 | export const GetError = new Error("Read request failed") 4 | export const PutError = new Error("Write request failed") 5 | -------------------------------------------------------------------------------- /src/ca/get.ts: -------------------------------------------------------------------------------- 1 | import { connect } from "./connect" 2 | import { Channel } from "./channel" 3 | 4 | export const get = async (pvname: string): ReturnType => { 5 | const channel = await connect(pvname) 6 | const value = await channel.get() 7 | await channel.disconnect() 8 | return value 9 | } 10 | -------------------------------------------------------------------------------- /src/ca/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./channel" 2 | export * from "./connect" 3 | export * from "./get" 4 | export * from "./put" 5 | -------------------------------------------------------------------------------- /src/ca/put.ts: -------------------------------------------------------------------------------- 1 | import { connect } from "./connect" 2 | import { Value } from "./channel" 3 | 4 | export const put = async (pvname: string, value: Value): Promise => { 5 | const channel = await connect(pvname) 6 | await channel.put(value) 7 | await channel.disconnect() 8 | } 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "./init" 2 | 3 | export * as CA from "./ca" 4 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from "path" 2 | 3 | declare global { 4 | /* eslint-disable-next-line @typescript-eslint/no-namespace */ 5 | namespace NodeJS { 6 | interface Global { 7 | epicsRootPath: string 8 | } 9 | } 10 | } 11 | 12 | global.epicsRootPath = dirname(__dirname) 13 | process.env.PATH += `:${global.epicsRootPath}` 14 | -------------------------------------------------------------------------------- /src/types/ref-array-napi.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for ref-array 2 | // Project: https://github.com/TooTallNate/ref-array 3 | // Definitions by: Paul Loyd 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | import ref = require("ref-napi") 7 | 8 | export interface ArrayType extends ref.Type { 9 | BYTES_PER_ELEMENT: number 10 | fixedLength: number 11 | /** The reference to the base type. */ 12 | type: ref.Type 13 | 14 | /** 15 | * Accepts a Buffer instance that should be an already-populated with data 16 | * for the ArrayType. The "length" of the Array is determined by searching 17 | * through the buffer's contents until an aligned NULL pointer is encountered. 18 | */ 19 | untilZeros( 20 | buffer: Buffer 21 | ): { 22 | [i: number]: T 23 | length: number 24 | toArray(): T[] 25 | toJSON(): T[] 26 | inspect(): string 27 | buffer: Buffer 28 | ref(): Buffer 29 | } 30 | 31 | new (length?: number): { 32 | [i: number]: T 33 | length: number 34 | toArray(): T[] 35 | toJSON(): T[] 36 | inspect(): string 37 | buffer: Buffer 38 | ref(): Buffer 39 | } 40 | new (data: number[], length?: number): { 41 | [i: number]: T 42 | length: number 43 | toArray(): T[] 44 | toJSON(): T[] 45 | inspect(): string 46 | buffer: Buffer 47 | ref(): Buffer 48 | } 49 | new (data: Buffer, length?: number): { 50 | [i: number]: T 51 | length: number 52 | toArray(): T[] 53 | toJSON(): T[] 54 | inspect(): string 55 | buffer: Buffer 56 | ref(): Buffer 57 | } 58 | (length?: number): { 59 | [i: number]: T 60 | length: number 61 | toArray(): T[] 62 | toJSON(): T[] 63 | inspect(): string 64 | buffer: Buffer 65 | ref(): Buffer 66 | } 67 | (data: number[], length?: number): { 68 | [i: number]: T 69 | length: number 70 | toArray(): T[] 71 | toJSON(): T[] 72 | inspect(): string 73 | buffer: Buffer 74 | ref(): Buffer 75 | } 76 | (data: Buffer, length?: number): { 77 | [i: number]: T 78 | length: number 79 | toArray(): T[] 80 | toJSON(): T[] 81 | inspect(): string 82 | buffer: Buffer 83 | ref(): Buffer 84 | } 85 | } 86 | 87 | /** 88 | * The array type meta-constructor. 89 | * The returned constructor's API is highly influenced by the WebGL 90 | * TypedArray API. 91 | */ 92 | export const Array: { 93 | new (type: ref.Type, length?: number): ArrayType 94 | new (type: string, length?: number): ArrayType 95 | (type: ref.Type, length?: number): ArrayType 96 | (type: string, length?: number): ArrayType 97 | } 98 | 99 | export default Array 100 | -------------------------------------------------------------------------------- /src/types/ref-napi.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | // Type definitions for ref-napi 1.4 3 | // Project: https://github.com/node-ffi-napi/ref-napi 4 | // Definitions by: Keerthi Niranjan , Kiran Niranjan 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | 7 | /// 8 | 9 | export interface Type { 10 | /** The size in bytes required to hold this datatype. */ 11 | size: number 12 | /** The current level of indirection of the buffer. */ 13 | indirection: number 14 | /** To invoke when `ref.get` is invoked on a buffer of this type. */ 15 | get(buffer: Buffer, offset: number): any 16 | /** To invoke when `ref.set` is invoked on a buffer of this type. */ 17 | set(buffer: Buffer, offset: number, value: any): void 18 | /** The name to use during debugging for this datatype. */ 19 | name?: string 20 | /** The alignment of this datatype when placed inside a struct. */ 21 | alignment?: number 22 | } 23 | 24 | /** A Buffer that references the C NULL pointer. */ 25 | export declare const NULL: Buffer 26 | /** A pointer-sized buffer pointing to NULL. */ 27 | export declare const NULL_POINTER: Buffer 28 | /** Get the memory address of buffer. */ 29 | export declare function address(buffer: Buffer): number 30 | /** Allocate the memory with the given value written to it. */ 31 | export declare function alloc(type: Type, value?: any): Buffer 32 | /** Allocate the memory with the given value written to it. */ 33 | export declare function alloc(type: string, value?: any): Buffer 34 | 35 | /** 36 | * Allocate the memory with the given string written to it with the given 37 | * encoding (defaults to utf8). The buffer is 1 byte longer than the 38 | * string itself, and is NULL terminated. 39 | */ 40 | export declare function allocCString(string: string, encoding?: string): Buffer 41 | 42 | /** Coerce a type. */ 43 | export declare function coerceType(type: Type): Type 44 | /** Coerce a type. String are looked up from the ref.types object. */ 45 | export declare function coerceType(type: string): Type 46 | 47 | /** 48 | * Get value after dereferencing buffer. 49 | * That is, first it checks the indirection count of buffer's type, and 50 | * if it's greater than 1 then it merely returns another Buffer, but with 51 | * one level less indirection. 52 | */ 53 | export declare function deref(buffer: Buffer): any 54 | 55 | /** Create clone of the type, with decremented indirection level by 1. */ 56 | export declare function derefType(type: Type): Type 57 | /** Create clone of the type, with decremented indirection level by 1. */ 58 | export declare function derefType(type: string): Type 59 | /** Represents the native endianness of the processor ("LE" or "BE"). */ 60 | export declare const endianness: string 61 | /** Check the indirection level and return a dereferenced when necessary. */ 62 | export declare function get(buffer: Buffer, offset?: number, type?: Type): any 63 | /** Check the indirection level and return a dereferenced when necessary. */ 64 | export declare function get(buffer: Buffer, offset?: number, type?: string): any 65 | /** Get type of the buffer. Create a default type when none exists. */ 66 | export declare function getType(buffer: Buffer): Type 67 | /** Check the NULL. */ 68 | export declare function isNull(buffer: Buffer): boolean 69 | /** Read C string until the first NULL. */ 70 | export declare function readCString(buffer: Buffer, offset?: number): string 71 | 72 | /** 73 | * Read a big-endian signed 64-bit int. 74 | * If there is losing precision, then return a string, otherwise a number. 75 | * @return {number|string} 76 | */ 77 | export declare function readInt64BE(buffer: Buffer, offset?: number): any 78 | 79 | /** 80 | * Read a little-endian signed 64-bit int. 81 | * If there is losing precision, then return a string, otherwise a number. 82 | * @return {number|string} 83 | */ 84 | export declare function readInt64LE(buffer: Buffer, offset?: number): any 85 | 86 | /** Read a JS Object that has previously been written. */ 87 | export declare function readObject( 88 | buffer: Buffer, 89 | offset?: number 90 | ): Record 91 | /** Read data from the pointer. */ 92 | export declare function readPointer( 93 | buffer: Buffer, 94 | offset?: number, 95 | length?: number 96 | ): Buffer 97 | /** 98 | * Read a big-endian unsigned 64-bit int. 99 | * If there is losing precision, then return a string, otherwise a number. 100 | * @return {number|string} 101 | */ 102 | export declare function readUInt64BE(buffer: Buffer, offset?: number): any 103 | 104 | /** 105 | * Read a little-endian unsigned 64-bit int. 106 | * If there is losing precision, then return a string, otherwise a number. 107 | * @return {number|string} 108 | */ 109 | export declare function readUInt64LE(buffer: Buffer, offset?: number): any 110 | 111 | /** Create pointer to buffer. */ 112 | export declare function ref(buffer: Buffer): Buffer 113 | /** Create clone of the type, with incremented indirection level by 1. */ 114 | export declare function refType(type: Type): Type 115 | /** Create clone of the type, with incremented indirection level by 1. */ 116 | export declare function refType(type: string): Type 117 | 118 | /** 119 | * Create buffer with the specified size, with the same address as source. 120 | * This function "attaches" source to the returned buffer to prevent it from 121 | * being garbage collected. 122 | */ 123 | export declare function reinterpret( 124 | buffer: Buffer, 125 | size: number, 126 | offset?: number 127 | ): Buffer 128 | /** 129 | * Scan past the boundary of the buffer's length until it finds size number 130 | * of aligned NULL bytes. 131 | */ 132 | export declare function reinterpretUntilZeros( 133 | buffer: Buffer, 134 | size: number, 135 | offset?: number 136 | ): Buffer 137 | 138 | /** Write pointer if the indirection is 1, otherwise write value. */ 139 | export declare function set( 140 | buffer: Buffer, 141 | offset: number, 142 | value: any, 143 | type?: Type 144 | ): void 145 | /** Write pointer if the indirection is 1, otherwise write value. */ 146 | export declare function set( 147 | buffer: Buffer, 148 | offset: number, 149 | value: any, 150 | type?: string 151 | ): void 152 | /** Write the string as a NULL terminated. Default encoding is utf8. */ 153 | export declare function writeCString( 154 | buffer: Buffer, 155 | offset: number, 156 | string: string, 157 | encoding?: string 158 | ): void 159 | /** Write a big-endian signed 64-bit int. */ 160 | export declare function writeInt64BE( 161 | buffer: Buffer, 162 | offset: number, 163 | input: number 164 | ): void 165 | /** Write a big-endian signed 64-bit int. */ 166 | export declare function writeInt64BE( 167 | buffer: Buffer, 168 | offset: number, 169 | input: string 170 | ): void 171 | /** Write a little-endian signed 64-bit int. */ 172 | export declare function writeInt64LE( 173 | buffer: Buffer, 174 | offset: number, 175 | input: number 176 | ): void 177 | /** Write a little-endian signed 64-bit int. */ 178 | export declare function writeInt64LE( 179 | buffer: Buffer, 180 | offset: number, 181 | input: string 182 | ): void 183 | 184 | /** 185 | * Write the JS Object. This function "attaches" object to buffer to prevent 186 | * it from being garbage collected. 187 | */ 188 | export declare function writeObject( 189 | buffer: Buffer, 190 | offset: number, 191 | object: Record 192 | ): void 193 | 194 | /** 195 | * Write the memory address of pointer to buffer at the specified offset. This 196 | * function "attaches" object to buffer to prevent it from being garbage collected. 197 | */ 198 | export declare function writePointer( 199 | buffer: Buffer, 200 | offset: number, 201 | pointer: Buffer 202 | ): void 203 | 204 | /** Write a little-endian unsigned 64-bit int. */ 205 | export declare function writeUInt64BE( 206 | buffer: Buffer, 207 | offset: number, 208 | input: number 209 | ): void 210 | /** Write a little-endian unsigned 64-bit int. */ 211 | export declare function writeUInt64BE( 212 | buffer: Buffer, 213 | offset: number, 214 | input: string 215 | ): void 216 | 217 | /** 218 | * Attach object to buffer such. 219 | * It prevents object from being garbage collected until buffer does. 220 | */ 221 | export declare function _attach( 222 | buffer: Buffer, 223 | object: Record 224 | ): void 225 | 226 | /** Same as ref.reinterpret, except that this version does not attach buffer. */ 227 | export declare function _reinterpret( 228 | buffer: Buffer, 229 | size: number, 230 | offset?: number 231 | ): Buffer 232 | /** Same as ref.reinterpretUntilZeros, except that this version does not attach buffer. */ 233 | export declare function _reinterpretUntilZeros( 234 | buffer: Buffer, 235 | size: number, 236 | offset?: number 237 | ): Buffer 238 | /** Same as ref.writePointer, except that this version does not attach pointer. */ 239 | export declare function _writePointer( 240 | buffer: Buffer, 241 | offset: number, 242 | pointer: Buffer 243 | ): void 244 | /** Same as ref.writeObject, except that this version does not attach object. */ 245 | export declare function _writeObject( 246 | buffer: Buffer, 247 | offset: number, 248 | object: Record 249 | ): void 250 | 251 | /** Default types. */ 252 | export declare const types: { 253 | void: Type 254 | int64: Type 255 | ushort: Type 256 | int: Type 257 | uint64: Type 258 | float: Type 259 | uint: Type 260 | long: Type 261 | double: Type 262 | int8: Type 263 | ulong: Type 264 | Object: Type 265 | uint8: Type 266 | longlong: Type 267 | CString: Type 268 | int16: Type 269 | ulonglong: Type 270 | bool: Type 271 | uint16: Type 272 | char: Type 273 | byte: Type 274 | int32: Type 275 | uchar: Type 276 | size_t: Type 277 | uint32: Type 278 | short: Type 279 | } 280 | 281 | declare global { 282 | interface Buffer { 283 | type: Type 284 | deref: typeof deref 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/types/ref-struct-napi.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | // Type definitions for ref-struct 3 | // Project: https://github.com/TooTallNate/ref-struct 4 | // Definitions by: Paul Loyd 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | // TypeScript Version: 2.2 7 | 8 | import ref = require("ref-napi") 9 | 10 | /** 11 | * This is the `constructor` of the Struct type that gets returned. 12 | * 13 | * Invoke it with `new` to create a new Buffer instance backing the struct. 14 | * Pass it an existing Buffer instance to use that as the backing buffer. 15 | * Pass in an Object containing the struct fields to auto-populate the 16 | * struct with the data. 17 | * 18 | * @constructor 19 | */ 20 | interface StructType extends ref.Type { 21 | /** Pass it an existing Buffer instance to use that as the backing buffer. */ 22 | new (arg: Buffer, data?: {}): any 23 | new (data?: {}): any 24 | /** Pass it an existing Buffer instance to use that as the backing buffer. */ 25 | (arg: Buffer, data?: {}): any 26 | (data?: {}): any 27 | 28 | fields: { [key: string]: { type: ref.Type } } 29 | 30 | /** 31 | * Adds a new field to the struct instance with the given name and type. 32 | * Note that this function will throw an Error if any instances of the struct 33 | * type have already been created, therefore this function must be called at the 34 | * beginning, before any instances are created. 35 | */ 36 | defineProperty(name: string, type: ref.Type): void 37 | 38 | /** 39 | * Adds a new field to the struct instance with the given name and type. 40 | * Note that this function will throw an Error if any instances of the struct 41 | * type have already been created, therefore this function must be called at the 42 | * beginning, before any instances are created. 43 | */ 44 | defineProperty(name: string, type: string): void 45 | 46 | /** 47 | * Custom for struct type instances. 48 | * @override 49 | */ 50 | toString(): string 51 | } 52 | 53 | /** The struct type meta-constructor. */ 54 | export const Struct: { 55 | new (fields?: object, opt?: object): StructType 56 | new (fields?: any[]): StructType 57 | (fields?: object, opt?: object): StructType 58 | (fields?: any[]): StructType 59 | } 60 | export default Struct 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "exclude":[ 4 | "**/__mocks__" 5 | ], 6 | "compilerOptions": { 7 | "noEmit":false, 8 | "target": "ES6", 9 | "module": "esnext", 10 | "lib": ["dom", "esnext"], 11 | "importHelpers": true, 12 | "declaration": true, 13 | "sourceMap": true, 14 | "rootDir": "./src", 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "strictFunctionTypes": true, 19 | "strictPropertyInitialization": true, 20 | "noImplicitThis": true, 21 | "alwaysStrict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "moduleResolution": "node", 27 | "baseUrl": "./", 28 | "paths": { 29 | "*": ["src/types/*"] 30 | }, 31 | "jsx": "react", 32 | "esModuleInterop": true 33 | } 34 | } 35 | --------------------------------------------------------------------------------