Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ jobs:
run: julia --color=yes tests/julia/runtests.jl
shell: bash

julia-docs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: julia-actions/setup-julia@v3
with:
version: '1.10'

- name: Install documentation dependencies
run: julia --project=docs -e 'using Pkg; Pkg.instantiate()'

- name: Build Julia documentation
run: julia --project=docs docs/make.jl

julia-matlab-verilog-test:
runs-on: ubuntu-latest

Expand Down
6 changes: 6 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

[compat]
Documenter = "1"
julia = "1.10"
18 changes: 18 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Documenter

include(joinpath(@__DIR__, "..", "concore.jl"))
using .Concore

makedocs(;
modules=[Concore],
sitename="Concore.jl",
checkdocs=:exports,
format=Documenter.HTML(; inventory_version="dev"),
pages=[
"Home" => "index.md",
"Getting Started" => "getting-started.md",
"API Reference" => "api.md",
"Backends" => "backends.md",
"Wire Format" => "wire-format.md",
],
)
48 changes: 48 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# [API Reference](@id api-reference)

```@meta
CurrentModule = Concore
```

## Protocol

```@docs
concore_read
concore_write
initval
unchanged
```

## Configuration

```@docs
safe_parse_list
tryparam
default_maxtime!
load_iport!
load_oport!
load_params!
concore_init!
```

The compatibility names `default_maxtime`, `load_iport`, `load_oport`,
`load_params`, and `concore_init` are aliases for the corresponding functions
above.

`FileTransport`, `MmapTransport`, and `ZmqTransport` are aliases for the
corresponding backend types.

## Backends

```@docs
AbstractBackend
FileBackend
MmapBackend
mmap_cleanup
ZmqBackend
```

ZMQ ports are registered with
`init_zmq_port(name, mode, address, socket_type)` and closed with
`terminate_zmq()`. These functions require the optional ZMQ.jl package. See
[ZMQ](@ref zmq) for a complete call sequence.
102 changes: 102 additions & 0 deletions docs/src/backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# [Backends](@id backends)

The active backend controls how `concore_read` and `concore_write` move the
same wire-format strings. File is the default backend.

## File

`FileBackend` uses ordinary files and has no external Julia dependencies.

```julia
include("concore.jl")
using .Concore

concore_init!(FileBackend())
```

Local input and output path prefixes are `./in` and `./out`. The numeric port
is appended to the prefix, so port 1 uses `./in1` and `./out1`.

## Mmap

`MmapBackend` keeps the file-based naming and wire format but accesses each
file through a fixed-size memory-mapped segment. The default segment size is
4096 bytes.

```julia
concore_init!(MmapBackend())

try
value = concore_read(1, "ym", "[0.0, 0.0]")
concore_write(1, "u", value)
finally
mmap_cleanup()
end
```

Pass a different segment size when constructing the backend if the wire value
does not fit in the default segment:

```julia
concore_init!(MmapBackend(8192))
```

## [ZMQ](@id zmq)

ZMQ support is optional. Install ZMQ.jl in the active Julia environment before
including `concore.jl`:

```julia
using Pkg
Pkg.add("ZMQ")
```

`Concore.HAS_ZMQ` reports whether the package was available when the runtime
was loaded. ZMQ ports use string names instead of numeric file ports. This
example shows the request side of a REQ/REP pair:

```julia
include("concore.jl")
using .Concore

concore_init!(ZmqBackend())
init_zmq_port("req", "connect", "tcp://127.0.0.1:5555", "REQ")

try
concore_write("req", "u", [1.0])
ym = concore_read("req", "ym", "[0.0, 0.0]")
finally
terminate_zmq()
end
```

A REP peer must bind the same address and receive before replying.

## Docker

Docker is a runtime path variant, not a separate backend type.
`concoredocker.jl` uses `/in` and `/out` as its path prefixes, producing paths
such as `/in1/ym` and `/out1/u`.

For generated Docker studies, `mkconcore.py` copies `concoredocker.jl` into the
node build directory as `concore.jl`. The node source therefore keeps the same
include statement used locally:

```julia
include("concore.jl")
using .Concore
```

Build the mixed Julia controller and Python plant example with the existing
study generator:

```sh
concore build demo/sampleJ.graphml --source demo --output docker-julia-demo --type docker --compose
cd docker-julia-demo
./build
./maxtime 5
docker compose up
```

The generated Compose file mounts the connected input and output directories
at the paths expected by each container.
99 changes: 99 additions & 0 deletions docs/src/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# [Getting Started](@id getting-started)

