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
37 changes: 37 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,43 @@ ending with a `---` line and no `---` line inside a section

---

## v0.10.0

**Date:** 2026-11-08
**Previous release:** v0.9.0

### Summary

profinetctl is built on go-profinet v0.8.0, which was validated against
traffic recorded from real equipment (a SIMATIC S7-1500 CPU 1515-2 PN and
older Siemens, GE and switch devices; go-profinet's testdata/captures).
`lldp neighbors` shows the management address's interface number and OID,
and the go-profinet fixes reach every command.

### Added

- `lldp neighbors` JSON: `management_if_index` and `management_oid` (dotted,
for example `1.3.6.1.4.1.24686`, the PNO enterprise OID Siemens devices
send), both omitted when absent ([docs/json-output.md](docs/json-output.md)).

### Changed (from go-profinet v0.8.0)

- `discover` and every DCP output: a NameOfStation or TypeOfStation padded
with NULs (an S7-300 pads its name to 108 octets) is shown without the
padding.
- `lldp neighbors`, `topology`: an LLDPDU with a second Chassis ID, Port ID
or TTL TLV is ignored (IEEE 802.1AB); a non-IPv4 management address no
longer hides the rest of the TLV list.
- `alarms`, `connect`, `io`: profinetctl as IO-Controller answers a
device's ERR-RTA with ERR-RTA "AR alarm.ind(err)" (`CF 81 FD 0B`), as a
CPU 1515-2 PN does.

### Dependencies

- go-profinet v0.7.0 → v0.8.0.

---

## v0.9.0

**Date:** 2026-11-07
Expand Down
39 changes: 39 additions & 0 deletions cmd/lldp.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ type neighborView struct {
SystemDescription string `json:"system_description,omitempty"`
PortDescription string `json:"port_description,omitempty"`
ManagementAddress string `json:"management_address,omitempty"`
// ManagementIfIndex and ManagementOID are the interface number and the
// object identifier of the management address TLV (Siemens devices
// send their port number and the PNO enterprise OID 1.3.6.1.4.1.24686).
ManagementIfIndex uint32 `json:"management_if_index,omitempty"`
ManagementOID string `json:"management_oid,omitempty"`
ChassisMAC string `json:"chassis_mac,omitempty"`
// Alias is the PROFINET AliasName of the device connected to this
// neighbor's port: the name a device without station name answers to.
Expand Down Expand Up @@ -80,6 +85,8 @@ func neighborViews(ns []lldp.Neighbor) []neighborView {
SystemName: n.Frame.SystemName, SystemDescription: n.Frame.SystemDescription, PortDescription: n.Frame.PortDescription, Alias: n.Alias()}
if n.Frame.ManagementAddr.IsValid() {
v.ManagementAddress = n.Frame.ManagementAddr.String()
v.ManagementIfIndex = n.Frame.ManagementIfIndex
v.ManagementOID = berOID(n.Frame.ManagementOID)
}
if n.Frame.ChassisMAC != nil {
v.ChassisMAC = n.Frame.ChassisMAC.String()
Expand Down Expand Up @@ -382,3 +389,35 @@ without an IP address cannot be read. Exit code 7 when some could not be read.`,
c.AddCommand(newTopologyCheckCmd(a))
return c
}

// berOID returns a BER-encoded object identifier (without tag and length)
// in dotted form, or the octets in hex when they are no valid OID.
func berOID(b []byte) string {
if len(b) == 0 {
return ""
}
var parts []string
var v uint64
first := true
for i, c := range b {
if v > 0xFFFFFFFF>>7 { // sub-identifiers are 32-bit (SNMP)
return fmt.Sprintf("%x", b)
}
v = v<<7 | uint64(c&0x7F)
if c&0x80 != 0 {
if i == len(b)-1 {
return fmt.Sprintf("%x", b)
}
continue
}
if first {
x := min(v/40, 2)
parts = append(parts, fmt.Sprint(x), fmt.Sprint(v-40*x))
first = false
} else {
parts = append(parts, fmt.Sprint(v))
}
v = 0
}
return strings.Join(parts, ".")
}
2 changes: 1 addition & 1 deletion cmd/records_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ func TestLLDPAndTopology(t *testing.T) {
a = nb
}
}
if a.PortID != "port-001.dev-a" || a.ManagementAddress != "192.168.0.10" || a.TTL == 0 || a.Alias == "" {
if a.PortID != "port-001.dev-a" || a.ManagementAddress != "192.168.0.10" || a.ManagementIfIndex != 1 || a.ManagementOID != "" || a.TTL == 0 || a.Alias == "" {
t.Fatalf("dev-a %+v", a)
}
out := n.ok("-i", "pn0", "lldp", "neighbors", "--wait", "400ms")
Expand Down
16 changes: 16 additions & 0 deletions cmd/unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cmd
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"net"
Expand Down Expand Up @@ -245,3 +246,18 @@ func TestHelpers(t *testing.T) {
t.Error("target")
}
}

func TestBerOID(t *testing.T) {
for in, want := range map[string]string{
"": "",
"2b0601040181c06e": "1.3.6.1.4.1.24686",
"8837": "2.999",
"2b81": "2b81", // cut inside a sub-identifier
"2bffffffffffffffff7f": "2bffffffffffffffff7f",
} {
b, _ := hex.DecodeString(in)
if got := berOID(b); got != want {
t.Errorf("berOID(%s) = %q, want %q", in, got, want)
}
}
}
2 changes: 1 addition & 1 deletion docs/json-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ These conventions are a **stable contract** for scripting. Field names are snake
| `diagnostics` | `{"mac", "station", "entries": [{"api", "slot", "subslot", "channel", "direction", "severity", "error_type", "error", "ext_error_type", "ext_add_value", "qualifier", "usi", "vendor_data"}]}` |
| `device info` | `{"device", "im0", "modules": [{"api", "slot", "module_ident_number", "submodules": [{"subslot", "submodule_ident_number"}]}], "errors"}` |
| `status` | `{"device", "dcp", "rpc", "diagnoses": {"fault": n, ...}, "ports": [{"subslot", "port_id", "link", "peer"}], "errors"}` |
| `lldp neighbors` | `{"interface", "neighbors": [{"mac", "chassis_id", "port_id", "ttl", "system_name", "system_description", "port_description", "management_address", "chassis_mac", "alias", "line_delay"}]}` |
| `lldp neighbors` | `{"interface", "neighbors": [{"mac", "chassis_id", "port_id", "ttl", "system_name", "system_description", "port_description", "management_address", "management_if_index", "management_oid", "chassis_mac", "alias", "line_delay"}]}` |
| `topology` | `{"host_neighbors", "devices": [{"station", "mac", "ip", "ports": [{"subslot", "port_id", "link", "peers": [{"station", "port", "mac", "line_delay"}]}], "error"}], "links": [{"a", "a_port", "b", "b_port"}]}` |
| `gsdml inspect` | `{"file", "schema_version", "vendor_id", "device_id", "vendor_name", "family", "device_access_points": [...], "valid", "issues"}` |
| `gsdml validate` | `{"file", "valid", "issues": [{"path", "message", "severity", "rule"}], "scope"}`; `severity` is `"error"` (validate reports errors only), `rule` names the rule broken: `"structure"` (or absent), `"reference"` or `"io-data-length"` (input or output data too long for an RT frame) |
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/otfabric/profinetctl
go 1.26

require (
github.com/otfabric/go-profinet v0.7.0
github.com/otfabric/go-profinet v0.8.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/otfabric/go-profinet v0.7.0 h1:3o8Kp5evYlyZWjD/PSQLoJmHoqDSbm3jIpTJacbIbUM=
github.com/otfabric/go-profinet v0.7.0/go.mod h1:eLxHQxabRs/OloPKSXKo/0OOgMjwjJUwnkQstdfE2wI=
github.com/otfabric/go-profinet v0.8.0 h1:GgFz2cBksm3ObT45IKGodaeFs7kNyq+bDiXIxQ1Hh9I=
github.com/otfabric/go-profinet v0.8.0/go.mod h1:eLxHQxabRs/OloPKSXKo/0OOgMjwjJUwnkQstdfE2wI=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
Expand Down
Loading