Compare commits

...

195 Commits

Author SHA1 Message Date
SPC-code
5b655a9354
Merge pull request #9 from SciProgCentre/dev
0.2.0
2023-08-23 16:27:13 +03:00
61af2780ed fix repositores 2023-08-23 16:23:27 +03:00
990e4794a1 All done for 0.2.0 release 2023-08-23 16:21:11 +03:00
641daec7e9 ControlsMagix refactor 2023-08-18 20:17:23 +03:00
a9f29f92ca Add magix registry access service 2023-08-18 13:01:46 +03:00
82dbb3a1f0 broadcast -> send 2023-08-16 20:27:23 +03:00
ce03540b37 Add magix message registry service 2023-08-16 18:26:21 +03:00
583fe5b54c Replace JSSC with JSerialComm 2023-08-16 10:42:44 +03:00
69617e73b4 Replace JSSC with JSerialComm 2023-08-16 10:37:02 +03:00
fa4f578a64 Update version 2023-08-03 19:21:48 +03:00
a172da0a3d Add abstraction for writeable magix history 2023-08-03 19:21:35 +03:00
dc2d0094fc add filter implementation for Json 2023-07-30 22:34:47 +03:00
d3d8413837 Add storage API test (inclomplete) 2023-07-29 18:08:25 +03:00
f9e20f8766 Make it buildable again 2023-07-29 11:52:04 +03:00
cf70339a9f Rename field in MagixMessage in accordance with https://github.com/waltz-controls/rfc/issues/12 2023-07-29 09:47:59 +03:00
870cb7ef40 [WIP] Storage API 2023-07-29 09:46:13 +03:00
fcabd9aed4 Documentation update and minor refactoring 2023-07-29 09:45:34 +03:00
405bcd6ba3 Add ZMQ context customization 2023-07-23 14:29:26 +03:00
6f5270ee37 Add MQTT topic specialization 2023-07-23 14:29:11 +03:00
c28944e10f Refactor connection infrastructure 2023-07-23 14:01:47 +03:00
4d4a9fba1c MQTT topic selectors 2023-06-29 09:19:05 +03:00
d1e9b0a5a5 Add device lifecycle state 2023-06-10 16:55:00 +03:00
d0d8869a1e Eternal happiness 2023-06-10 16:28:11 +03:00
20951a0b0f Modbus revision acording to modern standards 2023-06-10 16:17:54 +03:00
33e1aafa01 WIP fixing angles 2023-06-08 10:03:41 +03:00
bfeeed00d5 process images cleanup 2023-06-05 17:12:01 +03:00
166cc03fe2 Add modbus client 2023-06-04 21:18:42 +03:00
2aa26ea802 Update UDP ports design. Rename ktor ports module. 2023-06-04 11:23:26 +03:00
76cec0469f add useProperty subscription 2023-05-30 20:23:43 +03:00
2191bb7a77 Update visualization 2023-05-30 15:02:09 +03:00
9c39f44897 fix async port receive 2023-05-27 22:55:21 +03:00
22359a5570 Remove non-blocking mode for ports to avoid CPU clogging. 2023-05-27 22:44:30 +03:00
ced42779be Fix UDP connection 2023-05-27 21:36:46 +03:00
ab1a478867 Add publishing 2023-05-27 19:50:48 +03:00
7b792afd7a Fix nullability in actions 2023-05-25 10:07:51 +03:00
25d7fe0a8e Update actions signatures to avoid unnecessary nulls 2023-05-25 09:51:27 +03:00
b7be570271 Fix property listener 2023-05-24 09:56:02 +03:00
2a966d8cb3 Minor refactor of property listeners 2023-05-24 09:37:56 +03:00
0612c6e3a2 Modbus registry validation and print 2023-05-21 17:36:26 +03:00
b8a82feed0 Modbus-based implementation of controller 2023-05-21 16:53:42 +03:00
e7d02be849 Add Pi serial port 2023-05-19 21:55:21 +03:00
a5a2edc81a Add Pi serial port 2023-05-19 20:27:08 +03:00
28cb9af267 [WIP] PI 2023-05-18 09:50:39 +03:00
a6bf9b8db6 Generalize nio ports architecture. Add Udp(datagram) port 2023-05-18 09:30:26 +03:00
Alexander Nozik
64e240f6c2
Merge SCI-MR-188: dev 2023-05-12 10:36:42 +00:00
5e91ec9a97 update version 2023-05-12 13:35:34 +03:00
1d7403ef30 Update to stable DataForge 2023-05-09 22:50:35 +03:00
aabf2b85a4 Documentation and DeviceSpec fix 2023-05-08 21:25:25 +03:00
7103786ec9 Stress test demo 2023-05-08 15:39:34 +03:00
53e506893f Add RPC for devices 2023-05-07 21:04:08 +03:00
4c33c16c94 Fix demo 2023-05-07 18:03:46 +03:00
b66e23cca6 Clean up property access syntax 2023-05-07 16:39:58 +03:00
691b2ae67a Clean up property access syntax 2023-05-07 11:13:42 +03:00
86273101b6
Change command for CI 2023-05-03 08:51:19 +00:00
fd1194205c
change docker image for CI 2023-05-03 08:50:52 +00:00
96305b1be6
Update space.kts JVM version 2023-05-03 08:16:46 +00:00
8cd191bb0d Documentation update 2023-05-03 11:05:54 +03:00
e7cfb1d2ba Add opcua test 2023-04-16 12:29:24 +03:00
197675fc15 Add MQTT prototype (not tested) 2023-04-15 20:00:47 +03:00
SPC-code
631eb73d23
Merge pull request #8 from SciProgCentre/dev
Dev
2023-03-04 16:47:54 +03:00
00c66da847 Update pressure sensor demo 2023-03-02 10:07:54 +03:00
ba84553be5 Add port demo. Nullable properties 2023-03-01 20:59:18 +03:00
5b16b07172 Fix build 2023-02-26 22:07:19 +03:00
3ef471d49e Move ZMQ to a separate module 2023-02-26 20:23:30 +03:00
fca718cfc4 Move ZMQ to a separate module 2023-02-25 18:15:19 +03:00
2a386568f9 Refactor magix server flow plugins 2023-02-25 17:45:09 +03:00
fe24c0ee31 Add magix-rfc as a submodule 2023-02-25 10:55:21 +03:00
68805d6f42 Refactor onOpen in device spec 2023-02-25 10:53:53 +03:00
a0fd8913eb Update versions 2023-02-18 20:05:26 +03:00
dc6847196d
Updated dependencies 2022-08-10 19:20:06 +03:00
619c2c280d
Hotfix for jdkRelease problem 2022-08-02 10:12:16 +03:00
4a09e66c42
continue package refactor 2022-08-02 09:46:31 +03:00
c22902cc91
Updata to DataForge 0.6 2022-08-02 09:37:36 +03:00
bd29789545
Change package name to space.kscience 2022-08-02 09:10:02 +03:00
20345f846b
Add rabbitMQ connector (untested) 2022-06-06 14:10:48 +03:00
535e44286c
Add port configuration to magix server 2022-06-06 10:57:24 +03:00
cfaeb964e7
Add benchmark demo. Fix some issues with RSocket 2022-06-05 19:06:50 +03:00
025a444db8
Fix demos and project structure 2022-06-05 12:26:34 +03:00
879073e339
Add native rsocket-tcp 2022-06-05 10:28:46 +03:00
07adf143cf
Remove generic from MagixMessage 2022-06-04 15:46:13 +03:00
eb7507191e
Refactor xodus storage for local history 2022-06-02 09:21:07 +03:00
b6f3769529
Refactor xodus storage for local history 2022-06-01 10:11:12 +03:00
ba4d2abd27
Add native targets 2022-05-24 21:28:42 +03:00
3d0b888c11
Update dependencies 2022-05-23 23:30:38 +03:00
2e7eefeba9
rename methods 2022-01-18 18:39:19 +03:00
427ecbf91a
Refactor storage 2022-01-18 18:25:40 +03:00
786e1637b4
Fix ktor imports 2022-01-18 17:37:25 +03:00
Atos1337
0c4c2e4cc0 Fix issues 2022-01-17 12:19:35 +03:00
Atos1337
23e821d2c2 Merge remote-tracking branch 'origin/victorsam/finish-hostory-access' into victorsam/finish-hostory-access
# Conflicts:
#	controls-mongo/build.gradle.kts
#	controls-mongo/src/main/kotlin/ru/mipt/npm/controls/mongo/connections.kt
#	controls-xodus/src/main/kotlin/ru/mipt/npm/controls/xodus/connections.kt
#	controls-xodus/src/main/kotlin/ru/mipt/npm/controls/xodus/util/queries.kt
#	demo/car/src/main/kotlin/ru/mipt/npm/controls/demo/car/VirtualCarController.kt
2022-01-15 16:01:50 +03:00
Atos1337
21c13ce3af Update propertyHistoryTest for xodus 2022-01-15 16:00:23 +03:00
Atos1337
cbdd6a477b Refactor asynchronous storage api 2022-01-15 15:42:26 +03:00
Atos1337
89bb132e36 Refactor synchronous storage api 2022-01-14 22:00:52 +03:00
Atos1337
3ebbec35fe Add some docs 2022-01-09 12:48:26 +03:00
Atos1337
fe1575c5ee Add getPropertyHistory for xodus 2022-01-09 12:48:26 +03:00
Atos1337
f977aca237 Add storeInMongo for magix server 2022-01-09 12:48:26 +03:00
Atos1337
0c7cd951aa Make this work 2022-01-09 12:48:26 +03:00
798c8eb4ef
Merge remote-tracking branch 'space/dev' into dev
# Conflicts:
#	gradle/wrapper/gradle-wrapper.properties
2021-12-24 20:52:40 +03:00
29c0291fa3
Add details for device description message.
Update build plugin and kotlin.
2021-12-24 20:52:04 +03:00
Atos1337
7ba36f2eb1 Add some docs 2021-12-16 23:14:32 +03:00
Atos1337
476dbfdeaf Add getPropertyHistory for xodus 2021-12-16 18:18:47 +03:00
Atos1337
0c4d2fc9e1 Add storeInMongo for magix server 2021-12-16 17:25:35 +03:00
Atos1337
dec45b6050 Make this work 2021-12-16 16:54:48 +03:00
cdd1286365 Merge CONTROLS-MR-11: Try to add xodus dependency 2021-12-15 12:28:25 +00:00
76846a50cd
Some refactoring 2021-12-11 17:59:19 +03:00
Atos1337
f880b2d637 Move to xodus-serialization 2021-12-04 02:15:57 +03:00
Atos1337
e07780e7da Add entityStoreFactory and refactor mongo/xodus connections.kt 2021-12-04 02:10:10 +03:00
Atos1337
b79ad40a9b Fix some issues 2021-12-04 00:52:22 +03:00
Atos1337
cfd7bce2f3 Add some tests for encoding/decoding 2021-12-04 00:35:12 +03:00
Atos1337
e793cdefee Add more extensions for encoder and decoder 2021-12-03 23:55:43 +03:00
Atos1337
f77f6b7211 Add encoding and decoding to/from entity by json encoding/decoding 2021-12-03 23:29:13 +03:00
5da1559882 Architecture rework 2021-11-27 13:37:39 +03:00
Atos1337
312cd06706 Add mongo module 2021-11-27 00:52:14 +03:00
Atos1337
3639783b4e Add connection to xodus from magix server 2021-11-26 17:26:41 +03:00
Atos1337
0b14c7ed7f Make it work 2021-11-26 16:25:09 +03:00
85f6b81334 Architecture rework 2021-11-20 13:13:32 +03:00
Atos1337
099649f090 Add fromTo util function for storeTransacton 2021-11-19 23:03:54 +03:00
Atos1337
53f9af3ba4 Add converters from Entity and ConvertersTest 2021-11-19 22:15:50 +03:00
Atos1337
1d8cc33c91 Refactor connection to magix 2021-11-15 17:18:06 +03:00
Atos1337
a1c3902b92 Add entity store at controller 2021-11-15 16:38:28 +03:00
Atos1337
81a1afa2b7 Add converters for PropertyChangedMessage and add overload for magix connection 2021-11-15 15:35:51 +03:00
Atos1337
817d32a7e9 Try to add xodus dependency 2021-11-15 14:40:19 +03:00
df6defd637 add duplicating local filter for RSocketMagixEndpoint 2021-11-13 13:25:06 +03:00
Atos1337
039e0d083b Refactor MagixVirtualCar.kt 2021-11-06 09:12:19 +03:00
Atos1337
dcd496660c Implement MagixVirtualCar 2021-11-04 15:52:49 +03:00
Atos1337
289ff6ffd0 Add magix for debugging and moved DeviceSpec to IVirtualCar 2021-11-02 22:21:32 +03:00
4dc33c012d Refactor car demo. Add meta property. 2021-10-30 13:08:45 +03:00
Atos1337
c1a1b6e696 Merge remote-tracking branch 'origin/victorsam/demo' into victorsam/demo
# Conflicts:
#	demo/src/main/kotlin/ru/mipt/npm/controls/demo/virtual_car/VirtualCar.kt
#	demo/src/main/kotlin/ru/mipt/npm/controls/demo/virtual_car/VirtualCarController.kt
2021-10-28 11:17:45 +03:00
Atos1337
357fcec4fe Fix issues 2021-10-28 11:16:19 +03:00
Atos1337
14015178ab Create Coordinates class instead of Pair and refactor state update 2021-10-27 13:55:15 +03:00
Atos1337
bd23ca1129 Add location property in virtual car 2021-10-27 13:55:15 +03:00
Atos1337
24c5188c30 Change VirtualCar state and add controller 2021-10-27 13:55:15 +03:00
Atos1337
77496aedec Implement VirtualCar 2021-10-27 13:55:14 +03:00
7b56436983 Add open to device initializer 2021-10-23 20:00:00 +03:00
ed2a2a29af Remove stand-alone properties and use specs instead 2021-10-23 19:44:13 +03:00
Atos1337
418a97f99a Create Coordinates class instead of Pair and refactor state update 2021-10-23 12:12:19 +03:00
Alexander Nozik
a20fb97c02
Merge pull request #6 from mipt-npm/dev
Dev
2021-10-23 11:02:48 +03:00
1b021ca5ca Merge remote-tracking branch 'origin/master' into dev
# Conflicts:
#	README.md
2021-10-23 11:02:31 +03:00
Atos1337
b1d9d8e8ba Add location property in virtual car 2021-10-20 16:59:30 +03:00
Atos1337
7c4d0a5f36 Change VirtualCar state and add controller 2021-10-20 16:50:18 +03:00
Atos1337
5322bf743b Implement VirtualCar 2021-10-20 12:53:48 +03:00
7ae75d8bd4 Add proper read after write for properties 2021-10-11 22:55:15 +03:00
Alexander Nozik
ded8790f39
Update README.md 2021-10-06 22:39:38 +03:00
b3898d2ab3 Add timestamp to all messages 2021-09-29 12:10:16 +03:00
553d819c54 Fix property names and types for DeviceSpec 2021-09-29 10:43:19 +03:00
3aa00ec491 Cleanup DeviceWithSpec API. Now all public things are in Spec 2021-08-04 14:14:05 +03:00
d503f0499e Add Eclipse Milo client 2021-08-03 21:05:36 +03:00
3606bc3a46 Migration to DF-0.5 and bug fixes 2021-08-02 16:42:19 +03:00
b068403429 Remove unnecessary active flag in ZMQ client 2021-07-19 09:41:41 +03:00
64d3f04469 Add additional application configuration to magix server 2021-07-06 14:02:51 +03:00
1cf7058778 add auto-reload to magix server status page 2021-07-05 17:31:46 +03:00
d0f22eec93 Use blocking access to solve ZMQ performance issues. 2021-07-04 14:44:53 +03:00
6e01e28015 ZMQ fully functional 2021-07-03 14:16:31 +03:00
89190db653 ZMQ support(untested) 2021-06-28 20:58:27 +03:00
596e3a0cfc Refactoring properties getters 2021-06-26 20:34:40 +03:00
fe3958fd08 Add alternative device syntax 2021-06-25 19:57:04 +03:00
e182af403f Add alternative device syntax 2021-06-24 10:41:42 +03:00
b1d3ba59bc Add alternative device syntax 2021-06-23 22:29:49 +03:00
a87b46cd2b Add alternative device syntax 2021-06-23 22:23:44 +03:00
28e6e24cf7 RSocket and demo device name refactor 2021-06-22 17:37:22 +03:00
3cbabd5d4b ZMQ client implementation 2021-06-21 15:37:36 +03:00
5e9cbab94d package refactor 2021-06-21 14:09:59 +03:00
d79d345a44 doocs payload 2021-06-20 21:11:13 +03:00
87440f688f Fix package names and imports 2021-06-20 17:35:50 +03:00
a510a8aedf Fix package names and imports 2021-06-20 17:05:07 +03:00
a093f1921e Implement Tango messages 2021-06-20 16:37:03 +03:00
6e4bf51a6f Move serializer to endpoint parameter 2021-06-20 14:46:02 +03:00
e3ea2304ef package rename 2021-04-11 15:12:09 +03:00
bf51768fe1 Upgrade versions 2021-04-05 17:41:36 +03:00
cb22374da5 Java rsocket bindings 2021-02-09 16:39:39 +03:00
b883bece48 message refactoring 2021-01-10 20:50:45 +03:00
d716eac07f fix build inconsistencies 2021-01-10 19:19:04 +03:00
2b4503c2fa Merge branch 'specialized-messages' into dev
# Conflicts:
#	build.gradle.kts
#	dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/HubController.kt
#	dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt
#	magix/magix-service/src/commonMain/kotlin/hep/dataforge/magix/service/RSocketMagixEndpoint.kt
2021-01-10 18:45:19 +03:00
65acfe824b fix build inconsistencies 2021-01-10 18:41:52 +03:00
Alexander Nozik
319665e330
Update README.md 2021-01-07 20:13:10 +03:00
93c82db08e Message class hierarchy. 2020-12-15 14:01:03 +03:00
d68f5a9840 A lot of small refactoring in html 2020-12-12 10:44:41 +03:00
17beb29217 Message class hierarchy. 2020-12-02 12:35:16 +03:00
7fc27c49a3 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	build.gradle.kts
#	dataforge-magix-client/src/commonMain/kotlin/hep/dataforge/control/client/magixClient.kt
#	magix/magix-service/src/commonMain/kotlin/hep/dataforge/magix/service/RSocketMagixEndpoint.kt
2020-12-01 10:46:02 +03:00
92ab801967 Message format rework WIP 2020-12-01 10:33:17 +03:00
356f3e15a6 Remove CoroutineScope from Endpoint interface 2020-11-09 19:22:34 +03:00
dcf08d4426 Minor refactoring to adopt SharedFlow 2020-11-06 16:28:21 +03:00
78ee05371b Replace Scheme by data class in DeviceMessage 2020-11-05 11:29:40 +03:00
f3cfe9c6db Rsocket tcp client 2020-11-04 14:49:57 +03:00
5c90e8e07b Magix format converter 2020-11-03 22:33:13 +03:00
32c29240d2 Replace even id counter with uuid 2020-11-03 22:25:02 +03:00
f0acbbb8cc Add rsocket service 2020-11-03 18:54:52 +03:00
e5883dc318 Add rsocket server 2020-11-02 17:53:53 +03:00
8c8d53b187 Move to stand-alone sse 2020-10-21 23:16:15 +03:00
599d08b62a More or less working motors app 2020-10-12 15:37:46 +03:00
62dc6ef127 More or less working motors 2020-10-11 22:37:39 +03:00
23c66d9703 Fix motors parser 2020-10-10 22:38:43 +03:00
eb3121aed4 Refactor delegated properties 2020-10-10 17:21:28 +03:00
4b9f535002 refactoring 2020-10-08 11:20:08 +03:00
64043cf2c0 Fix motors debug server 2020-10-06 22:45:33 +03:00
dbf0466c64 Update and finalize message design 2020-10-06 15:33:15 +03:00
241 changed files with 13124 additions and 3044 deletions

24
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Gradle build
on:
push:
branches: [ dev, master ]
pull_request:
jobs:
build:
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3.5.1
with:
java-version: '11'
distribution: 'liberica'
cache: 'gradle'
- name: Gradle Wrapper Validation
uses: gradle/wrapper-validation-action@v1.0.4
- name: Gradle Build
uses: gradle/gradle-build-action@v2.4.2
with:
arguments: test jvmTest

View File

@ -1,17 +0,0 @@
name: Gradle build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Build with Gradle
run: ./gradlew build

31
.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Dokka publication
on:
workflow_dispatch:
release:
types: [ created ]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v3.0.0
- uses: actions/setup-java@v3.0.0
with:
java-version: 11
distribution: liberica
- name: Cache konan
uses: actions/cache@v3.0.1
with:
path: ~/.konan
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }}
restore-keys: |
${{ runner.os }}-gradle-
- uses: gradle/gradle-build-action@v2.4.2
with:
arguments: dokkaHtmlMultiModule --no-parallel
- uses: JamesIves/github-pages-deploy-action@v4.3.0
with:
branch: gh-pages
folder: build/dokka/htmlMultiModule

50
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,50 @@
name: Gradle publish
on:
workflow_dispatch:
release:
types: [ created ]
jobs:
publish:
environment:
name: publish
strategy:
matrix:
os: [ macOS-latest, windows-latest ]
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v3.0.0
- uses: actions/setup-java@v3.10.0
with:
java-version: 11
distribution: liberica
- name: Cache konan
uses: actions/cache@v3.0.1
with:
path: ~/.konan
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Publish Windows Artifacts
if: matrix.os == 'windows-latest'
uses: gradle/gradle-build-action@v2.4.2
with:
arguments: |
publishAllPublicationsToSpaceRepository
-Ppublishing.targets=all
-Ppublishing.space.user=${{ secrets.SPACE_APP_ID }}
-Ppublishing.space.token=${{ secrets.SPACE_APP_SECRET }}
- name: Publish Mac Artifacts
if: matrix.os == 'macOS-latest'
uses: gradle/gradle-build-action@v2.4.2
with:
arguments: |
publishMacosX64PublicationToSpaceRepository
publishMacosArm64PublicationToSpaceRepository
publishIosX64PublicationToSpaceRepository
publishIosArm64PublicationToSpaceRepository
publishIosSimulatorArm64PublicationToSpaceRepository
-Ppublishing.targets=all
-Ppublishing.space.user=${{ secrets.SPACE_APP_ID }}
-Ppublishing.space.token=${{ secrets.SPACE_APP_SECRET }}

4
.gitignore vendored
View File

@ -1,7 +1,11 @@
# Created by .ignore support plugin (hsz.mobi)
.idea/
.gradle
*.iws
*.iml
*.ipr
out/
build/
!gradle-wrapper.jar

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "magix/rfc"]
path = magix/rfc
url = https://github.com/waltz-controls/rfc

45
.space.kts Normal file
View File

@ -0,0 +1,45 @@
import kotlin.io.path.readText
job("Build") {
gradlew("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3", "build")
}
job("Publish") {
startOn {
gitPush { enabled = false }
}
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
env["SPACE_USER"] = "{{ project:space_user }}"
env["SPACE_TOKEN"] = "{{ project:space_token }}"
kotlinScript { api ->
val spaceUser = System.getenv("SPACE_USER")
val spaceToken = System.getenv("SPACE_TOKEN")
// write the version to the build directory
api.gradlew("version")
//read the version from build file
val version = java.nio.file.Path.of("build/project-version.txt").readText()
val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
"-" + api.gitRevision().take(7)
} else {
""
}
api.space().projects.automation.deployments.start(
project = api.projectIdentifier(),
targetIdentifier = TargetIdentifier.Key("maps-kt"),
version = version+revisionSuffix,
// automatically update deployment status based on the status of a job
syncWithAutomationJob = true
)
api.gradlew(
"publishAllPublicationsToSpaceRepository",
"-Ppublishing.space.user=\"$spaceUser\"",
"-Ppublishing.space.token=\"$spaceToken\"",
)
}
}
}

1
.space/CODEOWNERS Normal file
View File

