final use of c libepics
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: all frontend backend backend-epics backend-debug test bench race lint clean run
|
.PHONY: all frontend backend backend-epics backend-debug release release-epics test bench race lint clean run
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Sources — adding any file here triggers a frontend or backend rebuild #
|
# Sources — adding any file here triggers a frontend or backend rebuild #
|
||||||
@@ -88,6 +88,76 @@ backend-debug: $(FRONTEND_OUT)
|
|||||||
@mkdir -p dist
|
@mkdir -p dist
|
||||||
go build -o $(BINARY) ./cmd/uopi
|
go build -o $(BINARY) ./cmd/uopi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Portable release targets #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
#
|
||||||
|
# Two flavours depending on whether EPICS is needed:
|
||||||
|
#
|
||||||
|
# make release — pure-Go, CGO_ENABLED=0, 100 % static binary.
|
||||||
|
# No EPICS; works anywhere without extra libs.
|
||||||
|
#
|
||||||
|
# make release-epics — CGO build with EPICS CA.
|
||||||
|
# Tries to link libca/libCom statically (if .a
|
||||||
|
# archives are present in $(EPICS_LIBDIR)).
|
||||||
|
# Falls back to bundled shared libs + $ORIGIN
|
||||||
|
# rpath when only .so files are available.
|
||||||
|
# The output directory (dist/release/) contains
|
||||||
|
# everything needed to run on the target machine.
|
||||||
|
#
|
||||||
|
# Cross-compilation (no-EPICS only): override RELEASE_OS / RELEASE_ARCH.
|
||||||
|
# make release RELEASE_OS=linux RELEASE_ARCH=arm64
|
||||||
|
|
||||||
|
RELEASE_OS ?= linux
|
||||||
|
RELEASE_ARCH ?= amd64
|
||||||
|
RELEASE_DIR := dist/release
|
||||||
|
EPICS_LIBDIR := $(EPICS_BASE)/lib/$(EPICS_ARCH)
|
||||||
|
|
||||||
|
# Fully static, no CGO — the most portable build possible.
|
||||||
|
release: $(FRONTEND_OUT)
|
||||||
|
@mkdir -p $(RELEASE_DIR)
|
||||||
|
CGO_ENABLED=0 GOOS=$(RELEASE_OS) GOARCH=$(RELEASE_ARCH) \
|
||||||
|
go build -ldflags="-s -w" -o $(RELEASE_DIR)/uopi ./cmd/uopi
|
||||||
|
@echo "Built: $(RELEASE_DIR)/uopi (fully static, no EPICS)"
|
||||||
|
|
||||||
|
# EPICS release — maximum static linking.
|
||||||
|
# Strategy A (preferred): if libca.a + libCom.a exist, embed them directly
|
||||||
|
# → single self-contained binary, no extra files needed.
|
||||||
|
# Strategy B (fallback): copy the shared libs next to the binary and set an
|
||||||
|
# $ORIGIN rpath so the OS finds them regardless of installation prefix.
|
||||||
|
release-epics: $(FRONTEND_OUT)
|
||||||
|
@if [ -z "$(EPICS_BASE)" ]; then \
|
||||||
|
echo "ERROR: EPICS_BASE is not set."; \
|
||||||
|
echo " export EPICS_BASE=/path/to/epics/base"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@mkdir -p $(RELEASE_DIR)
|
||||||
|
@echo "Building portable EPICS release"
|
||||||
|
@echo " EPICS_BASE = $(EPICS_BASE)"
|
||||||
|
@echo " EPICS_ARCH = $(EPICS_ARCH)"
|
||||||
|
@if [ -f "$(EPICS_LIBDIR)/libca.a" ] && [ -f "$(EPICS_LIBDIR)/libCom.a" ]; then \
|
||||||
|
echo " Strategy A: static .a archives found — embedding libca + libCom"; \
|
||||||
|
CGO_ENABLED=1 GOOS=linux GOARCH=$(RELEASE_ARCH) \
|
||||||
|
CGO_CFLAGS="$(EPICS_CGO_CFLAGS)" \
|
||||||
|
CGO_LDFLAGS="-Wl,-Bstatic -L$(EPICS_LIBDIR) -lca -lCom -Wl,-Bdynamic -lm -lpthread -lrt -ldl" \
|
||||||
|
go build -tags epics \
|
||||||
|
-ldflags="-s -w -linkmode external" \
|
||||||
|
-o $(RELEASE_DIR)/uopi ./cmd/uopi; \
|
||||||
|
echo "Built: $(RELEASE_DIR)/uopi (EPICS statically embedded)"; \
|
||||||
|
else \
|
||||||
|
echo " Strategy B: no .a archives — bundling shared libs with \$$ORIGIN rpath"; \
|
||||||
|
CGO_ENABLED=1 GOOS=linux GOARCH=$(RELEASE_ARCH) \
|
||||||
|
CGO_CFLAGS="$(EPICS_CGO_CFLAGS)" \
|
||||||
|
CGO_LDFLAGS="-L$(EPICS_LIBDIR) -lca -lCom -Wl,-rpath,\$$ORIGIN" \
|
||||||
|
go build -tags epics \
|
||||||
|
-ldflags="-s -w" \
|
||||||
|
-o $(RELEASE_DIR)/uopi ./cmd/uopi; \
|
||||||
|
cp $(EPICS_LIBDIR)/libca.so* $(RELEASE_DIR)/; \
|
||||||
|
cp $(EPICS_LIBDIR)/libCom.so* $(RELEASE_DIR)/; \
|
||||||
|
echo "Built: $(RELEASE_DIR)/ (deploy the whole directory together)"; \
|
||||||
|
ls $(RELEASE_DIR)/; \
|
||||||
|
fi
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Dev / CI #
|
# Dev / CI #
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|||||||
@@ -444,41 +444,105 @@ func (e *EPICS) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write puts a new value onto a CA channel.
|
// Write puts a new value onto a CA channel.
|
||||||
func (e *EPICS) Write(_ context.Context, signal string, value any) error {
|
// If the signal is not currently subscribed (e.g. a button in oneshot mode),
|
||||||
|
// a temporary CA channel is created, used for the put, then torn down.
|
||||||
|
func (e *EPICS) Write(ctx context.Context, signal string, value any) error {
|
||||||
e.attachCAContext()
|
e.attachCAContext()
|
||||||
|
|
||||||
|
// Resolve the chid: use an existing subscribed channel if available,
|
||||||
|
// otherwise create a temporary one (mirrors GetMetadata's approach).
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
caCh, ok := e.channels[signal]
|
caCh, ok := e.channels[signal]
|
||||||
e.mu.Unlock()
|
e.mu.Unlock()
|
||||||
if !ok {
|
|
||||||
return datasource.ErrNotFound
|
var chid C.chid
|
||||||
|
tempChannel := false
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
chid = caCh.chid
|
||||||
|
} else {
|
||||||
|
// Create a temporary channel for the put.
|
||||||
|
handle := nextHandle()
|
||||||
|
entry := &connEntry{connCh: make(chan struct{}, 1)}
|
||||||
|
handleMu.Lock()
|
||||||
|
connTable[handle] = entry
|
||||||
|
handleMu.Unlock()
|
||||||
|
|
||||||
|
pvName := C.CString(signal)
|
||||||
|
defer C.free(unsafe.Pointer(pvName))
|
||||||
|
|
||||||
|
status := C.caCreateChannel(pvName, C.uintptr_t(handle), &chid)
|
||||||
|
if status != C.ECA_NORMAL {
|
||||||
|
handleMu.Lock()
|
||||||
|
delete(connTable, handle)
|
||||||
|
handleMu.Unlock()
|
||||||
|
return fmt.Errorf("epics: ca_create_channel(%q) failed: status %d", signal, int(status))
|
||||||
|
}
|
||||||
|
C.ca_flush_io()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-entry.connCh:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
C.ca_clear_channel(chid)
|
||||||
|
handleMu.Lock()
|
||||||
|
delete(connTable, handle)
|
||||||
|
handleMu.Unlock()
|
||||||
|
return fmt.Errorf("epics: timeout connecting to %q for write", signal)
|
||||||
|
case <-ctx.Done():
|
||||||
|
C.ca_clear_channel(chid)
|
||||||
|
handleMu.Lock()
|
||||||
|
delete(connTable, handle)
|
||||||
|
handleMu.Unlock()
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMu.Lock()
|
||||||
|
delete(connTable, handle)
|
||||||
|
handleMu.Unlock()
|
||||||
|
tempChannel = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var status C.int
|
var status C.int
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
cv := C.double(v)
|
cv := C.double(v)
|
||||||
status = C.caPut(C.DBR_DOUBLE, caCh.chid, unsafe.Pointer(&cv))
|
status = C.caPut(C.DBR_DOUBLE, chid, unsafe.Pointer(&cv))
|
||||||
case int64:
|
case int64:
|
||||||
cv := C.long(v)
|
cv := C.long(v)
|
||||||
status = C.caPut(C.DBR_LONG, caCh.chid, unsafe.Pointer(&cv))
|
status = C.caPut(C.DBR_LONG, chid, unsafe.Pointer(&cv))
|
||||||
case string:
|
case string:
|
||||||
cs := C.CString(v)
|
cs := C.CString(v)
|
||||||
defer C.free(unsafe.Pointer(cs))
|
defer C.free(unsafe.Pointer(cs))
|
||||||
status = C.caPut(C.DBR_STRING, caCh.chid, unsafe.Pointer(cs))
|
status = C.caPut(C.DBR_STRING, chid, unsafe.Pointer(cs))
|
||||||
case bool:
|
case bool:
|
||||||
var iv C.short
|
var iv C.short
|
||||||
if v {
|
if v {
|
||||||
iv = 1
|
iv = 1
|
||||||
}
|
}
|
||||||
status = C.caPut(C.DBR_SHORT, caCh.chid, unsafe.Pointer(&iv))
|
status = C.caPut(C.DBR_SHORT, chid, unsafe.Pointer(&iv))
|
||||||
default:
|
default:
|
||||||
|
if tempChannel {
|
||||||
|
C.ca_clear_channel(chid)
|
||||||
|
C.ca_flush_io()
|
||||||
|
}
|
||||||
return fmt.Errorf("epics: unsupported value type %T for Write", value)
|
return fmt.Errorf("epics: unsupported value type %T for Write", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if status != C.ECA_NORMAL {
|
if status != C.ECA_NORMAL {
|
||||||
|
if tempChannel {
|
||||||
|
C.ca_clear_channel(chid)
|
||||||
|
C.ca_flush_io()
|
||||||
|
}
|
||||||
return fmt.Errorf("epics: ca_put(%q) failed: status %d", signal, int(status))
|
return fmt.Errorf("epics: ca_put(%q) failed: status %d", signal, int(status))
|
||||||
}
|
}
|
||||||
C.ca_flush_io()
|
|
||||||
|
if tempChannel {
|
||||||
|
C.ca_pend_io(3.0) // flush + wait for put completion before tearing down
|
||||||
|
C.ca_clear_channel(chid)
|
||||||
|
C.ca_flush_io()
|
||||||
|
} else {
|
||||||
|
C.ca_flush_io()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user