| // Copyright 2024 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package allion provides support for interacting with Allion switches. |
| package allion |
| |
| import ( |
| "context" |
| "fmt" |
| "log/slog" |
| "strings" |
| "time" |
| |
| "go.bug.st/serial" |
| |
| "go.chromium.org/chromiumos/config/go/test/lab/api/passport" |
| "go.chromiumos.org/chromiumos/platform/passport/server" |
| ) |
| |
| // Aliases for switch port states. |
| const ( |
| SwitchDisabled = passport.SwitchPortState_SWITCH_PORT_DISABLED |
| SwitchEnabled = passport.SwitchPortState_SWITCH_PORT_ENABLED |
| SwitchFlip = passport.SwitchPortState_SWITCH_PORT_FLIP |
| ) |
| |
| var ( |
| // each switch model has a specific command needed to apply a state change |
| defaultPort = "default" |
| |
| // switchCommands maps switch model ID to a command map for disabling and flipping that specific switch |
| switchCommands = map[string]commandMap{ |
| "AUS19129": {SwitchDisabled: "0", SwitchFlip: "2"}, |
| "AHS20079": {SwitchDisabled: "2"}, |
| "AUS20019": {SwitchDisabled: "2"}, |
| "ADT21090": {SwitchDisabled: "3"}, |
| "XXRJ45SW": {SwitchDisabled: "2"}, |
| "AUS22095": {SwitchDisabled: "3"}, |
| "AHS24067": {SwitchDisabled: "3"}, |
| "ADS24068": {SwitchDisabled: "3"}, |
| } |
| |
| // switchEnabledByPortIdCommands maps switch model ID to a command map for enabling that specific switch by port ID |
| switchEnabledByPortIdCommands = map[string]portCommandMap{ |
| "AUS19129": {defaultPort: "1"}, |
| "AHS20079": {defaultPort: "1"}, |
| "AUS20019": {defaultPort: "1"}, |
| "ADT21090": {defaultPort: "1"}, |
| "XXRJ45SW": {defaultPort: "1"}, |
| "AUS22095": {defaultPort: "1", "A": "1", "B": "2"}, |
| "AHS24067": {defaultPort: "1"}, |
| "ADS24068": {defaultPort: "1"}, |
| } |
| ) |
| |
| // Maps switch states to commands to be sent to the device. |
| type commandMap map[passport.SwitchPortState]string |
| |
| // Maps switch port to commands to be sent to the device. |
| type portCommandMap map[string]string |
| |
| // switchInfo contains cached information about an Allion switch. |
| type switchInfo struct { |
| uid string |
| port string |
| model string |
| commands commandMap |
| enableByPortCommands portCommandMap |
| lastCommand string |
| } |
| |
| // switchPlugin is an allion switch plugin. |
| type switchPlugin struct { |
| // Maps switch UID to switch details. |
| switches map[string]*switchInfo |
| } |
| |
| func init() { |
| // Register the switch plugin with the main passport application. |
| server.RegisterSwitchPlugin( |
| &switchPlugin{ |
| switches: make(map[string]*switchInfo), |
| }) |
| } |
| |
| // GetSwitches probes all allion switches connected to the host. |
| func (s *switchPlugin) GetSwitches(ctx context.Context, req *passport.GetSwitchesRequest) (*passport.GetSwitchesResponse, error) { |
| slog.Info("Probing for switches") |
| if err := s.refreshSwitches(ctx); err != nil { |
| return nil, fmt.Errorf("failed to probe for switches: %w", err) |
| } |
| |
| var switches []*passport.SwitchFixture |
| for id := range s.switches { |
| switches = append(switches, &passport.SwitchFixture{Id: id}) |
| } |
| |
| return &passport.GetSwitchesResponse{ |
| Switches: switches, |
| }, nil |
| } |
| |
| // ConfigureSwitchPort configures a single port on a switch. |
| func (s *switchPlugin) ConfigureSwitchPort(ctx context.Context, req *passport.ConfigureSwitchPortRequest) (*passport.ConfigureSwitchPortResponse, error) { |
| slog.Info("Configuring switch", "switch", req.GetSwitchId(), "state", req.GetState(), "port id", req.GetPortId()) |
| |
| portId := req.GetPortId() |
| if len(portId) == 0 { |
| portId = defaultPort |
| } |
| if err := s.controlSwitch(ctx, req.GetSwitchId(), req.GetState(), portId); err != nil { |
| return nil, err |
| } |
| |
| return &passport.ConfigureSwitchPortResponse{}, nil |
| } |
| |
| // ResetAllSwitches re-initializes all found switches and sets them to the "disabled" state. |
| func (s *switchPlugin) ResetAllSwitches(ctx context.Context, req *passport.ResetAllSwitchesRequest) (*passport.ResetAllSwitchesResponse, error) { |
| slog.Info("Resetting switches") |
| resp, err := s.GetSwitches(ctx, &passport.GetSwitchesRequest{}) |
| if err != nil { |
| } |
| |
| for _, sw := range resp.GetSwitches() { |
| slog.Info("Resetting switch", "switch", sw.GetId()) |
| if err := s.controlSwitch(ctx, sw.GetId(), SwitchDisabled, defaultPort); err != nil { |
| return nil, fmt.Errorf("failed to disable switch: %q: %w", sw.GetId(), err) |
| } |
| } |
| |
| return &passport.ResetAllSwitchesResponse{}, nil |
| } |
| |
| // Name returns the plugin's name for logging purposes. |
| func (s *switchPlugin) Name() string { |
| return "allion_switch_plugin" |
| } |
| |
| func (s *switchPlugin) refreshSwitches(ctx context.Context) error { |
| ports, err := serial.GetPortsList() |
| if err != nil { |
| return fmt.Errorf("failed to get port list: %w", err) |
| } |
| |
| // Retrieve the switch information from each serial port. |
| switches := make(map[string]*switchInfo) |
| for _, port := range ports { |
| // Only use serial ports containing ACM*, e.g. /dev/ttyACM0. |
| if !strings.Contains(port, "ACM") { |
| slog.Debug("Skipping port", "port", port) |
| continue |
| } |
| |
| // Sending "i" to an allion serial device should respond with a single |
| // line containing the product ID e.g. AHS20079_A00_01_2206201400. |
| response, err := sendDataToSerialPort(ctx, port, "i", time.Second/2) |
| if err != nil { |
| slog.Error("Failed to read serial port", "port", port, "error", err) |
| continue |
| } |
| |
| // Scan the response for the line containing the ID info. It's possible |
| // there are extranious lines in the device output that have been buffered |
| // but not read yet so we need to check each one for the expected line. |
| for _, line := range strings.Split(response, "\n") { |
| validDevice, info := isAllionDevice(line) |
| if !validDevice { |
| slog.Debug("Skipping port info line", "line", line) |
| continue |
| } |
| |
| info.port = port |
| slog.Info("Found valid device", "device", line, "port", info.port, "uid", info.uid) |
| |
| if infoOld, ok := s.switches[info.uid]; ok { |
| // if we've previously seen this switch, then reuse some info. |
| info.lastCommand = infoOld.lastCommand |
| } |
| |
| switches[info.uid] = info |
| break |
| } |
| } |
| |
| // Log switches that we were not able to detect this time. |
| for k, v := range s.switches { |
| if _, ok := switches[k]; !ok { |
| slog.Warn("unable to find previously detected switch", "switch id", k, "port", v.port) |
| } |
| } |
| |
| s.switches = switches |
| return nil |
| } |
| |
| // controlSwitch sets the switch status to on or off. |
| func (s *switchPlugin) controlSwitch(ctx context.Context, id string, state passport.SwitchPortState, portId string) error { |
| id = strings.ToUpper(id) |
| sw, ok := s.switches[id] |
| if !ok { |
| return fmt.Errorf("unable to find the serial port of the switch ID: %s", id) |
| } |
| |
| cmd := "" |
| if state == SwitchEnabled { |
| if cmd, ok = sw.enableByPortCommands[portId]; !ok { |
| return fmt.Errorf("unable to find the corresponding port command of the switch ID: %q, port ID: %s", id, portId) |
| } |
| |
| } else { |
| if cmd, ok = sw.commands[state]; !ok { |
| return fmt.Errorf("unable to find the corresponding command of the switch ID: %q, status: %s", id, state) |
| } |
| } |
| |
| slog.Info("previous switch command", "switch", id, "command", sw.lastCommand) |
| if sw.lastCommand == cmd { |
| slog.Info("requested switch state matches previous, skipping", "switch", id, "command", cmd) |
| return nil |
| } |
| // Clear last command for now so if there's an error anywhere we're not left in an incorrect state. |
| sw.lastCommand = "" |
| |
| slog.Info("Setting switch state", "switch id", id, "state", state, "port id", portId, "switch port", sw.port, "cmd", cmd) |
| if _, err := sendDataToSerialPort(ctx, sw.port, cmd, 3*time.Second); err != nil { |
| return fmt.Errorf("failed to request serial port: %w", err) |
| } |
| sw.lastCommand = cmd |
| |
| return nil |
| } |
| |
| // sendDataToSerialPort returns a response after sends a request to the serial port. |
| func sendDataToSerialPort(ctx context.Context, port, req string, timeout time.Duration) (string, error) { |
| mode := &serial.Mode{ |
| BaudRate: 9600, |
| } |
| usbPort, err := serial.Open(port, mode) |
| if err != nil { |
| return "", fmt.Errorf("failed to open serial port: %w", err) |
| } |
| defer usbPort.Close() |
| |
| if err := usbPort.SetReadTimeout(timeout); err != nil { |
| return "", fmt.Errorf("failed to set read timeout: %w", err) |
| } |
| |
| if _, err := usbPort.Write([]byte(req)); err != nil { |
| return "", fmt.Errorf("failed to write serial port: %w", err) |
| } |
| |
| // Read the response. |
| var resp string |
| buff := make([]byte, 1000) |
| for ctx.Err() == nil { |
| n, err := usbPort.Read(buff) |
| if err != nil { |
| return "", fmt.Errorf("failed to read serial port: %w", err) |
| } |
| if n == 0 { |
| fmt.Println("\nEOF") |
| break |
| } |
| |
| resp = strings.TrimSpace(string(buff[:n])) |
| } |
| |
| return resp, nil |
| } |
| |
| // isAllionDevice checks the usb device's info line to see if it corresponds to |
| // a known allion device. If yes, it returns the device's control information. |
| // |
| // example: |
| // |
| // AHS20079_A00_01_2206201400 -> (true, AHS20079, 2007901) |
| func isAllionDevice(device string) (bool, *switchInfo) { |
| const switchIDLen = 22 |
| if len(device) < switchIDLen { |
| return false, nil |
| } |
| |
| // For allion devices, serial is first 15 characters. |
| // Serials are case insensitive. |
| serial := strings.ToUpper(device[0:15]) |
| |
| // Model number is the first 8 characters of the serial. |
| model := serial[0:8] |
| if _, ok := switchCommands[model]; !ok { |
| return false, nil |
| } |
| |
| if _, ok := switchEnabledByPortIdCommands[model]; !ok { |
| return false, nil |
| } |
| |
| // Extract the uid from serial, this is the expected id used by |
| // the test. |
| return true, &switchInfo{ |
| uid: device[3:8] + device[13:15], |
| model: model, |
| commands: switchCommands[model], |
| enableByPortCommands: switchEnabledByPortIdCommands[model], |
| } |
| } |