blob: 50bfd297b17d28d28f1304b81683f85ed292a503 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package plc4go
import (
"context"
"github.com/apache/plc4x/plc4go/pkg/api/config"
"github.com/apache/plc4x/plc4go/spi/options/converter"
"github.com/rs/zerolog"
"net/url"
"github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/apache/plc4x/plc4go/spi/options"
"github.com/apache/plc4x/plc4go/spi/transports"
"github.com/pkg/errors"
)
// PlcDriverManager is the main entry point for PLC4Go applications
type PlcDriverManager interface {
// RegisterDriver Manually register a new driver
RegisterDriver(driver PlcDriver)
// ListDriverNames List the names of all drivers registered in the system
ListDriverNames() []string
// GetDriver Get access to a driver instance for a given driver-name
GetDriver(driverName string) (PlcDriver, error)
// GetConnection Get a connection to a remote PLC for a given plc4x connection-string
GetConnection(connectionString string) <-chan PlcConnectionConnectResult
// Discover Execute all available discovery methods on all available drivers using all transports
Discover(callback func(event model.PlcDiscoveryItem), discoveryOptions ...WithDiscoveryOption) error
// DiscoverWithContext Execute all available discovery methods on all available drivers using all transports
DiscoverWithContext(ctx context.Context, callback func(event model.PlcDiscoveryItem), discoveryOptions ...WithDiscoveryOption) error
}
func NewPlcDriverManager(_options ...config.WithOption) PlcDriverManager {
localLog := options.ExtractCustomLogger(converter.WithOptionToInternal(_options...)...)
localLog.Trace().Msg("Creating plc driver manager")
return &plcDriverManger{
drivers: map[string]PlcDriver{},
transports: map[string]transports.Transport{},
log: localLog,
}
}
// WithDiscoveryOptionProtocol sets an option for a protocol
func WithDiscoveryOptionProtocol(protocolName string) WithDiscoveryOption {
return withDiscoveryOption{options.WithDiscoveryOptionProtocol(protocolName)}
}
// WithDiscoveryOptionTransport sets an option for a transportName
func WithDiscoveryOptionTransport(transportName string) WithDiscoveryOption {
return withDiscoveryOption{options.WithDiscoveryOptionTransport(transportName)}
}
// WithDiscoveryOptionDeviceName sets an option for a deviceName
func WithDiscoveryOptionDeviceName(deviceName string) WithDiscoveryOption {
return withDiscoveryOption{options.WithDiscoveryOptionDeviceName(deviceName)}
}
// WithDiscoveryOptionLocalAddress sets an option for a localAddress
func WithDiscoveryOptionLocalAddress(localAddress string) WithDiscoveryOption {
return withDiscoveryOption{options.WithDiscoveryOptionLocalAddress(localAddress)}
}
// WithDiscoveryOptionRemoteAddress sets an option for a remoteAddress
func WithDiscoveryOptionRemoteAddress(remoteAddress string) WithDiscoveryOption {
return withDiscoveryOption{options.WithDiscoveryOptionRemoteAddress(remoteAddress)}
}
func WithDiscoveryOptionProtocolSpecific(key string, value any) WithDiscoveryOption {
return withDiscoveryOption{options.WithDiscoveryOptionProtocolSpecific(key, value)}
}
// WithDiscoveryOption is a marker interface for options regarding discovery
// FIXME: this is to avoid leaks spi in the signature move to spi driver or create interfaces. Can also be done by moving spi in a proper module
type WithDiscoveryOption interface {
isDiscoveryOption() bool
}
///////////////////////////////////////
///////////////////////////////////////
//
// Internal section
//
type plcDriverManger struct {
drivers map[string]PlcDriver
transports map[string]transports.Transport
log zerolog.Logger
}
type plcConnectionConnectResult struct {
connection PlcConnection
err error
}
func (d *plcConnectionConnectResult) GetConnection() PlcConnection {
return d.connection
}
func (d *plcConnectionConnectResult) GetErr() error {
return d.err
}
type withDiscoveryOption struct {
options.WithDiscoveryOption
}
func (w withDiscoveryOption) isDiscoveryOption() bool {
return true
}
func convertToInternalOptions(withDiscoveryOptions ...WithDiscoveryOption) []options.WithDiscoveryOption {
result := make([]options.WithDiscoveryOption, len(withDiscoveryOptions))
for i, discoveryOption := range withDiscoveryOptions {
result[i] = discoveryOption.(withDiscoveryOption).WithDiscoveryOption
}
return result
}
//
// Internal section
//
///////////////////////////////////////
///////////////////////////////////////
func (m *plcDriverManger) RegisterDriver(driver PlcDriver) {
m.log.Debug().Str("protocolName", driver.GetProtocolName()).Msg("Registering driver")
// If this driver is already registered, just skip resetting it
for driverName := range m.drivers {
if driverName == driver.GetProtocolCode() {
m.log.Warn().Str("protocolName", driver.GetProtocolName()).Msg("Already registered")
return
}
}
m.drivers[driver.GetProtocolCode()] = driver
m.log.Info().Str("protocolName", driver.GetProtocolName()).Msgf("Driver for %s registered", driver.GetProtocolName())
}
func (m *plcDriverManger) ListDriverNames() []string {
m.log.Trace().Msg("Listing driver names")
var driverNames []string
for driverName := range m.drivers {
driverNames = append(driverNames, driverName)
}
m.log.Trace().Msgf("Found %d driver(s)", len(driverNames))
return driverNames
}
func (m *plcDriverManger) GetDriver(driverName string) (PlcDriver, error) {
if val, ok := m.drivers[driverName]; ok {
return val, nil
}
return nil, errors.Errorf("couldn't find driver %s", driverName)
}
func (m *plcDriverManger) RegisterTransport(transport transports.Transport) {
m.log.Debug().Str("transportName", transport.GetTransportName()).Msg("Registering transport")
// If this transport is already registered, just skip resetting it
for transportName := range m.transports {
if transportName == transport.GetTransportCode() {
m.log.Warn().Str("transportName", transport.GetTransportName()).Msg("Transport already registered")
return
}
}
m.transports[transport.GetTransportCode()] = transport
m.log.Info().Str("transportName", transport.GetTransportName()).Msgf("Transport for %s registered", transport.GetTransportName())
}
func (m *plcDriverManger) ListTransportNames() []string {
m.log.Trace().Msg("Listing transport names")
var transportNames []string
for transportName := range m.transports {
transportNames = append(transportNames, transportName)
}
m.log.Trace().Msgf("Found %d transports", len(transportNames))
return transportNames
}
func (m *plcDriverManger) GetTransport(transportName string, _ string, _ map[string][]string) (transports.Transport, error) {
if val, ok := m.transports[transportName]; ok {
m.log.Debug().Str("transportName", transportName).Msg("Returning transport")
return val, nil
}
return nil, errors.Errorf("couldn't find transport %s", transportName)
}
func (m *plcDriverManger) GetConnection(connectionString string) <-chan PlcConnectionConnectResult {
m.log.Debug().Str("connectionString", connectionString).Msgf("Getting connection for %s", connectionString)
// Parse the connection string.
connectionUrl, err := url.Parse(connectionString)
if err != nil {
m.log.Error().Err(err).Msg("Error parsing connection")
ch := make(chan PlcConnectionConnectResult, 1)
ch <- &plcConnectionConnectResult{err: errors.Wrap(err, "error parsing connection string")}
return ch
}
m.log.Debug().Stringer("connectionUrl", connectionUrl).Msg("parsed connection URL")
// The options will be used to configure both the transports as well as the connections/drivers
configOptions := connectionUrl.Query()
// Find the driver specified in the url.
driverName := connectionUrl.Scheme
driver, err := m.GetDriver(driverName)
if err != nil {
m.log.Err(err).Str("driverName", driverName).Msgf("Couldn't get driver for %s", driverName)
ch := make(chan PlcConnectionConnectResult, 1)
ch <- &plcConnectionConnectResult{err: errors.Wrap(err, "error getting driver for connection string")}
return ch
}
m.log.Debug().Stringer("connectionUrl", connectionUrl).Msgf("got driver %s", driver.GetProtocolName())
// If a transport is provided alongside the driver, the URL content is decoded as "opaque" data
// Then we have to re-parse that to get the transport code as well as the host & port information.
var transportName string
var transportConnectionString string
var transportPath string
if len(connectionUrl.Opaque) > 0 {
m.log.Trace().Msg("we handling a opaque connectionUrl")
connectionUrl, err := url.Parse(connectionUrl.Opaque)
if err != nil {
m.log.Err(err).Str("connectionUrl.Opaque", connectionUrl.Opaque).Msg("Couldn't get transport due to parsing error")
ch := make(chan PlcConnectionConnectResult, 1)
ch <- &plcConnectionConnectResult{err: errors.Wrap(err, "error parsing connection string")}
return ch
}
transportName = connectionUrl.Scheme
transportConnectionString = connectionUrl.Host
transportPath = connectionUrl.Path
} else {
m.log.Trace().Msg("we handling a non-opaque connectionUrl")
// If no transport was provided the driver has to provide a default transport.
transportName = driver.GetDefaultTransport()
transportConnectionString = connectionUrl.Host
transportPath = connectionUrl.Path
}
m.log.Debug().
Str("transportName", transportName).
Str("transportConnectionString", transportConnectionString).
Msgf("got a transport %s", transportName)
// If no transport has been specified explicitly or per default, we have to abort.
if transportName == "" {
m.log.Error().Msg("got a empty transport")
ch := make(chan PlcConnectionConnectResult, 1)
ch <- &plcConnectionConnectResult{err: errors.New("no transport specified and no default defined by driver")}
return ch
}
// Assemble a correct transport url
transportUrl := url.URL{
Scheme: transportName,
Host: transportConnectionString,
Path: transportPath,
}
m.log.Debug().Stringer("transportUrl", &transportUrl).Msg("Assembled transport url")
// Create a new connection
return driver.GetConnection(transportUrl, m.transports, configOptions)
}
func (m *plcDriverManger) Discover(callback func(event model.PlcDiscoveryItem), discoveryOptions ...WithDiscoveryOption) error {
return m.DiscoverWithContext(context.TODO(), callback, discoveryOptions...)
}
func (m *plcDriverManger) DiscoverWithContext(ctx context.Context, callback func(event model.PlcDiscoveryItem), discoveryOptions ...WithDiscoveryOption) error {
// Check if we've got at least one option to restrict to certain protocols only.
// If there is at least one, we only check that protocol, if there are none, all
// available protocols are checked.
internalOptions := convertToInternalOptions(discoveryOptions...)
protocolOptions := options.FilterDiscoveryOptionsProtocol(internalOptions)
discoveryDrivers := map[string]PlcDriver{}
if len(protocolOptions) > 0 {
for _, protocolOption := range protocolOptions {
if driver, ok := m.drivers[protocolOption.GetProtocolName()]; ok {
discoveryDrivers[driver.GetProtocolName()] = driver
}
}
} else {
discoveryDrivers = m.drivers
}
// Execute discovery on all selected drivers
for _, driver := range discoveryDrivers {
if driver.SupportsDiscovery() {
err := driver.DiscoverWithContext(ctx, callback, internalOptions...)
if err != nil {
return errors.Wrapf(err, "Error running Discover on driver %s", driver.GetProtocolName())
}
}
}
return nil
}