@ -0,0 +1 @@
./space/* "Project Admin"

32
CHANGELOG.md Normal file
View File

@ -0,0 +1,32 @@
# Changelog
## Unreleased
### Added
- Core interfaces for building a device server
- Magix service for binding controls devices (both as RPC client and server)
- A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
- A client and server connectors for OPC-UA via Eclipse Milo
- Implementation of byte ports on top os ktor-io asynchronous API
- Implementation of direct serial port communication with JSerialComm
- A combined Magix event loop server with web server for visualization.
- An API for stand-alone Controls-kt device or a hub.
- An implementation of controls-storage on top of JetBrains Xodus.
- A kotlin API for magix standard and some zero-dependency magix services
- Java API to work with magix endpoints without Kotlin
- MQTT client magix endpoint
- RabbitMQ client magix endpoint
- Magix endpoint (client) based on RSocket
- A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
- Magix history database API
- ZMQ client endpoint for Magix
### Changed
### Deprecated
### Removed
### Fixed
### Security

199
README.md
View File

@ -1,25 +1,28 @@
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
# DataForge-control
# Controls.kt
DataForge-control is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
Controls.kt (former DataForge-control) is a data acquisition framework (work in progress). It is based on DataForge, a software framework for automated data processing.
This repository contains a prototype of API and simple implementation
of a slow control system, including a demo.
DataForge-control uses some concepts and modules of DataForge,
such as `Meta` (immutable tree-like structure) and `MetaItem` (which
includes a scalar value, or a tree of values, easily convertable to/from JSON
if needed).
Controls.kt uses some concepts and modules of DataForge,
such as `Meta` (tree-like value structure).
To learn more about DataForge, please consult the following URLs:
* [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core)
* [DataForge documentation](http://npm.mipt.ru/dataforge/)
* [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/)
* [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core)
* [DataForge documentation](http://npm.mipt.ru/dataforge/)
* [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/)
DataForge-control is a [Kotlin-multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html)
application. Asynchronous operations are implemented with
[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) library.
## Materials and publications
* Video - [A general overview seminar](https://youtu.be/LO-qjWgXMWc)
* Video - [A seminar about the system mechanics](https://youtu.be/wES0RV5GpoQ)
* Article - [A Novel Solution for Controlling Hardware Components of Accelerators and Beamlines](https://www.preprints.org/manuscript/202108.0336/v1)
### Features
Among other things, you can:
@ -28,19 +31,173 @@ Among other things, you can:
- Property values can be cached in the system and requested from devices as needed, asynchronously.
- Connect devices to event bus via bidirectional message flows.
### `dataforge-control-core` module packages
Example view of a demo:
- `api` - defines API for device management. The main class here is
[`Device`](dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt).
Generally, a Device has Properties that can be read and written. Also, some Actions
can optionally be applied on a device (may or may not affect properties).
![](docs/pictures/demo-view.png)
- `base` - contains baseline `Device` implementation
[`DeviceBase`](dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt)
and property implementation, including property asynchronous flows.
## Documentation
* [Creating a device](docs/Device%20and%20DeviceSpec.md)
## Modules
### [controls-core](controls-core)
> Core interfaces for building a device server
>
> **Maturity**: EXPERIMENTAL
>
> **Features:**
> - [device](controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt) : Device API with subscription (asynchronous and pseudo-synchronous properties)
> - [deviceMessage](controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt) : Specification for messages used to communicate between Controls-kt devices.
> - [deviceHub](controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt) : Grouping of devices into local tree-like hubs.
> - [deviceSpec](controls-core/src/commonMain/kotlin/space/kscience/controls/spec) : Mechanics and type-safe builders for devices. Including separation of device specification and device state.
> - [deviceManager](controls-core/src/commonMain/kotlin/space/kscience/controls/manager) : DataForge DI integration for devices. Includes device builders.
> - [ports](controls-core/src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
### [controls-magix](controls-magix)
> Magix service for binding controls devices (both as RPC client and server)
>
> **Maturity**: EXPERIMENTAL
>
> **Features:**
> - [controlsMagix](controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt) : Connect a `DeviceManage` with one or many devices to the Magix endpoint
> - [DeviceClient](controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt) : A remote connector to Controls-kt device via Magix
### [controls-modbus](controls-modbus)
> A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
>
> **Maturity**: EXPERIMENTAL
>
> **Features:**
> - [modbusRegistryMap](controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt) : Type-safe modbus registry map. Allows to define both single-register and multi-register entries (using DataForge IO).
Automatically checks consistency.
> - [modbusProcessImage](controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt) : Binding of slave (server) modbus device to Controls-kt device
> - [modbusDevice](controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt) : A device with additional methods to work with modbus registers.
### [controls-opcua](controls-opcua)
> A client and server connectors for OPC-UA via Eclipse Milo
>
> **Maturity**: EXPERIMENTAL
>
> **Features:**
> - [opcuaClient](controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client) : Connect a Controls-kt as a client to OPC UA server
> - [opcuaServer](controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server) : Create an OPC UA server on top of Controls-kt device (or device hub)
### [controls-pi](controls-pi)
> Utils to work with controls-kt on Raspberry pi
>
> **Maturity**: EXPERIMENTAL
### [controls-ports-ktor](controls-ports-ktor)
> Implementation of byte ports on top os ktor-io asynchronous API
>
> **Maturity**: PROTOTYPE
### [controls-serial](controls-serial)
> Implementation of direct serial port communication with JSerialComm
>
> **Maturity**: EXPERIMENTAL
### [controls-server](controls-server)
> A combined Magix event loop server with web server for visualization.
>
> **Maturity**: PROTOTYPE
### [controls-storage](controls-storage)
> An API for stand-alone Controls-kt device or a hub.
>
> **Maturity**: PROTOTYPE
### [demo](demo)
>
> **Maturity**: EXPERIMENTAL
### [magix](magix)
>
> **Maturity**: EXPERIMENTAL
### [controls-storage/controls-xodus](controls-storage/controls-xodus)
> An implementation of controls-storage on top of JetBrains Xodus.
>
> **Maturity**: PROTOTYPE
### [demo/all-things](demo/all-things)
>
> **Maturity**: EXPERIMENTAL
### [demo/car](demo/car)
>
> **Maturity**: EXPERIMENTAL
### [demo/echo](demo/echo)
>
> **Maturity**: EXPERIMENTAL
### [demo/magix-demo](demo/magix-demo)
>
> **Maturity**: EXPERIMENTAL
### [demo/many-devices](demo/many-devices)
>
> **Maturity**: EXPERIMENTAL
### [demo/mks-pdr900](demo/mks-pdr900)
>
> **Maturity**: EXPERIMENTAL
### [demo/motors](demo/motors)
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-api](magix/magix-api)
> A kotlin API for magix standard and some zero-dependency magix services
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-java-endpoint](magix/magix-java-endpoint)
> Java API to work with magix endpoints without Kotlin
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-mqtt](magix/magix-mqtt)
> MQTT client magix endpoint
>
> **Maturity**: PROTOTYPE
### [magix/magix-rabbit](magix/magix-rabbit)
> RabbitMQ client magix endpoint
>
> **Maturity**: PROTOTYPE
### [magix/magix-rsocket](magix/magix-rsocket)
> Magix endpoint (client) based on RSocket
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-server](magix/magix-server)
> A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-storage](magix/magix-storage)
> Magix history database API
>
> **Maturity**: PROTOTYPE
### [magix/magix-zmq](magix/magix-zmq)
> ZMQ client endpoint for Magix
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-storage/magix-storage-xodus](magix/magix-storage/magix-storage-xodus)
>
> **Maturity**: PROTOTYPE
- `controllers` - implements Message Controller that can be attached to the event bus, Message
and Property flows.
### `demo` module
@ -49,7 +206,3 @@ the current time. The device is configurable via a simple TornadoFX-based contro
You can run a demo by executing `application/run` Gradle task.
The graphs are displayed using [plotly.kt](https://github.com/mipt-npm/plotly.kt) library.
Example view of a demo:
![](docs/pictures/demo-view.png)

View File

@ -1,30 +1,37 @@
import space.kscience.gradle.isInDevelopment
import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam
plugins {
id("ru.mipt.npm.project")
kotlin("jvm") apply false
kotlin("js") apply false
id("space.kscience.gradle.project")
}
val dataforgeVersion: String by extra("0.2.0-dev-3")
val ktorVersion: String by extra("1.4.1")
val dataforgeVersion: String by extra("0.6.2-dev-3")
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
val rsocketVersion by extra("0.15.4")
val xodusVersion by extra("2.0.1")
allprojects {
repositories {
mavenLocal()
maven("https://dl.bintray.com/pdvrieze/maven")
maven("http://maven.jzy3d.org/releases")
maven("https://kotlin.bintray.com/js-externals")
maven("https://maven.pkg.github.com/altavir/kotlin-logging/")
group = "space.kscience"
version = "0.2.0"
repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
}
group = "hep.dataforge"
version = "0.0.1"
}
ksciencePublish {
githubProject = "dataforge-control"
bintrayRepo = "dataforge"
pom("https://github.com/SciProgCentre/controls.kt") {
useApache2Licence()
useSPCTeam()
}
github("controls.kt", "SciProgCentre")
space(
if (isInDevelopment) {
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
} else {
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
}
)
}
apiValidation {
validationDisabled = true
}
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")

33
controls-core/README.md Normal file
View File

@ -0,0 +1,33 @@
# Module controls-core
Core interfaces for building a device server
## Features
- [device](src/commonMain/kotlin/space/kscience/controls/api/Device.kt) : Device API with subscription (asynchronous and pseudo-synchronous properties)
- [deviceMessage](src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt) : Specification for messages used to communicate between Controls-kt devices.
- [deviceHub](src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt) : Grouping of devices into local tree-like hubs.
- [deviceSpec](src/commonMain/kotlin/space/kscience/controls/spec) : Mechanics and type-safe builders for devices. Including separation of device specification and device state.
- [deviceManager](src/commonMain/kotlin/space/kscience/controls/manager) : DataForge DI integration for devices. Includes device builders.
- [ports](src/commonMain/kotlin/space/kscience/controls/ports) : Working with asynchronous data sending and receiving raw byte arrays
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-core:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-core:0.2.0")
}
```

View File

@ -0,0 +1,915 @@
public final class space/kscience/controls/api/ActionDescriptor {
public static final field Companion Lspace/kscience/controls/api/ActionDescriptor$Companion;
public synthetic fun <init> (ILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;)V
public final fun getInfo ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun setInfo (Ljava/lang/String;)V
public static final synthetic fun write$Self (Lspace/kscience/controls/api/ActionDescriptor;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/ActionDescriptor$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/ActionDescriptor$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/ActionDescriptor;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/ActionDescriptor;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/ActionDescriptor$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/ActionExecuteMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/ActionExecuteMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lspace/kscience/dataforge/names/Name;
public final fun component5 ()Lspace/kscience/dataforge/names/Name;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/ActionExecuteMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/ActionExecuteMessage;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/ActionExecuteMessage;
public fun equals (Ljava/lang/Object;)Z
public final fun getAction ()Ljava/lang/String;
public final fun getArgument ()Lspace/kscience/dataforge/meta/Meta;
public fun getComment ()Ljava/lang/String;
public final fun getRequestId ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/ActionExecuteMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/ActionExecuteMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/ActionExecuteMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/ActionExecuteMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/ActionExecuteMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/ActionExecuteMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/ActionResultMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/ActionResultMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lspace/kscience/dataforge/names/Name;
public final fun component5 ()Lspace/kscience/dataforge/names/Name;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/ActionResultMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/ActionResultMessage;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/ActionResultMessage;
public fun equals (Ljava/lang/Object;)Z
public final fun getAction ()Ljava/lang/String;
public fun getComment ()Ljava/lang/String;
public final fun getRequestId ()Ljava/lang/String;
public final fun getResult ()Lspace/kscience/dataforge/meta/Meta;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/ActionResultMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/ActionResultMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/ActionResultMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/ActionResultMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/ActionResultMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/ActionResultMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/BinaryNotificationMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/BinaryNotificationMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lspace/kscience/dataforge/names/Name;
public final fun component3 ()Lspace/kscience/dataforge/names/Name;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/BinaryNotificationMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/BinaryNotificationMessage;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/BinaryNotificationMessage;
public fun equals (Ljava/lang/Object;)Z
public final fun getBinaryID ()Ljava/lang/String;
public fun getComment ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/BinaryNotificationMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/BinaryNotificationMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/BinaryNotificationMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/BinaryNotificationMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/BinaryNotificationMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/BinaryNotificationMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/DescriptionMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/DescriptionMessage$Companion;
public synthetic fun <init> (ILspace/kscience/dataforge/meta/Meta;Ljava/util/Collection;Ljava/util/Collection;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Lspace/kscience/dataforge/meta/Meta;Ljava/util/Collection;Ljava/util/Collection;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Lspace/kscience/dataforge/meta/Meta;Ljava/util/Collection;Ljava/util/Collection;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component2 ()Ljava/util/Collection;
public final fun component3 ()Ljava/util/Collection;
public final fun component4 ()Lspace/kscience/dataforge/names/Name;
public final fun component5 ()Lspace/kscience/dataforge/names/Name;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Lkotlinx/datetime/Instant;
public final fun copy (Lspace/kscience/dataforge/meta/Meta;Ljava/util/Collection;Ljava/util/Collection;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/DescriptionMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/DescriptionMessage;Lspace/kscience/dataforge/meta/Meta;Ljava/util/Collection;Ljava/util/Collection;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/DescriptionMessage;
public fun equals (Ljava/lang/Object;)Z
public final fun getActions ()Ljava/util/Collection;
public fun getComment ()Ljava/lang/String;
public final fun getDescription ()Lspace/kscience/dataforge/meta/Meta;
public final fun getProperties ()Ljava/util/Collection;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/DescriptionMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/DescriptionMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/DescriptionMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/DescriptionMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/DescriptionMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/DescriptionMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/DescriptorsKt {
public static final fun metaDescriptor (Lspace/kscience/controls/api/PropertyDescriptor;Lkotlin/jvm/functions/Function1;)V
}
public abstract interface class space/kscience/controls/api/Device : java/lang/AutoCloseable, kotlinx/coroutines/CoroutineScope, space/kscience/dataforge/context/ContextAware {
public static final field Companion Lspace/kscience/controls/api/Device$Companion;
public static final field DEVICE_TARGET Ljava/lang/String;
public fun close ()V
public abstract fun execute (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun execute$default (Lspace/kscience/controls/api/Device;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public abstract fun getActionDescriptors ()Ljava/util/Collection;
public abstract fun getLifecycleState ()Lspace/kscience/controls/api/DeviceLifecycleState;
public abstract fun getMessageFlow ()Lkotlinx/coroutines/flow/Flow;
public fun getMeta ()Lspace/kscience/dataforge/meta/Meta;
public abstract fun getProperty (Ljava/lang/String;)Lspace/kscience/dataforge/meta/Meta;
public abstract fun getPropertyDescriptors ()Ljava/util/Collection;
public abstract fun invalidate (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun open (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun open$suspendImpl (Lspace/kscience/controls/api/Device;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun readProperty (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun writeProperty (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/api/Device$Companion {
public static final field DEVICE_TARGET Ljava/lang/String;
}
public final class space/kscience/controls/api/DeviceErrorMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/DeviceErrorMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lspace/kscience/dataforge/names/Name;
public final fun component5 ()Lspace/kscience/dataforge/names/Name;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/DeviceErrorMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/DeviceErrorMessage;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/DeviceErrorMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getComment ()Ljava/lang/String;
public final fun getErrorMessage ()Ljava/lang/String;
public final fun getErrorStackTrace ()Ljava/lang/String;
public final fun getErrorType ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/DeviceErrorMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/DeviceErrorMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/DeviceErrorMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/DeviceErrorMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/DeviceErrorMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/DeviceErrorMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public abstract interface class space/kscience/controls/api/DeviceHub : space/kscience/dataforge/provider/Provider {
public static final field Companion Lspace/kscience/controls/api/DeviceHub$Companion;
public fun content (Ljava/lang/String;)Ljava/util/Map;
public fun getDefaultChainTarget ()Ljava/lang/String;
public fun getDefaultTarget ()Ljava/lang/String;
public abstract fun getDevices ()Ljava/util/Map;
}
public final class space/kscience/controls/api/DeviceHub$Companion {
}
public final class space/kscience/controls/api/DeviceHubKt {
public static final fun execute (Lspace/kscience/controls/api/DeviceHub;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun get (Lspace/kscience/controls/api/DeviceHub;Ljava/lang/String;)Lspace/kscience/controls/api/Device;
public static final fun get (Lspace/kscience/controls/api/DeviceHub;Lspace/kscience/dataforge/names/Name;)Lspace/kscience/controls/api/Device;
public static final fun get (Lspace/kscience/controls/api/DeviceHub;Lspace/kscience/dataforge/names/NameToken;)Lspace/kscience/controls/api/Device;
public static final fun getOrNull (Lspace/kscience/controls/api/DeviceHub;Ljava/lang/String;)Lspace/kscience/controls/api/Device;
public static final fun getOrNull (Lspace/kscience/controls/api/DeviceHub;Lspace/kscience/dataforge/names/Name;)Lspace/kscience/controls/api/Device;
public static final fun readProperty (Lspace/kscience/controls/api/DeviceHub;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun writeProperty (Lspace/kscience/controls/api/DeviceHub;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/api/DeviceKt {
public static final fun getAllProperties (Lspace/kscience/controls/api/Device;)Lspace/kscience/dataforge/meta/Meta;
public static final fun getOrReadProperty (Lspace/kscience/controls/api/Device;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun onPropertyChange (Lspace/kscience/controls/api/Device;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;
}
public final class space/kscience/controls/api/DeviceLifecycleState : java/lang/Enum {
public static final field CLOSED Lspace/kscience/controls/api/DeviceLifecycleState;
public static final field INIT Lspace/kscience/controls/api/DeviceLifecycleState;
public static final field OPEN Lspace/kscience/controls/api/DeviceLifecycleState;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lspace/kscience/controls/api/DeviceLifecycleState;
public static fun values ()[Lspace/kscience/controls/api/DeviceLifecycleState;
}
public final class space/kscience/controls/api/DeviceLogMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/DeviceLogMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component3 ()Lspace/kscience/dataforge/names/Name;
public final fun component4 ()Lspace/kscience/dataforge/names/Name;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/DeviceLogMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/DeviceLogMessage;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/DeviceLogMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getComment ()Ljava/lang/String;
public final fun getData ()Lspace/kscience/dataforge/meta/Meta;
public final fun getMessage ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/DeviceLogMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/DeviceLogMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/DeviceLogMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/DeviceLogMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/DeviceLogMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/DeviceLogMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public abstract class space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/DeviceMessage$Companion;
public synthetic fun <init> (ILkotlinx/serialization/internal/SerializationConstructorMarker;)V
public abstract fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public abstract fun getComment ()Ljava/lang/String;
public abstract fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public abstract fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public abstract fun getTime ()Lkotlinx/datetime/Instant;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/DeviceMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/DeviceMessage$Companion {
public final fun error (Ljava/lang/Throwable;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;)Lspace/kscience/controls/api/DeviceErrorMessage;
public static synthetic fun error$default (Lspace/kscience/controls/api/DeviceMessage$Companion;Ljava/lang/Throwable;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;ILjava/lang/Object;)Lspace/kscience/controls/api/DeviceErrorMessage;
public final fun fromMeta (Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/api/DeviceMessage;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/DeviceMessageKt {
public static final fun toEnvelope (Lspace/kscience/controls/api/DeviceMessage;)Lspace/kscience/dataforge/io/Envelope;
public static final fun toMeta (Lspace/kscience/controls/api/DeviceMessage;)Lspace/kscience/dataforge/meta/Meta;
}
public final class space/kscience/controls/api/EmptyDeviceMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/EmptyDeviceMessage$Companion;
public fun <init> ()V
public synthetic fun <init> (ILspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Lspace/kscience/dataforge/names/Name;
public final fun component2 ()Lspace/kscience/dataforge/names/Name;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lkotlinx/datetime/Instant;
public final fun copy (Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/EmptyDeviceMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/EmptyDeviceMessage;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/EmptyDeviceMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getComment ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/EmptyDeviceMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/EmptyDeviceMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/EmptyDeviceMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/EmptyDeviceMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/EmptyDeviceMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/EmptyDeviceMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/GetDescriptionMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/GetDescriptionMessage$Companion;
public synthetic fun <init> (ILspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Lspace/kscience/dataforge/names/Name;
public final fun component2 ()Lspace/kscience/dataforge/names/Name;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lkotlinx/datetime/Instant;
public final fun copy (Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/GetDescriptionMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/GetDescriptionMessage;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/GetDescriptionMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getComment ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/GetDescriptionMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/GetDescriptionMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/GetDescriptionMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/GetDescriptionMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/GetDescriptionMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/GetDescriptionMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertyChangedMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/PropertyChangedMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component3 ()Lspace/kscience/dataforge/names/Name;
public final fun component4 ()Lspace/kscience/dataforge/names/Name;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/PropertyChangedMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/PropertyChangedMessage;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/PropertyChangedMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getComment ()Ljava/lang/String;
public final fun getProperty ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public final fun getValue ()Lspace/kscience/dataforge/meta/Meta;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/PropertyChangedMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/PropertyChangedMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/PropertyChangedMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/PropertyChangedMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/PropertyChangedMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertyChangedMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertyDescriptor {
public static final field Companion Lspace/kscience/controls/api/PropertyDescriptor$Companion;
public synthetic fun <init> (ILjava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/descriptors/MetaDescriptor;ZZLkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/descriptors/MetaDescriptor;ZZ)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/descriptors/MetaDescriptor;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getInfo ()Ljava/lang/String;
public final fun getMetaDescriptor ()Lspace/kscience/dataforge/meta/descriptors/MetaDescriptor;
public final fun getName ()Ljava/lang/String;
public final fun getReadable ()Z
public final fun getWritable ()Z
public final fun setInfo (Ljava/lang/String;)V
public final fun setMetaDescriptor (Lspace/kscience/dataforge/meta/descriptors/MetaDescriptor;)V
public final fun setReadable (Z)V
public final fun setWritable (Z)V
public static final synthetic fun write$Self (Lspace/kscience/controls/api/PropertyDescriptor;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/PropertyDescriptor$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/PropertyDescriptor$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/PropertyDescriptor;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/PropertyDescriptor;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertyDescriptor$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertyGetMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/PropertyGetMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lspace/kscience/dataforge/names/Name;
public final fun component3 ()Lspace/kscience/dataforge/names/Name;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/PropertyGetMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/PropertyGetMessage;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/PropertyGetMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getComment ()Ljava/lang/String;
public final fun getProperty ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/PropertyGetMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/PropertyGetMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/PropertyGetMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/PropertyGetMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/PropertyGetMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertyGetMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertySetMessage : space/kscience/controls/api/DeviceMessage {
public static final field Companion Lspace/kscience/controls/api/PropertySetMessage$Companion;
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)V
public synthetic fun <init> (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun changeSource (Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/api/DeviceMessage;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component3 ()Lspace/kscience/dataforge/names/Name;
public final fun component4 ()Lspace/kscience/dataforge/names/Name;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Lkotlinx/datetime/Instant;
public final fun copy (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;)Lspace/kscience/controls/api/PropertySetMessage;
public static synthetic fun copy$default (Lspace/kscience/controls/api/PropertySetMessage;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lkotlinx/datetime/Instant;ILjava/lang/Object;)Lspace/kscience/controls/api/PropertySetMessage;
public fun equals (Ljava/lang/Object;)Z
public fun getComment ()Ljava/lang/String;
public final fun getProperty ()Ljava/lang/String;
public fun getSourceDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTargetDevice ()Lspace/kscience/dataforge/names/Name;
public fun getTime ()Lkotlinx/datetime/Instant;
public final fun getValue ()Lspace/kscience/dataforge/meta/Meta;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/api/PropertySetMessage;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/api/PropertySetMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/api/PropertySetMessage$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/api/PropertySetMessage;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/api/PropertySetMessage;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/api/PropertySetMessage$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public abstract interface class space/kscience/controls/api/Socket : java/io/Closeable {
public abstract fun isOpen ()Z
public abstract fun receiving ()Lkotlinx/coroutines/flow/Flow;
public abstract fun send (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/api/SocketKt {
public static final fun connectInput (Lspace/kscience/controls/api/Socket;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/Job;
}
public final class space/kscience/controls/manager/DeviceManager : space/kscience/dataforge/context/AbstractPlugin, space/kscience/controls/api/DeviceHub {
public static final field Companion Lspace/kscience/controls/manager/DeviceManager$Companion;
public fun <init> ()V
public fun content (Ljava/lang/String;)Ljava/util/Map;
public fun getDevices ()Ljava/util/Map;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
public final fun registerDevice (Lspace/kscience/dataforge/names/NameToken;Lspace/kscience/controls/api/Device;)V
}
public final class space/kscience/controls/manager/DeviceManager$Companion : space/kscience/dataforge/context/PluginFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/manager/DeviceManager;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/manager/DeviceManagerKt {
public static final fun install (Lspace/kscience/controls/manager/DeviceManager;Ljava/lang/String;Lspace/kscience/controls/api/Device;)Lspace/kscience/controls/api/Device;
public static final fun install (Lspace/kscience/controls/manager/DeviceManager;Ljava/lang/String;Lspace/kscience/dataforge/context/Factory;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/api/Device;
public static synthetic fun install$default (Lspace/kscience/controls/manager/DeviceManager;Ljava/lang/String;Lspace/kscience/dataforge/context/Factory;Lspace/kscience/dataforge/meta/Meta;ILjava/lang/Object;)Lspace/kscience/controls/api/Device;
public static final fun installing (Lspace/kscience/controls/manager/DeviceManager;Lspace/kscience/dataforge/context/Factory;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/ReadOnlyProperty;
public static synthetic fun installing$default (Lspace/kscience/controls/manager/DeviceManager;Lspace/kscience/dataforge/context/Factory;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/ReadOnlyProperty;
}
public final class space/kscience/controls/manager/RespondMessageKt {
public static final fun hubMessageFlow (Lspace/kscience/controls/api/DeviceHub;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/flow/Flow;
public static final fun respondHubMessage (Lspace/kscience/controls/api/DeviceHub;Lspace/kscience/controls/api/DeviceMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun respondMessage (Lspace/kscience/controls/api/Device;Lspace/kscience/dataforge/names/Name;Lspace/kscience/controls/api/DeviceMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/misc/TimeMetaKt {
public static final fun instant (Lspace/kscience/dataforge/meta/Meta;)Lkotlinx/datetime/Instant;
public static final fun toMeta (Lkotlinx/datetime/Instant;)Lspace/kscience/dataforge/meta/Meta;
}
public abstract class space/kscience/controls/ports/AbstractPort : space/kscience/controls/ports/Port {
public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;)V
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun getContext ()Lspace/kscience/dataforge/context/Context;
protected final fun getScope ()Lkotlinx/coroutines/CoroutineScope;
public fun isOpen ()Z
protected final fun receive ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun receiving ()Lkotlinx/coroutines/flow/Flow;
public synthetic fun send (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun send ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
protected abstract fun write ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/ports/ChannelPort : space/kscience/controls/ports/AbstractPort, java/lang/AutoCloseable {
public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public final fun getStartJob ()Lkotlinx/coroutines/Job;
}
public final class space/kscience/controls/ports/ChannelPortKt {
public static final fun toArray (Ljava/nio/ByteBuffer;I)[B
public static synthetic fun toArray$default (Ljava/nio/ByteBuffer;IILjava/lang/Object;)[B
}
public final class space/kscience/controls/ports/JvmPortsPlugin : space/kscience/dataforge/context/AbstractPlugin {
public static final field Companion Lspace/kscience/controls/ports/JvmPortsPlugin$Companion;
public fun <init> ()V
public fun content (Ljava/lang/String;)Ljava/util/Map;
public final fun getPorts ()Lspace/kscience/controls/ports/Ports;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/ports/JvmPortsPlugin$Companion : space/kscience/dataforge/context/PluginFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/JvmPortsPlugin;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/ports/PhrasesKt {
public static final fun delimitedIncoming (Lspace/kscience/controls/ports/Port;[B)Lkotlinx/coroutines/flow/Flow;
public static final fun stringsDelimitedIncoming (Lspace/kscience/controls/ports/Port;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public static final fun withDelimiter (Lkotlinx/coroutines/flow/Flow;[B)Lkotlinx/coroutines/flow/Flow;
public static final fun withStringDelimiter (Lkotlinx/coroutines/flow/Flow;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
}
public abstract interface class space/kscience/controls/ports/Port : space/kscience/controls/api/Socket, space/kscience/dataforge/context/ContextAware {
}
public abstract interface class space/kscience/controls/ports/PortFactory : space/kscience/dataforge/context/Factory {
public static final field Companion Lspace/kscience/controls/ports/PortFactory$Companion;
public static final field TYPE Ljava/lang/String;
public abstract fun getType ()Ljava/lang/String;
}
public final class space/kscience/controls/ports/PortFactory$Companion {
public static final field TYPE Ljava/lang/String;
}
public final class space/kscience/controls/ports/PortKt {
public static final fun send (Lspace/kscience/controls/ports/Port;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/ports/PortProxy : space/kscience/controls/ports/Port, space/kscience/dataforge/context/ContextAware {
public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun getContext ()Lspace/kscience/dataforge/context/Context;
public final fun getFactory ()Lkotlin/jvm/functions/Function1;
public fun isOpen ()Z
public fun receiving ()Lkotlinx/coroutines/flow/Flow;
public synthetic fun send (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun send ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/ports/Ports : space/kscience/dataforge/context/AbstractPlugin {
public static final field Companion Lspace/kscience/controls/ports/Ports$Companion;
public fun <init> ()V
public final fun buildPort (Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/Port;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/ports/Ports$Companion : space/kscience/dataforge/context/PluginFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/Ports;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/ports/SynchronousPort : space/kscience/controls/ports/Port {
public fun <init> (Lspace/kscience/controls/ports/Port;Lkotlinx/coroutines/sync/Mutex;)V
public fun close ()V
public fun getContext ()Lspace/kscience/dataforge/context/Context;
public final fun getPort ()Lspace/kscience/controls/ports/Port;
public fun isOpen ()Z
public fun receiving ()Lkotlinx/coroutines/flow/Flow;
public final fun respond ([BLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public synthetic fun send (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun send ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/ports/SynchronousPortKt {
public static final fun respondStringWithDelimiter (Lspace/kscience/controls/ports/SynchronousPort;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun respondWithDelimiter (Lspace/kscience/controls/ports/SynchronousPort;[B[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun synchronous (Lspace/kscience/controls/ports/Port;Lkotlinx/coroutines/sync/Mutex;)Lspace/kscience/controls/ports/SynchronousPort;
public static synthetic fun synchronous$default (Lspace/kscience/controls/ports/Port;Lkotlinx/coroutines/sync/Mutex;ILjava/lang/Object;)Lspace/kscience/controls/ports/SynchronousPort;
}
public final class space/kscience/controls/ports/TcpPort : space/kscience/controls/ports/PortFactory {
public static final field INSTANCE Lspace/kscience/controls/ports/TcpPort;
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/ChannelPort;
public fun getType ()Ljava/lang/String;
public final fun open (Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILkotlin/coroutines/CoroutineContext;)Lspace/kscience/controls/ports/ChannelPort;
public static synthetic fun open$default (Lspace/kscience/controls/ports/TcpPort;Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lspace/kscience/controls/ports/ChannelPort;
}
public final class space/kscience/controls/ports/UdpPort : space/kscience/controls/ports/PortFactory {
public static final field INSTANCE Lspace/kscience/controls/ports/UdpPort;
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/ChannelPort;
public fun getType ()Ljava/lang/String;
public final fun open (Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILjava/lang/Integer;Ljava/lang/String;Lkotlin/coroutines/CoroutineContext;)Lspace/kscience/controls/ports/ChannelPort;
public static synthetic fun open$default (Lspace/kscience/controls/ports/UdpPort;Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILjava/lang/Integer;Ljava/lang/String;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lspace/kscience/controls/ports/ChannelPort;
}
public abstract interface class space/kscience/controls/spec/DeviceActionSpec {
public abstract fun execute (Lspace/kscience/controls/api/Device;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getDescriptor ()Lspace/kscience/controls/api/ActionDescriptor;
public abstract fun getInputConverter ()Lspace/kscience/dataforge/meta/transformations/MetaConverter;
public abstract fun getOutputConverter ()Lspace/kscience/dataforge/meta/transformations/MetaConverter;
}
public abstract class space/kscience/controls/spec/DeviceBase : space/kscience/controls/api/Device {
public fun <init> (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)V
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun execute (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getActionDescriptors ()Ljava/util/Collection;
public abstract fun getActions ()Ljava/util/Map;
public final fun getContext ()Lspace/kscience/dataforge/context/Context;
public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext;
public fun getLifecycleState ()Lspace/kscience/controls/api/DeviceLifecycleState;
public synthetic fun getMessageFlow ()Lkotlinx/coroutines/flow/Flow;
public fun getMessageFlow ()Lkotlinx/coroutines/flow/SharedFlow;
public fun getMeta ()Lspace/kscience/dataforge/meta/Meta;
public abstract fun getProperties ()Ljava/util/Map;
public fun getProperty (Ljava/lang/String;)Lspace/kscience/dataforge/meta/Meta;
public fun getPropertyDescriptors ()Ljava/util/Collection;
public fun invalidate (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun open (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun readProperty (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun readPropertyOrNull (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
protected fun setLifecycleState (Lspace/kscience/controls/api/DeviceLifecycleState;)V
public abstract fun toString ()Ljava/lang/String;
protected final fun updateLogical (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun updateLogical (Lspace/kscience/controls/spec/DevicePropertySpec;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun writeProperty (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public class space/kscience/controls/spec/DeviceBySpec : space/kscience/controls/spec/DeviceBase {
public fun <init> (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)V
public synthetic fun <init> (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun getActions ()Ljava/util/Map;
public fun getProperties ()Ljava/util/Map;
public final fun getSpec ()Lspace/kscience/controls/spec/DeviceSpec;
public fun open (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/controls/spec/DeviceExtensionsKt {
public static final fun doRecurring-8Mi8wO0 (Lspace/kscience/controls/api/Device;JLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;
public static final fun readRecurring-8Mi8wO0 (Lspace/kscience/controls/api/Device;JLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;
}
public abstract interface class space/kscience/controls/spec/DevicePropertySpec {
public abstract fun getConverter ()Lspace/kscience/dataforge/meta/transformations/MetaConverter;
public abstract fun getDescriptor ()Lspace/kscience/controls/api/PropertyDescriptor;
public abstract fun read (Lspace/kscience/controls/api/Device;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/spec/DevicePropertySpecKt {
public static final fun execute (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DeviceActionSpec;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun execute (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DeviceActionSpec;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun get (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DevicePropertySpec;)Ljava/lang/Object;
public static final fun getName (Lspace/kscience/controls/spec/DeviceActionSpec;)Ljava/lang/String;
public static final fun getName (Lspace/kscience/controls/spec/DevicePropertySpec;)Ljava/lang/String;
public static final fun invalidate (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DevicePropertySpec;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun onPropertyChange (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DevicePropertySpec;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/Job;
public static final fun propertyFlow (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DevicePropertySpec;)Lkotlinx/coroutines/flow/Flow;
public static final fun read (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DevicePropertySpec;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun readOrNull (Lspace/kscience/controls/spec/DeviceBase;Lspace/kscience/controls/spec/DevicePropertySpec;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun set (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/WritableDevicePropertySpec;Ljava/lang/Object;)Lkotlinx/coroutines/Job;
public static final fun useProperty (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/DevicePropertySpec;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job;
public static final fun write (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/spec/WritableDevicePropertySpec;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract class space/kscience/controls/spec/DeviceSpec {
public fun <init> ()V
public final fun action (Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun action$default (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public final fun getActions ()Ljava/util/Map;
public final fun getProperties ()Ljava/util/Map;
public final fun metaAction (Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun metaAction$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public final fun mutableProperty (Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public final fun mutableProperty (Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/reflect/KMutableProperty1;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun mutableProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun mutableProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/reflect/KMutableProperty1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public fun onClose (Lspace/kscience/controls/api/Device;)V
public fun onOpen (Lspace/kscience/controls/api/Device;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun property (Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/properties/PropertyDelegateProvider;
public final fun property (Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/reflect/KProperty1;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun property$default (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun property$default (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/reflect/KProperty1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public final fun registerAction (Lspace/kscience/controls/spec/DeviceActionSpec;)Lspace/kscience/controls/spec/DeviceActionSpec;
public final fun registerProperty (Lspace/kscience/controls/spec/DevicePropertySpec;)Lspace/kscience/controls/spec/DevicePropertySpec;
public final fun unitAction (Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun unitAction$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
}
public final class space/kscience/controls/spec/DeviceSpecKt {
public static final fun getUnit (Lspace/kscience/dataforge/meta/transformations/MetaConverter$Companion;)Lspace/kscience/dataforge/meta/transformations/MetaConverter;
public static final fun logicalProperty (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun logicalProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/dataforge/meta/transformations/MetaConverter;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
}
public final class space/kscience/controls/spec/DurationConverter : space/kscience/dataforge/meta/transformations/MetaConverter {
public static final field INSTANCE Lspace/kscience/controls/spec/DurationConverter;
public synthetic fun metaToObject (Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun metaToObject-5sfh64U (Lspace/kscience/dataforge/meta/Meta;)J
public synthetic fun objectToMeta (Ljava/lang/Object;)Lspace/kscience/dataforge/meta/Meta;
public fun objectToMeta-LRDsOJo (J)Lspace/kscience/dataforge/meta/Meta;
}
public abstract interface annotation class space/kscience/controls/spec/InternalDeviceAPI : java/lang/annotation/Annotation {
}
public final class space/kscience/controls/spec/MiscKt {
public static final fun asMeta (D)Lspace/kscience/dataforge/meta/Meta;
public static final fun getDuration (Lspace/kscience/dataforge/meta/transformations/MetaConverter$Companion;)Lspace/kscience/dataforge/meta/transformations/MetaConverter;
}
public final class space/kscience/controls/spec/PropertySpecDelegatesKt {
public static final fun booleanProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun booleanProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun booleanProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun booleanProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun doubleProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun doubleProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun doubleProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun doubleProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun metaProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun metaProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun metaProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun metaProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun numberProperty (Lspace/kscience/controls/spec/DeviceSpec;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun numberProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun numberProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun numberProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun stringProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkotlin/properties/PropertyDelegateProvider;
public static final fun stringProperty (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun stringProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
public static synthetic fun stringProperty$default (Lspace/kscience/controls/spec/DeviceSpec;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider;
}
public final class space/kscience/controls/spec/UnitMetaConverter : space/kscience/dataforge/meta/transformations/MetaConverter {
public static final field INSTANCE Lspace/kscience/controls/spec/UnitMetaConverter;
public synthetic fun metaToObject (Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun metaToObject (Lspace/kscience/dataforge/meta/Meta;)V
public synthetic fun objectToMeta (Ljava/lang/Object;)Lspace/kscience/dataforge/meta/Meta;
public fun objectToMeta (Lkotlin/Unit;)Lspace/kscience/dataforge/meta/Meta;
}
public abstract interface class space/kscience/controls/spec/WritableDevicePropertySpec : space/kscience/controls/spec/DevicePropertySpec {
public abstract fun write (Lspace/kscience/controls/api/Device;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

View File

@ -0,0 +1,68 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
description = """
Core interfaces for building a device server
""".trimIndent()
val dataforgeVersion: String by rootProject.extra
kscience {
jvm()
js()
native()
useCoroutines()
useSerialization{
json()
}
useContextReceivers()
dependencies {
api("space.kscience:dataforge-io:$dataforgeVersion")
api(spclibs.kotlinx.datetime)
}
}
readme{
maturity = Maturity.EXPERIMENTAL
feature("device", ref = "src/commonMain/kotlin/space/kscience/controls/api/Device.kt"){
"""
Device API with subscription (asynchronous and pseudo-synchronous properties)
""".trimIndent()
}
feature("deviceMessage", ref = "src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt"){
"""
Specification for messages used to communicate between Controls-kt devices.
""".trimIndent()
}
feature("deviceHub", ref = "src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt"){
"""
Grouping of devices into local tree-like hubs.
""".trimIndent()
}
feature("deviceSpec", ref = "src/commonMain/kotlin/space/kscience/controls/spec"){
"""
Mechanics and type-safe builders for devices. Including separation of device specification and device state.
""".trimIndent()
}
feature("deviceManager", ref = "src/commonMain/kotlin/space/kscience/controls/manager"){
"""
DataForge DI integration for devices. Includes device builders.
""".trimIndent()
}
feature("ports", ref = "src/commonMain/kotlin/space/kscience/controls/ports"){
"""
Working with asynchronous data sending and receiving raw byte arrays
""".trimIndent()
}
}

View File

@ -0,0 +1,128 @@
package space.kscience.controls.api
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.info
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name
/**
* A lifecycle state of a device
*/
public enum class DeviceLifecycleState{
INIT,
OPEN,
CLOSED
}
/**
* General interface describing a managed Device.
* [Device] is a supervisor scope encompassing all operations on a device.
* When canceled, cancels all running processes.
*/
@Type(DEVICE_TARGET)
public interface Device : AutoCloseable, ContextAware, CoroutineScope {
/**
* Initial configuration meta for the device
*/
public val meta: Meta get() = Meta.EMPTY
/**
* List of supported property descriptors
*/
public val propertyDescriptors: Collection<PropertyDescriptor>
/**
* List of supported action descriptors. Action is a request to the device that
* may or may not change the properties
*/
public val actionDescriptors: Collection<ActionDescriptor>
/**
* Read the physical state of property and update/push notifications if needed.
*/
public suspend fun readProperty(propertyName: String): Meta
/**
* Get the logical state of property or return null if it is invalid
*/
public fun getProperty(propertyName: String): Meta?
/**
* Invalidate property (set logical state to invalid)
*
* This message is suspended to provide lock-free local property changes (they require coroutine context).
*/
public suspend fun invalidate(propertyName: String)
/**
* Set property [value] for a property with name [propertyName].
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
*/
public suspend fun writeProperty(propertyName: String, value: Meta)
/**
* A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable
* multiple times.
*/
public val messageFlow: Flow<DeviceMessage>
/**
* Send an action request and suspend caller while request is being processed.
* Could return null if request does not return a meaningful answer.
*/
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
/**
* Initialize the device. This function suspends until the device is finished initialization
*/
public suspend fun open(): Unit = Unit
/**
* Close and terminate the device. This function does not wait for the device to be closed.
*/
override fun close() {
logger.info { "Device $this is closed" }
cancel("The device is closed")
}
@DFExperimental
public val lifecycleState: DeviceLifecycleState
public companion object {
public const val DEVICE_TARGET: String = "device"
}
}
/**
* Get the logical state of property or suspend to read the physical value.
*/
public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
getProperty(propertyName) ?: readProperty(propertyName)
/**
* Get a snapshot of the device logical state
*
*/
public fun Device.getAllProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) {
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
}
}
/**
* Subscribe on property changes for the whole device
*/
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)