```@meta
DocTestSetup = :(using Main.Concore)
```

## Setup

Concore.jl requires Julia 1.10 or later. Clone the Concore repository and run
Julia from the repository root:

```sh
git clone https://github.com/ControlCore-Project/concore.git
cd concore
julia
```

Load the standalone runtime from a Julia node:

```julia
include("concore.jl")
using .Concore
```

There are no required Julia package dependencies for the default File backend.
ZMQ.jl is only needed when using the ZMQ backend.

## Node loop

A Concore node reads until its input changes, performs its calculation, and
writes its output. This controller reads `ym` from input port 1 and writes `u`
to output port 1:

```julia
include("concore.jl")
using .Concore

Concore.default_maxtime!(100)
Concore.delay = 0.02

ym = initval("[0.0, 0.0]")

while Concore.simtime < Concore.maxtime
while unchanged()
ym = concore_read(1, "ym", "[0.0, 0.0]")
end

u = 1.01 .* ym
concore_write(1, "u", u; delta=0)
end
```

With the default paths, port 1 reads from `./in1/ym` and writes to
`./out1/u`. The study runner creates and connects those directories.

The repository also contains a mixed Python and Julia file-backend demo:

```sh
julia demo/run_julia_mixed_demo.jl
```

## Initial values and simulation time

`initval` parses a wire value and sets `Concore.simtime` from its first value:

```jldoctest
julia> ym = initval("[0.0, 1.5]"); (Concore.simtime, ym)
(0.0, [1.5])
```

`ym` is `[1.5]` and `Concore.simtime` is `0.0`. A read returns only the data
values and updates simulation time to the largest timestamp seen. A write uses
`Concore.simtime + delta` as the outgoing timestamp.

## Configuration

The runtime loads the same files as the other Concore implementations:

- `concore.iport` maps named input ports to numbers.
- `concore.oport` maps named output ports to numbers.
- `concore.params` supplies values returned by `tryparam`.
- `concore.maxtime` sets the simulation limit used by `default_maxtime!`.

Parameters can use Python dictionary syntax or semicolon-separated key/value
pairs:

```text
{'gain': 1.5, 'mode': 'auto'}
```

```text
gain=1.5;mode=auto
```

Use a default when a parameter is absent:

```julia
gain = tryparam("gain", 1.0)
```
24 changes: 24 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Concore.jl

Concore.jl is the native Julia implementation of the Concore protocol. It uses
the same wire format and synchronization pattern as the existing Concore
runtimes, without calling through Python.

The runtime is the standalone file `concore.jl`. Julia nodes keep it next to
their source and include it directly:

```julia
include("concore.jl")
using .Concore
```

File transport is the default. Memory-mapped files and ZeroMQ are available as
optional backend selections, and `concoredocker.jl` provides the path defaults
used by generated Docker studies.

## Contents

- [Getting Started](@ref getting-started) covers setup, the standard node loop, and configuration.
- [API Reference](@ref api-reference) lists the public Julia interface.
- [Backends](@ref backends) describes File, Mmap, ZMQ, and Docker usage.
- [Wire Format](@ref wire-format) documents message encoding and simulation time.
55 changes: 55 additions & 0 deletions docs/src/wire-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# [Wire Format](@id wire-format)

```@meta
DocTestSetup = :(using Main.Concore)
```

Concore messages are text lists containing a simulation timestamp followed by
zero or more data values:

```text
[simtime, value1, value2, ...]
```

For example:

```text
[5.0, 1.5, -2.0]
```

The first value is the timestamp. `concore_read` updates `Concore.simtime` and
returns only `[1.5, -2.0]` to the node.

## Writing

For vector writes, the outgoing timestamp is `Concore.simtime + delta`:

```julia
Concore.simtime = 5.0
concore_write(1, "u", [1.5, -2.0]; delta=1)
```

This writes:

```text
[6.0, 1.5, -2.0]
```

Integer-valued finite floats are written with a `.0` suffix. Other values are
rounded to 15 significant digits to keep output consistent with the existing
Concore wire format.

## Reading

`safe_parse_list` parses wire values without calling `eval` or `Meta.parse`.
For compatibility with Python and NumPy output, it also accepts wrapped values
and Python literals:

```jldoctest
julia> safe_parse_list("[0.0, np.float64(1.5), True, None]") == [0.0, 1.5, 1.0, 0.0]
true
```

Malformed input raises `ArgumentError`. The protocol, wire compatibility, and
interop tests cover Julia exchanges with the existing Python, C++, MATLAB, and
Verilog implementations.
Loading