Phase 2/4 done, working on phase 3/5
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
//go:build epics
|
||||
|
||||
package epics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/uopi/uopi/internal/datasource"
|
||||
)
|
||||
|
||||
// archiveResponse is the top-level JSON structure returned by the EPICS Archive
|
||||
// Appliance getData.json endpoint.
|
||||
//
|
||||
// Example response shape:
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "meta": { "name": "PV:NAME", "EGU": "A" },
|
||||
// "data": [
|
||||
// { "secs": 1700000000, "nanos": 123000000, "val": 3.14, "severity": 0, "status": 0 },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
type archiveResponse []struct {
|
||||
Meta struct {
|
||||
Name string `json:"name"`
|
||||
EGU string `json:"EGU"`
|
||||
} `json:"meta"`
|
||||
Data []archivePoint `json:"data"`
|
||||
}
|
||||
|
||||
type archivePoint struct {
|
||||
Secs int64 `json:"secs"`
|
||||
Nanos int64 `json:"nanos"`
|
||||
Val any `json:"val"`
|
||||
Severity int `json:"severity"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// fetchArchiveHistory queries the EPICS Archive Appliance JSON API for
|
||||
// historical data for the given PV in the time range [start, end].
|
||||
func fetchArchiveHistory(ctx context.Context, archiveURL, signal string, start, end time.Time, maxPoints int) ([]datasource.Value, error) {
|
||||
// Build the request URL.
|
||||
// Format: GET {archiveURL}/retrieval/data/getData.json?pv={name}&from={start}&to={end}&maxSamples={n}
|
||||
reqURL, err := url.Parse(archiveURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("epics archive: invalid archive URL %q: %w", archiveURL, err)
|
||||
}
|
||||
reqURL = reqURL.JoinPath("retrieval", "data", "getData.json")
|
||||
|
||||
q := reqURL.Query()
|
||||
q.Set("pv", signal)
|
||||
q.Set("from", start.UTC().Format(time.RFC3339Nano))
|
||||
q.Set("to", end.UTC().Format(time.RFC3339Nano))
|
||||
if maxPoints > 0 {
|
||||
q.Set("maxSamples", strconv.Itoa(maxPoints))
|
||||
}
|
||||
reqURL.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("epics archive: building request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("epics archive: HTTP request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("epics archive: unexpected status %d for %q", resp.StatusCode, signal)
|
||||
}
|
||||
|
||||
var ar archiveResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ar); err != nil {
|
||||
return nil, fmt.Errorf("epics archive: decoding response: %w", err)
|
||||
}
|
||||
|
||||
if len(ar) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
points := ar[0].Data
|
||||
out := make([]datasource.Value, 0, len(points))
|
||||
|
||||
// EPICS Archive Appliance uses the POSIX epoch (unlike the CA wire protocol
|
||||
// which uses the EPICS epoch). The JSON API returns POSIX seconds.
|
||||
for _, p := range points {
|
||||
ts := time.Unix(p.Secs, p.Nanos).UTC()
|
||||
|
||||
var q datasource.Quality
|
||||
switch p.Severity {
|
||||
case 0:
|
||||
q = datasource.QualityGood
|
||||
case 1, 2:
|
||||
q = datasource.QualityUncertain
|
||||
default:
|
||||
q = datasource.QualityBad
|
||||
}
|
||||
|
||||
data := coerceArchiveValue(p.Val)
|
||||
|
||||
out = append(out, datasource.Value{
|
||||
Timestamp: ts,
|
||||
Data: data,
|
||||
Quality: q,
|
||||
})
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// coerceArchiveValue converts the raw JSON value (which json.Unmarshal decodes
|
||||
// as float64, string, bool, []interface{}, or nil) into one of the types
|
||||
// accepted by datasource.Value.Data.
|
||||
func coerceArchiveValue(v any) any {
|
||||
if v == nil {
|
||||
return float64(0)
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return t
|
||||
case string:
|
||||
return t
|
||||
case bool:
|
||||
return t
|
||||
case []interface{}:
|
||||
// Waveform: convert to []float64.
|
||||
out := make([]float64, 0, len(t))
|
||||
for _, elem := range t {
|
||||
if f, ok := elem.(float64); ok {
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return float64(0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user