View File

@ -0,0 +1,73 @@
package space.kscience.controls.api
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.*
import space.kscience.dataforge.provider.Provider
/**
* A hub that could locate multiple devices and redirect actions to them
*/
public interface DeviceHub : Provider {
public val devices: Map<NameToken, Device>
override val defaultTarget: String get() = Device.DEVICE_TARGET
override val defaultChainTarget: String get() = Device.DEVICE_TARGET
override fun content(target: String): Map<Name, Any> = if (target == Device.DEVICE_TARGET) {
buildMap {
fun putAll(prefix: Name, hub: DeviceHub) {
hub.devices.forEach {
put(prefix + it.key, it.value)
}
}
devices.forEach {
val name = it.key.asName()
put(name, it.value)
(it.value as? DeviceHub)?.let { hub ->
putAll(name, hub)
}
}
}
} else {
emptyMap()
}
public companion object
}
public operator fun DeviceHub.get(nameToken: NameToken): Device =
devices[nameToken] ?: error("Device with name $nameToken not found in $this")
public fun DeviceHub.getOrNull(name: Name): Device? = when {
name.isEmpty() -> this as? Device
name.length == 1 -> get(name.firstOrNull()!!)
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
}
public operator fun DeviceHub.get(name: Name): Device =
getOrNull(name) ?: error("Device with name $name not found in $this")
public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(Name.parse(nameString))
public operator fun DeviceHub.get(nameString: String): Device =
getOrNull(nameString) ?: error("Device with name $nameString not found in $this")
public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta =
this[deviceName].readProperty(propertyName)
public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) {
this[deviceName].writeProperty(propertyName, value)
}
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? =
this[deviceName].execute(command, argument)
//suspend fun DeviceHub.respond(request: Envelope): EnvelopeBuilder {
// val target = request.meta[DeviceMessage.TARGET_KEY].string ?: defaultTarget
// val device = this[target.toName()]
//
// return device.respond(device, target, request)
//}

View File

@ -0,0 +1,234 @@
@file:OptIn(ExperimentalSerializationApi::class)
package space.kscience.controls.api
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import space.kscience.dataforge.io.Envelope
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toJson
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
@Serializable
public sealed class DeviceMessage {
public abstract val sourceDevice: Name?
public abstract val targetDevice: Name?
public abstract val comment: String?
public abstract val time: Instant?
/**
* Update the source device name for composition. If the original name is null, resulting name is also null.
*/
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
public companion object {
public fun error(
cause: Throwable,
sourceDevice: Name,
targetDevice: Name? = null,
): DeviceErrorMessage = DeviceErrorMessage(
errorMessage = cause.message,
errorType = cause::class.simpleName,
errorStackTrace = cause.stackTraceToString(),
sourceDevice = sourceDevice,
targetDevice = targetDevice
)
public fun fromMeta(meta: Meta): DeviceMessage = Json.decodeFromJsonElement(meta.toJson())
}
}
/**
* Notify that property is changed. [sourceDevice] is mandatory.
* [property] corresponds to property name.
*
*/
@Serializable
@SerialName("property.changed")
public data class PropertyChangedMessage(
public val property: String,
public val value: Meta,
override val sourceDevice: Name = Name.EMPTY,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* A command to set or invalidate property. [targetDevice] is mandatory.
*/
@Serializable
@SerialName("property.set")
public data class PropertySetMessage(
public val property: String,
public val value: Meta?,
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* A command to request property value asynchronously. [targetDevice] is mandatory.
* The property value should be returned asynchronously via [PropertyChangedMessage].
*/
@Serializable
@SerialName("property.get")
public data class PropertyGetMessage(
public val property: String,
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* Request device description. The result is returned in form of [DescriptionMessage]
*/
@Serializable
@SerialName("description.get")
public data class GetDescriptionMessage(
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* The full device description message
*/
@Serializable
@SerialName("description")
public data class DescriptionMessage(
val description: Meta,
val properties: Collection<PropertyDescriptor>,
val actions: Collection<ActionDescriptor>,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* A request to execute an action. [targetDevice] is mandatory
*
* @param requestId action request id that should be returned in a response
*/
@Serializable
@SerialName("action.execute")
public data class ActionExecuteMessage(
public val action: String,
public val argument: Meta?,
public val requestId: String,
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* Asynchronous action result. [sourceDevice] is mandatory
*
* @param requestId request id passed in the request
*/
@Serializable
@SerialName("action.result")
public data class ActionResultMessage(
public val action: String,
public val result: Meta?,
public val requestId: String,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API.
*/
@Serializable
@SerialName("binary.notification")
public data class BinaryNotificationMessage(
val binaryID: String,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
/**
* The message states that the message is received, but no meaningful response is produced.
* This message could be used for a heartbeat.
*/
@Serializable
@SerialName("empty")
public data class EmptyDeviceMessage(
override val sourceDevice: Name? = null,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* Information log message
*/
@Serializable
@SerialName("log")
public data class DeviceLogMessage(
val message: String,
val data: Meta? = null,
override val sourceDevice: Name? = null,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
}
/**
* The evaluation of the message produced a service error
*/
@Serializable
@SerialName("error")
public data class DeviceErrorMessage(
public val errorMessage: String?,
public val errorType: String? = null,
public val errorStackTrace: String? = null,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
}
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()
public fun DeviceMessage.toEnvelope(): Envelope = Envelope(toMeta(), null)

View File

@ -0,0 +1,33 @@
package space.kscience.controls.api
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
/**
* A generic bidirectional sender/receiver object
*/
public interface Socket<T> : Closeable {
/**
* Send an object to the socket
*/
public suspend fun send(data: T)
/**
* Flow of objects received from socket
*/
public fun receiving(): Flow<T>
public fun isOpen(): Boolean
}
/**
* Connect an input to this socket using designated [scope] for it and return a handler [Job].
* Multiple inputs could be connected to the same [Socket].
*/
public fun <T> Socket<T>.connectInput(scope: CoroutineScope, flow: Flow<T>): Job = scope.launch {
flow.collect { send(it) }
}

View File

@ -0,0 +1,32 @@
package space.kscience.controls.api
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
//TODO add proper builders
/**
* A descriptor for property
*/
@Serializable
public class PropertyDescriptor(
public val name: String,
public var info: String? = null,
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
public var readable: Boolean = true,
public var writable: Boolean = false
)
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
metaDescriptor = MetaDescriptor(block)
}
/**
* A descriptor for property
*/
@Serializable
public class ActionDescriptor(public val name: String) {
public var info: String? = null
}

View File

@ -0,0 +1,76 @@
package space.kscience.controls.manager
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.getOrNull
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import kotlin.collections.set
import kotlin.properties.ReadOnlyProperty
/**
* DataForge Context plugin that allows to manage devices locally
*/
public class DeviceManager : AbstractPlugin(), DeviceHub {
override val tag: PluginTag get() = Companion.tag
/**
* Actual list of connected devices
*/
private val top = HashMap<NameToken, Device>()
override val devices: Map<NameToken, Device> get() = top
public fun registerDevice(name: NameToken, device: Device) {
top[name] = device
}
override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target)
public companion object : PluginFactory<DeviceManager> {
override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): DeviceManager = DeviceManager()
}
}
public fun <D : Device> DeviceManager.install(name: String, device: D): D {
registerDevice(NameToken(name), device)
device.launch {
device.open()
}
return device
}
/**
* Register and start a device built by [factory] with current [Context] and [meta].
*/
public fun <D : Device> DeviceManager.install(name: String, factory: Factory<D>, meta: Meta = Meta.EMPTY): D =
install(name, factory(meta, context))
/**
* A delegate that initializes device on the first use
*/
public inline fun <D : Device> DeviceManager.installing(
factory: Factory<D>,
builder: MutableMeta.() -> Unit = {},
): ReadOnlyProperty<Any?, D> {
val meta = Meta(builder)
return ReadOnlyProperty { _, property ->
val name = property.name
val current = getOrNull(name)
if (current == null) {
install(name, factory, meta)
} else if (current.meta != meta) {
error("Meta mismatch. Current device meta: ${current.meta}, but factory meta is $meta")
} else {
@Suppress("UNCHECKED_CAST")
current as D
}
}
}

View File

@ -0,0 +1,112 @@
package space.kscience.controls.manager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus
/**
* Process a message targeted at this [Device], assuming its name is [deviceTarget].
*/
public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMessage): DeviceMessage? = try {
when (request) {
is PropertyGetMessage -> {
PropertyChangedMessage(
property = request.property,
value = getOrReadProperty(request.property),
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is PropertySetMessage -> {
if (request.value == null) {
invalidate(request.property)
} else {
writeProperty(request.property, request.value)
}
PropertyChangedMessage(
property = request.property,
value = getOrReadProperty(request.property),
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is ActionExecuteMessage -> {
ActionResultMessage(
action = request.action,
result = execute(request.action, request.argument),
requestId = request.requestId,
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is GetDescriptionMessage -> {
DescriptionMessage(
description = meta,
properties = propertyDescriptors,
actions = actionDescriptors,
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
}
is DescriptionMessage,
is PropertyChangedMessage,
is ActionResultMessage,
is BinaryNotificationMessage,
is DeviceErrorMessage,
is EmptyDeviceMessage,
is DeviceLogMessage,
-> null
}
} catch (ex: Exception) {
DeviceMessage.error(ex, sourceDevice = deviceTarget, targetDevice = request.sourceDevice)
}
/**
* Process incoming [DeviceMessage], using hub naming to evaluate target.
*/
public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMessage? {
return try {
val targetName = request.targetDevice ?: return null
val device = getOrNull(targetName) ?: error("The device with name $targetName not found in $this")
device.respondMessage(targetName, request)
} catch (ex: Exception) {
DeviceMessage.error(ex, sourceDevice = Name.EMPTY, targetDevice = request.sourceDevice)
}
}
/**
* Collect all messages from given [DeviceHub], applying proper relative names.
*/
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
//TODO could we avoid using downstream scope?
val outbox = MutableSharedFlow<DeviceMessage>()
if (this is Device) {
messageFlow.onEach {
outbox.emit(it)
}.launchIn(scope)
}
//TODO maybe better create map of all devices to limit copying
devices.forEach { (token, childDevice) ->
val flow = if (childDevice is DeviceHub) {
childDevice.hubMessageFlow(scope)
} else {
childDevice.messageFlow
}
flow.onEach { deviceMessage ->
outbox.emit(
deviceMessage.changeSource { token + it }
)
}.launchIn(scope)
}
return outbox
}

View File

@ -0,0 +1,18 @@
package space.kscience.controls.misc
import kotlinx.datetime.Instant
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.long
// TODO move to core
public fun Instant.toMeta(): Meta = Meta {
"seconds" put epochSeconds
"nanos" put nanosecondsOfSecond
}
public fun Meta.instant(): Instant = value?.long?.let { Instant.fromEpochMilliseconds(it) } ?: Instant.fromEpochSeconds(
get("seconds")?.long ?: 0L,
get("nanos")?.long ?: 0L,
)

View File

@ -1,24 +1,38 @@
package hep.dataforge.control.ports
package space.kscience.controls.ports
import hep.dataforge.context.Context
import hep.dataforge.context.ContextAware
import hep.dataforge.context.Factory
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.io.Closeable
import space.kscience.controls.api.Socket
import space.kscience.dataforge.context.*
import space.kscience.dataforge.misc.Type
import kotlin.coroutines.CoroutineContext
public interface Port: Closeable, ContextAware {
public suspend fun send(data: ByteArray)
public suspend fun receiving(): Flow<ByteArray>
public fun isOpen(): Boolean
/**
* Raw [ByteArray] port
*/
public interface Port : ContextAware, Socket<ByteArray>
/**
* A specialized factory for [Port]
*/
@Type(PortFactory.TYPE)
public interface PortFactory : Factory<Port> {
public val type: String
public companion object {
public const val TYPE: String = "controls.port"
}
}
public typealias PortFactory = Factory<Port>
public abstract class AbstractPort(override val context: Context, coroutineContext: CoroutineContext = context.coroutineContext) : Port {
/**
* Common abstraction for [Port] based on [Channel]
*/
public abstract class AbstractPort(
override val context: Context,
coroutineContext: CoroutineContext = context.coroutineContext,
) : Port {
protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job]))
@ -39,18 +53,16 @@ public abstract class AbstractPort(override val context: Context, coroutineConte
/**
* Internal method to receive data synchronously
*/
protected fun receive(data: ByteArray) {
scope.launch {
logger.debug { "[${this@AbstractPort}] RECEIVED: ${data.decodeToString()}" }
incoming.send(data)
}
protected suspend fun receive(data: ByteArray) {
logger.debug { "${this@AbstractPort} RECEIVED: ${data.decodeToString()}" }
incoming.send(data)
}
private val sendJob = scope.launch {
for (data in outgoing) {
try {
write(data)
logger.debug { "[${this@AbstractPort}] SENT: ${data.decodeToString()}" }
logger.debug { "${this@AbstractPort} SENT: ${data.decodeToString()}" }
} catch (ex: Exception) {
if (ex is CancellationException) throw ex
logger.error(ex) { "Error while writing data to the port" }
@ -67,12 +79,10 @@ public abstract class AbstractPort(override val context: Context, coroutineConte
/**
* Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases.
* In order to form phrases some condition should used on top of it.
* For example [delimitedIncoming] generates phrases with fixed delimiter.
* In order to form phrases, some condition should be used on top of it.
* For example [stringsDelimitedIncoming] generates phrases with fixed delimiter.
*/
override suspend fun receiving(): Flow<ByteArray> {
return incoming.receiveAsFlow()
}
override fun receiving(): Flow<ByteArray> = incoming.receiveAsFlow()
override fun close() {
outgoing.close()

View File

@ -1,15 +1,12 @@
package hep.dataforge.control.ports
package space.kscience.controls.ports
import hep.dataforge.context.Context
import hep.dataforge.context.ContextAware
import hep.dataforge.context.Global
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.isActive
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import space.kscience.dataforge.context.*
/**
* A port that could be closed multiple times and opens automatically on request
@ -31,40 +28,37 @@ public class PortProxy(override val context: Context = Global, public val factor
}
}
/**
* Ensure that the port is open. If it is already open, does nothing. Otherwise, open a new port.
*/
public suspend fun open() {
port()//ignore result
}
override suspend fun send(data: ByteArray) {
port().send(data)
}
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun receiving(): Flow<ByteArray> = channelFlow {
while (isActive) {
override fun receiving(): Flow<ByteArray> = flow {
while (true) {
try {
//recreate port and Flow on cancel
//recreate port and Flow on connection problems
port().receiving().collect {
send(it)
emit(it)
}
} catch (t: Throwable) {
logger.warn(t){"Port read failed. Reconnecting."}
//cancel
// if (t is CancellationException) {
// cancel(t)
// }
logger.warn{"Port read failed: ${t.message}. Reconnecting."}
mutex.withLock {
actualPort?.close()
actualPort = null
}
}
}
}// port().receiving()
}
// open by default
override fun isOpen(): Boolean = true
override fun close() {
actualPort?.close()
actualPort = null
context.launch {
mutex.withLock {
actualPort?.close()
actualPort = null
}
}
}
}

View File

@ -0,0 +1,37 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
/**
* A DataForge plugin for managing ports
*/
public class Ports : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
private val portFactories by lazy {
context.gather<PortFactory>(PortFactory.TYPE)
}
private val portCache = mutableMapOf<Meta, Port>()
/**
* Create a new [Port] according to specification
*/
public fun buildPort(meta: Meta): Port = portCache.getOrPut(meta) {
val type by meta.string { error("Port type is not defined") }
val factory = portFactories.values.firstOrNull { it.type == type }
?: error("Port factory for type $type not found")
factory.build(context, meta)
}
public companion object : PluginFactory<Ports> {
override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): Ports = Ports()
}
}

View File

@ -0,0 +1,42 @@
package space.kscience.controls.ports
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
* The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
*/
public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port {
/**
* Send a single message and wait for the flow of respond messages.
*/
public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = mutex.withLock {
port.send(data)
transform(port.receiving())
}
}
/**
* Provide a synchronous wrapper for a port
*/
public fun Port.synchronous(mutex: Mutex = Mutex()): SynchronousPort = SynchronousPort(this, mutex)
/**
* Send request and read incoming data blocks until the delimiter is encountered
*/
public suspend fun SynchronousPort.respondWithDelimiter(
data: ByteArray,
delimiter: ByteArray,
): ByteArray = respond(data) {
withDelimiter(delimiter).first()
}
public suspend fun SynchronousPort.respondStringWithDelimiter(
data: String,
delimiter: String,
): String = respond(data.encodeToByteArray()) {
withStringDelimiter(delimiter).first()
}

View File

@ -1,21 +1,22 @@
package hep.dataforge.control.ports
package space.kscience.controls.ports
import io.ktor.utils.io.core.BytePacketBuilder
import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.core.reset
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.io.ByteArrayOutput
import kotlinx.coroutines.flow.transform
/**
* Transform byte fragments into complete phrases using given delimiter
* Transform byte fragments into complete phrases using given delimiter. Not thread safe.
*/
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> = flow {
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> {
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
var output = ByteArrayOutput(expectedMessageSize)
val output = BytePacketBuilder()
var matcherPosition = 0
collect { chunk ->
return transform { chunk ->
chunk.forEach { byte ->
output.writeByte(byte)
//matching current symbol in delimiter
@ -23,8 +24,9 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSi
matcherPosition++
if (matcherPosition == delimiter.size) {
//full match achieved, sending result
emit(output.toByteArray())
output = ByteArrayOutput(expectedMessageSize)
val bytes = output.build()
emit(bytes.readBytes())
output.reset()
matcherPosition = 0
}
} else if (matcherPosition > 0) {
@ -38,12 +40,16 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSi
/**
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
*/
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
public fun Flow<ByteArray>.withStringDelimiter(delimiter: String): Flow<String> {
return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() }
}
/**
* A flow of delimited phrases
*/
public suspend fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> =
receiving().withDelimiter(delimiter, expectedMessageSize)
public fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = receiving().withDelimiter(delimiter)
/**
* A flow of delimited phrases with string content
*/
public fun Port.stringsDelimitedIncoming(delimiter: String): Flow<String> = receiving().withStringDelimiter(delimiter)

View File

@ -0,0 +1,179 @@
package space.kscience.controls.spec
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import kotlin.coroutines.CoroutineContext
@OptIn(InternalDeviceAPI::class)
private suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
}
@OptIn(InternalDeviceAPI::class)
private suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
read(device)?.let(converter::objectToMeta)
private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
device: D,
item: Meta,
): Meta? {
val arg: I = inputConverter.metaToObject(item) ?: error("Failed to convert $item with $inputConverter")
val res = execute(device, arg)
return res?.let { outputConverter.objectToMeta(res) }
}
/**
* A base abstractions for [Device], introducing specifications for properties
*/
public abstract class DeviceBase<D : Device>(
final override val context: Context,
override val meta: Meta = Meta.EMPTY,
) : Device {
/**
* Collection of property specifications
*/
public abstract val properties: Map<String, DevicePropertySpec<D, *>>
/**
* Collection of action specifications
*/
public abstract val actions: Map<String, DeviceActionSpec<D, *, *>>
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor }
override val actionDescriptors: Collection<ActionDescriptor>
get() = actions.values.map { it.descriptor }
override val coroutineContext: CoroutineContext by lazy {
context.newCoroutineContext(
SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") +
CoroutineExceptionHandler { _, throwable ->
logger.error(throwable) { "Exception in device $this job" }
}
)
}
/**
* Logical state store
*/
private val logicalState: HashMap<String, Meta?> = HashMap()
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
@Suppress("UNCHECKED_CAST")
internal val self: D
get() = this as D
private val stateLock = Mutex()
/**
* Update logical property state and notify listeners
*/
protected suspend fun updateLogical(propertyName: String, value: Meta?) {
if (value != logicalState[propertyName]) {
stateLock.withLock {
logicalState[propertyName] = value
}
if (value != null) {
sharedMessageFlow.emit(PropertyChangedMessage(propertyName, value))
}
}
}
/**
* Update logical state using given [spec] and its convertor
*/
public suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
updateLogical(spec.name, spec.converter.objectToMeta(value))
}
/**
* Force read physical value and push an update if it is changed. It does not matter if logical state is present.
* The logical state is updated after read
*/
override suspend fun readProperty(propertyName: String): Meta {
val spec = properties[propertyName] ?: error("Property with name $propertyName not found")
val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName")
updateLogical(propertyName, meta)
return meta
}
/**
* Read property if it exists and read correctly. Return null otherwise.
*/
public suspend fun readPropertyOrNull(propertyName: String): Meta? {
val spec = properties[propertyName] ?: return null
val meta = spec.readMeta(self) ?: return null
updateLogical(propertyName, meta)
return meta
}
override fun getProperty(propertyName: String): Meta? = logicalState[propertyName]
override suspend fun invalidate(propertyName: String) {
stateLock.withLock {
logicalState.remove(propertyName)
}
}
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
when (val property = properties[propertyName]) {
null -> {
//If there is a physical property with a given name, invalidate logical property and write physical one
updateLogical(propertyName, value)
}
is WritableDevicePropertySpec -> {
invalidate(propertyName)
property.writeMeta(self, value)
}
else -> {
error("Property $property is not writeable")
}
}
}
override suspend fun execute(actionName: String, argument: Meta?): Meta? {
val spec = actions[actionName] ?: error("Action with name $actionName not found")
return spec.executeWithMeta(self, argument ?: Meta.EMPTY)
}
@DFExperimental
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT
protected set
@OptIn(DFExperimental::class)
override suspend fun open() {
super.open()
lifecycleState = DeviceLifecycleState.OPEN
}
@OptIn(DFExperimental::class)
override fun close() {
lifecycleState = DeviceLifecycleState.CLOSED
super.close()
}
abstract override fun toString(): String
}

View File

@ -0,0 +1,30 @@
package space.kscience.controls.spec
import space.kscience.controls.api.Device
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
/**
* A device generated from specification
* @param D recursive self-type for properties and actions
*/
public open class DeviceBySpec<D : Device>(
public val spec: DeviceSpec<in D>,
context: Context,
meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override suspend fun open(): Unit = with(spec) {
super.open()
self.onOpen()
}
override fun close(): Unit = with(spec) {
self.onClose()
super.close()
}
override fun toString(): String = "Device(spec=$spec)"
}

View File

@ -0,0 +1,15 @@
package space.kscience.controls.spec
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
internal object DeviceMetaPropertySpec: DevicePropertySpec<Device,Meta> {
override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta")
override val converter: MetaConverter<Meta> = MetaConverter.meta
@InternalDeviceAPI
override suspend fun read(device: Device): Meta = device.meta
}

View File

@ -0,0 +1,163 @@
package space.kscience.controls.spec
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* This API is internal and should not be used in user code
*/
@RequiresOptIn("This API should not be called outside of Device internals")
public annotation class InternalDeviceAPI
/**
* Specification for a device read-only property
*/
public interface DevicePropertySpec<in D : Device, T> {
/**
* Property descriptor
*/
public val descriptor: PropertyDescriptor
/**
* Meta item converter for the resulting type
*/
public val converter: MetaConverter<T>
/**
* Read physical value from the given [device]
*/
@InternalDeviceAPI
public suspend fun read(device: D): T?
}
/**
* Property name should be unique in a device
*/
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/**
* Write physical value to a device
*/
@InternalDeviceAPI
public suspend fun write(device: D, value: T)
}
public interface DeviceActionSpec<in D : Device, I, O> {
/**
* Action descriptor
*/
public val descriptor: ActionDescriptor
public val inputConverter: MetaConverter<I>
public val outputConverter: MetaConverter<O>
/**
* Execute action on a device
*/
public suspend fun execute(device: D, input: I): O
}
/**
* Action name. Should be unique in the device
*/
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Property read result is not valid")
/**
* Read typed value and update/push event if needed.
* Return null if property read is not successful or property is undefined.
*/
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
/**
* Write typed property state and invalidate logical state
*/
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
}
/**
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
*/
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch {
write(propertySpec, value)
}
/**
* A type safe flow of property changes for given property
*/
public fun <D : Device, T> D.propertyFlow(spec: DevicePropertySpec<D, T>): Flow<T> = messageFlow
.filterIsInstance<PropertyChangedMessage>()
.filter { it.property == spec.name }
.mapNotNull { spec.converter.metaToObject(it.value) }
/**
* A type safe property change listener. Uses the device [CoroutineScope].
*/
public fun <D : Device, T> D.onPropertyChange(
spec: DevicePropertySpec<D, T>,
callback: suspend PropertyChangedMessage.(T) -> Unit,
): Job = messageFlow
.filterIsInstance<PropertyChangedMessage>()
.filter { it.property == spec.name }
.onEach { change ->
val newValue = spec.converter.metaToObject(change.value)
if (newValue != null) {
change.callback(newValue)
}
}.launchIn(this)
/**
* Call [callback] on initial property value and each value change
*/
public fun <D : Device, T> D.useProperty(
spec: DevicePropertySpec<D, T>,
callback: suspend (T) -> Unit,
): Job = launch {
callback(read(spec))
messageFlow
.filterIsInstance<PropertyChangedMessage>()
.filter { it.property == spec.name }
.collect { change ->
val newValue = spec.converter.metaToObject(change.value)
if (newValue != null) {
callback(newValue)
}
}
}
/**
* Reset the logical state of a property
*/
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
invalidate(propertySpec.name)
}
/**
* Execute the action with name according to [actionSpec]
*/
public suspend fun <I, O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, I, O>, input: I): O =
actionSpec.execute(this, input)
public suspend fun <O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, Unit, O>): O =
actionSpec.execute(this, Unit)

View File

@ -0,0 +1,240 @@
package space.kscience.controls.spec
import kotlinx.coroutines.withContext
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
public object UnitMetaConverter: MetaConverter<Unit>{
override fun metaToObject(meta: Meta): Unit = Unit
override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY
}
public val MetaConverter.Companion.unit: MetaConverter<Unit> get() = UnitMetaConverter
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceSpec<D : Device> {
//initializing meta property for everyone
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
)
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
public open suspend fun D.onOpen() {
}
public open fun D.onClose() {
}
public fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty
return deviceProperty
}
public fun <T> property(
converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add type from converter
writable = true
}.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) {
readOnlyProperty.get(device)
}
}
registerProperty(deviceProperty)
ReadOnlyProperty { _, _ ->
deviceProperty
}
}
public fun <T> mutableProperty(
converter: MetaConverter<T>,
readWriteProperty: KMutableProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add the type from converter
writable = true
}.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) {
readWriteProperty.get(device)
}
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
readWriteProperty.set(device, value)
}
}
registerProperty(deviceProperty)
ReadOnlyProperty { _, _ ->
deviceProperty
}
}
public fun <T> property(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val propertyName = name ?: property.name
val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
}
registerProperty(deviceProperty)
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
deviceProperty
}
}
public fun <T> mutableProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T?,
write: suspend D.(T) -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val propertyName = name ?: property.name
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
device.write(value)
}
}
_properties[propertyName] = deviceProperty
ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>> { _, _ ->
deviceProperty
}
}
public fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
_actions[deviceAction.name] = deviceAction
return deviceAction
}
public fun <I, O> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.(I) -> O,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val actionName = name ?: property.name
val deviceAction = object : DeviceActionSpec<D, I, O> {
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
override val inputConverter: MetaConverter<I> = inputConverter
override val outputConverter: MetaConverter<O> = outputConverter
override suspend fun execute(device: D, input: I): O = withContext(device.coroutineContext) {
device.execute(input)
}
}
_actions[actionName] = deviceAction
ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>> { _, _ ->
deviceAction
}
}
/**
* An action that takes [Meta] and returns [Meta]. No conversions are done
*/
public fun metaAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.(Meta) -> Meta,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
) {
execute(it)
}
/**
* An action that takes no parameters and returns no values
*/
public fun unitAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.() -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
action(
MetaConverter.Companion.unit,
MetaConverter.Companion.unit,
descriptorBuilder,
name
) {
execute()
}
}
/**
* Register a mutable logical property for a device
*/
@OptIn(InternalDeviceAPI::class)
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
val propertyName = name ?: property.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
//TODO add type from converter
writable = true
}.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T? = device.getProperty(propertyName)?.let(converter::metaToObject)
override suspend fun write(device: D, value: T): Unit =
device.writeProperty(propertyName, converter.objectToMeta(value))
}
registerProperty(deviceProperty)
ReadOnlyProperty { _, _ ->
deviceProperty
}
}

