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
11 changes: 7 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ version: 2

updates:
# ---- Go modules -------------------------------------------------------
# The Go module lives in the go/ subdirectory, so `directory` must point
# there (not the repo root) or Dependabot finds no go.mod. Indirect
# requires move alongside the direct ones that pull them in.
# The Go modules live in go/ and go/protocol, so `directories` must point
# there (not the repo root) or Dependabot finds no go.mod. The wire-format
# module in go/protocol is what FTW Cloud imports. Indirect requires move
# alongside the direct ones that pull them in.
#
# Go major bumps are import-path changes (/v2), which Dependabot cannot
# rewrite — expect majors here to surface rarely or not at all.
- package-ecosystem: "gomod"
directory: "/go"
directories:
- "/go"
- "/go/protocol"
schedule:
interval: "weekly"
day: "monday"
Expand Down
30 changes: 30 additions & 0 deletions .github/scripts/check-protocol-boundary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# go/protocol is the wire format that FTW Cloud imports. It must build without
# the box, so that a service holding the protocol links no control, planner or
# storage code. Go's internal rule does not stop it: github.com/srcfl/ftw/go/
# protocol sits under the box's path and may import the box's internal
# packages. This check does.
#
# It fails when the module, its tests included, reaches any package of
# github.com/srcfl/ftw/go outside go/protocol, or requires the box module.

set -euo pipefail

dir=${1:-go/protocol}
cd "${dir}"

box_packages=$(go list -deps -test -f '{{.ImportPath}}' ./... | awk '
$1 ~ /^github\.com\/srcfl\/ftw\/go(\/|$)/ &&
$1 !~ /^github\.com\/srcfl\/ftw\/go\/protocol(\/|\.test|$)/ { print $1 }
' | sort -u)

box_module=$(go list -m all | awk '$1 == "github.com/srcfl/ftw/go"')

if [ -n "${box_packages}" ] || [ -n "${box_module}" ]; then
echo "go/protocol must not depend on the box module." >&2
[ -z "${box_packages}" ] || printf 'It reaches:\n%s\n' "${box_packages}" >&2
[ -z "${box_module}" ] || printf 'It requires:\n%s\n' "${box_module}" >&2
exit 1
fi

echo "go/protocol: $(go list -deps ./... | wc -l | tr -d ' ') packages, none from the box"
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ jobs:
# tests still check queue latency, cancellation and operation limits.
run: go test -count=1 -timeout 300s ./...

# go/protocol is its own module, so ./... above skips it. FTW Cloud
# imports it, and it must build without the box.
- name: Protocol module
working-directory: go/protocol
run: |
go vet ./...
go test -count=1 ./...
- name: Protocol module imports nothing from the box
run: bash .github/scripts/check-protocol-boundary.sh go/protocol

web:
name: web
needs: changes
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ WORKDIR /src
# Cache the module download as its own layer so source edits don't
# bust the dep cache.
COPY go/go.mod go/go.sum ./go/
# The wire-format module is replaced from ./protocol, so its go.mod must be
# here before any module command runs.
COPY go/protocol/go.mod go/protocol/go.sum ./go/protocol/
RUN cd go && go mod download

COPY go/ ./go/
Expand Down
5 changes: 5 additions & 0 deletions LICENSING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ This version uses GNU AGPL v3 only, with the narrow Energyplan combination
permission in [LICENSE](LICENSE). [NOTICE](NOTICE) preserves prior Apache
rights and attribution. Third-party files retain their own licenses.

The wire-format module in [`go/protocol`](go/protocol) is the exception: it
uses Apache-2.0 only, as its own [LICENSE](go/protocol/LICENSE) states.
Clients and services that speak the protocol, such as FTW Cloud, may import it
on those terms. Core links it; that does not change Core's license.

## Use and redistribution

You may inspect, run, modify and sell the AGPL-covered software. When you
Expand Down
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

.PHONY: help test compose-migration-test container-boundary-test release-workflow-test build build-arm64 build-amd64 release release-linux release-linux-arm64 release-linux-amd64 \
run-sim sim-ocpp dev fmt vet clean e2e ci ci-ui ci-hw-pi docs \
verify verify-all install-hooks driver-repository-validate driver-versions \
verify verify-all install-hooks driver-repository-validate driver-versions protocol-boundary \
drivers drivers-present driver-versions-across-pin

VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
Expand Down Expand Up @@ -47,6 +47,7 @@ help:
@echo " ci-ui browser smoke against FTW_BASE_URL"
@echo " ci-hw-pi deploy candidate to Pi CI slot + browser smoke"
@echo " fmt vet Go format + static checks"
@echo " protocol-boundary prove go/protocol imports nothing from the box"
@echo " drivers fetch drivers/ from the pinned device-drivers commit"
@echo " clean nuke build artifacts"

Expand Down Expand Up @@ -84,6 +85,7 @@ drivers-present:

test: drivers-present
cd go && go test -tags=$(GO_TAGS) ./...
cd go/protocol && go test ./...

compose-migration-test:
bash -n scripts/enable-modular-stack.sh scripts/migrate-legacy-compose.sh scripts/upgrade-paired-release.sh scripts/install-macos.sh scripts/sync-bundled-drivers.sh scripts/check-driver-versions.sh scripts/check-debian-base.sh
Expand Down Expand Up @@ -145,8 +147,9 @@ ci-hw-pi:
#
# verify-all checks both supported Linux targets on the developer host.

verify: test compose-migration-test container-boundary-test release-workflow-test native-solver-test
verify: test compose-migration-test container-boundary-test release-workflow-test native-solver-test protocol-boundary
cd go && go vet -tags=$(GO_TAGS) ./...
cd go/protocol && go vet ./...
cd go && go build -tags=$(GO_TAGS) ./...
@echo "verify: vet + test + build clean"

Expand Down Expand Up @@ -233,9 +236,16 @@ dev: config.local.yaml

fmt:
cd go && go fmt ./...
cd go/protocol && go fmt ./...

vet:
cd go && go vet -tags=$(GO_TAGS) ./...
cd go/protocol && go vet ./...

# go/protocol is the wire format FTW Cloud imports. It is its own module, so
# ./... in go/ skips it, and it must build without the box.
protocol-boundary:
bash .github/scripts/check-protocol-boundary.sh go/protocol

clean:
rm -rf bin release
Expand Down
7 changes: 7 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ The following prior notices and Apache license apply to that material.
They do not offer later changes under Apache-2.0. Git history records authors,
changes and dates. Separately licensed third-party files keep their notices.

Wire-format module: 2026-10-12
------------------------------
The Go module in go/protocol, github.com/srcfl/ftw/go/protocol, is licensed
under Apache-2.0 only, as go/protocol/LICENSE states. This includes its
changes made after the snapshot above. The rest of the project keeps the
license stated above.

Prior notice, retained verbatim
-------------------------------
FTW
Expand Down
6 changes: 6 additions & 0 deletions go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,9 @@ require (
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
)

// The wire format shared with FTW Cloud is its own module, Apache-2.0, in
// ./protocol. It has no tags yet, so the box builds it from this checkout.
require github.com/srcfl/ftw/go/protocol v0.0.0-00010101000000-000000000000

replace github.com/srcfl/ftw/go/protocol => ./protocol
2 changes: 1 addition & 1 deletion go/internal/appenroll/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
"time"

"github.com/srcfl/ftw/go/internal/apiauth"
"github.com/srcfl/ftw/go/internal/appwire"
"github.com/srcfl/ftw/go/protocol/appwire"
)

