diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c59f2e1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: "build" +on: + push: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + cache-dependency-path: 'go.sum' + - name: build cmd + run: + go build ./cmd/optic-programmer.go + - uses: actions/upload-artifact@v4 + with: + name: optic-programmer + path: optic-programmer + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c6b1dc2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: "test" +on: + push: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + cache-dependency-path: 'go.sum' + - name: test internal + run: 'go test ./internal/...' diff --git a/README.md b/README.md index 74f42ee..a6768b7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ # rtbrick-optic-programmer -A project to programm Finisar DWDM optics plugged into an RtBrick device. +CMIS & SFF8636 optics programmer for rtbrick remote devices. Features a JSON output for machine parsing. +Tested with Finisar CMIS optics, Finisar SFF8636, FlexOptix SFF8636 C-band programmable. + +You are expected to have already set up your ssh key on the device, password entry is currently not supported. # Building @@ -14,71 +17,94 @@ Set the `LOG_LEVEL` env var to "debug" to view i2c dumps as they are read. # Usage -```shell -./optic-programmer show --user horst lrmd0001.infra.lab.wobcom.de ifp-0/0/2 -2026/01/09 16:54:56 Running command=sudo i2cset -y 33 0x50 127 0 -2026/01/09 16:54:56 Running command=sudo i2cdump -y 33 0x50 b -2026/01/09 16:54:57 Running command=sudo i2cset -y 33 0x50 127 0 -2026/01/09 16:54:57 Running command=sudo i2cset -y 33 0x50 127 18 -2026/01/09 16:54:57 Running command=sudo i2cdump -y 33 0x50 b -2026/01/09 16:54:58 Running command=sudo i2cset -y 33 0x50 127 0 -2026/01/09 16:54:58 Running command=sudo i2cset -y 33 0x50 127 30 -2026/01/09 16:54:58 Running command=sudo i2cdump -y 33 0x50 b -2026/01/09 16:54:59 Running command=sudo i2cset -y 33 0x50 127 0 -2026/01/09 16:54:59 Running command=sudo i2cset -y 33 0x50 127 27 -2026/01/09 16:54:59 Running command=sudo i2cdump -y 33 0x50 b -2026/01/09 16:55:00 Running command=sudo i2cset -y 33 0x50 127 0 -2026/01/09 16:55:00 Vendor Name: FINISAR CORP. -2026/01/09 16:55:00 Vendor PN: FTLC3351R3PL1 -2026/01/09 16:55:00 Vendor SN: 2511W1653 -2026/01/09 16:55:00 Grid Spacing: 100 -2026/01/09 16:55:00 Frequency Offset: -9 -2026/01/09 16:55:00 Frequency: 192.2 THz -2026/01/09 16:55:00 Channel: 22 -2026/01/09 16:55:00 Flex Tune Enabled: false -2026/01/09 16:55:00 Low Power Mode: false -2026/01/09 16:55:00 Nominal Wavelength Control Enabled: false +## Manual ``` +NAME: + optic-programmer - in-field optical module programming for rtbrick + +USAGE: + optic-programmer [global options] [command [command options]] + +DESCRIPTION: + CMIS & SFF8636 optics programmer for rtbrick remote devices. Only works with RBFS, + uses SMBus & i2c utils to issue direct i2c commands to optical modules. + +COMMANDS: + show show information about an optic in a specific device. show basic (failsafe) or show all. + reset WARNING, YOU MAY LOSE CONFIG!!!! request module software reset + set, s sets parameter + set low-power toggle low power mode on/off + set dwdmgrid sets dwdm grid mode and channel + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --user string [$USER] + --device string [$DEVICE] + --interface string [$INTERFACE] + --help, -h show help + +COPYRIGHT: + WDZ GmbH 2026 +``` +## Examples + +### Showing basic information +Show basic only shows standard protocol information. Manufacturer protocol extensions are not used with +this command. You can get a detailed output, including protocol and manufacturer extensions, +using the command `show all`. ```shell -./optic-programmer program --user jwagner lrma0002.infra.lab.wobcom.de ifp-0/0/2 --grid-spacing=100 --channel=22 -2026/01/09 16:53:22 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:22 Running command=sudo i2cdump -y 11 0x50 b -2026/01/09 16:53:23 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:23 Running command=sudo i2cset -y 11 0x50 127 18 -2026/01/09 16:53:23 Running command=sudo i2cdump -y 11 0x50 b -2026/01/09 16:53:23 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:24 Running command=sudo i2cset -y 11 0x50 127 30 -2026/01/09 16:53:24 Running command=sudo i2cdump -y 11 0x50 b -2026/01/09 16:53:24 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:24 Running command=sudo i2cset -y 11 0x50 127 27 -2026/01/09 16:53:25 Running command=sudo i2cdump -y 11 0x50 b -2026/01/09 16:53:25 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:25 Vendor Name: FINISAR CORP. -2026/01/09 16:53:25 Vendor PN: FTLC3351R3PL1 -2026/01/09 16:53:25 Vendor SN: 2511W1695 -2026/01/09 16:53:25 Setting Low Power Mode... -2026/01/09 16:53:25 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:25 Running command=sudo i2cset -y 11 0x50 93 2 -2026/01/09 16:53:26 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:27 Flex Tune is already disabled... -2026/01/09 16:53:27 Grid Spacing is already programmed at 100 GHz, no programming needed... -2026/01/09 16:53:27 Channel must be programmed to 22, currently 37 -2026/01/09 16:53:27 Running command=sudo i2cset -y 11 0x50 127 18 -2026/01/09 16:53:27 Running command=sudo i2cset -y 11 0x50 137 247 -2026/01/09 16:53:27 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:27 Running command=sudo i2cset -y 11 0x50 127 18 -2026/01/09 16:53:27 Running command=sudo i2cset -y 11 0x50 136 255 -2026/01/09 16:53:27 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:28 Setting Nominal Wavelength Control Programming... -2026/01/09 16:53:28 Running command=sudo i2cset -y 11 0x50 127 176 -2026/01/09 16:53:28 Running command=sudo i2cset -y 11 0x50 129 1 -2026/01/09 16:53:28 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:29 Enabling High Power Mode... -2026/01/09 16:53:29 Running command=sudo i2cset -y 11 0x50 127 0 -2026/01/09 16:53:30 Running command=sudo i2cset -y 11 0x50 93 4 -2026/01/09 16:53:30 Running command=sudo i2cset -y 11 0x50 127 0 +./optic-programmer --user llecrivain --device lrma0001.infra.lab.wobcom.de --interface ifp-0/0/2 show basic +{ + "sff8636": { + "active": true, + "enable_high_power_class_8": false, + "enable_high_power_class_57": true, + "low_pwr_override": true + }, + "management_protocol": "sff8636", + "sff_8024_identifier": 17, + "sff_8024_revision": 8, + "low_pwr_request_sw": false, + "software_reset": false, + "vendor_name": "FLEXOPTIX", + "vendor_part_number": "Q.16S1HG.14.O2D", + "vendor_part_revision": "Q.16S1HG.14.O2D", + "vendor_serial_number": "FQM0023" +} +``` + +### Programming the tunable laser +Lane and bank are optional, by default the tool will always select first bank, first lane or 1st lane (when +lanes are not banked). Be aware that not specifying grid spacing will reset it to default (100Ghz). +```shell +./optic-programmer --user llecrivain --device lrma0001.infra.lab.wobcom.de --interface ifp-0/0/0 set dwdmgrid \ + --grid-spacing 50 --channel -20 --lane 1 --bank 1 +module has processed command. check module status. ``` +## Some jq examples for filtering output +### Checking tuning progress status for lane 0 + +Tuning complete flag will clear when being read. +```shell +./optic-programmer --user llecrivain --device lrma0001.infra.lab.wobcom.de --interface ifp-0/0/0 show all | \ +jq "\ +.cmis.tunable_laser.control_status[0] | +{ + grid_spacing_tx: .grid_spacing_tx[0], + channel_number_tx: .channel_number_tx[0], + current_laser_frequency_mhz_tx: .current_laser_frequency_mhz_tx[0], + current_laser_frequency_mhz_tx: .current_laser_frequency_mhz_tx[0], + tuning_in_progress_tx: .tuning_in_progress_tx[0], + tuning_complete_flag_tx: .tuning_complete_flag_tx[0] +}" + +``` +### Only show CMIS tunable laser capabilities +```shell +./optic-programmer --user llecrivain --device lrma0001.infra.lab.wobcom.de --interface ifp-0/0/0 show all | \ +jq ".cmis.tunable_laser.capabilities" +``` diff --git a/cmd/optic-programmer.go b/cmd/optic-programmer.go index 63d1bfd..53ed573 100644 --- a/cmd/optic-programmer.go +++ b/cmd/optic-programmer.go @@ -2,14 +2,23 @@ package main import ( "context" + "errors" + "fmt" "log/slog" "os" + "strconv" "strings" "github.com/urfave/cli/v3" - "github.com/wobcom/rtbrick-optic-programmer/internal/pkg/routines" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" + cmis2 "github.com/wobcom/rtbrick-optic-programmer/internal/optic/cmis" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic/default" + sff8637 "github.com/wobcom/rtbrick-optic-programmer/internal/optic/sff8636" + "github.com/wobcom/rtbrick-optic-programmer/internal/rtbrick/ssh" ) +const OK = "module has processed command. check module status." + func getLoglevel(level string) slog.Level { switch strings.ToLower(level) { case "error": @@ -24,55 +33,304 @@ func getLoglevel(level string) slog.Level { panic("invalid log level provided") } +var concreteManagementStrategies = [...]func(state *optic.ModuleState) optic.ConcreteManagementStrategy{ + func(state *optic.ModuleState) optic.ConcreteManagementStrategy { return sff8637.New(state) }, + func(state *optic.ModuleState) optic.ConcreteManagementStrategy { return cmis2.New(state) }, +} + +var concreteExtensionStrategies = [...]func(state *optic.ModuleState) optic.ConcreteExtensionManagementStrategy{ + func(state *optic.ModuleState) optic.ConcreteExtensionManagementStrategy { + return sff8637.NewSFF8636Extension(state) + }, + func(state *optic.ModuleState) optic.ConcreteExtensionManagementStrategy { + return sff8637.NewFlexOptixSFF8636Extension(state) + }, + func(state *optic.ModuleState) optic.ConcreteExtensionManagementStrategy { + return cmis2.NewCMISExtension(state) + }, +} + +var safeModeConcreteExtensionStrategies = [...]func(state *optic.ModuleState) optic.ConcreteExtensionManagementStrategy{ + // no manufacturers enabled, only lower mem and page 00 + func(state *optic.ModuleState) optic.ConcreteExtensionManagementStrategy { + return sff8637.NewSFF8636Extension(state) + }, + func(state *optic.ModuleState) optic.ConcreteExtensionManagementStrategy { + return cmis2.NewCMISExtension(state) + }, +} + +var defaultManagementStrategy = func(state *optic.ModuleState) optic.ConcreteManagementStrategy { + return _default.New(state) +} + +var restrictedFeatureSetFactory = func(handle *connection.I2cRWHandle) *optic.ModuleState { + return optic.NewModuleState( + defaultManagementStrategy, + concreteManagementStrategies[:], + safeModeConcreteExtensionStrategies[:], + handle, + ) +} + +var allFeatureSetFactory = func(handle *connection.I2cRWHandle) *optic.ModuleState { + return optic.NewModuleState( + defaultManagementStrategy, + concreteManagementStrategies[:], + concreteExtensionStrategies[:], + handle, + ) +} + +// ActionTemplateMethod allows to factorize common action code and let caller plug callback +// module will always have basic administrative data (safe lower memory and safe page 00) fetched +// before being passed to callback with the rest of the other arguments. +func ActionTemplateMethod( + moduleFactory func(handle *connection.I2cRWHandle) *optic.ModuleState, + call func(module *optic.ModuleState, context context.Context, cmd *cli.Command) error) cli.ActionFunc { + return func(context context.Context, cmd *cli.Command) error { + user := cmd.String("user") + router := cmd.String("device") + iface := cmd.String("interface") + + handle, err := connection.NewI2cRWHandle(user, router, iface) + if err != nil { + return err + } + defer connection.CloseI2CRWHandle(handle) + + module, err := moduleFactory(handle).Get() + if err != nil { + return err + } + err = call(module, context, cmd) + if err != nil { + return err + } + + return nil + } +} + func main() { - slog.SetLogLoggerLevel(slog.LevelInfo) + slog.SetLogLoggerLevel(slog.LevelWarn) if level, ok := os.LookupEnv("LOG_LEVEL"); ok { slog.SetLogLoggerLevel(getLoglevel(level)) } cmd := &cli.Command{ + Copyright: "WDZ GmbH 2026", + Usage: "in-field optical module programming for rtbrick", + Description: "CMIS & SFF8636 optics programmer for rtbrick remote devices. Only works with RBFS,\n" + + "uses SMBus & i2c utils to issue direct i2c commands to optical modules.", Flags: []cli.Flag{ &cli.StringFlag{ Name: "user", Sources: cli.EnvVars("USER"), }, + &cli.StringFlag{ + Name: "device", + Sources: cli.EnvVars("DEVICE"), + }, + &cli.StringFlag{ + Name: "interface", + Sources: cli.EnvVars("INTERFACE"), + }, }, Commands: []*cli.Command{ { - Name: "show", - Aliases: []string{"s"}, - Usage: "Shows information about an optic in a specific device", - Arguments: []cli.Argument{ - &cli.StringArg{ - Name: "device", + Name: "show", + Usage: "show information about an optic in a specific device. show basic (failsafe) or show all.", + Commands: []*cli.Command{ + { + Name: "basic", + Action: ActionTemplateMethod(restrictedFeatureSetFactory, func( + module *optic.ModuleState, + context context.Context, + command *cli.Command, + ) error { + bytes, err := module.ToJson() + if err != nil { + return err + } + _, err = os.Stdout.Write(bytes) + if err != nil { + return err + } + return nil + }), }, - &cli.StringArg{ - Name: "interface", + { + Name: "all", + Action: ActionTemplateMethod(allFeatureSetFactory, func( + module *optic.ModuleState, + context context.Context, + command *cli.Command, + ) error { + _, err2 := module.GetExtensionsState() + if err2 != nil { + return err2 + } + bytes, err := module.ToJson() + if err != nil { + return err + } + _, err = os.Stdout.Write(bytes) + if err != nil { + return err + } + return nil + }), }, }, - Action: routines.I2CReadAll, }, { - Name: "program", + Name: "reset", + Usage: "WARNING, YOU MAY LOSE CONFIG!!!! request module software reset", + Action: ActionTemplateMethod(restrictedFeatureSetFactory, func( + module *optic.ModuleState, + context context.Context, + command *cli.Command, + ) error { + module.SoftwareReset = true + + _, err := module.SetAdministrativeInformation() + if err != nil { + return err + } + + println(OK) + return nil + }), + }, + { + Name: "set", Aliases: []string{"s"}, - Usage: "Programs an optic in a specific device", - Arguments: []cli.Argument{ - &cli.StringArg{ - Name: "device", - }, - &cli.StringArg{ - Name: "interface", - }, - }, - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "grid-spacing", + Usage: "sets parameter", + Commands: []*cli.Command{ + { + Name: "low-power", + Usage: "toggle low power mode on/off", + Action: ActionTemplateMethod(restrictedFeatureSetFactory, func( + module *optic.ModuleState, + context context.Context, + command *cli.Command, + ) error { + if command.Args().Len() != 1 { + return errors.New("please provide on/off argument") + } + toggle := command.Args().Get(0) + + if toggle == "on" { + module.LowPwrRequestSW = true + // software set enabled for sff8636 (overrides LPMode pad) + module.SFF8636.LowPwrOverride = true + // SFF8636 software override, disable high power classes + module.SFF8636.EnableHighPowerClass57 = false + module.SFF8636.EnableHighPowerClass8 = false + } else if toggle == "off" { + module.LowPwrRequestSW = false + module.SFF8636.LowPwrOverride = true + // SFF8636 software override, enable high power classes + module.SFF8636.EnableHighPowerClass57 = true + module.SFF8636.EnableHighPowerClass8 = true + } else { + return errors.New(fmt.Sprintf("cannot parse on/off argument %s", toggle)) + } + + _, err := module.SetAdministrativeInformation() + if err != nil { + return err + } + + println(OK) + return nil + }), }, - &cli.IntFlag{ - Name: "channel", + { + Name: "dwdmgrid", + Usage: "sets dwdm grid mode and channel", + Flags: []cli.Flag{ + &cli.Float64Flag{ + Name: "grid-spacing", + Usage: "grid to use, in ghz", + Value: 100.000, + }, + &cli.IntFlag{ + Name: "channel", + Usage: "channel number to use", + }, + &cli.IntFlag{ + Name: "lane", + Usage: "media lane number", + Value: 1, + }, + &cli.IntFlag{ + Name: "bank", + Usage: "lane bank number to use", + Value: 1, + }, + }, + Action: ActionTemplateMethod(allFeatureSetFactory, func( + module *optic.ModuleState, + context context.Context, + command *cli.Command, + ) error { + _, err := module.GetExtensionsState() // non-basic, fetch extension states first. + if err != nil { + return err + } + + gridSpacing := command.Float64("grid-spacing") + gridSpacingStr := strconv.FormatFloat(gridSpacing, 'f', 3, 64) + channel := int16(command.Int("channel")) + lane := command.Int("lane") - 1 + bank := command.Int("bank") - 1 + + // putting it here cos I need to capture args. + var setChannelAndGrid = func( + extension *optic.CommonTunableLaserFields, + ) error { + if !extension.Capabilities.SupportedGridSpacings[gridSpacingStr] { + panic("Module does not support this frequency.") + } + + if channel > extension.Capabilities.GridHighChannel[gridSpacingStr] || + channel < extension.Capabilities.GridLowChannel[gridSpacingStr] { + panic("target offset is above or below maximum frequencies for this grid.") + } + + extension.CtrlStatus[bank].GridSpacingTx[lane] = optic.FloatGhzToCMISGridSpacing[gridSpacingStr] + extension.CtrlStatus[bank].ChannelNumberTx[lane] = channel + + _, err := module.SetExtensionsState() + if err != nil { + return err + } + + println(OK) + + return nil + } + + if module.FlexOptixSFF8636.Active { + err := setChannelAndGrid(&module.FlexOptixSFF8636.TunableLaser) + if err != nil { + return err + } + } else if module.CMIS.Active { + err := setChannelAndGrid(&module.CMIS.TunableLaser) + if err != nil { + return err + } + } else { + panic("Module does not support grid programming") + } + + return nil + }), }, }, - Action: routines.I2CWriteAll, }, }, } diff --git a/dwdm.md b/dwdm.md new file mode 100644 index 0000000..6b0975d --- /dev/null +++ b/dwdm.md @@ -0,0 +1,68 @@ + +ITU rec G.694.1 +========================================== + +From +G.694.1 2020-10-29 version 3 +https://www.itu.int/itu-t/recommendations/rec.aspx?rec=14498 + +For C-Band, $`DWDMCenterFrequency (Hz) = 193.1 \times 10^{12}`$ + + +# STANDARD GRIDS + +## 12.5Ghz +For channel spacings of 12.5 GHz on a fiber, the allowed channel frequencies (in THz) are defined by: +$`193.1 + n × 0.0125`$ where n is a positive or negative integer including 0 + +## 25.0Ghz +For channel spacings of 25 GHz on a fiber, the allowed channel frequencies (in THz) are defined by: +$`193.1 + n × 0.025`$ where n is a positive or negative integer including 0 + +## 50.0Ghz +For channel spacings of 50 GHz on a fiber, the allowed channel frequencies (in THz) are defined by: +$`193.1 + n × 0.05`$ where n is a positive or negative integer including 0 + +## 100.0Ghz +For channel spacings of 100 GHz or more on a fiber, the allowed channel frequencies (in THz) are defined by: +$`193.1 + n × 0.1`$ where n is a positive or negative integer including 0 + +# NON-ITU GRIDS + +Cf OIForum CMIS 5.4 ver. + +## 75.0Ghz +tuning on the 75 GHz grid defined as: +$`Frequency (THz) = 193.1 + n × 0.025`$ +where n must be an integer multiple of 3. + +## 33.0Ghz +tuning on the 33 GHz grid defined as: +$`Frequency (THz) = 193.1 + n × 0.1/3`$ + +## 6.25Ghz +tuning on the 6.25 GHz grid defined as: +$`Frequency (THz) = 193.1 + n × 0.00625`$ + +## 3.125Ghz +tuning on 3.125 GHz grid defined as: +$`Frequency (THz) = 193.1 + n × 0.003125`$ + +## 150Ghz +tuning on the 150 GHz grid defined as: +$`Frequency (THz) = 193.1 + (n+3) × 0.025`$ +where n must be an integer multiple of 6. + +## 300Ghz +tuning on the 300 GHz grid defined as: +$`Frequency (THz) = 193.1 + (n-9) × 0.0125`$ +where n must be an integer multiple of 24. + + +# CHANNEL OFFSET NUMBER + +This channel offset number effectively specifies a laser frequency in terms of a (signed) frequency offset from +a nominal reference frequency at 193.1THz, in units of the grid resolution (except for the 75GHz, 150GHz +and 300GHz grids, where the offset is defined in units of a third, a sixth, or a 24th of the grid resolution, +respectively). + diff --git a/internal/optic/cmis/cmis.go b/internal/optic/cmis/cmis.go new file mode 100644 index 0000000..1ba4c57 --- /dev/null +++ b/internal/optic/cmis/cmis.go @@ -0,0 +1,163 @@ +package cmis + +import ( + "fmt" + + "github.com/wobcom/rtbrick-optic-programmer/internal" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" + util2 "github.com/wobcom/rtbrick-optic-programmer/internal/optic/util" +) + +type ManagementStrategy struct { + state *optic.ModuleState +} + +func New(state *optic.ModuleState) *ManagementStrategy { + return &ManagementStrategy{ + state: state, + } +} + +func (s2 *ManagementStrategy) GetPageBin(page byte, bank byte) ([]byte, error) { + handle := s2.state.GetHandle() + + // we do not have one-shot 2 bytes write available + // currently this is out of spec + // > Note: In other words, a host needs to write both the BankSelect and the PageSelect register in a single WRITE + // > transaction, except for the case when BankIndex value in the BankSelect register does not change. In this case + // > it is sufficient to WRITE to the PageSelect register. + // > Note: However, modules may also choose to accept the two registers written in two subsequent WRITE + // > transactions, to work with non-compliant hosts. Note that rainy day scenarios remain unspecified in this case, + // > and the PageMapping Validity assertion described below is compromised. + + // do bank select first (will not be cleared if incorrect) + err := handle.Connection.DoI2CSet(handle.I2cBusId, BankSelectRegisterAddress, bank) + if err != nil { + panic(BankSelectErrorString) + } + + // do page select next (will be cleared if page + bank is incorrect) + err = handle.Connection.DoI2CSet(handle.I2cBusId, PageSelectRegisterAddress, page) + if err != nil { + panic(PageSelectErrorString) + } + + // decode page + pageStr, err := handle.Connection.GetI2CDump(handle.I2cBusId) + if err != nil { + return []byte{}, err + } + dumpBin := internal.ParseI2CDump(*pageStr) + + // check page + bank select was authorized only and only if target page select = page select + if dumpBin[PageSelectRegisterAddress] != page { + panic(fmt.Sprintf(PageOrBankNotAvailableTemplateErrorString, page, bank)) + } + + // return if everything went ok + return dumpBin, nil +} + +func (s2 *ManagementStrategy) WritePageByteBin(page byte, bank byte, offset byte, value byte) error { + handle := s2.state.GetHandle() + + _, err := s2.GetPageBin(page, bank) // select page by read + if err != nil { + return err + } + + // write is authorized if nothing failed + err = handle.Connection.DoI2CSet(handle.I2cBusId, (int)(offset), value) + if err != nil { + panic(RegisterWriteErrorString) + } + + return nil +} + +func (s2 *ManagementStrategy) AcceptsSFF8024(sff8024Identifier byte, sff8024Revision byte) bool { + return checkSFF8024(sff8024Identifier, sff8024Revision) +} + +func (s2 *ManagementStrategy) Set() (*optic.ModuleState, error) { + _, err := s2.SetAdministrativeInformation() + if err != nil { + return nil, err + } + return s2.state, nil // noop +} + +func (s2 *ManagementStrategy) Get() (*optic.ModuleState, error) { + _, err := s2.GetAdministrativeInformation() + if err != nil { + return nil, err + } + return s2.state, nil +} + +func (s2 *ManagementStrategy) GetAdministrativeInformation() (*optic.ModuleState, error) { + dumpBin, err := s2.state.GetPageBin(0x00, 0x00) + if err != nil { + return nil, err + } + + s2.state.ManagementProtocol = "cmis" + + // lower mem + s2.state.LowPwrRequestSW = dumpBin[0x1A]&LowPwrRequestMask != 0 + s2.state.SoftwareReset = dumpBin[0x1A]&SoftwareResetMask != 0 + s2.state.CMIS.MemoryModelPaged = dumpBin[0x02]&MemoryModelMask == 0 // 0 is paged memory, 1 is flat + s2.state.CMIS.SteppedConfigOnly = dumpBin[0x02]&SteppedConfigOnlyMask == 0 + s2.state.CMIS.I2CMciMaxSpeedKhz = I2CMciMaxSpeedToKhz[dumpBin[0x02]&I2CMciMaxSpeedMask] + s2.state.CMIS.SPIMciMaxSpeedKhz = SPIMciMaxSpeedToKhz[dumpBin[0x02]&SPIMCIMaxSpeedMask] + AutoCommissioning := dumpBin[0x02] & AutoCommissioningMask + s2.state.CMIS.AutoCommissioningNone = AutoCommissioning == 0b0000 + s2.state.CMIS.AutoCommissioningRegular = AutoCommissioning == 0b0001 + s2.state.CMIS.AutoCommissioningHot = AutoCommissioning == 0b0010 // 0x11 is reserved + + // page 00 + s2.state.VendorName = util2.ParseASCIIToString(dumpBin[0x81:0x90]) + s2.state.CMIS.VendorOUI = dumpBin[0x91:0x93] + s2.state.VendorPartNumber = util2.ParseASCIIToString(dumpBin[0x94:0xA3]) + s2.state.VendorPartRevision = util2.ParseASCIIToString(dumpBin[0xA4:0xA5]) + s2.state.VendorSerialNumber = util2.ParseASCIIToString(dumpBin[0xA6:0xB5]) + s2.state.CMIS.DateCode = util2.ParseASCIIToString(dumpBin[0xB6:0xBD]) + s2.state.CMIS.CLEICode = util2.ParseASCIIToString(dumpBin[0xBE:0xC7]) + s2.state.CMIS.PowerClass = PowerClassToInt[dumpBin[0xC8]&ModulePowerClassMask] + s2.state.CMIS.MaxPowerWatts = 0.25 * float64(dumpBin[0xC9]) // byte is interpreted as uint8. unit is ceil of quarter-watts + + s2.state.CMIS.SupportedMediaLanes = make(map[int]bool) + // fetching media lane support per lane + var mask byte = 0b1000_0000 + for i := 8; i >= 1; i -= 1 { // MSB countdown + s2.state.CMIS.SupportedMediaLanes[i] = dumpBin[0xD2]&mask == 0 // supported == 0, unsupported == 1 + mask >>= 1 + } + s2.state.CMIS.FarEndDetachableMedia = dumpBin[0xD3]&FarEndConfigurationMask == 0b0000_0000 + s2.state.CMIS.FarEnd1LaneModule = dumpBin[0xD3]&FarEndConfigurationMask == 0b0000_0001 + s2.state.CMIS.FarEnd2LanesModule = dumpBin[0xD3]&FarEndConfigurationMask == 0b000_0110 + s2.state.CMIS.FarEnd4LanesModule = dumpBin[0xD3]&FarEndConfigurationMask == 0b000_0011 + s2.state.CMIS.FarEnd8LanesModule = dumpBin[0xD3]&FarEndConfigurationMask == 0b000_0010 + s2.state.CMIS.FarEnd16LanesModule = dumpBin[0xD3]&FarEndConfigurationMask == 0b001_1011 + s2.state.CMIS.MediaInterfaceDescription = MediaInterfaceToStr[dumpBin[0xD4]] + + return s2.state, nil +} + +func (s2 *ManagementStrategy) SetAdministrativeInformation() (*optic.ModuleState, error) { + dumpBin, err := s2.state.GetPageBin(0x00, 0x00) + if err != nil { + return nil, err + } + + dumpBin[0x1A] &^= LowPwrRequestMask | SoftwareResetMask // clear + dumpBin[0x1A] |= LowPwrRequestMask & util2.YesNoByte(s2.state.LowPwrRequestSW) + dumpBin[0x1A] |= SoftwareResetMask & util2.YesNoByte(s2.state.SoftwareReset) + + err = s2.state.WritePageBin(0x00, 0, dumpBin) + if err != nil { + return nil, err + } + + return s2.state, nil +} diff --git a/internal/optic/cmis/common.go b/internal/optic/cmis/common.go new file mode 100644 index 0000000..dc7c31b --- /dev/null +++ b/internal/optic/cmis/common.go @@ -0,0 +1,193 @@ +package cmis + +import ( + "slices" + + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic/util" +) + +func checkSFF8024(sff8024Identifier byte, sff8024Revision byte) bool { + var CmisCompatibleSFF8024IDs = [...]byte{ + 0x1E, // qsfp+ or later with cmis + 0x1F, // sfp-dd with cmis + 0x20, // sfp+ or later with cmis + 0x21, // osfp-xd with cmis + 0x22, // oif-elfs with cmis + 0x23, // 4 lanes cdfp with cmis + 0x24, // 8 lanes cdfp with cmis + 0x25, // 16 lanes cdfp with cmis + 0x18, // qsfp-dd 8x - may support cmis + } + + const MajVerMask byte = 0xF0 // upper nibble is bits 7-4 per cmis rev 5.38 section 8.2.1 + const MinMajVer byte = 0x50 // minimum supported major version is 5 + + compatibleIdentifier := func(id byte) bool { return slices.Contains(CmisCompatibleSFF8024IDs[:], id) } + compatibleMajorRev := func(sff8024rev byte) bool { return (sff8024rev & MajVerMask) <= MinMajVer } + + // panic if CMIS and revision is above our maximum + if compatibleIdentifier(sff8024Identifier) && !compatibleMajorRev(sff8024Revision) { + panic(RefusalMajorTooHigh) + } + + // tells if compatible + return compatibleIdentifier(sff8024Identifier) +} + +// GetTunableLaserControlStatus maxN between 0 and 7 (channel N, used for only-channel-0 access) +func GetTunableLaserControlStatus( + state *optic.ModuleState, + caps *optic.CMISBankedTunableLaserControlAndStatus, + bank byte, + maxN int, +) (*optic.ModuleState, error) { + dumpBin, err := state.GetPageBin(TunableLaserControlStatusPage, bank) + if err != nil { + return nil, err + } + + for i := 0; i <= maxN; i += 1 { + caps.GridSpacingTx[i] = dumpBin[0x80+i] & optic.CMISGridSpacingTxMask + caps.GridSpacingTxROGhz[i] = optic.CMISGridSpacingToFloatGhzMap[caps.GridSpacingTx[i]] + caps.FineTuningEnableTx[i] = dumpBin[0x80+i]&optic.CMISFineTuningEnableTxMask != 0 + + caps.ChannelNumberTx[i] = util.ReadBeInt16(dumpBin, 0x88+byte(2*i)) // S16 over 2 bytes + caps.FineTuningOffsetMhzTx[i] = util.ReadBeInt16(dumpBin, 0x98+byte(2*i)) + caps.CurrentLaserFrequencyMhzTx[i] = util.ReadBeUint32(dumpBin, 0xA8+byte(4*i)) // U32 over 4 bytes, units Mhz + + caps.TargetOutputPowerTx[i] = util.ReadBeInt16(dumpBin, 0xC8+byte(2*i)) + + caps.TuningInProgressTx[i] = dumpBin[0xDE+i]&optic.CMISTuningInProgressTxMask != 0 + caps.WaveLengthUnlockStatus[i] = dumpBin[0xDE+i]&optic.CMISWavelengthUnlockStatusTxMask != 0 + + // per spec: the bit n-1 is set if and only if any of the latched flags are set to 1 + caps.LaserTuningFlagSummaryTx[i] = (dumpBin[0xE6+i] & (0b0000_0001 << i)) != 0 + + caps.TargetOutputPowerOORFlagTx[i] = dumpBin[0xE7+i]&optic.CMISTargetOutputPowerOORFlagTxMask != 0 + caps.FineTuningOutOfRangeFlagTx[i] = dumpBin[0xE7+i]&optic.CMISFineTuningOutOfRangeFlagTxMask != 0 + caps.TuningNotAcceptedMaskTx[i] = dumpBin[0xE7+i]&optic.CMISTuningNotAcceptedFlagTxMask != 0 + caps.InvalidChannelNumberFlagTx[i] = dumpBin[0xE7+i]&optic.CMISInvalidChannelNumberFlagTxMask != 0 + caps.WavelengthUnlockedFlagTx[i] = dumpBin[0xE7+i]&optic.CMISWavelengthUnlockedFlagTxMask != 0 + caps.TuningCompleteFlagTx[i] = dumpBin[0xE7+i]&optic.CMISTuningCompleteFlagTxMask != 0 + + // reading interrupt masks, bitfield pattern same as alarm + caps.TargetOutputPowerOORMaskTx[i] = dumpBin[0xEF+i]&optic.CMISTargetOutputPowerOORFlagTxMask != 0 + caps.FineTuningPowerOutOfRangeMaskTx[i] = dumpBin[0xEF+i]&optic.CMISFineTuningOutOfRangeFlagTxMask != 0 + caps.TuningNotAcceptedMaskTx[i] = dumpBin[0xEF+i]&optic.CMISTuningNotAcceptedFlagTxMask != 0 + caps.InvalidChannelMaskTx[i] = dumpBin[0xEF+i]&optic.CMISInvalidChannelNumberFlagTxMask != 0 + caps.WavelengthUnlockedMaskTx[i] = dumpBin[0xEF+i]&optic.CMISWavelengthUnlockedFlagTxMask != 0 + caps.TuningCompleteMaskTx[i] = dumpBin[0xEF+i]&optic.CMISTuningCompleteFlagTxMask != 0 + } + + return state, nil +} + +func SetTunableLaserControlStatus( + state *optic.ModuleState, + caps *optic.CMISBankedTunableLaserControlAndStatus, + bank byte, + maxN int, +) (*optic.ModuleState, error) { + dumpBin, err := state.GetPageBin(TunableLaserControlStatusPage, bank) + if err != nil { + return nil, err + } + + for i := 0; i <= maxN; i += 1 { + // rewrite to mem + dumpBin[0x80+i] = (caps.GridSpacingTx[i] & optic.CMISGridSpacingTxMask) | + (util.YesNoByte(caps.FineTuningEnableTx[i]) & optic.CMISFineTuningEnableTxMask) + + util.WriteBeInt16(caps.ChannelNumberTx[i], dumpBin, 0x88+byte(2*i)) + util.WriteBeInt16(caps.FineTuningOffsetMhzTx[i], dumpBin, 0x98+byte(2*i)) + // no write for CurrentLaserFrequency, read-only + + util.WriteBeInt16(caps.TargetOutputPowerTx[i], dumpBin, 0xC8+byte(2*i)) + // subsequent fields read-only + + dumpBin[0xEF+i] = (util.YesNoByte(caps.TargetOutputPowerOORMaskTx[i]) & optic.CMISTargetOutputPowerOORFlagTxMask) | + (util.YesNoByte(caps.FineTuningPowerOutOfRangeMaskTx[i]) & optic.CMISFineTuningOutOfRangeFlagTxMask) | + (util.YesNoByte(caps.TuningNotAcceptedMaskTx[i]) & optic.CMISTuningNotAcceptedFlagTxMask) | + (util.YesNoByte(caps.InvalidChannelMaskTx[i]) & optic.CMISInvalidChannelNumberFlagTxMask) | + (util.YesNoByte(caps.WavelengthUnlockedMaskTx[i]) & optic.CMISWavelengthUnlockedFlagTxMask) | + (util.YesNoByte(caps.TuningCompleteMaskTx[i]) & optic.CMISTuningCompleteFlagTxMask) + } + + err = state.WritePageBin(TunableLaserControlStatusPage, bank, dumpBin) + if err != nil { + return nil, err + } + + return state, nil +} + +func GetLaserCapabilitiesAdvertising( + state *optic.ModuleState, + caps *optic.CMISLaserCapabilitiesAdvertising, +) (*optic.ModuleState, error) { + dumpBin, err := state.GetPageBin(0x04, 0x00) // non-banked + if err != nil { + return nil, err + } + + caps.SupportedGridSpacings = make(map[string]bool) + + // I really hate that Go doesn't have bitfields. + caps.SupportedGridSpacings[optic.Grid75string] = dumpBin[0x80]&GridSupported75GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid33String] = dumpBin[0x80]&GridSupported33GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid100String] = dumpBin[0x80]&GridSupported100GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid50String] = dumpBin[0x80]&GridSupported50GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid25String] = dumpBin[0x80]&GridSupported25GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid12p5String] = dumpBin[0x80]&GridSupported12p5GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid6p250String] = dumpBin[0x80]&GridSupported6p25GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid3p125String] = dumpBin[0x80]&GridSupported3p125GhzMask != 0 + caps.SupportedGridSpacings[optic.Grid150String] = dumpBin[0x81]&GridSupported150GhzMask != 0 + + caps.FineTuningSupported = dumpBin[0x81]&FineTuningSupportedMask != 0 + + caps.GridLowChannel = make(map[string]int16) + caps.GridHighChannel = make(map[string]int16) + + var base byte = 0x82 + // 3.125Ghz + caps.GridLowChannel[optic.Grid3p125String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid3p125String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 6.25Ghz + caps.GridLowChannel[optic.Grid6p250String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid6p250String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 12.5Ghz + caps.GridLowChannel[optic.Grid12p5String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid12p5String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 25Ghz + caps.GridLowChannel[optic.Grid25String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid25String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 50Ghz + caps.GridLowChannel[optic.Grid50String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid50String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 100Ghz + caps.GridLowChannel[optic.Grid100String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid100String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 33Ghz + caps.GridLowChannel[optic.Grid33String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid33String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 75Ghz + caps.GridLowChannel[optic.Grid75string] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid75string] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + // 150Ghz + caps.GridLowChannel[optic.Grid150String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.GridHighChannel[optic.Grid150String] = util.ReadBeInt16AndShiftBase(dumpBin, &base) + + base = 0xBE // skip reserved region + caps.FineTuningResolution = util.ReadBeUint16AndShiftBase(dumpBin, &base) + caps.FineTuningLowOffset = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.FineTuningHighOffset = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.ProgOutputPowerPerLaneSupported = dumpBin[base]&ProgOutputPowerPerLaneSupported != 0 + + base = 0xC6 // skip reserved region + caps.ProgOutputPowerMin = util.ReadBeInt16AndShiftBase(dumpBin, &base) + caps.ProgOutputPowerMax = util.ReadBeInt16AndShiftBase(dumpBin, &base) + + // TODO checksum support + return state, nil +} diff --git a/internal/optic/cmis/constants.go b/internal/optic/cmis/constants.go new file mode 100644 index 0000000..06a56df --- /dev/null +++ b/internal/optic/cmis/constants.go @@ -0,0 +1,143 @@ +package cmis + +const ( + BankSelectRegisterAddress = 0x7E + PageSelectRegisterAddress = 0x7F +) + +const ( + GridSupported75GhzMask = 0b1000_0000 >> iota + GridSupported33GhzMask + GridSupported100GhzMask + GridSupported50GhzMask + GridSupported25GhzMask + GridSupported12p5GhzMask + GridSupported6p25GhzMask + GridSupported3p125GhzMask +) +const ( + FineTuningSupportedMask = 0b1000_0000 >> iota + GridSupported150GhzMask +) + +const ( + SoftwareResetMask = 0b0000_1000 + LowPwrRequestMask = 0b0001_0000 + ProgOutputPowerPerLaneSupported = 0b1000_0000 + MemoryModelMask = 0b1000_0000 + SteppedConfigOnlyMask = 0b0100_0000 + I2CMciMaxSpeedMask = 0b0011_0000 + SPIMCIMaxSpeedMask = 0b0000_1100 + AutoCommissioningMask = 0b0000_0011 + ModulePowerClassMask = 0b1110_0000 + FarEndConfigurationMask = 0b0001_0000 + BanksSupportedMask = 0b0000_0011 + ExtraLaneBanksSupportedMask = 0b0001_1111 +) + +const ( + WavelengthIsControllableMask = 0b1000_0000 + TransmitterIsTunableMask = 0b0100_0000 + SquelchMethodTxMask = 0b0011_0000 + ForcedSquelchTxSupportedMask = 0b0000_1000 + AutoSquelchDisableTxSupportedMask = 0b0000_0100 + OutputDisableTxSupportedMask = 0b0000_0010 + InputPolarityFlipTxSupportedMask = 0b0000_0001 + BankBroadcastSupportedMask = 0b1000_0000 + AutoSquelchDisableRxSupportedMask = 0b0000_0100 + OutputDisableRxSupportedMask = 0b0000_0010 + OutputPolarityFlipRxSupportedMask = 0b0000_0001 +) + +const TunableLaserControlStatusPage = 0x12 +const MaximumLaneNumber = 7 + +var I2CMciMaxSpeedToKhz = map[byte]int{ + 0x00: 400, + 0x10: 1_000, + 0x20: 3_400, + 0x30: 0, // reserved onwards + 0x40: 0, + 0x50: 0, + 0x60: 0, + 0x70: 0, + 0x80: 0, + 0x90: 0, + 0xA0: 0, + 0xB0: 0, + 0xC0: 0, + 0xD0: 0, + 0xE0: 0, + 0xF0: 0, +} + +var SPIMciMaxSpeedToKhz = map[byte]int{ + 0x00: 1_000, + 0x01: 2_000, + 0x02: 4_000, + 0x03: 8_000, + 0x04: 12_000, + 0x05: 16_000, + 0x06: 20_000, + 0x07: 30_000, + 0x08: 40_000, + 0x09: 50_000, + 0x0A: 0, // reserved onwards + 0x0B: 0, + 0x0C: 0, + 0x0D: 0, + 0x0E: 0, + 0x0F: 0, +} + +var PowerClassToInt = map[byte]int{ + 0b0000_0000: 1, + 0b0010_0000: 2, + 0b0100_0000: 3, + 0b0110_0000: 4, + 0b1000_0000: 5, + 0b1010_0000: 6, + 0b1100_0000: 7, + 0b1110_0000: 8, +} + +var MediaInterfaceToStr = map[byte]string{ + 0x00: "850nm VCSEL", + 0x01: "1310nm VCSEL", + 0x02: "1550nm VCSEL", + 0x03: "1310nm FP", + 0x04: "1310nm DFB", + 0x05: "1550nm DFB", + 0x06: "1310nm EML", + 0x07: "1550nm EML", + 0x08: "Other", + 0x09: "1490nm DFB", + 0x0A: "Copper cable, passive, unequalized", + 0x0B: "Copper cable, passive, equalized", + 0x0C: "Copper cable with near and far end limiting active equalizers", + 0x0D: "Copper cable with end limiting active equalizers", + 0x0E: "Copper cable with near end limiting active equalizers", + 0x0F: "Copper cable with linear active equalizers", + 0x10: "C-band tunable laser", + 0x11: "L-band tunable laser", + 0x12: "Copper cable with near and far end linear active equalizers", + 0x13: "Copper cable with far end linear active equalizers", + 0x14: "Copper cable with near end linear active equalizers", +} + +const ( + BankSelectErrorString = "I could not write to bank select register, aborting program now." + PageSelectErrorString = "I could not write to page select register, aborting program now." + RegisterWriteErrorString = "I could not write to arbitrary register, aborting program now." + PageOrBankNotAvailableTemplateErrorString = "Module responded that page 0x%x with bank 0x%x is not available," + + " its likely I failed to correctly identify the module capabilities. Aborting program now." + NoStagedConfigSupportErrorString = "Module has signaled that staged config change is mandatory for any change, " + + "but I do not have this capability. Aborting program now." + FlatMemoryMapErrorString = "Module signals a flat memory map. Only lower memory and page 00 are available, " + + "and you are requesting pages above this. Aborting program now." + RefusalMajorTooHigh string = "This CMIS module has" + + " a Major Revision number over what I can speak " + + "therefore it is unsupported. Program will be terminated " + + "now, as I cannot read nor write to this module without " + + "potential failure, data loss and/or equipment damage." +) diff --git a/internal/optic/cmis/extension.go b/internal/optic/cmis/extension.go new file mode 100644 index 0000000..53d0bac --- /dev/null +++ b/internal/optic/cmis/extension.go @@ -0,0 +1,166 @@ +package cmis + +import ( + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic/util" +) + +type ExtensionManagementStrategy struct { + state *optic.ModuleState +} + +func NewCMISExtension(state *optic.ModuleState) *ExtensionManagementStrategy { + return &ExtensionManagementStrategy{ + state: state, + } +} + +func (e *ExtensionManagementStrategy) assumeHigherPagesReadable() { + if !e.state.CMIS.MemoryModelPaged { + panic(FlatMemoryMapErrorString) + } +} + +// assumeConfigChange ensures that module is placed in a config change state prior to write attempt. +// Requires that administrative information has already been obtained prior to write attempt +func (e *ExtensionManagementStrategy) assumeConfigChange() { + e.assumeHigherPagesReadable() + if e.state.CMIS.SteppedConfigOnly { + panic(NoStagedConfigSupportErrorString) + } +} + +func (e *ExtensionManagementStrategy) GetExtensionState() (*optic.ModuleState, error) { + _, err := e.GetSupportedControlsAdvertising() + if err != nil { + return nil, err + } + _, err = e.GetLaserCapabilitiesAdvertising() + if err != nil { + return nil, err + } + _, err = e.GetTunableLaserControlStatus() + if err != nil { + return nil, err + } + + return e.state, nil +} + +func (e *ExtensionManagementStrategy) SetExtensionState() (*optic.ModuleState, error) { + e.assumeConfigChange() + + _, err := e.SetTunableLaserControlStatus(e.state) + if err != nil { + return nil, err + } + + return e.state, nil +} + +func (e *ExtensionManagementStrategy) ManufacturerIsCompatibleWithProtocolExtension(manufacturer string) bool { + return true // manufacturer names have 0 influence, only sff8024 does. so we defer decision +} + +func (e *ExtensionManagementStrategy) SFF8024IsCompatibleWithProtocolExtension( + sff8024Identifier byte, + sff8024Revision byte, +) bool { + return checkSFF8024(sff8024Identifier, sff8024Revision) +} + +func (e *ExtensionManagementStrategy) Activate() (*optic.ModuleState, error) { + e.state.CMIS.Active = true + return e.state, nil +} + +func (e *ExtensionManagementStrategy) GetTunableLaserControlStatus() (*optic.ModuleState, error) { + e.assumeHigherPagesReadable() + + var bank byte + e.state.CMIS.TunableLaser.CtrlStatus = nil + for bank = 0x00; bank <= e.state.CMIS.SupportedControls.MaximumBankSupported; bank += 1 { + e.state.CMIS.TunableLaser.CtrlStatus = append( + e.state.CMIS.TunableLaser.CtrlStatus, + optic.CMISBankedTunableLaserControlAndStatus{}, + ) // adding banks on the go to avoid having max banks all the time + _, err := GetTunableLaserControlStatus( + e.state, &e.state.CMIS.TunableLaser.CtrlStatus[bank], bank, MaximumLaneNumber, + ) + if err != nil { + return nil, err + } + } + + return e.state, nil +} + +func (e *ExtensionManagementStrategy) SetTunableLaserControlStatus(s *optic.ModuleState) (*optic.ModuleState, error) { + e.assumeHigherPagesReadable() + + for i, bank := range e.state.CMIS.TunableLaser.CtrlStatus { + _, err := SetTunableLaserControlStatus(e.state, &bank, byte(i), MaximumLaneNumber) + if err != nil { + return nil, err + } + } + + return e.state, nil +} + +func (e *ExtensionManagementStrategy) GetLaserCapabilitiesAdvertising() (*optic.ModuleState, error) { + e.assumeHigherPagesReadable() + + _, err := GetLaserCapabilitiesAdvertising( + e.state, &e.state.CMIS.TunableLaser.Capabilities, + ) + if err != nil { + return nil, err + } + + return e.state, nil +} + +func (e *ExtensionManagementStrategy) GetSupportedControlsAdvertising() (*optic.ModuleState, error) { + e.assumeHigherPagesReadable() + + dumpBin, err := e.state.GetPageBin(0x01, 0x00) + if err != nil { + return nil, err + } + + caps := &e.state.CMIS.SupportedControls + + caps.ModuleInactiveFirmwareMajorRevision = dumpBin[0x80] + caps.ModuleInactiveFirmwareMinorRevision = dumpBin[0x81] + caps.ModuleHardwareMajorRevision = dumpBin[0x82] + caps.ModuleHardwareMinorRevision = dumpBin[0x83] + + // skipping supported lengths + + // only defined for SINGLE WAVELENGTH modules + var base byte = 0x8A + caps.NominalWavelengthNm = 0.05 * float64(util.ReadBeUint16AndShiftBase(dumpBin, &base)) + caps.WavelengthToleranceNm = 0.005 * float64(util.ReadBeUint16AndShiftBase(dumpBin, &base)) + + // skipping network / vdm / diag / coherent / cmisff / 03h + caps.MaximumBankSupported = dumpBin[0x8E] & BanksSupportedMask + if caps.MaximumBankSupported == 0x03 { // signals lane banked + caps.MaximumBankSupported = dumpBin[0xAE] & ExtraLaneBanksSupportedMask + } + + // go should have bitfields + caps.WavelengthIsControllable = dumpBin[0x9B]&WavelengthIsControllableMask != 0 + caps.TransmitterIsTunable = dumpBin[0x9B]&TransmitterIsTunableMask != 0 + caps.SquelchMethodTx = dumpBin[0x9B] & SquelchMethodTxMask + caps.ForcedSquelchTxSupported = dumpBin[0x9B]&ForcedSquelchTxSupportedMask != 0 + caps.AutoSquelchDisableTxSupported = dumpBin[0x9B]&AutoSquelchDisableTxSupportedMask != 0 + caps.OutputDisableTxSupported = dumpBin[0x9B]&OutputDisableTxSupportedMask != 0 + caps.InputPolarityFlipTxSupported = dumpBin[0x9B]&InputPolarityFlipTxSupportedMask != 0 + caps.BankBroadcastSupported = dumpBin[0x9C]&BankBroadcastSupportedMask != 0 + caps.AutoSquelchDisableRxSupported = dumpBin[0x9C]&AutoSquelchDisableRxSupportedMask != 0 + caps.OutputDisableRxSupported = dumpBin[0x9C]&OutputDisableRxSupportedMask != 0 + caps.OutputPolarityFlipRxSupported = dumpBin[0x9C]&OutputPolarityFlipRxSupportedMask != 0 + + return e.state, nil +} diff --git a/internal/optic/default/default.go b/internal/optic/default/default.go new file mode 100644 index 0000000..79a5bdb --- /dev/null +++ b/internal/optic/default/default.go @@ -0,0 +1,71 @@ +package _default + +import ( + "github.com/wobcom/rtbrick-optic-programmer/internal" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" +) + +// ManagementStrategy is the default Management strategy which should be set at initialization +// it can only read page00, and does 0 write. It can tell which concrete strategy which you should +// be using by looking at identifier codes and constructor ASCII +type ManagementStrategy struct { + state *optic.ModuleState +} + +func New(state *optic.ModuleState) *ManagementStrategy { + return &ManagementStrategy{ + state: state, + } +} + +func (d *ManagementStrategy) AcceptsSFF8024(_ byte, _ byte) bool { + return true +} + +func (d *ManagementStrategy) GetPageBin(_ byte, _ byte) ([]byte, error) { + // we only care about lower mem, we can get it from whatever state + // the module currently is in + handle := d.state.GetHandle() + pageStr, err := handle.Connection.GetI2CDump(handle.I2cBusId) + if err != nil { + return []byte{}, err + } + return internal.ParseI2CDump(*pageStr), nil +} + +func (d *ManagementStrategy) WritePageByteBin(_ byte, _ byte, _ byte, _ byte) error { + panic(genericWriteErrorString) +} + +func (d *ManagementStrategy) Set() (*optic.ModuleState, error) { + panic(genericWriteErrorString) +} + +func (d *ManagementStrategy) Get() (*optic.ModuleState, error) { + panic(genericReadErrorString) +} + +func (d *ManagementStrategy) GetAdministrativeInformation() (*optic.ModuleState, error) { + bin, err := d.state.GetPageBin(0x00, 0) + if err != nil { + return nil, err + } + + d.state.SFF8024Identifier = bin[0x00] + d.state.SFF8024Revision = bin[0x01] + + return d.state, nil +} + +func (d *ManagementStrategy) SetAdministrativeInformation() (*optic.ModuleState, error) { + panic(genericWriteErrorString) +} + +var genericWriteErrorString = "Module type is unknown, therefore I cannot go on, " + + "and refuse to set any value as it might have undesired effect. " + + "Program will be terminated now, as it is likely I failed to detect the Module Type." + +var genericReadErrorString = "Module type is unknown, therefore I cannot go on, " + + "as you require me making sense of things I do not know the shape, " + + "nor give an absolute and complete answer, for I do not understand all that is before me. " + + "Program will be terminated now, as it is likely I failed to detect the Module Type." diff --git a/internal/optic/optic.go b/internal/optic/optic.go new file mode 100644 index 0000000..44ed04d --- /dev/null +++ b/internal/optic/optic.go @@ -0,0 +1,449 @@ +package optic + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/wobcom/rtbrick-optic-programmer/internal/optic/util" + "github.com/wobcom/rtbrick-optic-programmer/internal/rtbrick/ssh" +) + +const ( + Grid75string = "75.000" + Grid33String = "33.000" + Grid100String = "100.000" + Grid50String = "50.000" + Grid25String = "25.000" + Grid12p5String = "12.500" + Grid6p250String = "6.250" + Grid3p125String = "3.125" + Grid150String = "150.000" +) + +const ( + CMISGridSpacing3p125Ghz = 0b0000_0000 + CMISGridSpacing6p25Ghz = 0b0001_0000 + CMISGridSpacing12p5Ghz = 0b0010_0000 + CMISGridSpacing25Ghz = 0b0011_0000 + CMISGridSpacing50Ghz = 0b0100_0000 + CMISGridSpacing100Ghz = 0b0101_0000 + CMISGridSpacing33Ghz = 0b0110_0000 + CMISGridSpacing75Ghz = 0b0111_0000 + CMISGridSpacing150Ghz = 0b1000_0000 + // CMISGridSpacingNotAvailable = 0b1111_0000 +) + +var CMISGridSpacingToFloatGhzMap = map[byte]float64{ + CMISGridSpacing3p125Ghz: 3.125, + CMISGridSpacing6p25Ghz: 6.25, + CMISGridSpacing12p5Ghz: 12.5, + CMISGridSpacing25Ghz: 25.0, + CMISGridSpacing50Ghz: 50.0, + CMISGridSpacing100Ghz: 100.0, + CMISGridSpacing33Ghz: 33.0, + CMISGridSpacing75Ghz: 75.0, + CMISGridSpacing150Ghz: 150.0, +} + +var FloatGhzToCMISGridSpacing = map[string]byte{ // cannot use float as key since not serializable + Grid3p125String: CMISGridSpacing3p125Ghz, + Grid6p250String: CMISGridSpacing6p25Ghz, + Grid12p5String: CMISGridSpacing12p5Ghz, + Grid25String: CMISGridSpacing25Ghz, + Grid50String: CMISGridSpacing50Ghz, + Grid100String: CMISGridSpacing100Ghz, + Grid33String: CMISGridSpacing33Ghz, + Grid75string: CMISGridSpacing75Ghz, + Grid150String: CMISGridSpacing150Ghz, +} + +const ( + CMISGridSpacingTxMask = 0b1111_0000 + CMISFineTuningEnableTxMask = 0b0000_0001 + CMISTuningInProgressTxMask = 0b0000_0010 + CMISWavelengthUnlockStatusTxMask = 0b0000_0001 + + CMISTargetOutputPowerOORFlagTxMask = 0b0010_0000 + CMISFineTuningOutOfRangeFlagTxMask = 0b0001_0000 + CMISTuningNotAcceptedFlagTxMask = 0b0000_1000 + CMISInvalidChannelNumberFlagTxMask = 0b0000_0100 + CMISWavelengthUnlockedFlagTxMask = 0b0000_0010 + CMISTuningCompleteFlagTxMask = 0b0000_0001 +) + +// FinIsarCMISExtension client r/w interface for FinIsar specific settings +// delegates concrete operations to strategy +type FinIsarCMISExtension struct { + Active bool `json:"active"` +} + +// CommonTunableLaserFields is the common interface for tunable lasers, +type CommonTunableLaserFields struct { + Capabilities CMISLaserCapabilitiesAdvertising `json:"capabilities,omitzero"` + CtrlStatus []CMISBankedTunableLaserControlAndStatus `json:"control_status,omitzero"` +} + +// FlexOptixSFF8636Extension client r/w interface for FlexOptix specific settings +// delegates concrete operations to strategy +type FlexOptixSFF8636Extension struct { + Active bool `json:"active"` + + // FlexOptix has page 04h and 12h copied from CMIS + TunableLaser CommonTunableLaserFields `json:"tunable_laser"` +} + +// CMISOnlyExtension CMIS specific information +type CMISOnlyExtension struct { + Active bool `json:"active"` + + // lower mem + MemoryModelPaged bool `json:"memory_model_paged"` // MemoryModelPaged is true if paged, false if flat (flat is lower + page 0x00 only) + SteppedConfigOnly bool `json:"stepped_config_only"` // SteppedConfigOnly true if all types of reconfiguration (step by step hot + regular), false if step by step / none autocomm. + I2CMciMaxSpeedKhz int `json:"i2c_mci_max_speed_khz"` // I2CMciMaxSpeedKhz Maximum I2C MCI interface speed in Khz + SPIMciMaxSpeedKhz int `json:"spi_mci_max_speed_khz"` // SPIMciMaxSpeedKhz Maximum MCI SPI interface speed in Khz + AutoCommissioningNone bool `json:"auto_commissioning_none"` // AutoCommissioningNone true if no auto-commissioning is supported + AutoCommissioningRegular bool `json:"auto_commissioning_regular"` // AutoCommissioningRegular true if regular auto-commissioning is supported (Affects ApplyDPInit) + AutoCommissioningHot bool `json:"auto_commissioning_hot"` // AutoCommissioningHot true if only hot auto-commissioning is supported (Affects ApplyImmediate) + + // page 00, all + VendorOUI []byte `json:"vendor_oui"` + DateCode string `json:"date_code"` + CLEICode string `json:"clei_code"` + PowerClass int `json:"power_class"` + MaxPowerWatts float64 `json:"max_power_watts"` // MaxPowerWatts is in multiples of 0.25W, ceil. + + // page 00, copper and active + // CableAssemblyLengthMeters uint + // ConnectorType byte + // AttenuationAt5Ghz uint8 + // AttenuationAt7Ghz uint8 + // AttenuationAt12p9Ghz uint8 + // AttenuationAt25p8Ghz uint8 + // AttenuationAt53p1Ghz uint8 + + // page 00, cont. + SupportedMediaLanes map[int]bool `json:"supported_media_lanes"` + FarEndDetachableMedia bool `json:"far_end_detachable_media"` + FarEnd1LaneModule bool `json:"far_end_1_lane_module"` + FarEnd2LanesModule bool `json:"far_end_2_lanes_module"` + FarEnd4LanesModule bool `json:"far_end_4_lanes_module"` + FarEnd8LanesModule bool `json:"far_end_8_lanes_module"` + FarEnd16LanesModule bool `json:"far_end_16_lanes_module"` + MediaInterfaceDescription string `json:"media_interface_description"` + + // page 01, optional + SupportedControls CMISSupportedControlsAdvertising `json:"supported_controls,omitzero"` + + // supported flags tbd. + // supported monitors tbd. + // supported configuration and signal integrity controls adv tbd. + // CDB messaging support advertisement tbd. + // additional durations adv tbd. + // host lane polarity inversion adv tbd. + // supported pages and banks adv tbd. + // NAD banks + media lane assignment + additional app adv tbd. + // misc feature adv tbd. + + // page 12 + TunableLaser CommonTunableLaserFields `json:"tunable_laser"` +} + +type CMISSupportedControlsAdvertising struct { + ModuleInactiveFirmwareMajorRevision uint8 `json:"module_inactive_firmware_major_revision"` + ModuleInactiveFirmwareMinorRevision uint8 `json:"module_inactive_firmware_minor_revision"` + ModuleHardwareMajorRevision uint8 `json:"module_hardware_major_revision"` + ModuleHardwareMinorRevision uint8 `json:"module_hardware_minor_revision"` + // link length support tbd. + NominalWavelengthNm float64 `json:"nominal_wavelength_nm"` // NominalWavelengthNm at room temp. + WavelengthToleranceNm float64 `json:"wavelength_tolerance_nm"` // WavelengthToleranceNm worst case tolerance around nominal + MaximumBankSupported byte `json:"maximum_bank_supported"` // MaximumBankSupported upper bank limit (0, 0-1, 0-3 for 8, 16, 32 lanes respectively) + // module characteristics adv. tbd. + WavelengthIsControllable bool `json:"wavelength_is_controllable"` + TransmitterIsTunable bool `json:"transmitter_is_tunable"` + SquelchMethodTx byte `json:"squelch_method_tx"` + ForcedSquelchTxSupported bool `json:"forced_squelch_tx_supported"` + AutoSquelchDisableTxSupported bool `json:"auto_squelch_disable_tx_supported"` + AutoSquelchDisableRxSupported bool `json:"auto_squelch_disable_rx_supported"` + OutputDisableTxSupported bool `json:"output_disable_tx_supported"` + OutputDisableRxSupported bool `json:"output_disable_rx_supported"` + InputPolarityFlipTxSupported bool `json:"input_polarity_flip_tx_supported"` + OutputPolarityFlipRxSupported bool `json:"output_polarity_flip_rx_supported"` + BankBroadcastSupported bool `json:"bank_broadcast_supported"` +} + +type CMISLaserCapabilitiesAdvertising struct { + // page 04h laser capabilities adv + // is channel-based grid tuning supported on these frequencies + SupportedGridSpacings map[string]bool `json:"supported_grid_spacings"` + + // is fine-tuning supported in the vicinity of an on-grid channel + FineTuningSupported bool `json:"fine_tuning_supported"` + + // S16 encoded lowest N for spacing for each freq + GridLowChannel map[string]int16 `json:"grid_low_channel"` + + // S16 encoded higher N for spacing for each freq + GridHighChannel map[string]int16 `json:"grid_high_channel"` + + // fine-tuning res, 0.001 Ghz increments + FineTuningResolution uint16 `json:"fine_tuning_resolution"` + FineTuningLowOffset int16 `json:"fine_tuning_low_offset"` + FineTuningHighOffset int16 `json:"fine_tuning_high_offset"` + + // programmable output power + ProgOutputPowerPerLaneSupported bool `json:"prog_output_power_per_lane_supported"` // per-lane programmable y/n + ProgOutputPowerMin int16 `json:"prog_output_power_min"` // 0.001 dBm increments min power + ProgOutputPowerMax int16 `json:"prog_output_power_max"` // 0.001 dBm increments max power +} + +type CMISBankedTunableLaserControlAndStatus struct { + // page 12h tunable laser control and status + + // Grid + GridSpacingTx [8]byte `json:"-"` // selected grid spacing of media lanes 1-8 OF BANK + GridSpacingTxROGhz [8]float64 `json:"grid_spacing_tx"` // read only float64 ghz value + FineTuningEnableTx [8]bool `json:"fine_tuning_enable_tx"` // for each lane + + // tuning and status + ChannelNumberTx [8]int16 `json:"channel_number_tx"` // S16 selected N - channel number for media lane 1-8 OF BANK + FineTuningOffsetMhzTx [8]int16 `json:"fine_tuning_offset_mhz_tx"` // S16 fine-tuning frequency offset for media lane 1-8 OF BANK in offsets of 0.001 Ghz + CurrentLaserFrequencyMhzTx [8]uint32 `json:"current_laser_frequency_mhz_tx"` // U32 current frequency for media lane 1-8 OF BANK in units of 0.001 Ghz + + // power + TargetOutputPowerTx [8]int16 `json:"target_output_power_tx"` // s16 programmable output power for all media lanes IN BANK units of 0.01dBm + + // lock status + TuningInProgressTx [8]bool `json:"tuning_in_progress_tx"` // whether tuning is in progress on all media lanes IN BANK + WaveLengthUnlockStatus [8]bool `json:"wave_length_unlock_status"` // unlocked status indication for laser on all media lanes IN BANK + + // latched flags, cleared on module read + LaserTuningFlagSummaryTx [8]bool `json:"laser_tuning_flag_summary_tx"` + TargetOutputPowerOORFlagTx [8]bool `json:"target_output_power_oor_flag_tx"` // indicates whether target output power value was entered for media lane + FineTuningOutOfRangeFlagTx [8]bool `json:"fine_tuning_out_of_range_flag_tx"` // indicates whether fine-tuning target value was outside range + TuningNotAcceptedFlagTx [8]bool `json:"tuning_not_accepted_flag_tx"` // indicates a failed tuning operation for media lane + InvalidChannelNumberFlagTx [8]bool `json:"invalid_channel_number_flag_tx"` // required channel number not in advertised range of spacing + WavelengthUnlockedFlagTx [8]bool `json:"wavelength_unlocked_flag_tx"` + TuningCompleteFlagTx [8]bool `json:"tuning_complete_flag_tx"` // tuning has been completed y/n + + // masks for interrupt generation suppression + TargetOutputPowerOORMaskTx [8]bool `json:"target_output_power_oor_mask_tx"` + FineTuningPowerOutOfRangeMaskTx [8]bool `json:"fine_tuning_power_out_of_range_mask_tx"` + TuningNotAcceptedMaskTx [8]bool `json:"tuning_not_accepted_mask_tx"` + InvalidChannelMaskTx [8]bool `json:"invalid_channel_mask_tx"` + WavelengthUnlockedMaskTx [8]bool `json:"wavelength_unlocked_mask_tx"` + TuningCompleteMaskTx [8]bool `json:"tuning_complete_mask_tx"` +} + +// SFF8636OnlyExtension SFF8636 specific information +type SFF8636OnlyExtension struct { + Active bool `json:"active"` + + // lower mem + EnableHighPowerClass8 bool `json:"enable_high_power_class_8"` + EnableHighPowerClass57 bool `json:"enable_high_power_class_57"` + LowPwrOverride bool `json:"low_pwr_override"` +} + +// ModuleState is data exchange interface - delegates to strategy +// client can get / set freely from fields - not linked to pages or concrete implementation +// manufacturer specific fields can be read / set if struct is present, only one manufacturer +// will be present at a time +type ModuleState struct { + mgmtProtoConcreteStrategy ConcreteManagementStrategy // private pointer to concrete Strategy + mgmtProtoExtensionsConcreteStrategies []ConcreteExtensionManagementStrategy + handle *connection.I2cRWHandle // private pointer to connection handle. + + FinIsarCMIS FinIsarCMISExtension `json:"finisar_cmis,omitzero"` + FlexOptixSFF8636 FlexOptixSFF8636Extension `json:"flexoptix_sff8636,omitzero"` + SFF8636 SFF8636OnlyExtension `json:"sff8636,omitzero"` + CMIS CMISOnlyExtension `json:"cmis,omitzero"` + + // lower mem region + ManagementProtocol string `json:"management_protocol"` + SFF8024Identifier uint8 `json:"sff_8024_identifier"` // SFF8024Identifier lower mem public read-only sff8024 id field + SFF8024Revision uint8 `json:"sff_8024_revision"` // SFF8024Revision lower mem public read-only sff8024 revision id field + LowPwrRequestSW bool `json:"low_pwr_request_sw"` + SoftwareReset bool `json:"software_reset"` + + // page 00 region, common info between + // CMIS and SFF8636 but addresses differ + VendorName string `json:"vendor_name"` + VendorPartNumber string `json:"vendor_part_number"` + VendorPartRevision string `json:"vendor_part_revision"` + VendorSerialNumber string `json:"vendor_serial_number"` +} + +func NewModuleState( + withDefaultStrategyFactory func(state *ModuleState) ConcreteManagementStrategy, + withConcreteStrategiesFactories []func(state *ModuleState) ConcreteManagementStrategy, + withConcreteExtensionStrategiesFactories []func(state *ModuleState) ConcreteExtensionManagementStrategy, + withHandle *connection.I2cRWHandle) *ModuleState { + m := &ModuleState{ + handle: withHandle, + ManagementProtocol: "unknown", + } + m.mgmtProtoConcreteStrategy = withDefaultStrategyFactory(m) // self-referential + + m, err := m.GetAdministrativeInformation() + if err != nil { + panic("Failed to query module for basic administrative information") + } + + for _, s := range withConcreteStrategiesFactories { + if s(m).AcceptsSFF8024(m.SFF8024Identifier, m.SFF8024Revision) { + m.mgmtProtoConcreteStrategy = s(m) + } + } + + if reflect.TypeOf(m.mgmtProtoConcreteStrategy) == reflect.TypeOf(withDefaultStrategyFactory(m)) { + panic(fmt.Sprintf("Unknown SFF8024 Management interface type %x", m.SFF8024Identifier)) + } + + // re-query for more advanced admin info + // once we have the concrete management strategy confirmed + m, err = m.GetAdministrativeInformation() + if err != nil { + panic("Failed to query module for basic administrative information") + } + + for _, s := range withConcreteExtensionStrategiesFactories { + extensionStrategy := s(m) + if extensionStrategy.SFF8024IsCompatibleWithProtocolExtension(m.SFF8024Identifier, m.SFF8024Revision) && + extensionStrategy.ManufacturerIsCompatibleWithProtocolExtension(m.VendorName) { + _, err := extensionStrategy.Activate() + if err != nil { + panic("failed to activate protocol extension") + } + m.mgmtProtoExtensionsConcreteStrategies = append( + m.mgmtProtoExtensionsConcreteStrategies, + extensionStrategy, + ) + } + } + + return m +} + +func (m *ModuleState) GetHandle() *connection.I2cRWHandle { + return m.handle +} + +func (m *ModuleState) GetPageBin(page byte, bank byte) ([]byte, error) { + return m.mgmtProtoConcreteStrategy.GetPageBin(page, bank) +} + +func (m *ModuleState) WritePageByteBin(page byte, bank byte, offset byte, value byte) error { + return m.mgmtProtoConcreteStrategy.WritePageByteBin(page, bank, offset, value) +} + +func (m *ModuleState) WritePageBin(page byte, bank byte, new []byte) error { + old, err := m.GetPageBin(page, bank) + if err != nil { + return err + } + + for offset, newValue := range util.BinDiffIterator(old, new) { + err := m.WritePageByteBin(page, bank, byte(offset), newValue) + if err != nil { + return err + } + } + + return nil +} + +func (m *ModuleState) ToJson() ([]byte, error) { + marshal, err := json.MarshalIndent(m, "", "\t") + if err != nil { + return nil, err + } + + return marshal, nil +} + +func (m *ModuleState) Set() (*ModuleState, error) { + return m.mgmtProtoConcreteStrategy.Set() +} + +func (m *ModuleState) Get() (*ModuleState, error) { + _, err := m.mgmtProtoConcreteStrategy.Get() + if err != nil { + return nil, err + } + return m, nil +} + +func (m *ModuleState) GetAdministrativeInformation() (*ModuleState, error) { + return m.mgmtProtoConcreteStrategy.GetAdministrativeInformation() +} + +func (m *ModuleState) SetAdministrativeInformation() (*ModuleState, error) { + return m.mgmtProtoConcreteStrategy.SetAdministrativeInformation() +} + +func (m *ModuleState) SetExtensionsState() (*ModuleState, error) { + for _, e := range m.mgmtProtoExtensionsConcreteStrategies { + _, err := e.SetExtensionState() + if err != nil { + return nil, err + } + } + return m, nil +} + +func (m *ModuleState) GetExtensionsState() (*ModuleState, error) { + for _, e := range m.mgmtProtoExtensionsConcreteStrategies { // multiple extensions may be active + _, err := e.GetExtensionState() // force refresh + if err != nil { + return nil, err + } + } + return m, nil +} + +// Management is implemented by ModuleState by delegating to strategies which have concrete implementations +type Management interface { + GetPageBin(page byte, bank byte) ([]byte, error) // GetPageBin page is mandatory, bank is optional as it is only used in CMIS + WritePageByteBin(page byte, bank byte, offset byte, value byte) error // WritePageByteBin page is mandatory, bank is optional as it is only used in CMIS + Set() (*ModuleState, error) + Get() (*ModuleState, error) + GetAdministrativeInformation() (*ModuleState, error) + SetAdministrativeInformation() (*ModuleState, error) +} + +// ProtocolExtensionManagement is a generic interface for protocol extensions, +// be it protocol specific or manufacturer specific. I've avoided using Generics for +// the state struct and instead opted to use for an all-containing struct since there's +// no need for the added complexity of Generics in this case. +type ProtocolExtensionManagement interface { + GetExtensionState() (*ModuleState, error) + SetExtensionState() (*ModuleState, error) + Activate() (*ModuleState, error) +} + +// SFF8024Compatible to be implemented by concrete strategies +type SFF8024Compatible interface { + // AcceptsSFF8024 tells if the strategy is compatible with the sff8024 type + AcceptsSFF8024(sff8024Identifier byte, sff8024Revision byte) bool +} + +type ManufacturerIsCompatibleWithProtocolExtension interface { + ManufacturerIsCompatibleWithProtocolExtension(manufacturer string) bool +} + +type SFF8024IsCompatibleWithProtocolExtension interface { + SFF8024IsCompatibleWithProtocolExtension(sff8024Identifier byte, sff8024Revision byte) bool +} + +// ConcreteManagementStrategy should implement both Management and SFF8024Compatible interfaces +type ConcreteManagementStrategy interface { + Management + SFF8024Compatible +} + +type ConcreteExtensionManagementStrategy interface { + ProtocolExtensionManagement + ManufacturerIsCompatibleWithProtocolExtension + SFF8024IsCompatibleWithProtocolExtension +} diff --git a/internal/optic/sff8636/common.go b/internal/optic/sff8636/common.go new file mode 100644 index 0000000..5fb81a4 --- /dev/null +++ b/internal/optic/sff8636/common.go @@ -0,0 +1,35 @@ +package sff8636 + +import "slices" + +func checkSFF8024(sff8024Identifier byte, sff8024Revision byte) bool { + var SFF8636CompatibleSFF8024IDs = [...]byte{ + 0x0D, // qsfp+ or later with sff8646 or sff8436 mgmt interface + 0x11, // qsfp28 or later with sff-8636 management interface + } + + var CompatibleSFF8638Versions = [...]byte{ + // 0x00 not compatible, do not use for rev 2.5 or higher + // 0x01 sff 8436 not compatible + // 0x02 sff 8436 not compatible + 0x03, // 1.3 or earlier + 0x04, // 1.4 + 0x05, // 1.5 + 0x06, // 2.0 + 0x07, // 2.5, 2.6 and 2.7 + 0x08, // 2.8, 2.9 and 2.10 + 0x09, // 2.11 + 0x0A, // 2.12 + } + + compatibleIdentifier := func(id byte) bool { return slices.Contains(SFF8636CompatibleSFF8024IDs[:], id) } + compatibleRev := func(sff8024rev byte) bool { return slices.Contains(CompatibleSFF8638Versions[:], sff8024rev) } + + // panic if revision is unknown + if compatibleIdentifier(sff8024Identifier) && !compatibleRev(sff8024Revision) { + panic(RefusalVerMismatch) + } + + // tells if compatible + return compatibleIdentifier(sff8024Identifier) +} diff --git a/internal/optic/sff8636/constants.go b/internal/optic/sff8636/constants.go new file mode 100644 index 0000000..a00c24e --- /dev/null +++ b/internal/optic/sff8636/constants.go @@ -0,0 +1,22 @@ +package sff8636 + +const PageSelectRegisterAddress = 0x7F + +const ( + PageSelectRegisterWriteErrorString = "I could not write to Page Select register, aborting program now." + RegisterWriteErrorString = "I could not write to arbitrary register, aborting program now." + PageNotAvailableTemplateErrorString = "Module responded that page 0x%x is not available, its likely I failed to " + + "correctly identify the module capabilities. Aborting program now." + RefusalVerMismatch = "This module has an " + + "SFF8636 Revision number that I do not know of, therefore it is unsupported. Program will be terminated now, " + + "as I cannot read nor write to this module without potential failure, data loss and/or equipment damage." +) + +// page 00 register 0x5D +const ( + SoftwareResetMask = 0b1000_0000 + EnableHighPowerClass8Mask = 0b0000_1000 + EnableHighPowerClass57Mask = 0b0000_0100 + LowPwrRequestSWMask = 0b0000_0010 + LowPwrOverrideMask = 0b0000_0001 +) diff --git a/internal/optic/sff8636/extension.go b/internal/optic/sff8636/extension.go new file mode 100644 index 0000000..5a75298 --- /dev/null +++ b/internal/optic/sff8636/extension.go @@ -0,0 +1,41 @@ +package sff8636 + +import ( + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" +) + +type ExtensionManagementStrategy struct { + state *optic.ModuleState +} + +func NewSFF8636Extension(state *optic.ModuleState) *ExtensionManagementStrategy { + return &ExtensionManagementStrategy{ + state: state, + } +} + +func (s2 *ExtensionManagementStrategy) GetExtensionState() (*optic.ModuleState, error) { + // implement SFF8636 specific pages here if needed in the future + return s2.state, nil +} + +func (s2 *ExtensionManagementStrategy) SetExtensionState() (*optic.ModuleState, error) { + // implement SFF8636 specific pages here if needed in the future + return s2.state, nil +} + +func (s2 *ExtensionManagementStrategy) ManufacturerIsCompatibleWithProtocolExtension(_ string) bool { + return true // manufacturer names have 0 influence, only sff8024 does. so we defer decision +} + +func (s2 *ExtensionManagementStrategy) SFF8024IsCompatibleWithProtocolExtension( + sff8024Identifier byte, + sff8024Revision byte, +) bool { + return checkSFF8024(sff8024Identifier, sff8024Revision) +} + +func (s2 *ExtensionManagementStrategy) Activate() (*optic.ModuleState, error) { + s2.state.SFF8636.Active = true + return s2.state, nil +} diff --git a/internal/optic/sff8636/flexoptix.go b/internal/optic/sff8636/flexoptix.go new file mode 100644 index 0000000..f16c814 --- /dev/null +++ b/internal/optic/sff8636/flexoptix.go @@ -0,0 +1,67 @@ +package sff8636 + +import ( + "regexp" + + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic/cmis" +) + +type FlexOptixSFF8636ManagementStrategy struct { + state *optic.ModuleState +} + +var ValidFlexOptixVendorName = regexp.MustCompile(`(?i)^flexoptix$`) + +func NewFlexOptixSFF8636Extension(state *optic.ModuleState) *FlexOptixSFF8636ManagementStrategy { + return &FlexOptixSFF8636ManagementStrategy{ + state: state, + } +} + +func (f *FlexOptixSFF8636ManagementStrategy) GetExtensionState() (*optic.ModuleState, error) { + _, err := cmis.GetLaserCapabilitiesAdvertising(f.state, &f.state.FlexOptixSFF8636.TunableLaser.Capabilities) + if err != nil { + return nil, err + } + + f.state.FlexOptixSFF8636.TunableLaser.CtrlStatus = nil + f.state.FlexOptixSFF8636.TunableLaser.CtrlStatus = append( + f.state.FlexOptixSFF8636.TunableLaser.CtrlStatus, + optic.CMISBankedTunableLaserControlAndStatus{}, + ) + _, err = cmis.GetTunableLaserControlStatus( + f.state, &f.state.FlexOptixSFF8636.TunableLaser.CtrlStatus[0], 0x00, 0, + ) + if err != nil { + return nil, err + } + + return f.state, nil +} + +func (f *FlexOptixSFF8636ManagementStrategy) SetExtensionState() (*optic.ModuleState, error) { + // laser capabilities advertising is all-fields read-only so, no-op for this one + + _, err := cmis.SetTunableLaserControlStatus( + f.state, &f.state.FlexOptixSFF8636.TunableLaser.CtrlStatus[0], 0x00, 0, + ) + if err != nil { + return nil, err + } + + return f.state, nil +} + +func (f *FlexOptixSFF8636ManagementStrategy) ManufacturerIsCompatibleWithProtocolExtension(manufacturer string) bool { + return ValidFlexOptixVendorName.MatchString(manufacturer) +} + +func (f *FlexOptixSFF8636ManagementStrategy) SFF8024IsCompatibleWithProtocolExtension(sff8024Identifier byte, sff8024Revision byte) bool { + return checkSFF8024(sff8024Identifier, sff8024Revision) +} + +func (f *FlexOptixSFF8636ManagementStrategy) Activate() (*optic.ModuleState, error) { + f.state.FlexOptixSFF8636.Active = true + return f.state, nil +} diff --git a/internal/optic/sff8636/sff8636.go b/internal/optic/sff8636/sff8636.go new file mode 100644 index 0000000..54f8122 --- /dev/null +++ b/internal/optic/sff8636/sff8636.go @@ -0,0 +1,132 @@ +package sff8636 + +import ( + "fmt" + + "github.com/wobcom/rtbrick-optic-programmer/internal" + "github.com/wobcom/rtbrick-optic-programmer/internal/optic" + util2 "github.com/wobcom/rtbrick-optic-programmer/internal/optic/util" +) + +// ManagementStrategy SFF8636 is a concrete implementation of the Management interface for SFF8636 spec +type ManagementStrategy struct { + state *optic.ModuleState +} + +func New(state *optic.ModuleState) *ManagementStrategy { + return &ManagementStrategy{ + state: state, + } +} + +func (s2 *ManagementStrategy) GetPageBin(page byte, _ byte) ([]byte, error) { + handle := s2.state.GetHandle() + + // do raw Page Select write 1st + pageSelectErr := handle.Connection.DoI2CSet(handle.I2cBusId, PageSelectRegisterAddress, page) + if pageSelectErr != nil { + panic(PageSelectRegisterWriteErrorString) + } + + // then get page and decode + pageStr, err := handle.Connection.GetI2CDump(handle.I2cBusId) + if err != nil { + return []byte{}, err + } + dumpBin := internal.ParseI2CDump(*pageStr) + + // then check Page Select was authorized by reading back Page Select register + if dumpBin[PageSelectRegisterAddress] != page { + panic(fmt.Sprintf(PageNotAvailableTemplateErrorString, page)) + } + + // return if everything went O.K. + return dumpBin, nil +} + +func (s2 *ManagementStrategy) WritePageByteBin(page byte, _ byte, offset byte, value byte) error { + handle := s2.state.GetHandle() + + // do raw Page Select Write 1st + pageSelectErr := handle.Connection.DoI2CSet(handle.I2cBusId, PageSelectRegisterAddress, page) + if pageSelectErr != nil { + panic(PageSelectRegisterWriteErrorString) + } + + err := handle.Connection.DoI2CSet(handle.I2cBusId, (int)(offset), value) + if err != nil { + panic(RegisterWriteErrorString) + } + + return nil +} + +func (s2 *ManagementStrategy) AcceptsSFF8024(sff8024Identifier byte, sff8024Revision byte) bool { + return checkSFF8024(sff8024Identifier, sff8024Revision) +} + +func (s2 *ManagementStrategy) Set() (*optic.ModuleState, error) { + _, err := s2.SetAdministrativeInformation() + if err != nil { + return nil, err + } + return s2.state, nil // noop +} + +func (s2 *ManagementStrategy) Get() (*optic.ModuleState, error) { + _, err := s2.GetAdministrativeInformation() + if err != nil { + return nil, err + } + return s2.state, nil +} + +func (s2 *ManagementStrategy) GetAdministrativeInformation() (*optic.ModuleState, error) { + bin, err := s2.state.GetPageBin(0x00, 0) + if err != nil { + return nil, err + } + + s2.state.ManagementProtocol = "sff8636" + // lower mem + s2.state.SFF8024Identifier = bin[0x00] + s2.state.SFF8024Revision = bin[0x01] + s2.state.SoftwareReset = bin[0x5D]&SoftwareResetMask == SoftwareResetMask + s2.state.SFF8636.EnableHighPowerClass8 = bin[0x5D]&EnableHighPowerClass8Mask == EnableHighPowerClass8Mask + s2.state.SFF8636.EnableHighPowerClass57 = bin[0x5D]&EnableHighPowerClass57Mask == EnableHighPowerClass57Mask + s2.state.LowPwrRequestSW = bin[0x5D]&LowPwrRequestSWMask == LowPwrRequestSWMask + s2.state.SFF8636.LowPwrOverride = bin[0x5D]&LowPwrOverrideMask == LowPwrOverrideMask + + // page 0x00 + s2.state.VendorName = util2.ParseASCIIToString(bin[0x94:0xA3]) + s2.state.VendorPartNumber = util2.ParseASCIIToString(bin[0xA8:0xB7]) + s2.state.VendorPartRevision = util2.ParseASCIIToString(bin[0xA8:0xB7]) + s2.state.VendorSerialNumber = util2.ParseASCIIToString(bin[0xC4:0xD3]) + + return s2.state, nil +} + +func (s2 *ManagementStrategy) SetAdministrativeInformation() (*optic.ModuleState, error) { + bin, err := s2.state.GetPageBin(0x00, 0) + if err != nil { + return nil, err + } + + bin[0x5D] &^= LowPwrRequestSWMask | + SoftwareResetMask | + EnableHighPowerClass57Mask | + EnableHighPowerClass8Mask | + LowPwrOverrideMask // clear + bin[0x5D] |= LowPwrRequestSWMask & util2.YesNoByte(s2.state.LowPwrRequestSW) + bin[0x5D] |= SoftwareResetMask & util2.YesNoByte(s2.state.SoftwareReset) + bin[0x5D] |= LowPwrOverrideMask & util2.YesNoByte(s2.state.SFF8636.LowPwrOverride) + bin[0x5D] |= EnableHighPowerClass57Mask & util2.YesNoByte(s2.state.SFF8636.EnableHighPowerClass57) + bin[0x5D] |= EnableHighPowerClass8Mask & util2.YesNoByte(s2.state.SFF8636.EnableHighPowerClass8) + + err = s2.state.WritePageBin(0x00, 0, bin) + if err != nil { + return nil, err + } + + return s2.state, nil +} diff --git a/internal/optic/util/ascii.go b/internal/optic/util/ascii.go new file mode 100644 index 0000000..d6e80a4 --- /dev/null +++ b/internal/optic/util/ascii.go @@ -0,0 +1,11 @@ +package util + +import "strings" + +func ParseASCIIToString(part []byte) string { + var asciiString string + for _, code := range part { + asciiString += string(rune(code)) + } + return strings.TrimSpace(asciiString) +} diff --git a/internal/optic/util/bin.go b/internal/optic/util/bin.go new file mode 100644 index 0000000..2cb2fae --- /dev/null +++ b/internal/optic/util/bin.go @@ -0,0 +1,78 @@ +package util + +import ( + "encoding/binary" + "iter" + "math" +) + +func TwoComplement16(sizeBits uint8, data uint16) (v int16) { + n := float64(sizeBits - 1) + p := math.Pow(2, n) // 2^(N-1) + mask := uint16(p) + + tmp1 := -int16(data & mask) + tmp2 := int16(data & ^mask) + + v = tmp1 + tmp2 + return v +} + +func TwoComplement16Encode(data int16) []byte { + var buffer = []byte{0x00, 0x00} + binary.BigEndian.PutUint16(buffer, uint16(data)) // using int -> uint16 shortcut, thx go + + return buffer +} + +func ReadBeUint16(bin []byte, baseOffset byte) uint16 { + // CMIS states that the default endianness for numerical data types is big endian + // unless stated otherwise + return binary.BigEndian.Uint16(bin[baseOffset : baseOffset+0x02]) // go slices use half-open range, like this: [a,b[ +} + +func ReadBeUint32(bin []byte, baseOffset byte) uint32 { + return binary.BigEndian.Uint32(bin[baseOffset : baseOffset+0x04]) // go slices use half-open ranges +} + +func ReadBeInt16(bin []byte, baseOffset byte) int16 { + return TwoComplement16(16, ReadBeUint16(bin, baseOffset)) +} + +func WriteBeInt16(i int16, bin []byte, baseOffset byte) { + bin[baseOffset] = TwoComplement16Encode(i)[0] + bin[baseOffset+1] = TwoComplement16Encode(i)[1] +} + +func ReadBeInt16AndShiftBase(bin []byte, baseOffset *byte) int16 { + i := ReadBeInt16(bin, *baseOffset) + *baseOffset += 0x02 + return i +} + +func ReadBeUint16AndShiftBase(bin []byte, baseOffset *byte) uint16 { + i := ReadBeUint16(bin, *baseOffset) + *baseOffset += 0x02 + return i +} + +// BinDiffIterator will yield offset and value for each new byte that is different from original byte. old and new must +// be of the exact same size. I didn't fit old and new value in there cos go only has support for up to Seq2 iter. +func BinDiffIterator(old []byte, new []byte) iter.Seq2[int, byte] { + return func(yield func(int, byte) bool) { + for i, v := range new { + if new[i] != old[i] { + if !yield(i, v) { + return + } + } + } + } +} + +func YesNoByte(value bool) byte { + if value { + return 0xFF + } + return 0x00 +} diff --git a/internal/optic/util/bin_test.go b/internal/optic/util/bin_test.go new file mode 100644 index 0000000..fe6e780 --- /dev/null +++ b/internal/optic/util/bin_test.go @@ -0,0 +1,100 @@ +package util + +import ( + "encoding/binary" + "testing" +) + +const bufTemplate = "0x%2x%2x" + +func TestTwoComplement16EncodeZero(t *testing.T) { + buf := TwoComplement16Encode(0) + if buf[0] != 0x00 || buf[1] != 0x00 { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} + +func TestTwoComplement16EncodePositive(t *testing.T) { + buf := TwoComplement16Encode(32767) + if buf[0] != 0x7F || buf[1] != 0xFF { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} + +func TestTwoComplement16EncodeNegative(t *testing.T) { + buf := TwoComplement16Encode(-32768) + if buf[0] != 0x80 || buf[1] != 0x00 { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} +func TestTwoComplement16EncodeNegative2(t *testing.T) { + buf := TwoComplement16Encode(-11) + if buf[0] != 0xFF || buf[1] != 0xF5 { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} + +func TestTwoComplement16ReadZero(t *testing.T) { + buf := TwoComplement16Encode(0) + + data := binary.BigEndian.Uint16(buf) + n := TwoComplement16(16, data) + + if n != 0 { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} + +func TestTwoComplement16ReadPositive(t *testing.T) { + buf := TwoComplement16Encode(32767) + + data := binary.BigEndian.Uint16(buf) + n := TwoComplement16(16, data) + + if n != 32767 { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} + +func TestTwoComplement16ReadNegative(t *testing.T) { + buf := TwoComplement16Encode(-32768) + + data := binary.BigEndian.Uint16(buf) + n := TwoComplement16(16, data) + + if n != -32768 { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} + +func TestBinDiffIterator(t *testing.T) { + buf := []byte{ + /* 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f*/ + /*00*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /*10*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /*20*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /*30*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /*40*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /*50*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /*60*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + bufCopy := CopySlice(buf) + + bufCopy[0x00] = 0xFF + bufCopy[0x6f] = 0xFF + + var offsets []int + var values []byte + + for offset, value := range BinDiffIterator(buf, bufCopy) { + offsets = append(offsets, offset) + values = append(values, value) + } + + if !(offsets[0] == 0x00 && values[0] == 0xFF) { + t.Errorf(bufTemplate, buf[0], buf[1]) + } + if !(offsets[1] == 0x6F && values[1] == 0xFF) { + t.Errorf(bufTemplate, buf[0], buf[1]) + } +} diff --git a/internal/optic/util/slice.go b/internal/optic/util/slice.go new file mode 100644 index 0000000..f0b956d --- /dev/null +++ b/internal/optic/util/slice.go @@ -0,0 +1,7 @@ +package util + +func CopySlice[T any](old []T) []T { + newSlice := make([]T, len(old)) + copy(newSlice, old) + return newSlice +} diff --git a/internal/pkg/routines/read_actions.go b/internal/pkg/routines/read_actions.go deleted file mode 100644 index 9ecf1e4..0000000 --- a/internal/pkg/routines/read_actions.go +++ /dev/null @@ -1,45 +0,0 @@ -package routines - -import ( - "fmt" - "log/slog" -) - -func ActionShowBasicAdminInfo(args I2cActionArgs) error { - slog.Info("module_info", slog.String("vendor_name", args.Page00.VendorName)) - slog.Info("module_info", slog.String("vendor_phy", args.Page00.VendorPN)) - slog.Info("module_info", slog.String("vendor_serial", args.Page00.VendorSN)) - slog.Info("module_info", slog.Bool("low_pwr_mode_enabled", args.Page00.LowPowerMode)) - - return nil -} - -func ActionShowTunableLaserStatus(args I2cActionArgs) error { - slog.Info("module_info", slog.String( - "tuning_status", fmt.Sprintf("%b", args.Page12.Status), - )) - slog.Info("module_info", slog.String("grid_spacing", args.Page12.GridDisplay)) - slog.Info("module_info", slog.Float64("frequency", float64(args.Page12.Frequency)*1e-12)) - slog.Info("module_info", slog.Int("frequency_offset", args.Page12.FrequencyOffset)) - if args.Page12.Channel != nil { - slog.Info("module_info", slog.Int("channel", *args.Page12.Channel)) - } else { - slog.Warn("No Valid Channel found!") - } - - return nil -} - -func ActionShowFlexOptixCustomPages(args I2cActionArgs) error { - // TODO: check with args.page00.VendorName - slog.Info("module_info", slog.Bool("flex_tune_enabled", args.Page1E.FlexTuneEnabled)) - slog.Info("module_info", slog.String("power_class_override_status", - fmt.Sprintf("%x", args.Page1E.PowerClassOverride), - )) - slog.Info("module_info", slog.Bool( - "nominal_wavelength_control_enabled", - args.Page1B.NominalWavelengthControlEnabled, - )) - - return nil -} diff --git a/internal/pkg/routines/routines.go b/internal/pkg/routines/routines.go deleted file mode 100644 index 4869521..0000000 --- a/internal/pkg/routines/routines.go +++ /dev/null @@ -1,131 +0,0 @@ -package routines - -import ( - "context" - - "github.com/urfave/cli/v3" - "github.com/wobcom/rtbrick-optic-programmer/internal/pkg/rtbrick" - connection "github.com/wobcom/rtbrick-optic-programmer/internal/pkg/ssh" -) - -type I2cRWHandle struct { - Connection *connection.RouterConnection - I2cBusId int -} - -func newI2cRWHandle(user string, router string, iface string) (*I2cRWHandle, error) { - handle := I2cRWHandle{} - - routerConnection, err := connection.New(user, router) - if err != nil { - return nil, err - } - err = routerConnection.Connect() - if err != nil { - return nil, err - } - _, ppdConfig, err := routerConnection.GetDeviceInformation() - if err != nil { - return nil, err - } - for _, port := range ppdConfig.Ports { - if port.Name == iface { - handle.I2cBusId = port.I2CBus - } - } - - handle.Connection = routerConnection - return &handle, nil -} - -func closeI2CRWHandle(handle *I2cRWHandle) { - err := handle.Connection.Close() - if err != nil { - panic(err) - } -} - -type I2cActionArgs struct { - Handle *I2cRWHandle - Cmd *cli.Command - Page00 *rtbrick.I2CPage00 - Page12 *rtbrick.I2CPage12 - Page1E *rtbrick.I2CPage1E - Page1B *rtbrick.I2CPageB0 -} - -type I2cAction func(args I2cActionArgs) error - -func I2cTemplateMethod(actions []I2cAction) cli.ActionFunc { - return func(_ context.Context, cmd *cli.Command) error { - user := cmd.String("user") - router := cmd.StringArg("device") - iface := cmd.StringArg("interface") - - handle, err := newI2cRWHandle(user, router, iface) - if err != nil { - return err - } - defer closeI2CRWHandle(handle) - - page00, err := handle.Connection.GetI2CDump(handle.I2cBusId, 0x00) - if err != nil { - return err - } - page12, err := handle.Connection.GetI2CDump(handle.I2cBusId, 0x12) - if err != nil { - return err - } - page1E, err := handle.Connection.GetI2CDump(handle.I2cBusId, 0x1E) - if err != nil { - return err - } - page1B, err := handle.Connection.GetI2CDump(handle.I2cBusId, 0x1B) - if err != nil { - return err - } - - resultPage00 := rtbrick.InterpretPage00(page00) - resultPage12 := rtbrick.InterpretPage12(page12) - resultPage1E := rtbrick.InterpretPage1E(page1E) - resultPage1B := rtbrick.InterpretPageB0(page1B) - - actionArgs := I2cActionArgs{ - Handle: handle, - Cmd: cmd, - Page00: &resultPage00, - Page12: &resultPage12, - Page1E: &resultPage1E, - Page1B: &resultPage1B, - } - - for _, action := range actions { - err := action(actionArgs) - if err != nil { - return err - } - } - - return nil - } -} - -var i2cReadActions = [...]I2cAction{ - ActionShowBasicAdminInfo, - ActionShowTunableLaserStatus, - ActionShowFlexOptixCustomPages, -} - -var I2CReadAll = I2cTemplateMethod(i2cReadActions[:]) - -var i2cWriteActions = [...]I2cAction{ - ActionShowBasicAdminInfo, - ActionSetPowerModeTo(rtbrick.PowerModeLowPower), - ActionEnablePowerClassOverride, - ActionDisableFlexTune, - ActionSetGridProgramming, - ActionEnableNominalWavelengthControl, - ActionSetPowerModeTo(rtbrick.PowerModeHighPower), -} - -var I2CWriteAll = I2cTemplateMethod(i2cWriteActions[:]) diff --git a/internal/pkg/routines/write_actions.go b/internal/pkg/routines/write_actions.go deleted file mode 100644 index e1d4b44..0000000 --- a/internal/pkg/routines/write_actions.go +++ /dev/null @@ -1,136 +0,0 @@ -package routines - -import ( - "log/slog" - "strconv" - "time" - - "github.com/wobcom/rtbrick-optic-programmer/internal/pkg/rtbrick" -) - -func ActionSetPowerModeTo(power rtbrick.PowerMode) I2cAction { - powerModeToDescription := map[rtbrick.PowerMode]string{ - rtbrick.PowerModeHighPower: "high", - rtbrick.PowerModeLowPower: "low", - } - return func(args I2cActionArgs) error { - slog.Info("set_power", slog.String("state", powerModeToDescription[power])) - - wPage, wByte, wValue := rtbrick.GetPowerProgramming(power) - err := args.Handle.Connection.DoI2CSet(args.Handle.I2cBusId, wPage, wByte, wValue) - if err != nil { - return err - } - - time.Sleep(1 * time.Second) - - return nil - } -} - -func ActionEnablePowerClassOverride(args I2cActionArgs) error { - if args.Page1E.PowerClassOverride != 0x01 { - slog.Info("Setting Power Class Override...") - - wPage, wByte, wValue := rtbrick.GetPowerClassProgramming() - err := args.Handle.Connection.DoI2CSet(args.Handle.I2cBusId, wPage, wByte, wValue) - if err != nil { - return err - } - - time.Sleep(1 * time.Second) - } - - return nil -} - -func ActionDisableFlexTune(args I2cActionArgs) error { - if args.Page1E.FlexTuneEnabled { - slog.Info("Disabling Flex Tune...") - - wPage, wByte, wValue := rtbrick.GetFlexTuneProgramming() - err := args.Handle.Connection.DoI2CSet(args.Handle.I2cBusId, wPage, wByte, wValue) - if err != nil { - return err - } - - time.Sleep(1 * time.Second) - } else { - slog.Info("Flex Tune is already disabled...") - } - - return nil -} - -func ActionSetGridProgramming(args I2cActionArgs) error { - gridSpacing := args.Cmd.Float64("grid-spacing") - channel := args.Cmd.Int("channel") - - needsGridProgramming := args.Page12.GridDisplay != strconv.FormatFloat(gridSpacing, 'f', 3, 64) - needsChannelProgramming := args.Page12.Channel == nil || *args.Page12.Channel != channel - - if needsGridProgramming { - slog.Info( - "grid spacing mismatch, reprogramming", - slog.Float64("target", gridSpacing), - slog.String("current", args.Page12.GridDisplay), - ) - wPage, wByte, wValue := rtbrick.GetGridProgramming(gridSpacing) - err := args.Handle.Connection.DoI2CSet(args.Handle.I2cBusId, wPage, wByte, wValue) - if err != nil { - return err - } - - } else { - slog.Info( - "grid spacing already matching, will not reprogram", - slog.String("current", args.Page12.GridDisplay), - ) - } - - if needsGridProgramming || needsChannelProgramming { - slog.Info( - "channel mismatch, reprogramming", - slog.Int("target", channel), - slog.Int("current", *args.Page12.Channel), - ) - - wPage, wByte, wValue, wPage2, wByte2, wValue2 := rtbrick.GetChannelProgramming(gridSpacing, channel) - err := args.Handle.Connection.DoI2CSet(args.Handle.I2cBusId, wPage, wByte, wValue) - if err != nil { - return err - } - err = args.Handle.Connection.DoI2CSet(args.Handle.I2cBusId, wPage2, wByte2, wValue2) - if err != nil { - return err - } - - } else { - slog.Info( - "channel already matching, will not reprogram", - slog.Int("current", *args.Page12.Channel), - ) - } - - time.Sleep(1 * time.Second) - - return nil -} - -func ActionEnableNominalWavelengthControl(args I2cActionArgs) error { - if !args.Page1B.NominalWavelengthControlEnabled { - slog.Info("Setting Nominal Wavelength Control Programming...") - - wPage, wByte, wValue := rtbrick.GetNominalWavelengthControlProgramming() - err := args.Handle.Connection.DoI2CSet(args.Handle.I2cBusId, wPage, wByte, wValue) - if err != nil { - return err - } - - time.Sleep(1 * time.Second) - } else { - slog.Info("Nominal Wavelength Control is already enabled...") - } - - return nil -} diff --git a/internal/pkg/rtbrick/bitmask.go b/internal/pkg/rtbrick/bitmask.go deleted file mode 100644 index 253baf6..0000000 --- a/internal/pkg/rtbrick/bitmask.go +++ /dev/null @@ -1,19 +0,0 @@ -package rtbrick - -const ( - Bit0 Bitmask = 1 << iota - Bit1 - Bit2 - Bit3 - Bit4 - Bit5 - Bit6 - Bit7 -) - -type Bitmask uint8 - -func (b Bitmask) Has(flag Bitmask) bool { return b&flag != 0 } -func (b *Bitmask) Set(flag Bitmask) { *b |= flag } -func (b *Bitmask) Clear(flag Bitmask) { *b &= ^flag } -func (b *Bitmask) Toggle(flag Bitmask) { *b ^= flag } diff --git a/internal/pkg/rtbrick/dwdm.go b/internal/pkg/rtbrick/dwdm.go deleted file mode 100644 index 3369f70..0000000 --- a/internal/pkg/rtbrick/dwdm.go +++ /dev/null @@ -1,82 +0,0 @@ -package rtbrick - -// This is taken from https://www.opternus.de/wissen/netzwerkprotokoll-messtechnik/dwdm-grid-saemtliche-dwdm-kanaele-mit-ihren-absoluten-werten-nach-frequenz-und-wellenlaenge - -// DWDMGridMap Hz unit -var DWDMGridMap = map[int]int{ - 1: 190.10e12, - 38: 193.80e12, - 2: 190.20e12, - 39: 193.90e12, - 3: 190.30e12, - 40: 194.00e12, - 4: 190.40e12, - 41: 194.10e12, - 5: 190.50e12, - 42: 194.20e12, - 6: 190.60e12, - 43: 194.30e12, - 7: 190.70e12, - 44: 194.40e12, - 8: 190.80e12, - 45: 194.50e12, - 9: 190.90e12, - 46: 194.60e12, - 10: 191.00e12, - 47: 194.70e12, - 11: 191.10e12, - 48: 194.80e12, - 12: 191.20e12, - 49: 194.90e12, - 13: 191.30e12, - 50: 195.00e12, - 14: 191.40e12, - 51: 195.10e12, - 15: 191.50e12, - 52: 195.20e12, - 16: 191.60e12, - 53: 195.30e12, - 17: 191.70e12, - 54: 195.40e12, - 18: 191.80e12, - 55: 195.50e12, - 19: 191.90e12, - 56: 195.60e12, - 20: 192.00e12, - 57: 195.70e12, - 21: 192.10e12, - 58: 195.80e12, - 22: 192.20e12, - 59: 195.90e12, - 23: 192.30e12, - 60: 196.00e12, - 24: 192.40e12, - 61: 196.10e12, - 25: 192.50e12, - 62: 196.20e12, - 26: 192.60e12, - 63: 196.30e12, - 27: 192.70e12, - 64: 196.40e12, - 28: 192.80e12, - 65: 196.50e12, - 29: 192.90e12, - 66: 196.60e12, - 30: 193.00e12, - 67: 196.70e12, - 31: 193.10e12, - 68: 196.80e12, - 32: 193.20e12, - 69: 196.90e12, - 33: 193.30e12, - 70: 197.00e12, - 34: 193.40e12, - 71: 197.10e12, - 35: 193.50e12, - 72: 197.20e12, - 36: 193.60e12, - 73: 197.30e12, - 37: 193.70e12, -} - -const DWDMCenterFreqHz = 193.1e12 diff --git a/internal/pkg/rtbrick/i2c.go b/internal/pkg/rtbrick/i2c.go deleted file mode 100644 index b8b358a..0000000 --- a/internal/pkg/rtbrick/i2c.go +++ /dev/null @@ -1,301 +0,0 @@ -package rtbrick - -import ( - "bytes" - "encoding/binary" - "encoding/hex" - "log/slog" - "math" - "strconv" - "strings" -) - -func ParseI2CDump(dump string) ([]byte, error) { - slog.Debug("======== I2C Dump ========") - slog.Debug("\n" + dump) - slog.Debug("======== ======== ========") - - lines := strings.Split(dump, "\n") - - buf := make([]byte, 0, 1024) - w := bytes.NewBuffer(buf) - - for _, line := range lines[1:17] { - - p1 := strings.Split(line, ": ")[1] - p2 := strings.Split(p1, " ")[0] - - for _, x := range strings.Split(p2, " ") { - b, err := hex.DecodeString(x) - if err != nil { - slog.Error("could not parse", "code", err) - panic(err) - } - w.Write(b) - } - - } - - allBytes := w.Bytes() - slog.Debug("raw_decoded_i2c_bytes", slog.String("hex_string", hex.EncodeToString(allBytes))) - return allBytes, nil - -} - -func keysByValue[K comparable, V comparable](m map[K]V, value V) *K { - for k, v := range m { - if value == v { - return &k - } - } - return nil -} - -func TwoComplement16(sizeBits uint8, data uint16) (v int16) { - n := float64(sizeBits - 1) - p := math.Pow(2, n) // 2^(N-1) - mask := uint16(p) - - tmp1 := -int16(data & mask) - tmp2 := int16(data & ^mask) - - v = tmp1 + tmp2 - return v -} - -func ToTwoComplement16(a int16) []byte { - b := make([]byte, 2) - binary.LittleEndian.PutUint16(b, uint16(a)) - return b -} - -func ParseASCIIToString(part []byte) string { - var asciiString string - for _, code := range part { - asciiString += string(rune(code)) - } - return strings.TrimSpace(asciiString) -} - -type I2CPage12Grid int - -const ( - I2CPage12Grid3p125 = iota - I2CPage12Grid6p25 - I2CPage12Grid12p5 - I2CPage12Grid25 - I2CPage12Grid50 - I2CPage12Grid100 - I2CPage12Grid33 - I2CPage12Grid75 - I2CPage12GridRESERVED - I2CPage12GridUNAVAILABLE -) - -type DefaultMap[K comparable, V comparable] struct { - m map[K]V - d V -} - -func (m DefaultMap[K, V]) get(key K) V { - if v, ok := m.m[key]; ok { - return v - } - return m.d -} - -func (m DefaultMap[K, V]) getKey(val V) K { - for k, v := range m.m { - if v == val { - return k - } - } - panic("value not found") -} - -// I2CPage12GridMap cmis 12h reg 128-135 grid spacing -var I2CPage12GridMap = DefaultMap[byte, I2CPage12Grid]{ - m: map[byte]I2CPage12Grid{ - 0b0000_0000: I2CPage12Grid3p125, - 0b0010_0000: I2CPage12Grid12p5, - 0b0001_0000: I2CPage12Grid6p25, - 0b0011_0000: I2CPage12Grid25, - 0b0100_0000: I2CPage12Grid50, - 0b0101_0000: I2CPage12Grid100, - 0b0110_0000: I2CPage12Grid33, - 0b0111_0000: I2CPage12Grid75, - 0b1111_0000: I2CPage12GridUNAVAILABLE, - }, - d: I2CPage12GridRESERVED, -} - -// I2CPage12GridMultiplierMap Hz -var I2CPage12GridMultiplierMap = map[I2CPage12Grid]int{ - I2CPage12Grid3p125: 3.125e9, - I2CPage12Grid6p25: 6.25e9, - I2CPage12Grid12p5: 12.5e9, - I2CPage12Grid25: 25.0e9, - I2CPage12Grid50: 50.0e9, - I2CPage12Grid100: 100.0e9, - I2CPage12Grid33: 33.0e9, - I2CPage12Grid75: 75.0e9, -} - -// I2CPage12GridNameMap Ghz -var I2CPage12GridNameMap = map[I2CPage12Grid]float64{ - I2CPage12Grid3p125: 3.125, - I2CPage12Grid6p25: 6.25, - I2CPage12Grid12p5: 12.5, - I2CPage12Grid25: 25.0, - I2CPage12Grid50: 50.0, - I2CPage12Grid100: 100.0, - I2CPage12Grid33: 33.0, - I2CPage12Grid75: 75.0, -} - -type I2CPage12 struct { - Grid I2CPage12Grid - GridDisplay string - FrequencyOffset int - Frequency int - Channel *int - Status byte -} - -func InterpretPage12(dump []byte) I2CPage12 { - bitfieldGrid := dump[128] - gridSetting := I2CPage12GridMap.get(bitfieldGrid) - - u := binary.BigEndian.Uint16(dump[136:138]) - frequencyOffset := int(TwoComplement16(16, u)) - gridMultiplier := I2CPage12GridMultiplierMap[gridSetting] - - opticFrequency := DWDMCenterFreqHz + (frequencyOffset * gridMultiplier) - channelSetting := keysByValue(DWDMGridMap, opticFrequency) - - status := dump[231] - - return I2CPage12{ - Grid: gridSetting, - GridDisplay: strconv.FormatFloat(I2CPage12GridNameMap[gridSetting], 'f', 3, 64), - FrequencyOffset: frequencyOffset, - Frequency: opticFrequency, - Channel: channelSetting, - Status: status, - } -} - -func GetGridProgramming(gridStr float64) (page, byte int, value byte) { - grid := keysByValue(I2CPage12GridNameMap, gridStr) - newValue := I2CPage12GridMap.getKey(*grid) - return 0x12, 128, newValue -} - -func GetChannelProgramming(gridStr float64, newChannel int) (page, byte int, value byte, page2, byte2 int, value2 byte) { - gridSetting := keysByValue(I2CPage12GridNameMap, gridStr) - - targetFrequency := DWDMGridMap[newChannel] - gridMultiplier := I2CPage12GridMultiplierMap[*gridSetting] - - targetOffset := (targetFrequency - DWDMCenterFreqHz) / gridMultiplier - - sendBytes := ToTwoComplement16(int16(targetOffset)) - - return 0x12, 137, sendBytes[0], 0x12, 136, sendBytes[1] -} - -type I2CPage1E struct { - FlexTuneEnabled bool - PowerClassOverride uint8 -} - -func InterpretPage1E(dump []byte) I2CPage1E { - - flexTuneEnabled := false - if dump[200] == 0x01 { - flexTuneEnabled = true - } - - return I2CPage1E{ - FlexTuneEnabled: flexTuneEnabled, - PowerClassOverride: dump[253], - } -} - -func GetFlexTuneProgramming() (page, byte int, value byte) { - var flexTuneBit uint8 = 0b00000000 - - return 0x1E, 200, flexTuneBit -} - -func GetPowerClassProgramming() (page, byte int, value byte) { - var powerClassBit uint8 = 0x01 - - return 0x1E, 253, powerClassBit -} - -type I2CPageB0 struct { - NominalWavelengthControlEnabled bool -} - -func InterpretPageB0(dump []byte) I2CPageB0 { - - nominalWavelengthControlEnabled := false - if dump[129] == 0x01 { - nominalWavelengthControlEnabled = true - } - - return I2CPageB0{ - NominalWavelengthControlEnabled: nominalWavelengthControlEnabled, - } -} - -func GetNominalWavelengthControlProgramming() (page, byte int, value byte) { - var enableBit uint8 = 0b00000001 - return 0xB0, 129, enableBit -} - -type I2CPage00 struct { - VendorName string - VendorPN string - VendorSN string - LowPowerMode bool -} - -func InterpretPage00(dump []byte) I2CPage00 { - - bit99Bitmask := Bitmask(dump[99]) - - isLowPowerMode := bit99Bitmask.Has(Bit1) - - return I2CPage00{ - LowPowerMode: isLowPowerMode, - VendorName: ParseASCIIToString(dump[148:164]), - VendorPN: ParseASCIIToString(dump[168:184]), - VendorSN: ParseASCIIToString(dump[196:212]), - } -} - -type PowerMode int - -const ( - PowerModeLowPower PowerMode = iota - PowerModeHighPower -) - -var powerModeToProgramming = map[PowerMode]uint8{ - PowerModeHighPower: 0b00000100, - PowerModeLowPower: 0b00000010, -} - -func GetPowerProgramming(power PowerMode) (page, byte int, value byte) { - var powerClassBit = powerModeToProgramming[power] - - return 0, 93, powerClassBit -} - -func GetSoftReboot() (page, byte int, value byte) { - var softRebootBit uint8 = 0b10000000 - - return 0, 93, softRebootBit -} diff --git a/internal/pkg/rtbrick/rtbrick.go b/internal/rtbrick/rtbrick.go similarity index 100% rename from internal/pkg/rtbrick/rtbrick.go rename to internal/rtbrick/rtbrick.go diff --git a/internal/pkg/ssh/connection.go b/internal/rtbrick/ssh/connection.go similarity index 79% rename from internal/pkg/ssh/connection.go rename to internal/rtbrick/ssh/connection.go index 9a10011..4950fea 100644 --- a/internal/pkg/ssh/connection.go +++ b/internal/rtbrick/ssh/connection.go @@ -2,7 +2,6 @@ package connection import ( "bytes" - "encoding/hex" "encoding/json" "fmt" "io" @@ -12,7 +11,7 @@ import ( "github.com/pkg/errors" "github.com/pkg/sftp" - "github.com/wobcom/rtbrick-optic-programmer/internal/pkg/rtbrick" + "github.com/wobcom/rtbrick-optic-programmer/internal/rtbrick" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "gopkg.in/yaml.v3" @@ -155,33 +154,17 @@ func (r *RouterConnection) RunSSHCommand(command string) (string, error) { return stdoutBuffer.String(), nil } -func (r *RouterConnection) GetI2CDump(i2cbusId int, page byte) ([]byte, error) { - slog.Debug("page_dump_cmd", slog.String("hex_page", hex.EncodeToString([]byte{page}))) - _, err := r.RunSSHCommand(fmt.Sprintf("sudo i2cset -y %d 0x50 127 %d", i2cbusId, page)) +func (r *RouterConnection) GetI2CDump(i2cbusId int) (*string, error) { + slog.Debug("page_dump_cmd", slog.Int("bus_id", i2cbusId)) + out, err := r.RunSSHCommand(fmt.Sprintf("sudo i2cdump -y %d 0x50 i", i2cbusId)) if err != nil { return nil, err } - out, err := r.RunSSHCommand(fmt.Sprintf("sudo i2cdump -y %d 0x50 b", i2cbusId)) - if err != nil { - return nil, err - } - _, err = r.RunSSHCommand(fmt.Sprintf("sudo i2cset -y %d 0x50 127 %d", i2cbusId, 0)) - if err != nil { - return nil, err - } - return rtbrick.ParseI2CDump(out) + return &out, nil } -func (r *RouterConnection) DoI2CSet(i2cbusId int, page int, byte int, value byte) error { - _, err := r.RunSSHCommand(fmt.Sprintf("sudo i2cset -y %d 0x50 127 %d", i2cbusId, page)) - if err != nil { - return err - } - _, err = r.RunSSHCommand(fmt.Sprintf("sudo i2cset -y %d 0x50 %d %d", i2cbusId, byte, value)) - if err != nil { - return err - } - _, err = r.RunSSHCommand(fmt.Sprintf("sudo i2cset -y %d 0x50 127 %d", i2cbusId, 0)) +func (r *RouterConnection) DoI2CSet(i2cbusId int, offset int, value byte) error { + _, err := r.RunSSHCommand(fmt.Sprintf("sudo i2cset -y %d 0x50 %d %d", i2cbusId, offset, value)) if err != nil { return err } diff --git a/internal/rtbrick/ssh/i2c_handle.go b/internal/rtbrick/ssh/i2c_handle.go new file mode 100644 index 0000000..bdcd44b --- /dev/null +++ b/internal/rtbrick/ssh/i2c_handle.go @@ -0,0 +1,38 @@ +package connection + +type I2cRWHandle struct { + Connection *RouterConnection + I2cBusId int +} + +func NewI2cRWHandle(user string, router string, iface string) (*I2cRWHandle, error) { + handle := I2cRWHandle{} + + routerConnection, err := New(user, router) + if err != nil { + return nil, err + } + err = routerConnection.Connect() + if err != nil { + return nil, err + } + _, ppdConfig, err := routerConnection.GetDeviceInformation() + if err != nil { + return nil, err + } + for _, port := range ppdConfig.Ports { + if port.Name == iface { + handle.I2cBusId = port.I2CBus + } + } + + handle.Connection = routerConnection + return &handle, nil +} + +func CloseI2CRWHandle(handle *I2cRWHandle) { + err := handle.Connection.Close() + if err != nil { + panic(err) + } +} diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..1b7449a --- /dev/null +++ b/internal/util.go @@ -0,0 +1,40 @@ +package internal + +import ( + "bytes" + "encoding/hex" + "log/slog" + "strings" +) + +func ParseI2CDump(dump string) []byte { + slog.Debug("======== I2C Dump ========") + slog.Debug("\n" + dump) + slog.Debug("======== ======== ========") + + lines := strings.Split(dump, "\n") + + buf := make([]byte, 0, 1024) + w := bytes.NewBuffer(buf) + + for _, line := range lines[1:17] { + + p1 := strings.Split(line, ": ")[1] + p2 := strings.Split(p1, " ")[0] + + for _, x := range strings.Split(p2, " ") { + b, err := hex.DecodeString(x) + if err != nil { + slog.Error("could not parse", "code", err) + panic(err) + } + w.Write(b) + } + + } + + allBytes := w.Bytes() + slog.Debug("raw_decoded_i2c_bytes", slog.String("hex_string", hex.EncodeToString(allBytes))) + return allBytes + +}