View File

@ -0,0 +1,38 @@
package space.kscience.controls.spec
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import kotlin.time.Duration
/**
* Perform a recurring asynchronous read action and return a flow of results.
* The flow is lazy, so action is not performed unless flow is consumed.
* The flow uses called context. In order to call it on device context, use `flowOn(coroutineContext)`.
*
* The flow is canceled when the device scope is canceled
*/
public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
while (isActive) {
delay(interval)
launch {
emit(reader())
}
}
}
/**
* Do a recurring (with a fixed delay) task on a device.
*/
public fun <D : Device> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
while (isActive) {
delay(interval)
launch {
task()
}
}
}

View File

@ -0,0 +1,23 @@
package space.kscience.controls.spec
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
public fun Double.asMeta(): Meta = Meta(asValue())
//TODO to be moved to DF
public object DurationConverter : MetaConverter<Duration> {
override fun metaToObject(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS)
?: run {
val unit: DurationUnit = meta["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration")
return@run value.toDuration(unit)
}
override fun objectToMeta(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
}
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter

View File

@ -0,0 +1,145 @@
package space.kscience.controls.spec
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.metaDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
//read only delegates
public fun <D : Device> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
MetaConverter.boolean,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
name,
read
)
private inline fun numberDescriptor(
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): PropertyDescriptor.() -> Unit = {
metaDescriptor {
type(ValueType.NUMBER)
}
descriptorBuilder()
}
public fun <D : Device> DeviceSpec<D>.numberProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
MetaConverter.number,
numberDescriptor(descriptorBuilder),
name,
read
)
public fun <D : Device> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
MetaConverter.double,
numberDescriptor(descriptorBuilder),
name,
read
)
public fun <D : Device> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
MetaConverter.string,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
name,
read
)
public fun <D : Device> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
name,
read
)
//read-write delegates
public fun <D : Device> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean?,
write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
mutableProperty(
MetaConverter.boolean,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
name,
read,
write
)
public fun <D : Device> DeviceSpec<D>.numberProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Number,
write: suspend D.(Number) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double,
write: suspend D.(Double) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String,
write: suspend D.(String) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
public fun <D : Device> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta,
write: suspend D.(Meta) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)

View File

@ -0,0 +1,18 @@
package space.kscience.controls.api
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import space.kscience.controls.spec.asMeta
import kotlin.test.Test
import kotlin.test.assertEquals
class MessageTest {
@Test
fun messageSerialization() {
val changedMessage = PropertyChangedMessage("test", 22.0.asMeta())
val json = Json.encodeToString(changedMessage)
val reconstructed: PropertyChangedMessage = Json.decodeFromString(json)
assertEquals(changedMessage.time, reconstructed.time)
}
}

View File

@ -0,0 +1,133 @@
package space.kscience.controls.ports
import kotlinx.coroutines.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.info
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.*
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.ByteChannel
import java.nio.channels.DatagramChannel
import java.nio.channels.SocketChannel
import kotlin.coroutines.CoroutineContext
public fun ByteBuffer.toArray(limit: Int = limit()): ByteArray {
rewind()
val response = ByteArray(limit)
get(response)
rewind()
return response
}
/**
* A port based on nio [ByteChannel]
*/
public class ChannelPort(
context: Context,
coroutineContext: CoroutineContext = context.coroutineContext,
channelBuilder: suspend () -> ByteChannel,
) : AbstractPort(context, coroutineContext), AutoCloseable {
private val futureChannel: Deferred<ByteChannel> = this.scope.async(Dispatchers.IO) {
channelBuilder()
}
/**
* A handler to await port connection
*/
public val startJob: Job get() = futureChannel
private val listenerJob = this.scope.launch(Dispatchers.IO) {
val channel = futureChannel.await()
val buffer = ByteBuffer.allocate(1024)
while (isActive) {
try {
val num = channel.read(buffer)
if (num > 0) {
receive(buffer.toArray(num))
}
if (num < 0) cancel("The input channel is exhausted")
} catch (ex: Exception) {
logger.error(ex) { "Channel read error" }
delay(1000)
}
}
}
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) {
futureChannel.await().write(ByteBuffer.wrap(data))
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun close() {
listenerJob.cancel()
if (futureChannel.isCompleted) {
futureChannel.getCompleted().close()
} else {
futureChannel.cancel()
}
super.close()
}
}
/**
* A [PortFactory] for TCP connections
*/
public object TcpPort : PortFactory {
override val type: String = "tcp"
public fun open(
context: Context,
host: String,
port: Int,
coroutineContext: CoroutineContext = context.coroutineContext,
): ChannelPort = ChannelPort(context, coroutineContext) {
SocketChannel.open(InetSocketAddress(host, port))
}
override fun build(context: Context, meta: Meta): ChannelPort {
val host = meta["host"].string ?: "localhost"
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
return open(context, host, port)
}
}
/**
* A [PortFactory] for UDP connections
*/
public object UdpPort : PortFactory {
override val type: String = "udp"
/**
* Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages.
*/
public fun open(
context: Context,
remoteHost: String,
remotePort: Int,
localPort: Int? = null,
localHost: String = "localhost",
coroutineContext: CoroutineContext = context.coroutineContext,
): ChannelPort = ChannelPort(context, coroutineContext) {
DatagramChannel.open().apply {
//bind the channel to a local port to receive messages
localPort?.let { bind(InetSocketAddress(localHost, localPort)) }
//connect to remote port to send messages
connect(InetSocketAddress(remoteHost, remotePort))
context.logger.info { "Connected to UDP $remotePort on $remoteHost" }
}
}
override fun build(context: Context, meta: Meta): ChannelPort {
val remoteHost by meta.string { error("Remote host is not specified") }
val remotePort by meta.number { error("Remote port is not specified") }
val localHost: String? by meta.string()
val localPort: Int? by meta.int()
return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
}
}

View File

@ -0,0 +1,35 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
/**
* A plugin for loading JVM nio-based ports
*/
public class JvmPortsPlugin : AbstractPlugin() {
public val ports: Ports by require(Ports)
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(
TcpPort.type.parseAsName() to TcpPort,
UdpPort.type.parseAsName() to UdpPort
)
else -> emptyMap()
}
public companion object : PluginFactory<JvmPortsPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.jvm", group = PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): JvmPortsPlugin = JvmPortsPlugin()
}
}

View File

@ -1,4 +1,4 @@
package hep.dataforge.control.ports
package space.kscience.controls.ports
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

29
controls-magix/README.md Normal file
View File

@ -0,0 +1,29 @@
# Module controls-magix
Magix service for binding controls devices (both as RPC client and server)
## Features
- [controlsMagix](src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt) : Connect a `DeviceManage` with one or many devices to the Magix endpoint
- [DeviceClient](src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt) : A remote connector to Controls-kt device via Magix
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-magix:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-magix:0.2.0")
}
```

View File

@ -0,0 +1,199 @@
public final class space/kscience/controls/client/ControlsMagixKt {
public static final fun getMagixFormat (Lspace/kscience/controls/manager/DeviceManager$Companion;)Lspace/kscience/magix/api/MagixFormat;
public static final fun launchMagixService (Lspace/kscience/controls/manager/DeviceManager;Lspace/kscience/magix/api/MagixEndpoint;Ljava/lang/String;)Lkotlinx/coroutines/Job;
public static synthetic fun launchMagixService$default (Lspace/kscience/controls/manager/DeviceManager;Lspace/kscience/magix/api/MagixEndpoint;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
}
public final class space/kscience/controls/client/DeviceClient : space/kscience/controls/api/Device {
public fun <init> (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/names/Name;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;)V
public fun execute (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getActionDescriptors ()Ljava/util/Collection;
public fun getContext ()Lspace/kscience/dataforge/context/Context;
public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext;
public fun getLifecycleState ()Lspace/kscience/controls/api/DeviceLifecycleState;
public fun getMessageFlow ()Lkotlinx/coroutines/flow/Flow;
public fun getProperty (Ljava/lang/String;)Lspace/kscience/dataforge/meta/Meta;
public fun getPropertyDescriptors ()Ljava/util/Collection;
public fun invalidate (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun readProperty (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun writeProperty (Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/client/DeviceClientKt {
public static final fun remoteDevice (Lspace/kscience/magix/api/MagixEndpoint;Lspace/kscience/dataforge/context/Context;Ljava/lang/String;Lspace/kscience/dataforge/names/Name;)Lspace/kscience/controls/client/DeviceClient;
}
public final class space/kscience/controls/client/DoocsAction : java/lang/Enum {
public static final field Companion Lspace/kscience/controls/client/DoocsAction$Companion;
public static final field get Lspace/kscience/controls/client/DoocsAction;
public static final field names Lspace/kscience/controls/client/DoocsAction;
public static final field set Lspace/kscience/controls/client/DoocsAction;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lspace/kscience/controls/client/DoocsAction;
public static fun values ()[Lspace/kscience/controls/client/DoocsAction;
}
public final class space/kscience/controls/client/DoocsAction$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/DoocsPayload {
public static final field Companion Lspace/kscience/controls/client/DoocsPayload$Companion;
public synthetic fun <init> (ILspace/kscience/controls/client/DoocsAction;Ljava/lang/String;Lspace/kscience/controls/client/EqData;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Lspace/kscience/controls/client/DoocsAction;Ljava/lang/String;Lspace/kscience/controls/client/EqData;)V
public final fun component1 ()Lspace/kscience/controls/client/DoocsAction;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Lspace/kscience/controls/client/EqData;
public final fun copy (Lspace/kscience/controls/client/DoocsAction;Ljava/lang/String;Lspace/kscience/controls/client/EqData;)Lspace/kscience/controls/client/DoocsPayload;
public static synthetic fun copy$default (Lspace/kscience/controls/client/DoocsPayload;Lspace/kscience/controls/client/DoocsAction;Ljava/lang/String;Lspace/kscience/controls/client/EqData;ILjava/lang/Object;)Lspace/kscience/controls/client/DoocsPayload;
public fun equals (Ljava/lang/Object;)Z
public final fun getAction ()Lspace/kscience/controls/client/DoocsAction;
public final fun getAddress ()Ljava/lang/String;
public final fun getData ()Lspace/kscience/controls/client/EqData;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/client/DoocsPayload;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/client/DoocsPayload$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/client/DoocsPayload$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/client/DoocsPayload;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/client/DoocsPayload;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/DoocsPayload$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/EqData {
public static final field Companion Lspace/kscience/controls/client/EqData$Companion;
public synthetic fun <init> (IILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;)V
public synthetic fun <init> (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()I
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component4 ()Ljava/lang/Integer;
public final fun component5 ()Ljava/lang/Integer;
public final fun component6 ()Ljava/lang/Long;
public final fun component7 ()Ljava/lang/String;
public final fun copy (ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;)Lspace/kscience/controls/client/EqData;
public static synthetic fun copy$default (Lspace/kscience/controls/client/EqData;ILjava/lang/String;Lspace/kscience/dataforge/meta/Meta;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lspace/kscience/controls/client/EqData;
public fun equals (Ljava/lang/Object;)Z
public final fun getError ()Ljava/lang/Integer;
public final fun getEventId ()Ljava/lang/Integer;
public final fun getMessage ()Ljava/lang/String;
public final fun getTime ()Ljava/lang/Long;
public final fun getType ()Ljava/lang/String;
public final fun getTypeId ()I
public final fun getValue ()Lspace/kscience/dataforge/meta/Meta;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/client/EqData;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/client/EqData$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/client/EqData$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/client/EqData;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/client/EqData;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/EqData$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/TangoAction : java/lang/Enum {
public static final field Companion Lspace/kscience/controls/client/TangoAction$Companion;
public static final field exec Lspace/kscience/controls/client/TangoAction;
public static final field pipe Lspace/kscience/controls/client/TangoAction;
public static final field read Lspace/kscience/controls/client/TangoAction;
public static final field write Lspace/kscience/controls/client/TangoAction;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lspace/kscience/controls/client/TangoAction;
public static fun values ()[Lspace/kscience/controls/client/TangoAction;
}
public final class space/kscience/controls/client/TangoAction$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/TangoMagixKt {
public static final field TANGO_MAGIX_FORMAT Ljava/lang/String;
public static final fun launchTangoMagix (Lspace/kscience/controls/manager/DeviceManager;Lspace/kscience/magix/api/MagixEndpoint;Ljava/lang/String;)Lkotlinx/coroutines/Job;
public static synthetic fun launchTangoMagix$default (Lspace/kscience/controls/manager/DeviceManager;Lspace/kscience/magix/api/MagixEndpoint;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
}
public final class space/kscience/controls/client/TangoPayload {
public static final field Companion Lspace/kscience/controls/client/TangoPayload$Companion;
public synthetic fun <init> (ILspace/kscience/controls/client/TangoAction;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/controls/client/TangoQuality;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Ljava/util/List;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Lspace/kscience/controls/client/TangoAction;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/controls/client/TangoQuality;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Ljava/util/List;)V
public synthetic fun <init> (Lspace/kscience/controls/client/TangoAction;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/controls/client/TangoQuality;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lspace/kscience/controls/client/TangoAction;
public final fun component10 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component11 ()Ljava/util/List;
public final fun component2 ()I
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component7 ()Lspace/kscience/controls/client/TangoQuality;
public final fun component8 ()Lspace/kscience/dataforge/meta/Meta;
public final fun component9 ()Lspace/kscience/dataforge/meta/Meta;
public final fun copy (Lspace/kscience/controls/client/TangoAction;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/controls/client/TangoQuality;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Ljava/util/List;)Lspace/kscience/controls/client/TangoPayload;
public static synthetic fun copy$default (Lspace/kscience/controls/client/TangoPayload;Lspace/kscience/controls/client/TangoAction;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/controls/client/TangoQuality;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/meta/Meta;Ljava/util/List;ILjava/lang/Object;)Lspace/kscience/controls/client/TangoPayload;
public fun equals (Ljava/lang/Object;)Z
public final fun getAction ()Lspace/kscience/controls/client/TangoAction;
public final fun getArgin ()Lspace/kscience/dataforge/meta/Meta;
public final fun getArgout ()Lspace/kscience/dataforge/meta/Meta;
public final fun getData ()Lspace/kscience/dataforge/meta/Meta;
public final fun getDevice ()Ljava/lang/String;
public final fun getErrors ()Ljava/util/List;
public final fun getHost ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getQuality ()Lspace/kscience/controls/client/TangoQuality;
public final fun getTimestamp ()I
public final fun getValue ()Lspace/kscience/dataforge/meta/Meta;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lspace/kscience/controls/client/TangoPayload;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}
public final class space/kscience/controls/client/TangoPayload$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lspace/kscience/controls/client/TangoPayload$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/controls/client/TangoPayload;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/controls/client/TangoPayload;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/TangoPayload$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/controls/client/TangoQuality : java/lang/Enum {
public static final field ALARM Lspace/kscience/controls/client/TangoQuality;
public static final field Companion Lspace/kscience/controls/client/TangoQuality$Companion;
public static final field VALID Lspace/kscience/controls/client/TangoQuality;
public static final field WARNING Lspace/kscience/controls/client/TangoQuality;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lspace/kscience/controls/client/TangoQuality;
public static fun values ()[Lspace/kscience/controls/client/TangoQuality;
}
public final class space/kscience/controls/client/TangoQuality$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

View File

@ -0,0 +1,39 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
description = """
Magix service for binding controls devices (both as RPC client and server)
""".trimIndent()
kscience {
jvm()
js()
useSerialization {
json()
}
dependencies {
api(projects.magix.magixApi)
api(projects.controlsCore)
api("com.benasher44:uuid:0.8.0")
}
}
readme {
maturity = Maturity.EXPERIMENTAL
feature("controlsMagix", ref = "src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt"){
"""
Connect a `DeviceManage` with one or many devices to the Magix endpoint
""".trimIndent()
}
feature("DeviceClient", ref = "src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt"){
"""
A remote connector to Controls-kt device via Magix
""".trimIndent()
}
}

View File

@ -0,0 +1,117 @@
package space.kscience.controls.client
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.newCoroutineContext
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.*
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.send
import space.kscience.magix.api.subscribe
import kotlin.coroutines.CoroutineContext
private fun stringUID() = uuid4().leastSignificantBits.toString(16)
/**
* A remote accessible device that relies on connection via Magix
*/
public class DeviceClient(
override val context: Context,
private val deviceName: Name,
incomingFlow: Flow<DeviceMessage>,
private val send: suspend (DeviceMessage) -> Unit,
) : Device {
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
override val coroutineContext: CoroutineContext = newCoroutineContext(context.coroutineContext)
private val mutex = Mutex()
private val propertyCache = HashMap<String, Meta>()
override var propertyDescriptors: Collection<PropertyDescriptor> = emptyList()
private set
override var actionDescriptors: Collection<ActionDescriptor> = emptyList()
private set
private val flowInternal = incomingFlow.filter {
it.sourceDevice == deviceName
}.shareIn(this, started = SharingStarted.Eagerly).also {
it.onEach { message ->
when (message) {
is PropertyChangedMessage -> mutex.withLock {
propertyCache[message.property] = message.value
}
is DescriptionMessage -> mutex.withLock {
propertyDescriptors = message.properties
actionDescriptors = message.actions
}
else -> {
//ignore
}
}
}.launchIn(this)
}
override val messageFlow: Flow<DeviceMessage> get() = flowInternal
override suspend fun readProperty(propertyName: String): Meta {
send(
PropertyGetMessage(propertyName, targetDevice = deviceName)
)
return flowInternal.filterIsInstance<PropertyChangedMessage>().first {
it.property == propertyName
}.value
}
override fun getProperty(propertyName: String): Meta? = propertyCache[propertyName]
override suspend fun invalidate(propertyName: String) {
mutex.withLock {
propertyCache.remove(propertyName)
}
}
override suspend fun writeProperty(propertyName: String, value: Meta) {
send(
PropertySetMessage(propertyName, value, targetDevice = deviceName)
)
}
override suspend fun execute(actionName: String, argument: Meta?): Meta? {
val id = stringUID()
send(
ActionExecuteMessage(actionName, argument, id, targetDevice = deviceName)
)
return flowInternal.filterIsInstance<ActionResultMessage>().first {
it.action == actionName && it.requestId == id
}.result
}
@DFExperimental
override val lifecycleState: DeviceLifecycleState = DeviceLifecycleState.OPEN
}
/**
* Connect to a remote device via this endpoint.
*
* @param context a [Context] to run device in
* @param endpointName the name of endpoint in Magix to connect to
* @param deviceName the name of device within endpoint
*/
public fun MagixEndpoint.remoteDevice(context: Context, endpointName: String, deviceName: Name): DeviceClient {
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(endpointName)).map { it.second }
return DeviceClient(context, deviceName, subscription) {
send(DeviceManager.magixFormat, it, endpointName, id = stringUID())
}
}

View File

@ -0,0 +1,68 @@
package space.kscience.controls.client
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.hubMessageFlow
import space.kscience.controls.manager.respondHubMessage
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.magix.api.*
internal val controlsMagixFormat: MagixFormat<DeviceMessage> = MagixFormat(
DeviceMessage.serializer(),
setOf("controls-kt")
)
/**
* A magix message format to work with Controls-kt data
*/
public val DeviceManager.Companion.magixFormat: MagixFormat<DeviceMessage> get() = controlsMagixFormat
internal fun generateId(request: MagixMessage): String = if (request.id != null) {
"${request.id}.response"
} else {
"controls[${request.payload.hashCode().toString(16)}"
}
/**
* Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1)
*/
public fun DeviceManager.launchMagixService(
endpoint: MagixEndpoint,
endpointID: String = controlsMagixFormat.defaultFormat,
): Job = context.launch {
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID)).onEach { (request, payload) ->
val responsePayload = respondHubMessage(payload)
if (responsePayload != null) {
endpoint.send(
format = controlsMagixFormat,
payload = responsePayload,
source = endpointID,
target = request.sourceEndpoint,
id = generateId(request),
parentId = request.id
)
}
}.catch { error ->
logger.error(error) { "Error while responding to message: ${error.message}" }
}.launchIn(this)
hubMessageFlow(this).onEach { payload ->
endpoint.send(
format = controlsMagixFormat,
payload = payload,
source = endpointID,
id = "df[${payload.hashCode()}]"
)
}.catch { error ->
logger.error(error) { "Error while sending a message: ${error.message}" }
}.launchIn(this)
}

View File

@ -0,0 +1,119 @@
package space.kscience.controls.client
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
/*
"action":"get|set",
"eq_address": "string",
"eq_data": {
"type_id": "int[required]",
"type": "string[optional]",
"value": "object|value",
"event_id": "int[optional]",
"error": "int[optional]",
"time": "long[optional]",
"message": "string[optional]"
}
*/
@Serializable
public enum class DoocsAction {
get,
set,
names
}
@Serializable
public data class EqData(
@SerialName("type_id")
val typeId: Int,
val type: String? = null,
val value: Meta? = null,
@SerialName("event_id")
val eventId: Int? = null,
val error: Int? = null,
val time: Long? = null,
val message: String? = null
) {
public companion object {
internal const val DATA_NULL: Int = 0
internal const val DATA_INT: Int = 1
internal const val DATA_FLOAT: Int = 2
internal const val DATA_STRING: Int = 3
internal const val DATA_BOOL: Int = 4
internal const val DATA_STRING16: Int = 5
internal const val DATA_DOUBLE: Int = 6
internal const val DATA_TEXT: Int = 7
internal const val DATA_TDS: Int = 12
internal const val DATA_XY: Int = 13
internal const val DATA_IIII: Int = 14
internal const val DATA_IFFF: Int = 15
internal const val DATA_USTR: Int = 16
internal const val DATA_TTII: Int = 18
internal const val DATA_SPECTRUM: Int = 19
internal const val DATA_XML: Int = 20
internal const val DATA_XYZS: Int = 21
internal const val DATA_IMAGE: Int = 22
internal const val DATA_GSPECTRUM: Int = 24
internal const val DATA_SHORT: Int = 25
internal const val DATA_LONG: Int = 26
internal const val DATA_USHORT: Int = 27
internal const val DATA_UINT: Int = 28
internal const val DATA_ULONG: Int = 29
internal const val DATA_A_FLOAT: Int = 100
internal const val DATA_A_TDS: Int = 101
internal const val DATA_A_XY: Int = 102
internal const val DATA_A_USTR: Int = 103
internal const val DATA_A_INT: Int = 105
internal const val DATA_A_BYTE: Int = 106
internal const val DATA_A_XYZS: Int = 108
internal const val DATA_MDA_FLOAT: Int = 109
internal const val DATA_A_DOUBLE: Int = 110
internal const val DATA_A_BOOL: Int = 111
internal const val DATA_A_STRING: Int = 112
internal const val DATA_A_SHORT: Int = 113
internal const val DATA_A_LONG: Int = 114
internal const val DATA_MDA_DOUBLE: Int = 115
internal const val DATA_A_USHORT: Int = 116
internal const val DATA_A_UINT: Int = 117
internal const val DATA_A_ULONG: Int = 118
internal const val DATA_A_THUMBNAIL: Int = 120
internal const val DATA_A_TS_BOOL: Int = 1000
internal const val DATA_A_TS_INT: Int = 1001
internal const val DATA_A_TS_FLOAT: Int = 1002
internal const val DATA_A_TS_DOUBLE: Int = 1003
internal const val DATA_A_TS_LONG: Int = 1004
internal const val DATA_A_TS_STRING: Int = 1005
internal const val DATA_A_TS_USTR: Int = 1006
internal const val DATA_A_TS_XML: Int = 1007
internal const val DATA_A_TS_XY: Int = 1008
internal const val DATA_A_TS_IIII: Int = 1009
internal const val DATA_A_TS_IFFF: Int = 1010
internal const val DATA_A_TS_SPECTRUM: Int = 1013
internal const val DATA_A_TS_XYZS: Int = 1014
internal const val DATA_A_TS_GSPECTRUM: Int = 1015
internal const val DATA_KEYVAL: Int = 1016
internal const val DATA_A_TS_SHORT: Int = 1017
internal const val DATA_A_TS_USHORT: Int = 1018
internal const val DATA_A_TS_UINT: Int = 1019
internal const val DATA_A_TS_ULONG: Int = 1020
}
}
@Serializable
public data class DoocsPayload(
val action: DoocsAction,
@SerialName("eq_address")
val address: String,
@SerialName("eq_data")
val data: EqData?
)

View File

@ -0,0 +1,152 @@
package space.kscience.controls.client
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import space.kscience.controls.api.get
import space.kscience.controls.api.getOrReadProperty
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.magix.api.*
public const val TANGO_MAGIX_FORMAT: String = "tango"
/*
See https://github.com/waltz-controls/rfc/tree/master/4 for details
"action":"read|write|exec|pipe",
"timestamp": "int",
"host":"tango_host",
"device":"device name",
"name":"attribute, command or pipe's name",
"[value]":"attribute's value",
"[quality]":"VALID|WARNING|ALARM",
"[argin]":"command argin",
"[argout]":"command argout",
"[data]":"pipe's data",
"[errors]":[]
*/
@Serializable
public enum class TangoAction {
read,
write,
exec,
pipe
}
@Serializable
public enum class TangoQuality {
VALID,
WARNING,
ALARM
}
@Serializable
public data class TangoPayload(
val action: TangoAction,
val timestamp: Int,
val host: String,
val device: String,
val name: String,
val value: Meta? = null,
val quality: TangoQuality = TangoQuality.VALID,
val argin: Meta? = null,
val argout: Meta? = null,
val data: Meta? = null,
val errors: List<String>? = null,
)
internal val tangoMagixFormat = MagixFormat(
TangoPayload.serializer(),
setOf("tango")
)
/**
* Controls-kt device binding for Tango-flavored magix loop
*/
public fun DeviceManager.launchTangoMagix(
endpoint: MagixEndpoint,
endpointID: String = TANGO_MAGIX_FORMAT,
): Job {
suspend fun respond(request: MagixMessage, payload: TangoPayload, payloadBuilder: (TangoPayload) -> TangoPayload) {
endpoint.send(
tangoMagixFormat,
payload = payloadBuilder(payload),
source = endpointID,
id = generateId(request),
parentId = request.id
)
}
return context.launch {
endpoint.subscribe(tangoMagixFormat).onEach { (request, payload) ->
try {
val device = get(payload.device)
when (payload.action) {
TangoAction.read -> {
val value = device.getOrReadProperty(payload.name)
respond(request, payload) { requestPayload ->
requestPayload.copy(
value = value,
quality = TangoQuality.VALID
)
}
}
TangoAction.write -> {
payload.value?.let { value ->
device.writeProperty(payload.name, value)
}
//wait for value to be written and return final state
val value = device.getOrReadProperty(payload.name)
respond(request, payload) { requestPayload ->
requestPayload.copy(
value = value,
quality = TangoQuality.VALID
)
}
}
TangoAction.exec -> {
val result = device.execute(payload.name, payload.argin)
respond(request, payload) { requestPayload ->
requestPayload.copy(
argout = result,
quality = TangoQuality.VALID
)
}
}
TangoAction.pipe -> TODO("Pipe not implemented")
}
} catch (ex: Exception) {
logger.error(ex) { "Error while responding to message" }
endpoint.send(
tangoMagixFormat,
payload = payload.copy(quality = TangoQuality.WARNING),
source = endpointID,
id = generateId(request),
parentId = request.id
)
}
}.launchIn(this)
//TODO implement subscriptions?
// controller.messageOutput().onEach { payload ->
// endpoint.broadcast(
// MagixMessage(
// format = TANGO_MAGIX_FORMAT,
// id = "df[${payload.hashCode()}]",
// origin = endpointID,
// payload = payload
// )
// )
// }.catch { error ->
// logger.error(error) { "Error while sending a message" }
// }.launchIn(this)
}
}

31
controls-modbus/README.md Normal file
View File

@ -0,0 +1,31 @@
# Module controls-modbus
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
## Features
- [modbusRegistryMap](src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt) : Type-safe modbus registry map. Allows to define both single-register and multi-register entries (using DataForge IO).
Automatically checks consistency.
- [modbusProcessImage](src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt) : Binding of slave (server) modbus device to Controls-kt device
- [modbusDevice](src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt) : A device with additional methods to work with modbus registers.
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-modbus:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-modbus:0.2.0")
}
```