const (
Expand Down
2 changes: 1 addition & 1 deletion go/internal/appenroll/enroll_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"time"

"github.com/srcfl/ftw/go/internal/apiauth"
"github.com/srcfl/ftw/go/internal/appwire"
"github.com/srcfl/ftw/go/protocol/appwire"
)

func newIdentity(t *testing.T) (*Identity, string) {
Expand Down
40 changes: 8 additions & 32 deletions go/internal/appproto/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import (
"github.com/srcfl/ftw/go/internal/control"
)

const registryPath = "../../../contract/registry.yaml"
const (
registryPath = "../../../contract/registry.yaml"
// The generator lives here; its output lives in package protocol, which
// FTW Cloud imports too.
generatedPath = "../../protocol/contract_gen.go"
)

// The generated constants must be what the registry currently says. A snapshot
// updated without rerunning the generator is exactly the drift the registry
Expand All @@ -23,12 +28,12 @@ func TestContractGenIsCurrent(t *testing.T) {
if err != nil {
t.Fatalf("generate: %v", err)
}
got, err := os.ReadFile("contract_gen.go")
got, err := os.ReadFile(generatedPath)
if err != nil {
t.Fatalf("read generated file: %v", err)
}
if string(got) != string(want) {
t.Fatalf("contract_gen.go is stale; run: go generate ./internal/...\n"+
t.Fatalf("protocol/contract_gen.go is stale; run: go generate ./internal/...\n"+
"(registry: %s)", filepath.Clean(registryPath))
}
}
Expand All @@ -52,35 +57,6 @@ func TestRegistryModesMatchCatalog(t *testing.T) {
}
}

// Field ids 1-9 are frozen permanently as of v1. An app of any age draws the
// core view from these alone, so a change here breaks installs nobody can
// reach.
func TestFrozenFieldIdsAreFrozen(t *testing.T) {
want := map[Fid]string{
1: "mode",
2: "grid_w",
3: "pv_w",
4: "battery_w",
5: "battery_soc",
6: "load_w",
7: "src_grid",
8: "src_pv",
9: "src_battery",
// Extended deliberately on 2026-08-05 for EV charging. The freeze
// covers the id's meaning, not its presence: like battery_soc,
// ev_w is sent only when the hardware exists.
10: "ev_w",
}
for fid, name := range want {
if got := FrozenFieldNames[fid]; got != name {
t.Fatalf("field %d is %q, and was frozen as %q", fid, got, name)
}
}
if len(FrozenFieldNames) != len(want) {
t.Fatalf("the frozen set has %d fields, want %d", len(FrozenFieldNames), len(want))
}
}

// The ops in the registry are this repository's, copied there so neither side
// hand-writes an operation name or the scope it demands. defaultOps() in
// command.go stays the authority on what this box accepts; this catches the
Expand Down
12 changes: 7 additions & 5 deletions go/internal/appproto/gencontract/gencontract.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// Hand-copying those names is how this codebase ended up with three separate
// authorisation namespaces, so the box reads the file instead of retyping it.
//
// Run it with `go generate ./internal/appproto/...`.
// Run it with `go generate ./internal/...` from go/. It stays in the box but
// writes the wire constants and the push table into go/protocol, the module
// FTW Cloud imports.
package gencontract

import (
Expand Down Expand Up @@ -58,7 +60,7 @@ type Registry struct {
} `yaml:"resolutions"`
}

// Generate renders the constant file. Exported and byte-deterministic so the
// Generate renders the constant file for package protocol. Exported and byte-deterministic so the
// drift test can re-run it in memory and compare, rather than shelling out.
func Generate(raw []byte) ([]byte, error) {
var reg Registry
Expand All @@ -68,7 +70,7 @@ func Generate(raw []byte) ([]byte, error) {

var b bytes.Buffer
b.WriteString("// Code generated by gencontract from contract/registry.yaml. DO NOT EDIT.\n\n")
b.WriteString("package appproto\n\n")
b.WriteString("package protocol\n\n")

fmt.Fprintf(&b, "// ContractVersion is the registry revision these names came from.\n")
fmt.Fprintf(&b, "const ContractVersion = %d\n\n", reg.Version)
Expand Down Expand Up @@ -361,7 +363,7 @@ type PushCatalogue struct {
} `yaml:"events"`
}

// GeneratePush renders the push sentence table into package notifications.
// GeneratePush renders the push sentence table into package protocol.
//
// It lives beside the registry generator because the two files share one
// pairing rule: byte-identical with the app's copy, compared by CI on both
Expand All @@ -374,7 +376,7 @@ func GeneratePush(raw []byte) ([]byte, error) {

var b bytes.Buffer
b.WriteString("// Code generated by gencontract from contract/push-catalogue.yaml. DO NOT EDIT.\n\n")
b.WriteString("package notifications\n\n")
b.WriteString("package protocol\n\n")

fmt.Fprintf(&b, "// PushCatalogueVersion is the catalogue revision these sentences came from.\n")
fmt.Fprintf(&b, "const PushCatalogueVersion = %d\n\n", cat.Version)
Expand Down
5 changes: 4 additions & 1 deletion go/internal/appproto/generate.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package appproto

//go:generate go run ./gencontract/cmd proto ../../../contract/registry.yaml contract_gen.go
// The wire constants live in package protocol, which FTW Cloud imports too.
// The generator stays here, in the box, and writes across to it.

//go:generate go run ./gencontract/cmd proto ../../../contract/registry.yaml ../../protocol/contract_gen.go
2 changes: 1 addition & 1 deletion go/internal/appproto/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
// testCodec is the frame layout from docs/protocol.md, implemented here so the
// tests drive real bytes rather than passing structs to themselves.
//
// go/internal/appwire owns the production one. Keeping a copy in _test.go means
// go/protocol/appwire owns the production one. Keeping a copy in _test.go means
// a test passing here says the message layer works against the documented
// layout, not that a stub agreed with itself — and it cannot be mistaken for
// the real codec, because it does not ship.
Expand Down
Loading
Loading