View File

@ -0,0 +1,166 @@
public final class space/kscience/controls/modbus/DeviceProcessImageBuilder {
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;Lkotlin/jvm/functions/Function2;)Lcom/ghgande/j2mod/modbus/procimg/ObservableDigitalOut;
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;Lspace/kscience/controls/spec/WritableDevicePropertySpec;)Lcom/ghgande/j2mod/modbus/procimg/ObservableDigitalOut;
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;Lkotlin/jvm/functions/Function2;)Lcom/ghgande/j2mod/modbus/procimg/DigitalIn;
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;Lspace/kscience/controls/spec/DevicePropertySpec;)Lcom/ghgande/j2mod/modbus/procimg/DigitalIn;
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRange;Lspace/kscience/controls/spec/WritableDevicePropertySpec;)V
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;Lkotlin/jvm/functions/Function2;)Lcom/ghgande/j2mod/modbus/procimg/ObservableRegister;
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;Lspace/kscience/controls/spec/WritableDevicePropertySpec;)Lcom/ghgande/j2mod/modbus/procimg/ObservableRegister;
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRange;Lspace/kscience/controls/spec/DevicePropertySpec;)V
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRegister;Lkotlin/jvm/functions/Function2;)Lcom/ghgande/j2mod/modbus/procimg/SimpleInputRegister;
public final fun bind (Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRegister;Lspace/kscience/controls/spec/DevicePropertySpec;)Lcom/ghgande/j2mod/modbus/procimg/SimpleInputRegister;
public static synthetic fun bind$default (Lspace/kscience/controls/modbus/DeviceProcessImageBuilder;Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/ghgande/j2mod/modbus/procimg/ObservableDigitalOut;
public static synthetic fun bind$default (Lspace/kscience/controls/modbus/DeviceProcessImageBuilder;Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/ghgande/j2mod/modbus/procimg/DigitalIn;
public static synthetic fun bind$default (Lspace/kscience/controls/modbus/DeviceProcessImageBuilder;Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/ghgande/j2mod/modbus/procimg/ObservableRegister;
public static synthetic fun bind$default (Lspace/kscience/controls/modbus/DeviceProcessImageBuilder;Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRegister;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/ghgande/j2mod/modbus/procimg/SimpleInputRegister;
public final fun bindAction (Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;Lkotlin/jvm/functions/Function3;)Lcom/ghgande/j2mod/modbus/procimg/ObservableDigitalOut;
public final fun bindAction (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRange;Lkotlin/jvm/functions/Function3;)Ljava/util/List;
public final fun bindAction (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;Lkotlin/jvm/functions/Function3;)Lcom/ghgande/j2mod/modbus/procimg/ObservableRegister;
public final fun getImage ()Lcom/ghgande/j2mod/modbus/procimg/ProcessImageImplementation;
}
public final class space/kscience/controls/modbus/DeviceProcessImageKt {
public static final fun bindProcessImage (Lspace/kscience/controls/api/Device;ZLkotlin/jvm/functions/Function1;)Lcom/ghgande/j2mod/modbus/procimg/ProcessImage;
public static synthetic fun bindProcessImage$default (Lspace/kscience/controls/api/Device;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/ghgande/j2mod/modbus/procimg/ProcessImage;
}
public abstract interface class space/kscience/controls/modbus/ModbusDevice : space/kscience/controls/api/Device {
public abstract fun getClientId ()I
public abstract fun getMaster ()Lcom/ghgande/j2mod/modbus/facade/AbstractModbusMaster;
public fun getValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;Ljava/lang/Object;Lkotlin/reflect/KProperty;)Z
public fun getValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;Ljava/lang/Object;Lkotlin/reflect/KProperty;)Z
public fun getValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRange;Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public fun getValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;Ljava/lang/Object;Lkotlin/reflect/KProperty;)S
public fun getValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRange;Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public fun getValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRegister;Ljava/lang/Object;Lkotlin/reflect/KProperty;)S
public fun setValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;Ljava/lang/Object;Lkotlin/reflect/KProperty;Z)V
public fun setValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRange;Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
public fun setValue (Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;Ljava/lang/Object;Lkotlin/reflect/KProperty;S)V
}
public class space/kscience/controls/modbus/ModbusDeviceBySpec : space/kscience/controls/spec/DeviceBySpec, space/kscience/controls/modbus/ModbusDevice {
public fun <init> (Lspace/kscience/dataforge/context/Context;Lspace/kscience/controls/spec/DeviceSpec;ILcom/ghgande/j2mod/modbus/facade/AbstractModbusMaster;ZLspace/kscience/dataforge/meta/Meta;)V
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lspace/kscience/controls/spec/DeviceSpec;ILcom/ghgande/j2mod/modbus/facade/AbstractModbusMaster;ZLspace/kscience/dataforge/meta/Meta;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun getClientId ()I
public fun getMaster ()Lcom/ghgande/j2mod/modbus/facade/AbstractModbusMaster;
public fun open (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class space/kscience/controls/modbus/ModbusDeviceKt {
public static final fun modbusDoubleRegister (Lspace/kscience/controls/modbus/ModbusDevice;I)Lkotlin/properties/ReadWriteProperty;
public static final fun modbusRegister (Lspace/kscience/controls/modbus/ModbusDevice;I)Lkotlin/properties/ReadWriteProperty;
public static final fun readCoil (Lspace/kscience/controls/modbus/ModbusDevice;I)Z
public static final fun readCoils (Lspace/kscience/controls/modbus/ModbusDevice;II)Lcom/ghgande/j2mod/modbus/util/BitVector;
public static final fun readDoubleInput (Lspace/kscience/controls/modbus/ModbusDevice;I)D
public static final fun readDoubleRegister (Lspace/kscience/controls/modbus/ModbusDevice;I)D
public static final fun readHoldingRegister (Lspace/kscience/controls/modbus/ModbusDevice;I)S
public static final fun readHoldingRegisters (Lspace/kscience/controls/modbus/ModbusDevice;II)Ljava/util/List;
public static final fun readHoldingRegistersToBuffer (Lspace/kscience/controls/modbus/ModbusDevice;II)Ljava/nio/ByteBuffer;
public static final fun readHoldingRegistersToPacket (Lspace/kscience/controls/modbus/ModbusDevice;II)Lio/ktor/utils/io/core/ByteReadPacket;
public static final fun readInputDiscrete (Lspace/kscience/controls/modbus/ModbusDevice;I)Z
public static final fun readInputDiscretes (Lspace/kscience/controls/modbus/ModbusDevice;II)Lcom/ghgande/j2mod/modbus/util/BitVector;
public static final fun readInputRegister (Lspace/kscience/controls/modbus/ModbusDevice;I)S
public static final fun readInputRegisters (Lspace/kscience/controls/modbus/ModbusDevice;II)Ljava/util/List;
public static final fun readInputRegistersToBuffer (Lspace/kscience/controls/modbus/ModbusDevice;II)Ljava/nio/ByteBuffer;
public static final fun readInputRegistersToPacket (Lspace/kscience/controls/modbus/ModbusDevice;II)Lio/ktor/utils/io/core/ByteReadPacket;
public static final fun writeCoil (Lspace/kscience/controls/modbus/ModbusDevice;IZ)V
public static final fun writeCoil (Lspace/kscience/controls/modbus/ModbusDevice;Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;Z)V
public static final fun writeCoils (Lspace/kscience/controls/modbus/ModbusDevice;I[Z)V
public static final fun writeHoldingRegister (Lspace/kscience/controls/modbus/ModbusDevice;IS)I
public static final fun writeHoldingRegister (Lspace/kscience/controls/modbus/ModbusDevice;Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;S)I
public static final fun writeHoldingRegisters (Lspace/kscience/controls/modbus/ModbusDevice;ILjava/nio/ByteBuffer;)I
public static final fun writeHoldingRegisters (Lspace/kscience/controls/modbus/ModbusDevice;I[S)I
public static final fun writeShortRegister (Lspace/kscience/controls/modbus/ModbusDevice;IS)V
}
public final class space/kscience/controls/modbus/ModbusHub : java/lang/AutoCloseable, space/kscience/controls/api/DeviceHub {
public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/jvm/functions/Function0;Ljava/util/Map;)V
public fun close ()V
public final fun getContext ()Lspace/kscience/dataforge/context/Context;
public fun getDevices ()Ljava/util/Map;
public final fun getMaster ()Lcom/ghgande/j2mod/modbus/facade/AbstractModbusMaster;
public final fun getMasterBuilder ()Lkotlin/jvm/functions/Function0;
public final fun getSpecs ()Ljava/util/Map;
}
public abstract class space/kscience/controls/modbus/ModbusRegistryKey {
public abstract fun getAddress ()I
public fun getCount ()I
}
public final class space/kscience/controls/modbus/ModbusRegistryKey$Coil : space/kscience/controls/modbus/ModbusRegistryKey {
public fun <init> (I)V
public final fun component1 ()I
public final fun copy (I)Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;
public static synthetic fun copy$default (Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;IILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;
public fun equals (Ljava/lang/Object;)Z
public fun getAddress ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput : space/kscience/controls/modbus/ModbusRegistryKey {
public fun <init> (I)V
public final fun component1 ()I
public final fun copy (I)Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;
public static synthetic fun copy$default (Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;IILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;
public fun equals (Ljava/lang/Object;)Z
public fun getAddress ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/controls/modbus/ModbusRegistryKey$HoldingRange : space/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister {
public fun <init> (IILspace/kscience/dataforge/io/IOFormat;)V
public fun getCount ()I
public final fun getEndAddress ()I
public final fun getFormat ()Lspace/kscience/dataforge/io/IOFormat;
public fun toString ()Ljava/lang/String;
}
public class space/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister : space/kscience/controls/modbus/ModbusRegistryKey {
public fun <init> (I)V
public fun getAddress ()I
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/controls/modbus/ModbusRegistryKey$InputRange : space/kscience/controls/modbus/ModbusRegistryKey$InputRegister {
public fun <init> (IILspace/kscience/dataforge/io/IOFormat;)V
public fun getCount ()I
public final fun getEndAddress ()I
public final fun getFormat ()Lspace/kscience/dataforge/io/IOFormat;
public fun toString ()Ljava/lang/String;
}
public class space/kscience/controls/modbus/ModbusRegistryKey$InputRegister : space/kscience/controls/modbus/ModbusRegistryKey {
public fun <init> (I)V
public fun getAddress ()I
public fun toString ()Ljava/lang/String;
}
public abstract class space/kscience/controls/modbus/ModbusRegistryMap {
public static final field Companion Lspace/kscience/controls/modbus/ModbusRegistryMap$Companion;
public fun <init> ()V
protected final fun coil (ILjava/lang/String;)Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;
public static synthetic fun coil$default (Lspace/kscience/controls/modbus/ModbusRegistryMap;ILjava/lang/String;ILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$Coil;
protected final fun discrete (ILjava/lang/String;)Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;
public static synthetic fun discrete$default (Lspace/kscience/controls/modbus/ModbusRegistryMap;ILjava/lang/String;ILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$DiscreteInput;
public final fun getEntries ()Ljava/util/Map;
protected final fun input (IILspace/kscience/dataforge/io/IOFormat;Ljava/lang/String;)Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRange;
protected final fun input (ILjava/lang/String;)Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRegister;
public static synthetic fun input$default (Lspace/kscience/controls/modbus/ModbusRegistryMap;IILspace/kscience/dataforge/io/IOFormat;Ljava/lang/String;ILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRange;
public static synthetic fun input$default (Lspace/kscience/controls/modbus/ModbusRegistryMap;ILjava/lang/String;ILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$InputRegister;
protected final fun register (IILspace/kscience/dataforge/io/IOFormat;Ljava/lang/String;)Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRange;
protected final fun register (ILjava/lang/String;)Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;
protected final fun register (Lspace/kscience/controls/modbus/ModbusRegistryKey;Ljava/lang/String;)Lspace/kscience/controls/modbus/ModbusRegistryKey;
public static synthetic fun register$default (Lspace/kscience/controls/modbus/ModbusRegistryMap;IILspace/kscience/dataforge/io/IOFormat;Ljava/lang/String;ILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRange;
public static synthetic fun register$default (Lspace/kscience/controls/modbus/ModbusRegistryMap;ILjava/lang/String;ILjava/lang/Object;)Lspace/kscience/controls/modbus/ModbusRegistryKey$HoldingRegister;
}
public final class space/kscience/controls/modbus/ModbusRegistryMap$Companion {
public final fun print (Lspace/kscience/controls/modbus/ModbusRegistryMap;Ljava/lang/Appendable;)V
public static synthetic fun print$default (Lspace/kscience/controls/modbus/ModbusRegistryMap$Companion;Lspace/kscience/controls/modbus/ModbusRegistryMap;Ljava/lang/Appendable;ILjava/lang/Object;)V
public final fun validate (Lspace/kscience/controls/modbus/ModbusRegistryMap;)V
}

View File

@ -0,0 +1,39 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
description = """
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
""".trimIndent()
dependencies {
api(projects.controlsCore)
api("com.ghgande:j2mod:3.1.1")
}
readme{
maturity = Maturity.EXPERIMENTAL
feature("modbusRegistryMap", ref = "src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt"){
"""
Type-safe modbus registry map. Allows to define both single-register and multi-register entries (using DataForge IO).
Automatically checks consistency.
""".trimIndent()
}
feature("modbusProcessImage", ref = "src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt"){
"""
Binding of slave (server) modbus device to Controls-kt device
""".trimIndent()
}
feature("modbusDevice", ref = "src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt"){
"""
A device with additional methods to work with modbus registers.
""".trimIndent()
}
}

View File

@ -0,0 +1,219 @@
package space.kscience.controls.modbus
import com.ghgande.j2mod.modbus.procimg.*
import io.ktor.utils.io.core.buildPacket
import io.ktor.utils.io.core.readByteBuffer
import io.ktor.utils.io.core.writeShort
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.WritableDevicePropertySpec
import space.kscience.controls.spec.set
import space.kscience.controls.spec.useProperty
public class DeviceProcessImageBuilder<D : Device> internal constructor(
private val device: D,
public val image: ProcessImageImplementation,
) {
public fun bind(
key: ModbusRegistryKey.Coil,
block: D.(ObservableDigitalOut) -> Unit = {},
): ObservableDigitalOut {
val coil = ObservableDigitalOut()
device.block(coil)
image.addDigitalOut(key.address, coil)
return coil
}
public fun bind(
key: ModbusRegistryKey.Coil,
propertySpec: WritableDevicePropertySpec<D, Boolean>,
): ObservableDigitalOut = bind(key) { coil ->
coil.addObserver { _, _ ->
device[propertySpec] = coil.isSet
}
device.useProperty(propertySpec) { value ->
coil.set(value)
}
}
public fun bind(
key: ModbusRegistryKey.DiscreteInput,
block: D.(SimpleDigitalIn) -> Unit = {},
): DigitalIn {
val input = SimpleDigitalIn()
device.block(input)
image.addDigitalIn(key.address, input)
return input
}
public fun bind(
key: ModbusRegistryKey.DiscreteInput,
propertySpec: DevicePropertySpec<D, Boolean>,
): DigitalIn = bind(key) { input ->
device.useProperty(propertySpec) { value ->
input.set(value)
}
}
public fun bind(
key: ModbusRegistryKey.InputRegister,
block: D.(SimpleInputRegister) -> Unit = {},
): SimpleInputRegister {
val input = SimpleInputRegister()
device.block(input)
image.addInputRegister(key.address, input)
return input
}
public fun bind(
key: ModbusRegistryKey.InputRegister,
propertySpec: DevicePropertySpec<D, Short>,
): SimpleInputRegister = bind(key) { input ->
device.useProperty(propertySpec) { value ->
input.setValue(value)
}
}
public fun bind(
key: ModbusRegistryKey.HoldingRegister,
block: D.(ObservableRegister) -> Unit = {},
): ObservableRegister {
val register = ObservableRegister()
device.block(register)
image.addRegister(key.address, register)
return register
}
public fun bind(
key: ModbusRegistryKey.HoldingRegister,
propertySpec: WritableDevicePropertySpec<D, Short>,
): ObservableRegister = bind(key) { register ->
register.addObserver { _, _ ->
device[propertySpec] = register.toShort()
}
device.useProperty(propertySpec) { value ->
register.setValue(value)
}
}
public fun <T> bind(key: ModbusRegistryKey.InputRange<T>, propertySpec: DevicePropertySpec<D, T>) {
val registers = List(key.count) {
SimpleInputRegister()
}
registers.forEachIndexed { index, register ->
image.addInputRegister(key.address + index, register)
}
device.useProperty(propertySpec) { value ->
val packet = buildPacket {
key.format.writeObject(this, value)
}.readByteBuffer()
registers.forEachIndexed { index, register ->
register.setValue(packet.getShort(index * 2))
}
}
}
public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: WritableDevicePropertySpec<D, T>) {
val registers = List(key.count) {
ObservableRegister()
}
registers.forEachIndexed { index, register ->
register.addObserver { _, _ ->
val packet = buildPacket {
registers.forEach { value ->
writeShort(value.toShort())
}
}
device[propertySpec] = key.format.readObject(packet)
}
image.addRegister(key.address + index, register)
}
device.useProperty(propertySpec) { value ->
val packet = buildPacket {
key.format.writeObject(this, value)
}.readByteBuffer()
registers.forEachIndexed { index, observableRegister ->
observableRegister.setValue(packet.getShort(index * 2))
}
}
}
public fun bindAction(
key: ModbusRegistryKey.Coil,
action: suspend D.(Boolean) -> Unit,
): ObservableDigitalOut {
val coil = ObservableDigitalOut()
coil.addObserver { _, _ ->
device.launch {
device.action(coil.isSet)
}
}
image.addDigitalOut(key.address, coil)
return coil
}
public fun bindAction(
key: ModbusRegistryKey.HoldingRegister,
action: suspend D.(Short) -> Unit,
): ObservableRegister {
val register = ObservableRegister()
register.addObserver { _, _ ->
with(device) {
launch {
action(register.toShort())
}
}
}
image.addRegister(key.address, register)
return register
}
public fun <T> bindAction(
key: ModbusRegistryKey.HoldingRange<T>,
action: suspend D.(T) -> Unit,
): List<ObservableRegister> {
val registers = List(key.count) {
ObservableRegister()
}
registers.forEachIndexed { index, register ->
register.addObserver { _, _ ->
val packet = buildPacket {
registers.forEach { value ->
writeShort(value.toShort())
}
}
device.launch {
device.action(key.format.readObject(packet))
}
}
image.addRegister(key.address + index, register)
}
return registers
}
}
/**
* Bind the device to Modbus slave (server) image.
*/
public fun <D : Device> D.bindProcessImage(
openOnBind: Boolean = true,
binding: DeviceProcessImageBuilder<D>.() -> Unit,
): ProcessImage {
val image = SimpleProcessImage()
DeviceProcessImageBuilder(this, image).apply(binding)
if (openOnBind) {
launch {
open()
}
}
return image
}

View File

@ -0,0 +1,209 @@
package space.kscience.controls.modbus
import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster
import com.ghgande.j2mod.modbus.procimg.InputRegister
import com.ghgande.j2mod.modbus.procimg.Register
import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister
import com.ghgande.j2mod.modbus.util.BitVector
import io.ktor.utils.io.core.ByteReadPacket
import io.ktor.utils.io.core.buildPacket
import io.ktor.utils.io.core.readByteBuffer
import io.ktor.utils.io.core.writeShort
import space.kscience.controls.api.Device
import java.nio.ByteBuffer
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* A Modbus device backed by j2mod client
*/
public interface ModbusDevice : Device {
/**
* Client id for this specific device
*/
public val clientId: Int
/**
* The modubus master connector
*/
public val master: AbstractModbusMaster
public operator fun ModbusRegistryKey.Coil.getValue(thisRef: Any?, property: KProperty<*>): Boolean =
readCoil(address)
public operator fun ModbusRegistryKey.Coil.setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
writeCoil(address, value)
}
public operator fun ModbusRegistryKey.DiscreteInput.getValue(thisRef: Any?, property: KProperty<*>): Boolean =
readInputDiscrete(address)
public operator fun ModbusRegistryKey.InputRegister.getValue(thisRef: Any?, property: KProperty<*>): Short =
readInputRegister(address)
public operator fun <T> ModbusRegistryKey.InputRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
val packet = readInputRegistersToPacket(address, count)
return format.readObject(packet)
}
public operator fun ModbusRegistryKey.HoldingRegister.getValue(thisRef: Any?, property: KProperty<*>): Short =
readHoldingRegister(address)
public operator fun ModbusRegistryKey.HoldingRegister.setValue(
thisRef: Any?,
property: KProperty<*>,
value: Short,
) {
writeHoldingRegister(address, value)
}
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
val packet = readInputRegistersToPacket(address, count)
return format.readObject(packet)
}
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.setValue(
thisRef: Any?,
property: KProperty<*>,
value: T,
) {
val buffer = buildPacket {
format.writeObject(this, value)
}.readByteBuffer()
writeHoldingRegisters(address, buffer)
}
}
/**
* Read multiple sequential modbus coils (bit-values)
*/
public fun ModbusDevice.readCoils(address: Int, count: Int): BitVector =
master.readCoils(clientId, address, count)
public fun ModbusDevice.readCoil(address: Int): Boolean =
master.readCoils(clientId, address, 1).getBit(0)
public fun ModbusDevice.writeCoils(address: Int, values: BooleanArray) {
val bitVector = BitVector(values.size)
values.forEachIndexed { index, value ->
bitVector.setBit(index, value)
}
master.writeMultipleCoils(clientId, address, bitVector)
}
public fun ModbusDevice.writeCoil(address: Int, value: Boolean) {
master.writeCoil(clientId, address, value)
}
public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) {
master.writeCoil(clientId, key.address, value)
}
public fun ModbusDevice.readInputDiscretes(address: Int, count: Int): BitVector =
master.readInputDiscretes(clientId, address, count)
public fun ModbusDevice.readInputDiscrete(address: Int): Boolean =
master.readInputDiscretes(clientId, address, 1).getBit(0)
public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List<InputRegister> =
master.readInputRegisters(clientId, address, count).toList()
private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
val buffer: ByteBuffer = ByteBuffer.allocate(size * 2)
forEachIndexed { index, value ->
buffer.position(index * 2)
buffer.put(value.toBytes())
}
buffer.flip()
return buffer
}
private fun Array<out InputRegister>.toPacket(): ByteReadPacket = buildPacket {
forEach { value ->
writeShort(value.toShort())
}
}
public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer =
master.readInputRegisters(clientId, address, count).toBuffer()
public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket =
master.readInputRegisters(clientId, address, count).toPacket()
public fun ModbusDevice.readDoubleInput(address: Int): Double =
readInputRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
public fun ModbusDevice.readInputRegister(address: Int): Short =
readInputRegisters(address, 1).first().toShort()
public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Register> =
master.readMultipleRegisters(clientId, address, count).toList()
/**
* Read a number of registers to a [ByteBuffer]
* @param address of a register
* @param count number of 2-bytes registers to read. Buffer size is 2*[count]
*/
public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer =
master.readMultipleRegisters(clientId, address, count).toBuffer()
public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket =
master.readMultipleRegisters(clientId, address, count).toPacket()
public fun ModbusDevice.readDoubleRegister(address: Int): Double =
readHoldingRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
public fun ModbusDevice.readHoldingRegister(address: Int): Short =
readHoldingRegisters(address, 1).first().toShort()
public fun ModbusDevice.writeHoldingRegisters(address: Int, values: ShortArray): Int =
master.writeMultipleRegisters(
clientId,
address,
Array<Register>(values.size) { SimpleInputRegister(values[it].toInt()) }
)
public fun ModbusDevice.writeHoldingRegister(address: Int, value: Short): Int =
master.writeSingleRegister(
clientId,
address,
SimpleInputRegister(value.toInt())
)
public fun ModbusDevice.writeHoldingRegister(key: ModbusRegistryKey.HoldingRegister, value: Short): Int =
writeHoldingRegister(key.address, value)
public fun ModbusDevice.writeHoldingRegisters(address: Int, buffer: ByteBuffer): Int {
val array: ShortArray = ShortArray(buffer.limit().floorDiv(2)) { buffer.getShort(it * 2) }
return writeHoldingRegisters(address, array)
}
public fun ModbusDevice.writeShortRegister(address: Int, value: Short) {
master.writeSingleRegister(address, SimpleInputRegister(value.toInt()))
}
public fun ModbusDevice.modbusRegister(
address: Int,
): ReadWriteProperty<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> {
override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Short = readHoldingRegister(address)
override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Short) {
writeHoldingRegister(address, value)
}
}
public fun ModbusDevice.modbusDoubleRegister(
address: Int,
): ReadWriteProperty<ModbusDevice, Double> = object : ReadWriteProperty<ModbusDevice, Double> {
override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Double = readDoubleRegister(address)
override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Double) {
val buffer = ByteBuffer.allocate(Double.SIZE_BYTES).apply { putDouble(value) }
writeHoldingRegisters(address, buffer)
}
}

View File

@ -0,0 +1,59 @@
package space.kscience.controls.modbus
import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.NameToken
/**
* A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client
*/
public open class ModbusDeviceBySpec<D: Device>(
context: Context,
spec: DeviceSpec<D>,
override val clientId: Int,
override val master: AbstractModbusMaster,
private val disposeMasterOnClose: Boolean = true,
meta: Meta = Meta.EMPTY,
) : ModbusDevice, DeviceBySpec<D>(spec, context, meta){
override suspend fun open() {
master.connect()
super<DeviceBySpec>.open()
}
override fun close() {
if(disposeMasterOnClose){
master.disconnect()
}
super<ModbusDevice>.close()
}
}
public class ModbusHub(
public val context: Context,
public val masterBuilder: () -> AbstractModbusMaster,
public val specs: Map<NameToken, Pair<Int, DeviceSpec<*>>>,
) : DeviceHub, AutoCloseable {
public val master: AbstractModbusMaster by lazy(masterBuilder)
override val devices: Map<NameToken, ModbusDevice> by lazy {
specs.mapValues { (_, pair) ->
ModbusDeviceBySpec(
context,
pair.second,
pair.first,
master
)
}
}
override fun close() {
master.disconnect()
}
}

View File

@ -0,0 +1,163 @@
package space.kscience.controls.modbus
import space.kscience.dataforge.io.IOFormat
public sealed class ModbusRegistryKey {
public abstract val address: Int
public open val count: Int = 1
/**
* Read-only boolean value
*/
public data class Coil(override val address: Int) : ModbusRegistryKey()
/**
* Read-write boolean value
*/
public data class DiscreteInput(override val address: Int) : ModbusRegistryKey()
/**
* Read-only binary value
*/
public open class InputRegister(override val address: Int) : ModbusRegistryKey() {
override fun toString(): String = "InputRegister(address=$address)"
}
public class InputRange<T>(
address: Int,
override val count: Int,
public val format: IOFormat<T>,
) : InputRegister(address) {
public val endAddress: Int get() = address + count
override fun toString(): String = "InputRange(count=$count, format=$format)"
}
public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() {
override fun toString(): String = "HoldingRegister(address=$address)"
}
public class HoldingRange<T>(
address: Int,
override val count: Int,
public val format: IOFormat<T>,
) : HoldingRegister(address) {
public val endAddress: Int get() = address + count
override fun toString(): String = "HoldingRange(count=$count, format=$format)"
}
}
public abstract class ModbusRegistryMap {
private val _entries: MutableMap<ModbusRegistryKey, String> = mutableMapOf<ModbusRegistryKey, String>()
public val entries: Map<ModbusRegistryKey, String> get() = _entries
protected fun <T : ModbusRegistryKey> register(key: T, description: String): T {
_entries[key] = description
return key
}
protected fun coil(address: Int, description: String = ""): ModbusRegistryKey.Coil =
register(ModbusRegistryKey.Coil(address), description)
protected fun discrete(address: Int, description: String = ""): ModbusRegistryKey.DiscreteInput =
register(ModbusRegistryKey.DiscreteInput(address), description)
protected fun input(address: Int, description: String = ""): ModbusRegistryKey.InputRegister =
register(ModbusRegistryKey.InputRegister(address), description)
protected fun <T> input(
address: Int,
count: Int,
reader: IOFormat<T>,
description: String = "",
): ModbusRegistryKey.InputRange<T> =
register(ModbusRegistryKey.InputRange(address, count, reader), description)
protected fun register(address: Int, description: String = ""): ModbusRegistryKey.HoldingRegister =
register(ModbusRegistryKey.HoldingRegister(address), description)
protected fun <T> register(
address: Int,
count: Int,
format: IOFormat<T>,
description: String = "",
): ModbusRegistryKey.HoldingRange<T> =
register(ModbusRegistryKey.HoldingRange(address, count, format), description)
public companion object {
public fun validate(map: ModbusRegistryMap) {
var lastCoil: ModbusRegistryKey.Coil? = null
var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null
var lastInput: ModbusRegistryKey.InputRegister? = null
var lastRegister: ModbusRegistryKey.HoldingRegister? = null
map.entries.keys.sortedBy { it.address }.forEach { key ->
when (key) {
is ModbusRegistryKey.Coil -> if (lastCoil?.let { key.address >= it.address + it.count } != false) {
lastCoil = key
} else {
error("Key $lastCoil overlaps with key $key")
}
is ModbusRegistryKey.DiscreteInput -> if (lastDiscreteInput?.let { key.address >= it.address + it.count } != false) {
lastDiscreteInput = key
} else {
error("Key $lastDiscreteInput overlaps with key $key")
}
is ModbusRegistryKey.InputRegister -> if (lastInput?.let { key.address >= it.address + it.count } != false) {
lastInput = key
} else {
error("Key $lastInput overlaps with key $key")
}
is ModbusRegistryKey.HoldingRegister -> if (lastRegister?.let { key.address >= it.address + it.count } != false) {
lastRegister = key
} else {
error("Key $lastRegister overlaps with key $key")
}
}
}
}
private val ModbusRegistryKey.sectionNumber
get() = when (this) {
is ModbusRegistryKey.Coil -> 1
is ModbusRegistryKey.DiscreteInput -> 2
is ModbusRegistryKey.HoldingRegister -> 4
is ModbusRegistryKey.InputRegister -> 3
}
public fun print(map: ModbusRegistryMap, to: Appendable = System.out) {
validate(map)
map.entries.entries
.sortedWith(
Comparator.comparingInt<Map.Entry<ModbusRegistryKey, String>?> { it.key.sectionNumber }
.thenComparingInt { it.key.address }
)
.forEach { (key, description) ->
val typeString = when (key) {
is ModbusRegistryKey.Coil -> "Coil"
is ModbusRegistryKey.DiscreteInput -> "Discrete"
is ModbusRegistryKey.HoldingRegister -> "Register"
is ModbusRegistryKey.InputRegister -> "Input"
}
val rangeString = if (key.count == 1) {
key.address.toString()
} else {
"${key.address} - ${key.address + key.count}"
}
to.appendLine("${typeString}\t$rangeString\t$description")
}
}
}
}

29
controls-opcua/README.md Normal file
View File

@ -0,0 +1,29 @@
# Module controls-opcua
A client and server connectors for OPC-UA via Eclipse Milo
## Features
- [opcuaClient](src/main/kotlin/space/kscience/controls/opcua/client) : Connect a Controls-kt as a client to OPC UA server
- [opcuaServer](src/main/kotlin/space/kscience/controls/opcua/server) : Create an OPC UA server on top of Controls-kt device (or device hub)
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-opcua:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-opcua:0.2.0")
}
```

View File

@ -0,0 +1,82 @@
public final class space/kscience/controls/opcua/client/MetaBsdParser : org/eclipse/milo/opcua/binaryschema/parser/BsdParser {
public fun <init> ()V
}
public final class space/kscience/controls/opcua/client/MetaBsdParserKt {
public static final fun toMeta (Lorg/eclipse/milo/opcua/stack/core/types/builtin/Variant;Lorg/eclipse/milo/opcua/stack/core/serialization/SerializationContext;)Lspace/kscience/dataforge/meta/Meta;
}
public final class space/kscience/controls/opcua/client/MiloConfiguration : space/kscience/dataforge/meta/Scheme {
public static final field Companion Lspace/kscience/controls/opcua/client/MiloConfiguration$Companion;
public fun <init> ()V
public final fun getEndpointUrl ()Ljava/lang/String;
public final fun getSecurityPolicy ()Lorg/eclipse/milo/opcua/stack/core/security/SecurityPolicy;
public final fun getUsername ()Lspace/kscience/controls/opcua/client/MiloUsername;
public final fun setEndpointUrl (Ljava/lang/String;)V
public final fun setSecurityPolicy (Lorg/eclipse/milo/opcua/stack/core/security/SecurityPolicy;)V
public final fun setUsername (Lspace/kscience/controls/opcua/client/MiloUsername;)V
}
public final class space/kscience/controls/opcua/client/MiloConfiguration$Companion : space/kscience/dataforge/meta/SchemeSpec {
}
public abstract class space/kscience/controls/opcua/client/MiloIdentity : space/kscience/dataforge/meta/Scheme {
}
public final class space/kscience/controls/opcua/client/MiloUsername : space/kscience/controls/opcua/client/MiloIdentity {
public static final field Companion Lspace/kscience/controls/opcua/client/MiloUsername$Companion;
public fun <init> ()V
public final fun getPassword ()Ljava/lang/String;
public final fun getUsername ()Ljava/lang/String;
public final fun setPassword (Ljava/lang/String;)V
public final fun setUsername (Ljava/lang/String;)V
}
public final class space/kscience/controls/opcua/client/MiloUsername$Companion : space/kscience/dataforge/meta/SchemeSpec {
}
public abstract interface class space/kscience/controls/opcua/client/OpcUaDevice : space/kscience/controls/api/Device {
public abstract fun getClient ()Lorg/eclipse/milo/opcua/sdk/client/OpcUaClient;
}
public class space/kscience/controls/opcua/client/OpcUaDeviceBySpec : space/kscience/controls/spec/DeviceBySpec, space/kscience/controls/opcua/client/OpcUaDevice {
public fun <init> (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/controls/opcua/client/MiloConfiguration;Lspace/kscience/dataforge/context/Context;)V
public synthetic fun <init> (Lspace/kscience/controls/spec/DeviceSpec;Lspace/kscience/controls/opcua/client/MiloConfiguration;Lspace/kscience/dataforge/context/Context;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun getClient ()Lorg/eclipse/milo/opcua/sdk/client/OpcUaClient;
}
public final class space/kscience/controls/opcua/client/OpcUaDeviceKt {
public static final fun opcDouble (Lspace/kscience/controls/opcua/client/OpcUaDevice;Lorg/eclipse/milo/opcua/stack/core/types/builtin/NodeId;D)Lkotlin/properties/ReadWriteProperty;
public static synthetic fun opcDouble$default (Lspace/kscience/controls/opcua/client/OpcUaDevice;Lorg/eclipse/milo/opcua/stack/core/types/builtin/NodeId;DILjava/lang/Object;)Lkotlin/properties/ReadWriteProperty;
public static final fun opcInt (Lspace/kscience/controls/opcua/client/OpcUaDevice;Lorg/eclipse/milo/opcua/stack/core/types/builtin/NodeId;D)Lkotlin/properties/ReadWriteProperty;
public static synthetic fun opcInt$default (Lspace/kscience/controls/opcua/client/OpcUaDevice;Lorg/eclipse/milo/opcua/stack/core/types/builtin/NodeId;DILjava/lang/Object;)Lkotlin/properties/ReadWriteProperty;
public static final fun opcString (Lspace/kscience/controls/opcua/client/OpcUaDevice;Lorg/eclipse/milo/opcua/stack/core/types/builtin/NodeId;D)Lkotlin/properties/ReadWriteProperty;
public static synthetic fun opcString$default (Lspace/kscience/controls/opcua/client/OpcUaDevice;Lorg/eclipse/milo/opcua/stack/core/types/builtin/NodeId;DILjava/lang/Object;)Lkotlin/properties/ReadWriteProperty;
}
public final class space/kscience/controls/opcua/server/DeviceNameSpace : org/eclipse/milo/opcua/sdk/server/api/ManagedNamespaceWithLifecycle {
public static final field Companion Lspace/kscience/controls/opcua/server/DeviceNameSpace$Companion;
public static final field NAMESPACE_URI Ljava/lang/String;
public fun <init> (Lorg/eclipse/milo/opcua/sdk/server/OpcUaServer;Lspace/kscience/controls/manager/DeviceManager;)V
public final fun getDeviceManager ()Lspace/kscience/controls/manager/DeviceManager;
public fun onDataItemsCreated (Ljava/util/List;)V
public fun onDataItemsDeleted (Ljava/util/List;)V
public fun onDataItemsModified (Ljava/util/List;)V
public fun onMonitoringModeChanged (Ljava/util/List;)V
}
public final class space/kscience/controls/opcua/server/DeviceNameSpace$Companion {
}
public final class space/kscience/controls/opcua/server/DeviceNameSpaceKt {
public static final fun get (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/api/PropertyDescriptor;)Lspace/kscience/dataforge/meta/Meta;
public static final fun read (Lspace/kscience/controls/api/Device;Lspace/kscience/controls/api/PropertyDescriptor;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun serveDevices (Lorg/eclipse/milo/opcua/sdk/server/OpcUaServer;Lspace/kscience/controls/manager/DeviceManager;)Lspace/kscience/controls/opcua/server/DeviceNameSpace;
}
public final class space/kscience/controls/opcua/server/ServerUtilsKt {
public static final fun OpcUaServer (Lkotlin/jvm/functions/Function1;)Lorg/eclipse/milo/opcua/sdk/server/OpcUaServer;
public static final fun endpoint (Lorg/eclipse/milo/opcua/sdk/server/api/config/OpcUaServerConfigBuilder;Lkotlin/jvm/functions/Function1;)V
}

View File

@ -0,0 +1,41 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
description = """
A client and server connectors for OPC-UA via Eclipse Milo
""".trimIndent()
val ktorVersion: String by rootProject.extra
val miloVersion: String = "0.6.10"
dependencies {
api(projects.controlsCore)
api(spclibs.kotlinx.coroutines.jdk8)
api("org.eclipse.milo:sdk-client:$miloVersion")
api("org.eclipse.milo:bsd-parser:$miloVersion")
api("org.eclipse.milo:sdk-server:$miloVersion")
testImplementation(spclibs.kotlinx.coroutines.test)
}
readme{
maturity = Maturity.EXPERIMENTAL
feature("opcuaClient", ref = "src/main/kotlin/space/kscience/controls/opcua/client"){
"""
Connect a Controls-kt as a client to OPC UA server
""".trimIndent()
}
feature("opcuaServer", ref = "src/main/kotlin/space/kscience/controls/opcua/server"){
"""
Create an OPC UA server on top of Controls-kt device (or device hub)
""".trimIndent()
}
}

View File

@ -0,0 +1,209 @@
package space.kscience.controls.opcua.client
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toKotlinInstant
import org.eclipse.milo.opcua.binaryschema.AbstractCodec
import org.eclipse.milo.opcua.binaryschema.parser.BsdParser
import org.eclipse.milo.opcua.stack.core.UaSerializationException
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamDecoder
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamEncoder
import org.eclipse.milo.opcua.stack.core.serialization.SerializationContext
import org.eclipse.milo.opcua.stack.core.serialization.codecs.OpcUaBinaryDataTypeCodec
import org.eclipse.milo.opcua.stack.core.types.builtin.*
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.*
import org.opcfoundation.opcua.binaryschema.EnumeratedType
import org.opcfoundation.opcua.binaryschema.StructuredType
import space.kscience.controls.misc.instant
import space.kscience.controls.misc.toMeta
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import java.util.*
public class MetaBsdParser : BsdParser() {
override fun getEnumCodec(enumeratedType: EnumeratedType): OpcUaBinaryDataTypeCodec<*> {
return MetaEnumCodec()
}
override fun getStructCodec(structuredType: StructuredType): OpcUaBinaryDataTypeCodec<*> {
return MetaStructureCodec(structuredType)
}
}
internal class MetaEnumCodec : OpcUaBinaryDataTypeCodec<Number> {
override fun getType(): Class<Number> {
return Number::class.java
}
@Throws(UaSerializationException::class)
override fun encode(
context: SerializationContext,
encoder: OpcUaBinaryStreamEncoder,
value: Number
) {
encoder.writeInt32(value.toInt())
}
@Throws(UaSerializationException::class)
override fun decode(
context: SerializationContext,
decoder: OpcUaBinaryStreamDecoder
): Number {
return decoder.readInt32()
}
}
internal fun opcToMeta(value: Any?): Meta = when (value) {
null -> Meta(Null)
is Meta -> value
is Value -> Meta(value)
is Number -> when (value) {
is UByte -> Meta(value.toShort().asValue())
is UShort -> Meta(value.toInt().asValue())
is UInteger -> Meta(value.toLong().asValue())
is ULong -> Meta(value.toBigInteger().asValue())
else -> Meta(value.asValue())
}
is Boolean -> Meta(value.asValue())
is String -> Meta(value.asValue())
is Char -> Meta(value.toString().asValue())
is DateTime -> value.javaInstant.toKotlinInstant().toMeta()
is UUID -> Meta(value.toString().asValue())
is QualifiedName -> Meta {
"namespaceIndex" put value.namespaceIndex
"name" put value.name?.asValue()
}
is LocalizedText -> Meta {
"locale" put value.locale?.asValue()
"text" put value.text?.asValue()
}
is DataValue -> Meta {
"value" put opcToMeta(value.value) // need SerializationContext to do that properly
value.statusCode?.value?.let { "status" put Meta(it.asValue()) }
value.sourceTime?.javaInstant?.let { "sourceTime" put it.toKotlinInstant().toMeta() }
value.sourcePicoseconds?.let { "sourcePicoseconds" put Meta(it.asValue()) }
value.serverTime?.javaInstant?.let { "serverTime" put it.toKotlinInstant().toMeta() }
value.serverPicoseconds?.let { "serverPicoseconds" put Meta(it.asValue()) }
}
is ByteString -> Meta(value.bytesOrEmpty().asValue())
is XmlElement -> Meta(value.fragment?.asValue() ?: Null)
is NodeId -> Meta(value.toParseableString().asValue())
is ExpandedNodeId -> Meta(value.toParseableString().asValue())
is StatusCode -> Meta(value.value.asValue())
//is ExtensionObject -> value.decode(client.getDynamicSerializationContext())
else -> error("Could not create Meta for value: $value")
}
/**
* based on https://github.com/eclipse/milo/blob/master/opc-ua-stack/bsd-parser-gson/src/main/java/org/eclipse/milo/opcua/binaryschema/gson/JsonStructureCodec.java
*/
internal class MetaStructureCodec(
structuredType: StructuredType?
) : AbstractCodec<Meta, Meta>(structuredType) {
override fun getType(): Class<Meta> = Meta::class.java
override fun createStructure(name: String, members: LinkedHashMap<String, Meta>): Meta = Meta {
members.forEach { (property: String, value: Meta?) ->
setMeta(Name.parse(property), value)
}
}
override fun opcUaToMemberTypeScalar(name: String, value: Any?, typeName: String): Meta = opcToMeta(value)
override fun opcUaToMemberTypeArray(name: String, values: Any?, typeName: String): Meta = if (values == null) {
Meta(Null)
} else {
// This is a bit array...
when (values) {
is DoubleArray -> Meta(values.asValue())
is FloatArray -> Meta(values.asValue())
is IntArray -> Meta(values.asValue())
is ByteArray -> Meta(values.asValue())
is ShortArray -> Meta(values.asValue())
is Array<*> -> Meta {
setIndexed(Name.parse(name), values.map { opcUaToMemberTypeScalar(name, it, typeName) })
}
is Number -> Meta(values.asValue())
else -> error("Could not create Meta for value: $values")
}
}
override fun memberTypeToOpcUaScalar(member: Meta?, typeName: String): Any? =
if (member == null || member.isEmpty()) {
null
} else when (typeName) {
"Boolean" -> member.boolean
"SByte" -> member.value?.numberOrNull?.toByte()
"Int16" -> member.value?.numberOrNull?.toShort()
"Int32" -> member.value?.numberOrNull?.toInt()
"Int64" -> member.value?.numberOrNull?.toLong()
"Byte" -> member.value?.numberOrNull?.toShort()?.let { Unsigned.ubyte(it) }
"UInt16" -> member.value?.numberOrNull?.toInt()?.let { Unsigned.ushort(it) }
"UInt32" -> member.value?.numberOrNull?.toLong()?.let { Unsigned.uint(it) }
"UInt64" -> member.value?.numberOrNull?.toLong()?.let { Unsigned.ulong(it) }
"Float" -> member.value?.numberOrNull?.toFloat()
"Double" -> member.value?.numberOrNull?.toDouble()
"String" -> member.string
"DateTime" -> DateTime(member.instant().toJavaInstant())
"Guid" -> member.string?.let { UUID.fromString(it) }
"ByteString" -> member.value?.list?.let { list ->
ByteString(list.map { it.number.toByte() }.toByteArray())
}
"XmlElement" -> member.string?.let { XmlElement(it) }
"NodeId" -> member.string?.let { NodeId.parse(it) }
"ExpandedNodeId" -> member.string?.let { ExpandedNodeId.parse(it) }
"StatusCode" -> member.long?.let { StatusCode(it) }
"QualifiedName" -> QualifiedName(
member["namespaceIndex"].int ?: 0,
member["name"].string
)
"LocalizedText" -> LocalizedText(
member["locale"].string,
member["text"].string
)
else -> member.toString()
}
override fun memberTypeToOpcUaArray(member: Meta, typeName: String): Any = if ("Bit" == typeName) {
member.value?.int ?: error("Meta node does not contain int value")
} else {
when (typeName) {
"SByte" -> member.value?.list?.map { it.number.toByte() }?.toByteArray() ?: emptyArray<Byte>()
"Int16" -> member.value?.list?.map { it.number.toShort() }?.toShortArray() ?: emptyArray<Short>()
"Int32" -> member.value?.list?.map { it.number.toInt() }?.toIntArray() ?: emptyArray<Int>()
"Int64" -> member.value?.list?.map { it.number.toLong() }?.toLongArray() ?: emptyArray<Long>()
"Byte" -> member.value?.list?.map {
Unsigned.ubyte(it.number.toShort())
}?.toTypedArray() ?: emptyArray<UByte>()
"UInt16" -> member.value?.list?.map {
Unsigned.ushort(it.number.toInt())
}?.toTypedArray() ?: emptyArray<UShort>()
"UInt32" -> member.value?.list?.map {
Unsigned.uint(it.number.toLong())
}?.toTypedArray() ?: emptyArray<UInteger>()
"UInt64" -> member.value?.list?.map {
Unsigned.ulong(it.number.toLong())
}?.toTypedArray() ?: emptyArray<kotlin.ULong>()
"Float" -> member.value?.list?.map { it.number.toFloat() }?.toFloatArray() ?: emptyArray<Float>()
"Double" -> member.value?.list?.map { it.number.toDouble() }?.toDoubleArray() ?: emptyArray<Double>()
else -> member.getIndexed(Meta.JSON_ARRAY_KEY.asName()).map {
memberTypeToOpcUaScalar(it.value, typeName)
}.toTypedArray()
}
}
override fun getMembers(value: Meta): Map<String, Meta> = value.items.mapKeys { it.toString() }
}
public fun Variant.toMeta(serializationContext: SerializationContext): Meta = (value as? ExtensionObject)?.let {
it.decode(serializationContext) as Meta
} ?: opcToMeta(value)
//public fun Meta.toVariant(): Variant = if (items.isEmpty()) {
// Variant(value?.value)
//} else {
// TODO()
//}

View File

@ -0,0 +1,126 @@
package space.kscience.controls.opcua.client
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.*
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
import space.kscience.controls.api.Device
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* An OPC-UA device backed by Eclipse Milo client
*/
public interface OpcUaDevice : Device {
/**
* The OPC-UA client initialized on first use
*/
public val client: OpcUaClient
}
/**
* Read OPC-UA value with timestamp
* @param T the type of property to read. The value is coerced to it.
*/
public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): Pair<T, DateTime> {
val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await()
val time = data.serverTime ?: error("No server time provided")
val meta: Meta = when (val content = data.value.value) {
is T -> return content to time
is Meta -> content
is ExtensionObject -> content.decode(client.dynamicSerializationContext) as Meta
else -> error("Incompatible OPC property value $content")
}
val res: T = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
return res to time
}
/**
* Read and coerce value from OPC-UA
*/
public suspend inline fun <reified T> OpcUaDevice.readOpc(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): T {
val data: DataValue = client.readValue(magAge, TimestampsToReturn.Neither, nodeId).await()
val content = data.value.value
if(content is T) return content
val meta: Meta = when (content) {
is Meta -> content
//Always decode string as Json meta
is String -> Json.decodeFromString(MetaSerializer, content)
is Number -> Meta(content)
is Boolean -> Meta(content)
//content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
else -> error("Incompatible OPC property value $content")
}
return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
}
public suspend inline fun <reified T> OpcUaDevice.writeOpc(
nodeId: NodeId,
converter: MetaConverter<T>,
value: T
): StatusCode {
val meta = converter.objectToMeta(value)
return client.writeValue(nodeId, DataValue(Variant(meta))).await()
}
/**
* A device-bound OPC-UA property. Does not trigger device properties change.
*/
public inline fun <reified T> OpcUaDevice.opc(
nodeId: NodeId,
converter: MetaConverter<T>,
magAge: Double = 500.0
): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking {
readOpc(nodeId, converter, magAge)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
launch {
writeOpc(nodeId, converter, value)
}
}
}
/**
* Register a mutable OPC-UA based [Double] property in a device spec
*/
public fun OpcUaDevice.opcDouble(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Double> = opc<Double>(nodeId, MetaConverter.double, magAge)
/**
* Register a mutable OPC-UA based [Int] property in a device spec
*/
public fun OpcUaDevice.opcInt(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
/**
* Register a mutable OPC-UA based [String] property in a device spec
*/
public fun OpcUaDevice.opcString(
nodeId: NodeId,
magAge: Double = 1.0
): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)

View File

@ -0,0 +1,70 @@
package space.kscience.controls.opcua.client
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.*
public sealed class MiloIdentity : Scheme()
public class MiloUsername : MiloIdentity() {
public var username: String by string { error("Username not defined") }
public var password: String by string { error("Password not defined") }
public companion object : SchemeSpec<MiloUsername>(::MiloUsername)
}
//public class MiloKeyPair : MiloIdentity() {
//
// public companion object : SchemeSpec<MiloUsername>(::MiloUsername)
//}
public class MiloConfiguration : Scheme() {
public var endpointUrl: String by string { error("Endpoint url is not defined") }
public var username: MiloUsername? by specOrNull(MiloUsername)
public var securityPolicy: SecurityPolicy by enum(SecurityPolicy.None)
internal fun configureClient(builder: OpcUaClientConfigBuilder) {
username?.let {
builder.setIdentityProvider(UsernameProvider(it.username, it.password))
}
}
public companion object : SchemeSpec<MiloConfiguration>(::MiloConfiguration)
}
/**
* A variant of [DeviceBySpec] that includes OPC-UA client
*/
public open class OpcUaDeviceBySpec<D : Device>(
spec: DeviceSpec<D>,
config: MiloConfiguration,
context: Context = Global,
) : OpcUaDevice, DeviceBySpec<D>(spec, context, config.meta) {
override val client: OpcUaClient by lazy {
context.createOpcUaClient(
config.endpointUrl,
securityPolicy = config.securityPolicy,
opcClientConfig = { config.configureClient(this) }
).apply {
connect().get()
}
}
override fun close() {
client.disconnect()
super<DeviceBySpec>.close()
}
}

View File

@ -0,0 +1,63 @@
package space.kscience.controls.opcua.client
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider
import org.eclipse.milo.opcua.stack.client.security.DefaultClientCertificateValidator
import org.eclipse.milo.opcua.stack.core.security.DefaultTrustListManager
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.info
import space.kscience.dataforge.context.logger
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
internal fun <T : Any> T?.toOptional(): Optional<T> = Optional.ofNullable(this)
internal fun Context.createOpcUaClient(
endpointUrl: String, //"opc.tcp://localhost:12686/milo"
securityPolicy: SecurityPolicy = SecurityPolicy.Basic256Sha256,
endpointFilter: (EndpointDescription?) -> Boolean = { securityPolicy.uri == it?.securityPolicyUri },
opcClientConfig: OpcUaClientConfigBuilder.() -> Unit,
): OpcUaClient {
val securityTempDir: Path = Paths.get(System.getProperty("java.io.tmpdir"), "client", "security")
Files.createDirectories(securityTempDir)
check(Files.exists(securityTempDir)) { "Unable to create security dir: $securityTempDir" }
val pkiDir: Path = securityTempDir.resolve("pki")
logger.info { "Milo client security dir: ${securityTempDir.toAbsolutePath()}" }
logger.info { "Security pki dir: ${pkiDir.toAbsolutePath()}" }
//val loader: KeyStoreLoader = KeyStoreLoader().load(securityTempDir)
val trustListManager = DefaultTrustListManager(pkiDir.toFile())
val certificateValidator = DefaultClientCertificateValidator(trustListManager)
return OpcUaClient.create(
endpointUrl,
{ endpoints: List<EndpointDescription?> ->
endpoints.firstOrNull(endpointFilter).toOptional()
}
) { configBuilder: OpcUaClientConfigBuilder ->
configBuilder
.setApplicationName(LocalizedText.english("Controls-kt"))
.setApplicationUri("urn:space.kscience:controls:opcua")
// .setKeyPair(loader.getClientKeyPair())
// .setCertificate(loader.getClientCertificate())
// .setCertificateChain(loader.getClientCertificateChain())
.setCertificateValidator(certificateValidator)
.setIdentityProvider(AnonymousProvider())
.setRequestTimeout(uint(5000))
.apply(opcClientConfig)
.build()
}
// .apply {
// addSessionInitializer(DataTypeDictionarySessionInitializer(MetaBsdParser()))
// }
}

View File

@ -0,0 +1,197 @@
package space.kscience.controls.opcua.server
import kotlinx.coroutines.launch
import kotlinx.datetime.toJavaInstant
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.sdk.core.AccessLevel
import org.eclipse.milo.opcua.sdk.core.Reference
import org.eclipse.milo.opcua.sdk.server.Lifecycle
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.sdk.server.api.DataItem
import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle
import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem
import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode
import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode
import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel
import org.eclipse.milo.opcua.stack.core.AttributeId
import org.eclipse.milo.opcua.stack.core.Identifiers
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.onPropertyChange
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus
public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name)
public suspend fun Device.read(propertyDescriptor: PropertyDescriptor): Meta = readProperty(propertyDescriptor.name)
/*
https://github.com/eclipse/milo/blob/master/milo-examples/server-examples/src/main/java/org/eclipse/milo/examples/server/ExampleNamespace.java
*/
public class DeviceNameSpace(
server: OpcUaServer,
public val deviceManager: DeviceManager
) : ManagedNamespaceWithLifecycle(server, NAMESPACE_URI) {
private val subscription = SubscriptionModel(server, this)
init {
lifecycleManager.addLifecycle(subscription)
lifecycleManager.addStartupTask {
nodeContext.registerHub(deviceManager, Name.EMPTY)
}
lifecycleManager.addLifecycle(object : Lifecycle {
override fun startup() {
server.addressSpaceManager.register(this@DeviceNameSpace)
}
override fun shutdown() {
server.addressSpaceManager.unregister(this@DeviceNameSpace)
}
})
}
private fun UaFolderNode.registerDeviceNodes(deviceName: Name, device: Device) {
val nodes = device.propertyDescriptors.associate { descriptor ->
val propertyName = descriptor.name
val node: UaVariableNode = UaVariableNode.UaVariableNodeBuilder(nodeContext).apply {
//for now, use DF paths as ids
nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName")
when {
descriptor.readable && descriptor.writable -> {
setAccessLevel(AccessLevel.READ_WRITE)
setUserAccessLevel(AccessLevel.READ_WRITE)
}
descriptor.writable -> {
setAccessLevel(AccessLevel.WRITE_ONLY)
setUserAccessLevel(AccessLevel.WRITE_ONLY)
}
descriptor.readable -> {
setAccessLevel(AccessLevel.READ_ONLY)
setUserAccessLevel(AccessLevel.READ_ONLY)
}
else -> {
setAccessLevel(AccessLevel.NONE)
setUserAccessLevel(AccessLevel.NONE)
}
}
browseName = newQualifiedName(propertyName)
displayName = LocalizedText.english(propertyName)
dataType = if (descriptor.metaDescriptor.children.isNotEmpty()) {
Identifiers.String
} else when (descriptor.metaDescriptor.valueTypes?.first()) {
null, ValueType.STRING, ValueType.NULL -> Identifiers.String
ValueType.NUMBER -> Identifiers.Number
ValueType.BOOLEAN -> Identifiers.Boolean
ValueType.LIST -> Identifiers.ArrayItemType
}
setTypeDefinition(Identifiers.BaseDataVariableType)
}.build()
device[descriptor]?.toOpc(sourceTime = null, serverTime = null)?.let {
node.value = it
}
/**
* Subscribe to node value changes
*/
node.addAttributeObserver { _: UaNode, attributeId: AttributeId, value: Any ->
if (attributeId == AttributeId.Value) {
val meta: Meta = when (value) {
is Meta -> value
is Boolean -> Meta(value)
is Number -> Meta(value)
is String -> Json.decodeFromString(MetaSerializer, value)
else -> return@addAttributeObserver //TODO("other types not implemented")
}
deviceManager.context.launch {
device.writeProperty(propertyName, meta)
}
}
}
nodeManager.addNode(node)
addOrganizes(node)
propertyName to node
}
//Subscribe on properties updates
device.onPropertyChange {
nodes[property]?.let { node ->
val sourceTime = time?.let { DateTime(it.toJavaInstant()) }
node.value = value.toOpc(sourceTime = sourceTime)
}
}
//recursively add sub-devices
if (device is DeviceHub) {
nodeContext.registerHub(device, deviceName)
}
}
private fun UaNodeContext.registerHub(hub: DeviceHub, namePrefix: Name) {
hub.devices.forEach { (deviceName, device) ->
val tokenAsString = deviceName.toString()
val deviceFolder = UaFolderNode(
this,
newNodeId(tokenAsString),
newQualifiedName(tokenAsString),
LocalizedText.english(tokenAsString)
)
deviceFolder.addReference(
Reference(
deviceFolder.nodeId,
Identifiers.Organizes,
Identifiers.ObjectsFolder.expanded(),
false
)
)
deviceFolder.registerDeviceNodes(namePrefix + deviceName, device)
this.nodeManager.addNode(deviceFolder)
}
}
override fun onDataItemsCreated(dataItems: List<DataItem?>?) {
subscription.onDataItemsCreated(dataItems)
}
override fun onDataItemsModified(dataItems: List<DataItem?>?) {
subscription.onDataItemsModified(dataItems)
}
override fun onDataItemsDeleted(dataItems: List<DataItem?>?) {
subscription.onDataItemsDeleted(dataItems)
}
override fun onMonitoringModeChanged(monitoredItems: List<MonitoredItem?>?) {
subscription.onMonitoringModeChanged(monitoredItems)
}
public companion object {
public const val NAMESPACE_URI: String = "urn:space:kscience:controls:opcua:server"
}
}
/**
* Serve devices from [deviceManager] as OPC-UA
*/
public fun OpcUaServer.serveDevices(deviceManager: DeviceManager): DeviceNameSpace =
DeviceNameSpace(this, deviceManager).apply { startup() }

View File

@ -0,0 +1,35 @@
package space.kscience.controls.opcua.server
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant
import space.kscience.dataforge.meta.*
import java.time.Instant
/**
* Convert Meta to OPC data value using
*/
internal fun Meta.toOpc(
statusCode: StatusCode = StatusCode.GOOD,
sourceTime: DateTime? = null,
serverTime: DateTime? = null
): DataValue {
val variant: Variant = if (isLeaf) {
when (value?.type) {
null, ValueType.NULL -> Variant.NULL_VALUE
ValueType.NUMBER -> Variant(value!!.number)
ValueType.STRING -> Variant(value!!.string)
ValueType.BOOLEAN -> Variant(value!!.boolean)
ValueType.LIST -> if (value!!.list.all { it.type == ValueType.NUMBER }) {
Variant(value!!.doubleArray.toTypedArray())
} else {
Variant(value!!.stringList.toTypedArray())
}
}
} else {
Variant(Json.encodeToString(MetaSerializer,this))
}
return DataValue(variant, statusCode, sourceTime,serverTime ?: DateTime(Instant.now()))
}

View File

@ -0,0 +1,77 @@
package space.kscience.controls.opcua.server
import org.eclipse.milo.opcua.sdk.core.AccessLevel
import org.eclipse.milo.opcua.sdk.core.Reference
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode
import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode
import org.eclipse.milo.opcua.stack.core.Identifiers
import org.eclipse.milo.opcua.stack.core.types.builtin.*
internal fun UaNode.inverseReferenceTo(targetNodeId: NodeId, typeId: NodeId) {
addReference(
Reference(
nodeId,
typeId,
targetNodeId.expanded(),
Reference.Direction.INVERSE
)
)
}
internal fun NodeId.resolve(child: String): NodeId {
val id = this.identifier.toString()
return NodeId(this.namespaceIndex, "$id/$child")
}
internal fun UaNodeContext.addVariableNode(
parentNodeId: NodeId,
name: String,
nodeId: NodeId = parentNodeId.resolve(name),
dataTypeId: NodeId,
value: Any,
referenceTypeId: NodeId = Identifiers.HasComponent
): UaVariableNode {
val variableNode: UaVariableNode = UaVariableNode.UaVariableNodeBuilder(this).apply {
setNodeId(nodeId)
setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE))
setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE))
setBrowseName(QualifiedName(parentNodeId.namespaceIndex, name))
setDisplayName(LocalizedText.english(name))
setDataType(dataTypeId)
setTypeDefinition(Identifiers.BaseDataVariableType)
setMinimumSamplingInterval(100.0)
setValue(DataValue(Variant(value)))
}.build()
// variableNode.filterChain.addFirst(AttributeLoggingFilter())
nodeManager.addNode(variableNode)
variableNode.inverseReferenceTo(
parentNodeId,
referenceTypeId
)
return variableNode
}
//
//fun UaNodeContext.addVariableNode(
// parentNodeId: NodeId,
// name: String,
// nodeId: NodeId = parentNodeId.resolve(name),
// dataType: BuiltinDataType = BuiltinDataType.Int32,
// referenceTypeId: NodeId = Identifiers.HasComponent
//): UaVariableNode = addVariableNode(
// parentNodeId,
// name,
// nodeId,
// dataType.nodeId,
// dataType.defaultValue(),
// referenceTypeId
//)

View File

@ -0,0 +1,28 @@
package space.kscience.controls.opcua.server
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfigBuilder
import org.eclipse.milo.opcua.stack.server.EndpointConfiguration
public fun OpcUaServer(block: OpcUaServerConfigBuilder.() -> Unit): OpcUaServer {
// .setProductUri(DemoServer.PRODUCT_URI)
// .setApplicationUri("${DemoServer.APPLICATION_URI}:$applicationUuid")
// .setApplicationName(LocalizedText.english("Eclipse Milo OPC UA Demo Server"))
// .setBuildInfo(buildInfo())
// .setTrustListManager(trustListManager)
// .setCertificateManager(certificateManager)
// .setCertificateValidator(certificateValidator)
// .setIdentityValidator(identityValidator)
// .setEndpoints(endpoints)
// .setLimits(ServerLimits)
val config = OpcUaServerConfig.builder().apply(block)
return OpcUaServer(config.build())
}
public fun OpcUaServerConfigBuilder.endpoint(block: EndpointConfiguration.Builder.() -> Unit) {
val endpoint = EndpointConfiguration.Builder().apply(block).build()
setEndpoints(setOf(endpoint))
}

View File

@ -0,0 +1,48 @@
package space.kscience.controls.opcua.client
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
import org.junit.jupiter.api.Test
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.controls.spec.read
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.test.Ignore
class OpcUaClientTest {
class DemoOpcUaDevice(config: MiloConfiguration) : OpcUaDeviceBySpec<DemoOpcUaDevice>(DemoOpcUaDevice, config) {
//val randomDouble by opcDouble(NodeId(2, "Dynamic/RandomDouble"))
suspend fun readRandomDouble() = readOpc(NodeId(2, "Dynamic/RandomDouble"), MetaConverter.double)
companion object : DeviceSpec<DemoOpcUaDevice>() {
/**
* Build a device. This is not a part of the specification
*/
fun build(): DemoOpcUaDevice {
val config = MiloConfiguration {
endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo"
}
return DemoOpcUaDevice(config)
}
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
@Ignore
fun testReadDouble() = runTest {
DemoOpcUaDevice.build().use{
println(it.read(DemoOpcUaDevice.randomDouble))
}
}
}

23
controls-pi/README.md Normal file
View File

@ -0,0 +1,23 @@
# Module controls-pi
Utils to work with controls-kt on Raspberry pi
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-pi:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-pi:0.2.0")
}
```

View File

@ -0,0 +1,28 @@
public final class space/kscience/controls/pi/PiPlugin : space/kscience/dataforge/context/AbstractPlugin {
public static final field Companion Lspace/kscience/controls/pi/PiPlugin$Companion;
public fun <init> ()V
public final fun getPorts ()Lspace/kscience/controls/ports/Ports;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/pi/PiPlugin$Companion : space/kscience/dataforge/context/PluginFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/pi/PiPlugin;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/pi/PiSerialPort : space/kscience/controls/ports/AbstractPort {
public static final field Companion Lspace/kscience/controls/pi/PiSerialPort$Companion;
public fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;)V
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public final fun getSerialBuilder ()Lkotlin/jvm/functions/Function0;
}
public final class space/kscience/controls/pi/PiSerialPort$Companion : space/kscience/controls/ports/PortFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/Port;
public fun getType ()Ljava/lang/String;
public final fun open (Lspace/kscience/dataforge/context/Context;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lspace/kscience/controls/pi/PiSerialPort;
}

View File

@ -0,0 +1,16 @@
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
description = """
Utils to work with controls-kt on Raspberry pi
""".trimIndent()
dependencies{
api(project(":controls-core"))
api("com.pi4j:pi4j-ktx:2.4.0") // Kotlin DSL
api("com.pi4j:pi4j-core:2.3.0")
api("com.pi4j:pi4j-plugin-raspberrypi:2.3.0")
api("com.pi4j:pi4j-plugin-pigpio:2.3.0")
}

View File

@ -0,0 +1,22 @@
package space.kscience.controls.pi
import space.kscience.controls.ports.Ports
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
public class PiPlugin : AbstractPlugin() {
public val ports: Ports by require(Ports)
override val tag: PluginTag get() = Companion.tag
public companion object : PluginFactory<PiPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.pi", group = PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): PiPlugin = PiPlugin()
}
}

View File

@ -0,0 +1,75 @@
package space.kscience.controls.pi
import com.pi4j.Pi4J
import com.pi4j.io.serial.Baud
import com.pi4j.io.serial.Serial
import com.pi4j.io.serial.SerialConfigBuilder
import com.pi4j.ktx.io.serial
import kotlinx.coroutines.*
import space.kscience.controls.ports.AbstractPort
import space.kscience.controls.ports.Port
import space.kscience.controls.ports.PortFactory
import space.kscience.controls.ports.toArray
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.enum
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
import java.nio.ByteBuffer
import kotlin.coroutines.CoroutineContext
public class PiSerialPort(
context: Context,
coroutineContext: CoroutineContext = context.coroutineContext,
public val serialBuilder: () -> Serial,
) : AbstractPort(context, coroutineContext) {
private val serial: Serial by lazy { serialBuilder() }
private val listenerJob = this.scope.launch(Dispatchers.IO) {
val buffer = ByteBuffer.allocate(1024)
while (isActive) {
try {
val num = serial.read(buffer)
if (num > 0) {
receive(buffer.toArray(num))
}
if (num < 0) cancel("The input channel is exhausted")
} catch (ex: Exception) {
logger.error(ex) { "Channel read error" }
delay(1000)
}
}
}
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) {
serial.write(data)
}
override fun close() {
listenerJob.cancel()
serial.close()
}
public companion object : PortFactory {
override val type: String get() = "pi"
public fun open(context: Context, device: String, block: SerialConfigBuilder.() -> Unit): PiSerialPort =
PiSerialPort(context) {
Pi4J.newAutoContext().serial(device, block)
}
override fun build(context: Context, meta: Meta): Port = PiSerialPort(context) {
val device: String = meta["device"].string ?: error("Device name not defined")
val baudRate: Baud = meta["baudRate"].enum<Baud>() ?: Baud._9600
Pi4J.newAutoContext().serial(device) {
baud8N1(baudRate)
}
}
}
}

View File

@ -0,0 +1,23 @@
# Module controls-ports-ktor
Implementation of byte ports on top os ktor-io asynchronous API
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-ports-ktor:0.2.0")
}
```

View File

@ -0,0 +1,47 @@
public final class space/kscience/controls/ports/KtorPortsPlugin : space/kscience/dataforge/context/AbstractPlugin {
public static final field Companion Lspace/kscience/controls/ports/KtorPortsPlugin$Companion;
public fun <init> ()V
public fun content (Ljava/lang/String;)Ljava/util/Map;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/ports/KtorPortsPlugin$Companion : space/kscience/dataforge/context/PluginFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/KtorPortsPlugin;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/ports/KtorTcpPort : space/kscience/controls/ports/AbstractPort, java/io/Closeable {
public static final field Companion Lspace/kscience/controls/ports/KtorTcpPort$Companion;
public fun close ()V
public final fun getHost ()Ljava/lang/String;
public final fun getPort ()I
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/controls/ports/KtorTcpPort$Companion : space/kscience/controls/ports/PortFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/Port;
public fun getType ()Ljava/lang/String;
public final fun open (Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILkotlin/coroutines/CoroutineContext;)Lspace/kscience/controls/ports/KtorTcpPort;
public static synthetic fun open$default (Lspace/kscience/controls/ports/KtorTcpPort$Companion;Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lspace/kscience/controls/ports/KtorTcpPort;
}
public final class space/kscience/controls/ports/KtorUdpPort : space/kscience/controls/ports/AbstractPort, java/io/Closeable {
public static final field Companion Lspace/kscience/controls/ports/KtorUdpPort$Companion;
public fun close ()V
public final fun getLocalHost ()Ljava/lang/String;
public final fun getLocalPort ()Ljava/lang/Integer;
public final fun getRemoteHost ()Ljava/lang/String;
public final fun getRemotePort ()I
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/controls/ports/KtorUdpPort$Companion : space/kscience/controls/ports/PortFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/Port;
public fun getType ()Ljava/lang/String;
public final fun open (Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILjava/lang/Integer;Ljava/lang/String;Lkotlin/coroutines/CoroutineContext;)Lspace/kscience/controls/ports/KtorUdpPort;
public static synthetic fun open$default (Lspace/kscience/controls/ports/KtorUdpPort$Companion;Lspace/kscience/dataforge/context/Context;Ljava/lang/String;ILjava/lang/Integer;Ljava/lang/String;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lspace/kscience/controls/ports/KtorUdpPort;
}

View File

@ -0,0 +1,21 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
description = """
Implementation of byte ports on top os ktor-io asynchronous API
""".trimIndent()
val ktorVersion: String by rootProject.extra
dependencies {
api(projects.controlsCore)
api("io.ktor:ktor-network:$ktorVersion")
}
readme{
maturity = Maturity.PROTOTYPE
}

View File

@ -0,0 +1,28 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
public class KtorPortsPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when (target) {
PortFactory.TYPE -> mapOf("tcp".asName() to KtorTcpPort, "udp".asName() to KtorUdpPort)
else -> emptyMap()
}
public companion object : PluginFactory<KtorPortsPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.ktor", group = PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): KtorPortsPlugin = KtorPortsPlugin()
}
}

View File

@ -1,22 +1,21 @@
package hep.dataforge.control.ports
package space.kscience.controls.ports
import hep.dataforge.context.Context
import hep.dataforge.meta.Meta
import hep.dataforge.meta.get
import hep.dataforge.meta.int
import hep.dataforge.meta.string
import io.ktor.network.selector.ActorSelectorManager
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import io.ktor.util.KtorExperimentalAPI
import io.ktor.utils.io.consumeEachBufferRange
import io.ktor.utils.io.core.Closeable
import io.ktor.utils.io.writeAvailable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.net.InetSocketAddress
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import kotlin.coroutines.CoroutineContext
public class KtorTcpPort internal constructor(
@ -24,13 +23,12 @@ public class KtorTcpPort internal constructor(
public val host: String,
public val port: Int,
coroutineContext: CoroutineContext = context.coroutineContext,
) : AbstractPort(context, coroutineContext), AutoCloseable {
) : AbstractPort(context, coroutineContext), Closeable {
override fun toString(): String = "port[tcp:$host:$port]"
@OptIn(KtorExperimentalAPI::class)
private val futureSocket = scope.async {
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(InetSocketAddress(host, port))
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port)
}
private val writeChannel = scope.async {
@ -39,7 +37,7 @@ public class KtorTcpPort internal constructor(
private val listenerJob = scope.launch {
val input = futureSocket.await().openReadChannel()
input.consumeEachBufferRange { buffer, last ->
input.consumeEachBufferRange { buffer, _ ->
val array = ByteArray(buffer.remaining())
buffer.get(array)
receive(array)
@ -57,7 +55,10 @@ public class KtorTcpPort internal constructor(
super.close()
}
public companion object: PortFactory {
public companion object : PortFactory {
override val type: String = "tcp"
public fun open(
context: Context,
host: String,
@ -67,7 +68,7 @@ public class KtorTcpPort internal constructor(
return KtorTcpPort(context, host, port, coroutineContext)
}
override fun invoke(meta: Meta, context: Context): Port {
override fun build(context: Context, meta: Meta): Port {
val host = meta["host"].string ?: "localhost"
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
return open(context, host, port)

View File

@ -0,0 +1,87 @@
package space.kscience.controls.ports
import io.ktor.network.selector.ActorSelectorManager
import io.ktor.network.sockets.InetSocketAddress
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import io.ktor.utils.io.consumeEachBufferRange
import io.ktor.utils.io.core.Closeable
import io.ktor.utils.io.writeAvailable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.number
import space.kscience.dataforge.meta.string
import kotlin.coroutines.CoroutineContext
public class KtorUdpPort internal constructor(
context: Context,
public val remoteHost: String,
public val remotePort: Int,
public val localPort: Int? = null,
public val localHost: String = "localhost",
coroutineContext: CoroutineContext = context.coroutineContext,
) : AbstractPort(context, coroutineContext), Closeable {
override fun toString(): String = "port[udp:$remoteHost:$remotePort]"
private val futureSocket = scope.async {
aSocket(ActorSelectorManager(Dispatchers.IO)).udp().connect(
remoteAddress = InetSocketAddress(remoteHost, remotePort),
localAddress = localPort?.let { InetSocketAddress(localHost, localPort) }
)
}
private val writeChannel = scope.async {
futureSocket.await().openWriteChannel(true)
}
private val listenerJob = scope.launch {
val input = futureSocket.await().openReadChannel()
input.consumeEachBufferRange { buffer, _ ->
val array = ByteArray(buffer.remaining())
buffer.get(array)
receive(array)
isActive
}
}
override suspend fun write(data: ByteArray) {
writeChannel.await().writeAvailable(data)
}
override fun close() {
listenerJob.cancel()
futureSocket.cancel()
super.close()
}
public companion object : PortFactory {
override val type: String = "udp"
public fun open(
context: Context,
remoteHost: String,
remotePort: Int,
localPort: Int? = null,
localHost: String = "localhost",
coroutineContext: CoroutineContext = context.coroutineContext,
): KtorUdpPort {
return KtorUdpPort(context, remoteHost, remotePort, localPort, localHost, coroutineContext)
}
override fun build(context: Context, meta: Meta): Port {
val remoteHost by meta.string { error("Remote host is not specified") }
val remotePort by meta.number { error("Remote port is not specified") }
val localHost: String? by meta.string()
val localPort: Int? by meta.int()
return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
}
}
}

23
controls-serial/README.md Normal file
View File

@ -0,0 +1,23 @@
# Module controls-serial
Implementation of direct serial port communication with JSerialComm
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-serial:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-serial:0.2.0")
}
```

View File

@ -0,0 +1,29 @@
public final class space/kscience/controls/serial/JSerialCommPort : space/kscience/controls/ports/AbstractPort {
public static final field Companion Lspace/kscience/controls/serial/JSerialCommPort$Companion;
public fun <init> (Lspace/kscience/dataforge/context/Context;Lcom/fazecast/jSerialComm/SerialPort;Lkotlin/coroutines/CoroutineContext;)V
public synthetic fun <init> (Lspace/kscience/dataforge/context/Context;Lcom/fazecast/jSerialComm/SerialPort;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/controls/serial/JSerialCommPort$Companion : space/kscience/controls/ports/PortFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/ports/Port;
public fun getType ()Ljava/lang/String;
public final fun open (Lspace/kscience/dataforge/context/Context;Ljava/lang/String;IIIILkotlin/coroutines/CoroutineContext;)Lspace/kscience/controls/serial/JSerialCommPort;
public static synthetic fun open$default (Lspace/kscience/controls/serial/JSerialCommPort$Companion;Lspace/kscience/dataforge/context/Context;Ljava/lang/String;IIIILkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lspace/kscience/controls/serial/JSerialCommPort;
}
public final class space/kscience/controls/serial/SerialPortPlugin : space/kscience/dataforge/context/AbstractPlugin {
public static final field Companion Lspace/kscience/controls/serial/SerialPortPlugin$Companion;
public fun <init> ()V
public fun content (Ljava/lang/String;)Ljava/util/Map;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}
public final class space/kscience/controls/serial/SerialPortPlugin$Companion : space/kscience/dataforge/context/PluginFactory {
public synthetic fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Ljava/lang/Object;
public fun build (Lspace/kscience/dataforge/context/Context;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/controls/serial/SerialPortPlugin;
public fun getTag ()Lspace/kscience/dataforge/context/PluginTag;
}

View File

@ -0,0 +1,17 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
description = "Implementation of direct serial port communication with JSerialComm"
dependencies{
api(project(":controls-core"))
implementation("com.fazecast:jSerialComm:2.10.3")
}
readme{
maturity = Maturity.EXPERIMENTAL
}

View File

@ -0,0 +1,87 @@
package space.kscience.controls.serial
import com.fazecast.jSerialComm.SerialPort
import com.fazecast.jSerialComm.SerialPortDataListener
import com.fazecast.jSerialComm.SerialPortEvent
import kotlinx.coroutines.launch
import space.kscience.controls.ports.AbstractPort
import space.kscience.controls.ports.Port
import space.kscience.controls.ports.PortFactory
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import kotlin.coroutines.CoroutineContext
/**
* A port based on JSerialComm
*/
public class JSerialCommPort(
context: Context,
private val comPort: SerialPort,
coroutineContext: CoroutineContext = context.coroutineContext,
) : AbstractPort(context, coroutineContext) {
override fun toString(): String = "port[${comPort.descriptivePortName}]"
private val serialPortListener = object : SerialPortDataListener {
override fun getListeningEvents(): Int = SerialPort.LISTENING_EVENT_DATA_AVAILABLE
override fun serialEvent(event: SerialPortEvent) {
if (event.eventType == SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
scope.launch { receive(event.receivedData) }
}
}
}
init {
comPort.addDataListener(serialPortListener)
}
override suspend fun write(data: ByteArray) {
comPort.writeBytes(data, data.size)
}
override fun close() {
comPort.removeDataListener()
if (comPort.isOpen) {
comPort.closePort()
}
super.close()
}
public companion object : PortFactory {
override val type: String = "com"
/**
* Construct ComPort with given parameters
*/
public fun open(
context: Context,
portName: String,
baudRate: Int = 9600,
dataBits: Int = 8,
stopBits: Int = SerialPort.ONE_STOP_BIT,
parity: Int = SerialPort.NO_PARITY,
coroutineContext: CoroutineContext = context.coroutineContext,
): JSerialCommPort {
val serialPort = SerialPort.getCommPort(portName).apply {
setComPortParameters(baudRate, dataBits, stopBits, parity)
openPort()
}
return JSerialCommPort(context, serialPort, coroutineContext)
}
override fun build(context: Context, meta: Meta): Port {
val name by meta.string { error("Serial port name not defined") }
val baudRate by meta.int(9600)
val dataBits by meta.int(8)
val stopBits by meta.int(SerialPort.ONE_STOP_BIT)
val parity by meta.int(SerialPort.NO_PARITY)
return open(context, name, baudRate, dataBits, stopBits, parity)
}
}
}

View File

@ -0,0 +1,28 @@
package space.kscience.controls.serial
import space.kscience.controls.ports.PortFactory
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
public class SerialPortPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(Name.EMPTY to JSerialCommPort)
else -> emptyMap()
}
public companion object : PluginFactory<SerialPortPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): SerialPortPlugin = SerialPortPlugin()
}
}

23
controls-server/README.md Normal file
View File

@ -0,0 +1,23 @@
# Module controls-server
A combined Magix event loop server with web server for visualization.
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-server:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-server:0.2.0")
}
```

View File

@ -0,0 +1,9 @@
public final class space/kscience/controls/server/DeviceWebServerKt {
public static final fun deviceManagerModule (Lio/ktor/server/application/Application;Lspace/kscience/controls/manager/DeviceManager;[Lspace/kscience/magix/api/MagixFlowPlugin;Ljava/util/Collection;Ljava/lang/String;I)V
public static synthetic fun deviceManagerModule$default (Lio/ktor/server/application/Application;Lspace/kscience/controls/manager/DeviceManager;[Lspace/kscience/magix/api/MagixFlowPlugin;Ljava/util/Collection;Ljava/lang/String;IILjava/lang/Object;)V
public static final fun getWEB_SERVER_TARGET ()Lspace/kscience/dataforge/names/Name;
public static final fun startDeviceServer (Lkotlinx/coroutines/CoroutineScope;Lspace/kscience/controls/manager/DeviceManager;ILjava/lang/String;)Lio/ktor/server/engine/ApplicationEngine;
public static synthetic fun startDeviceServer$default (Lkotlinx/coroutines/CoroutineScope;Lspace/kscience/controls/manager/DeviceManager;ILjava/lang/String;ILjava/lang/Object;)Lio/ktor/server/engine/ApplicationEngine;
public static final fun whenStarted (Lio/ktor/server/engine/ApplicationEngine;Lkotlin/jvm/functions/Function1;)V
}

View File

@ -0,0 +1,29 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
description = """
A combined Magix event loop server with web server for visualization.
""".trimIndent()
val dataforgeVersion: String by rootProject.extra
val ktorVersion: String by rootProject.extra
dependencies {
implementation(projects.controlsCore)
implementation(projects.controlsPortsKtor)
implementation(projects.magix.magixServer)
implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-server-websockets:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
}
readme{
maturity = Maturity.PROTOTYPE
}

View File

@ -0,0 +1,221 @@
package space.kscience.controls.server
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.*
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.server.html.respondHtml
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.request.receiveText
import io.ktor.server.response.respond
import io.ktor.server.response.respondRedirect
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.server.util.getValue
import io.ktor.server.websocket.WebSockets
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.html.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.api.PropertyGetMessage
import space.kscience.controls.api.PropertySetMessage
import space.kscience.controls.api.getOrNull
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.respondHubMessage
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.MagixFlowPlugin
import space.kscience.magix.api.MagixMessage
import space.kscience.magix.server.magixModule
private fun Application.deviceServerModule(manager: DeviceManager) {
install(StatusPages) {
exception<IllegalArgumentException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
}
}
deviceManagerModule(manager)
routing {
get("/") {
call.respondRedirect("/dashboard")
}
}
}
/**
* Create and start a web server for several devices
*/
public fun CoroutineScope.startDeviceServer(
manager: DeviceManager,
port: Int = MagixEndpoint.DEFAULT_MAGIX_HTTP_PORT,
host: String = "localhost",
): ApplicationEngine = embeddedServer(CIO, port, host, module = { deviceServerModule(manager) }).start()
public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) {
environment.monitor.subscribe(ApplicationStarted, callback)
}
public val WEB_SERVER_TARGET: Name = "@webServer".asName()
public fun Application.deviceManagerModule(
manager: DeviceManager,
vararg plugins: MagixFlowPlugin,
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
route: String = "/",
buffer: Int = 100,
) {
if (pluginOrNull(WebSockets) == null) {
install(WebSockets)
}
// if (pluginOrNull(CORS) == null) {
// install(CORS) {
// anyHost()
// }
// }
routing {
route(route) {
get("dashboard") {
call.respondHtml {
head {
title("Device server dashboard")
}
body {
h1 {
+"Device server dashboard"
}
deviceNames.forEach { deviceName ->
val device =
manager.getOrNull(deviceName)
?: error("The device with name $deviceName not found in $manager")
div {
id = deviceName
h2 { +deviceName }
h3 { +"Properties" }
ul {
device.propertyDescriptors.forEach { property ->
li {
a(href = "../$deviceName/${property.name}/get") { +"${property.name}: " }
code {
+Json.encodeToString(property)
}
}
}
}
h3 { +"Actions" }
ul {
device.actionDescriptors.forEach { action ->
li {
+("${action.name}: ")
code {
+Json.encodeToString(action)
}
}
}
}
}
}
}
}
}
get("list") {
call.respondJson {
manager.devices.forEach { (name, device) ->
put("target", name.toString())
put("properties", buildJsonArray {
device.propertyDescriptors.forEach { descriptor ->
add(Json.encodeToJsonElement(descriptor))
}
})
put("actions", buildJsonArray {
device.actionDescriptors.forEach { actionDescriptor ->
add(Json.encodeToJsonElement(actionDescriptor))
}
})
}
}
}
post("message") {
val body = call.receiveText()
val request: DeviceMessage = MagixEndpoint.magixJson.decodeFromString(DeviceMessage.serializer(), body)
val response = manager.respondHubMessage(request)
if (response != null) {
call.respondMessage(response)
} else {
call.respondText("No response")
}
}
route("{target}") {
//global route for the device
route("{property}") {
get("get") {
val target: String by call.parameters
val property: String by call.parameters
val request = PropertyGetMessage(
sourceDevice = WEB_SERVER_TARGET,
targetDevice = Name.parse(target),
property = property,
)
val response = manager.respondHubMessage(request)
if (response != null) {
call.respondMessage(response)
} else {
call.respond(HttpStatusCode.InternalServerError)
}
}
post("set") {
val target: String by call.parameters
val property: String by call.parameters
val body = call.receiveText()
val json = Json.parseToJsonElement(body)
val request = PropertySetMessage(
sourceDevice = WEB_SERVER_TARGET,
targetDevice = Name.parse(target),
property = property,
value = json.toMeta()
)
val response = manager.respondHubMessage(request)
if (response != null) {
call.respondMessage(response)
} else {
call.respond(HttpStatusCode.InternalServerError)
}
}
}
}
}
}
val magixFlow = MutableSharedFlow<MagixMessage>(
buffer,
extraBufferCapacity = buffer
)
plugins.forEach {
it.start(this, magixFlow)
}
magixModule(magixFlow)
}

View File

@ -0,0 +1,31 @@
package space.kscience.controls.server
import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respondText
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonObject
import space.kscience.controls.api.DeviceMessage
import space.kscience.magix.api.MagixEndpoint
//internal fun Frame.toEnvelope(): Envelope {
// return data.asBinary().readWith(TaggedEnvelopeFormat)
//}
//
//internal fun Envelope.toFrame(): Frame {
// val data = buildByteArray {
// writeWith(TaggedEnvelopeFormat, this@toFrame)
// }
// return Frame.Binary(false, data)
//}
internal suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() -> Unit) {
val json = buildJsonObject(builder)
respondText(json.toString(), contentType = ContentType.Application.Json)
}
internal suspend fun ApplicationCall.respondMessage(message: DeviceMessage): Unit = respondText(
MagixEndpoint.magixJson.encodeToString(DeviceMessage.serializer(), message),
contentType = ContentType.Application.Json
)

View File

@ -0,0 +1,23 @@
# Module controls-storage
An API for stand-alone Controls-kt device or a hub.
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-storage:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-storage:0.2.0")
}
```

View File

@ -0,0 +1,27 @@
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
val dataforgeVersion: String by rootProject.extra
description = """
An API for stand-alone Controls-kt device or a hub.
""".trimIndent()
kscience{
jvm()
js()
dependencies {
api(projects.controlsCore)
}
dependencies(jvmMain){
api(projects.magix.magixApi)
api(projects.controlsMagix)
api(projects.magix.magixServer)
}
}
readme{
maturity = space.kscience.gradle.Maturity.PROTOTYPE
}

View File

@ -0,0 +1,23 @@
# Module controls-xodus
An implementation of controls-storage on top of JetBrains Xodus.
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-xodus:0.2.0`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
//uncomment to access development builds
//maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-xodus:0.2.0")
}
```

View File

@ -0,0 +1,23 @@
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
val xodusVersion: String by rootProject.extra
description = """
An implementation of controls-storage on top of JetBrains Xodus.
""".trimIndent()
dependencies {
api(projects.controlsStorage)
implementation("org.jetbrains.xodus:xodus-entity-store:$xodusVersion")
// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion")
// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion")
testImplementation(spclibs.kotlinx.coroutines.test)
}
readme{
maturity = space.kscience.gradle.Maturity.PROTOTYPE
}

View File

@ -0,0 +1,139 @@
package space.kscience.controls.xodus
import jetbrains.exodus.entitystore.Entity
import jetbrains.exodus.entitystore.PersistentEntityStore
import jetbrains.exodus.entitystore.PersistentEntityStores
import jetbrains.exodus.entitystore.StoreTransaction
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.storage.DeviceMessageStorage
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.io.IOPlugin
import space.kscience.dataforge.io.workDirectory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.matches
import space.kscience.dataforge.names.parseAsName
internal fun StoreTransaction.writeMessage(message: DeviceMessage): Unit {
val entity: Entity = newEntity(XodusDeviceMessageStorage.DEVICE_MESSAGE_ENTITY_TYPE)
val json = Json.encodeToJsonElement(DeviceMessage.serializer(), message).jsonObject
val type = json["type"]?.jsonPrimitive?.content ?: error("Message json representation must have type.")
entity.setProperty("type", type)
message.sourceDevice?.let {
entity.setProperty(DeviceMessage::sourceDevice.name, it.toString())
}
message.targetDevice?.let {
entity.setProperty(DeviceMessage::targetDevice.name, it.toString())
}
message.time?.let {
entity.setProperty(DeviceMessage::targetDevice.name, it.toString())
}
entity.setBlobString("json", Json.encodeToString(json))
}
@OptIn(DFExperimental::class)
private fun Entity.propertyMatchesName(propertyName: String, pattern: Name? = null) =
pattern == null || getProperty(propertyName).toString().parseAsName().matches(pattern)
private fun Entity.timeInRange(range: ClosedRange<Instant>?): Boolean {
if (range == null) return true
val time: Instant? = getProperty(DeviceMessage::time.name)?.let { entityString ->
Instant.parse(entityString.toString())
}
return time != null && time in range
}
public class XodusDeviceMessageStorage(
private val entityStore: PersistentEntityStore,
) : DeviceMessageStorage, AutoCloseable {
override suspend fun write(event: DeviceMessage) {
entityStore.executeInTransaction { txn ->
txn.writeMessage(event)
}
}
override suspend fun readAll(): List<DeviceMessage> = entityStore.computeInReadonlyTransaction { transaction ->
transaction.sort(
DEVICE_MESSAGE_ENTITY_TYPE,
DeviceMessage::time.name,
true
).map {
Json.decodeFromString(
DeviceMessage.serializer(),
it.getBlobString("json") ?: error("No json content found")
)
}
}
override suspend fun read(
eventType: String,
range: ClosedRange<Instant>?,
sourceDevice: Name?,
targetDevice: Name?,
): List<DeviceMessage> = entityStore.computeInReadonlyTransaction { transaction ->
transaction.find(
DEVICE_MESSAGE_ENTITY_TYPE,
"type",
eventType
).asSequence().filter {
it.timeInRange(range) &&
it.propertyMatchesName(DeviceMessage::sourceDevice.name, sourceDevice) &&
it.propertyMatchesName(DeviceMessage::targetDevice.name, targetDevice)
}.map {
Json.decodeFromString(
DeviceMessage.serializer(),
it.getBlobString("json") ?: error("No json content found")
)
}.sortedBy { it.time }.toList()
}
override fun close() {
entityStore.close()
}
public companion object : Factory<XodusDeviceMessageStorage> {
internal const val DEVICE_MESSAGE_ENTITY_TYPE = "controls-kt.message"
public val XODUS_STORE_PROPERTY: Name = Name.of("xodus", "storagePath")
override fun build(context: Context, meta: Meta): XodusDeviceMessageStorage {
val io = context.request(IOPlugin)
val storePath = io.workDirectory.resolve(
meta[XODUS_STORE_PROPERTY]?.string
?: context.properties[XODUS_STORE_PROPERTY]?.string ?: "storage"
)
val entityStore = PersistentEntityStores.newInstance(storePath.toFile())
return XodusDeviceMessageStorage(entityStore)
}
}
}
/**
* Query all messages of given type
*/
@OptIn(ExperimentalSerializationApi::class)
public suspend inline fun <reified T : DeviceMessage> XodusDeviceMessageStorage.query(
range: ClosedRange<Instant>? = null,
sourceDevice: Name? = null,
targetDevice: Name? = null,
): List<T> = read(serialDescriptor<T>().serialName, range, sourceDevice, targetDevice).map {
//Check that all types are correct
it as T
}

View File

@ -0,0 +1,77 @@
import jetbrains.exodus.entitystore.PersistentEntityStores
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.xodus.XodusDeviceMessageStorage
import space.kscience.controls.xodus.query
import space.kscience.controls.xodus.writeMessage
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import java.nio.file.Files
internal class PropertyHistoryTest {
companion object {
val storeFile = Files.createTempDirectory("controls-xodus").toFile()
private val propertyChangedMessages = listOf(
PropertyChangedMessage(
"speed",
Meta.EMPTY,
time = Instant.fromEpochMilliseconds(1000),
sourceDevice = Name.of("virtual-car")
),
PropertyChangedMessage(
"acceleration",
Meta.EMPTY,
time = Instant.fromEpochMilliseconds(1500),
sourceDevice = Name.of("virtual-car")
),
PropertyChangedMessage(
"speed",
Meta.EMPTY,
time = Instant.fromEpochMilliseconds(2000),
sourceDevice = Name.of("magix-virtual-car")
)
)
@BeforeAll
@JvmStatic
fun createEntities() {
PersistentEntityStores.newInstance(storeFile).use {
it.executeInTransaction { transaction ->
propertyChangedMessages.forEach { message ->
transaction.writeMessage(message)
}
}
}
}
@AfterAll
@JvmStatic
fun deleteDatabase() {
storeFile.deleteRecursively()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun getPropertyHistoryTest() = runTest {
PersistentEntityStores.newInstance(storeFile).use { entityStore ->
XodusDeviceMessageStorage(entityStore).use { storage ->
assertEquals(
propertyChangedMessages[0],
storage.query<PropertyChangedMessage>(
sourceDevice = "virtual-car".asName()
).first { it.property == "speed" }
)
}
}
}
}

View File

@ -0,0 +1,23 @@
package space.kscience.controls.storage
import kotlinx.datetime.Instant
import space.kscience.controls.api.DeviceMessage
import space.kscience.dataforge.names.Name
/**
* A storage for Controls-kt [DeviceMessage]
*/
public interface DeviceMessageStorage {
public suspend fun write(event: DeviceMessage)
public suspend fun readAll(): List<DeviceMessage>
public suspend fun read(
eventType: String,
range: ClosedRange<Instant>? = null,
sourceDevice: Name? = null,
targetDevice: Name? = null,
): List<DeviceMessage>
public fun close()
}

View File

@ -0,0 +1,64 @@
package space.kscience.controls.storage
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.hubMessageFlow
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.logger
//TODO replace by plugin?
public fun DeviceManager.storage(
factory: Factory<DeviceMessageStorage>,
): DeviceMessageStorage = factory.build(context, meta)
/**
* Begin to store DeviceMessages from this DeviceManager
* @param factory factory that will be used for creating persistent entity store instance. DefaultPersistentStoreFactory by default.
* DeviceManager's meta and context will be used for in invoke method.
* @param filterCondition allow you to specify messages which we want to store. Always true by default.
* @return Job which responsible for our storage
*/
public fun DeviceManager.storeMessages(
factory: Factory<DeviceMessageStorage>,
filterCondition: suspend (DeviceMessage) -> Boolean = { true },
): Job {
val storage = factory.build(context, meta)
logger.debug { "Message storage with meta = $meta created" }
return hubMessageFlow(context).filter(filterCondition).onEach { message ->
storage.write(message)
}.onCompletion {
storage.close()
logger.debug { "Message storage closed" }
}.launchIn(context)
}
///**
// * @return the list of deviceMessages that describes changes of specified property of specified device sorted by time
// * @param sourceDeviceName a name of device, history of which property we want to get
// * @param propertyName a name of property, history of which we want to get
// * @param factory a factory that produce mongo clients
// */
//public suspend fun getPropertyHistory(
// sourceDeviceName: String,
// propertyName: String,
// factory: Factory<EventStorage>,
// meta: Meta = Meta.EMPTY,
//): List<PropertyChangedMessage> {
// return factory(meta).use {
// it.getPropertyHistory(sourceDeviceName, propertyName)
// }
//}
//
//
//public enum class StorageKind {
// DEVICE_HUB,
// MAGIX_SERVER
//}

View File

@ -0,0 +1,46 @@
//package space.kscience.controls.storage
//
//import io.ktor.server.application.Application
//import kotlinx.coroutines.InternalCoroutinesApi
//import kotlinx.coroutines.flow.Flow
//import kotlinx.coroutines.flow.MutableSharedFlow
//import kotlinx.coroutines.flow.filter
//import kotlinx.coroutines.flow.onEach
//import kotlinx.coroutines.job
//import ru.mipt.npm.magix.server.GenericMagixMessage
//import space.kscience.dataforge.context.Factory
//import space.kscience.dataforge.meta.Meta
//
///**
// * Asynchronous version of synchronous API, so for more details check relative docs
// */
//
//internal fun Flow<GenericMagixMessage>.store(
// client: EventStorage,
// flowFilter: suspend (GenericMagixMessage) -> Boolean = { true },
//) {
// filter(flowFilter).onEach { message ->
// client.storeMagixMessage(message)
// }
//}
//
///** Begin to store MagixMessages from certain flow
// * @param flow flow of messages which we will store
// * @param meta Meta which may have some configuration parameters for our storage and will be used in invoke method of factory
// * @param factory factory that will be used for creating persistent entity store instance. DefaultPersistentStoreFactory by default.
// * @param flowFilter allow you to specify messages which we want to store. Always true by default.
// */
//@OptIn(InternalCoroutinesApi::class)
//public fun Application.store(
// flow: MutableSharedFlow<GenericMagixMessage>,
// factory: Factory<EventStorage>,
// meta: Meta = Meta.EMPTY,
// flowFilter: suspend (GenericMagixMessage) -> Boolean = { true },
//) {
// val client = factory(meta)
//
// flow.store(client, flowFilter)
// coroutineContext.job.invokeOnCompletion(onCancelling = true) {
// client.close()
// }
//}

View File

@ -1,27 +0,0 @@
plugins {
id("ru.mipt.npm.mpp")
id("ru.mipt.npm.publish")
}
val ktorVersion: String by rootProject.extra
kotlin {
sourceSets {
commonMain {
dependencies {
implementation(project(":dataforge-device-core"))
implementation("io.ktor:ktor-client-core:$ktorVersion")
}
}
jvmMain {
dependencies {
}
}
jsMain {
dependencies {
}
}
}
}

View File

@ -1,100 +0,0 @@
package hep.dataforge.control.client
import hep.dataforge.control.controllers.DeviceManager
import hep.dataforge.control.controllers.DeviceMessage
import hep.dataforge.control.controllers.respondMessage
import hep.dataforge.meta.toJson
import hep.dataforge.meta.toMeta
import hep.dataforge.meta.wrap
import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.http.ContentType
import io.ktor.http.Url
import io.ktor.http.contentType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.serialization.json.*
import kotlin.coroutines.CoroutineContext
/*
{
"id":"string|number[optional, but desired]",
"parentId": "string|number[optional]",
"target":"string[optional]",
"origin":"string[required]",
"user":"string[optional]",
"action":"string[optional, default='heartbeat']",
"payload":"object[optional]"
}
*/
/**
* Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1)
*/
public class MagixClient(
private val manager: DeviceManager,
private val postUrl: Url,
private val inbox: Flow<JsonObject>
): CoroutineScope {
override val coroutineContext: CoroutineContext = manager.context.coroutineContext + Job(manager.context.coroutineContext[Job])
private val client = HttpClient()
protected fun generateId(message: DeviceMessage, requestId: String?): String = if(requestId != null){
"$requestId.response"
} else{
"df[${message.hashCode()}"
}
private fun send(json: JsonObject) {
launch {
client.post<Unit>(postUrl) {
this.contentType(ContentType.Application.Json)
body = json.toString()
}
}
}
private fun wrapMessage(message: DeviceMessage, requestId: String? = null): JsonObject = buildJsonObject {
put("id", generateId(message, requestId))
if (requestId != null) {
put("parentId", requestId)
}
put("target", "magix")
put("origin", "df")
put("payload", message.config.toJson())
}
private val listenJob = launch {
manager.controller.messageOutput().collect { message ->
val json = wrapMessage(message)
send(json)
}
}
private val respondJob = launch {
inbox.collect { json ->
val requestId = json["id"]?.jsonPrimitive?.content
val payload = json["payload"]?.jsonObject
//TODO analyze action
if(payload != null){
val meta = payload.toMeta()
val request = DeviceMessage.wrap(meta)
val response = manager.respondMessage(request)
send(wrapMessage(response,requestId))
} else {
TODO("process heartbeat and other system messages")
}
}
}
}

View File

@ -1,13 +0,0 @@
package hep.dataforge.control.client
import io.ktor.client.HttpClient
import io.ktor.client.call.receive
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.HttpStatement
import io.ktor.utils.io.ByteReadChannel
suspend fun HttpClient.sse(address: String) = get<HttpStatement>(address).execute { response: HttpResponse ->
// Response is not downloaded here.
val channel = response.receive<ByteReadChannel>()
}

View File

@ -1,30 +0,0 @@
plugins {
id("ru.mipt.npm.mpp")
id("ru.mipt.npm.publish")
}
val dataforgeVersion: String by rootProject.extra
kscience {
useCoroutines()
useSerialization()
}
kotlin {
sourceSets {
commonMain{
dependencies {
api("hep.dataforge:dataforge-io:$dataforgeVersion")
}
}
jvmMain{
dependencies{
api("io.ktor:ktor-network:1.3.2")
}
}
jsMain{
dependencies{
}
}
}
}

View File

@ -1,87 +0,0 @@
package hep.dataforge.control.api
import hep.dataforge.context.ContextAware
import hep.dataforge.control.api.Device.Companion.DEVICE_TARGET
import hep.dataforge.io.Envelope
import hep.dataforge.io.EnvelopeBuilder
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaItem
import hep.dataforge.provider.Type
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.io.Closeable
/**
* General interface describing a managed Device
*/
@Type(DEVICE_TARGET)
public interface Device : Closeable, ContextAware {
/**
* List of supported property descriptors
*/
public val propertyDescriptors: Collection<PropertyDescriptor>
/**
* List of supported action descriptors. Action is a request to the device that
* may or may not change the properties
*/
public val actionDescriptors: Collection<ActionDescriptor>
/**
* The supervisor scope encompassing all operations on a device. When canceled, cancels all running processes
*/
public val scope: CoroutineScope
/**
* Register a new property change listener for this device.
* [owner] is provided optionally in order for listener to be
* easily removable
*/
public fun registerListener(listener: DeviceListener, owner: Any? = listener)
/**
* Remove all listeners belonging to the specified owner
*/
public fun removeListeners(owner: Any?)
/**
* Get the value of the property or throw error if property in not defined.
* Suspend if property value is not available
*/
public suspend fun getProperty(propertyName: String): MetaItem<*>
/**
* Invalidate property and force recalculate
*/
public suspend fun invalidateProperty(propertyName: String)
/**
* Set property [value] for a property with name [propertyName].
* In rare cases could suspend if the [Device] supports command queue and it is full at the moment.
*/
public suspend fun setProperty(propertyName: String, value: MetaItem<*>)
/**
* Send an action request and suspend caller while request is being processed.
* Could return null if request does not return a meaningful answer.
*/
public suspend fun execute(command: String, argument: MetaItem<*>? = null): MetaItem<*>?
/**
*
* A request with binary data or for binary response (or both). This request does not cover basic functionality like
* [setProperty], [getProperty] or [execute] and not defined for a generic device.
*
*/
public suspend fun respondWithData(request: Envelope): EnvelopeBuilder = error("No binary response defined")
override fun close() {
scope.cancel("The device is closed")
}
public companion object {
public const val DEVICE_TARGET: String = "device"
}
}
public suspend fun Device.execute(name: String, meta: Meta?): MetaItem<*>? = execute(name, meta?.let { MetaItem.NodeItem(it) })

Some files were not shown because too many files have changed in this diff Show More