openTCS二次开发简要事项(详细版)

1.juice guice 如何使用注入

在opentcs中使用注入非常的简单,只需要在类的构造方法中指定参数即可。例如需要用到TCSObjectService这个服务,在参数中指定,然后接收此参数,需要检测非空。

我们可以注意到一个有意思的现象,即打了@Inject标记的类它的参数个数是可变的,类型随意指定。这样的类无法被简单的调用,因为java再编译的过程中就会判断我们新建或调用的类传递的参数是否合法,但是通过反射我们可以获取到类的一些详细信息,比如需要哪些参数,这就可以根据需要来初始化调用它。

但是这么麻烦有什么用处呢,而且反射是比较慢的。其中的一个用处就是开发插件,插件的编译是独立于主体软件的,软件要加载什么插件也是不知道的,可以把一些信息写在插件里面或储存在文本文件里,主体启动的时候可以主动去初始化插件里的类。

  public OrderHandler(TransportOrderService orderService,
                      VehicleService vehicleService,
                      DispatcherService dispatcherService,
                      @KernelExecutor ExecutorService kernelExecutor,
//                      @ServiceCallWrapper CallWrapper callWrapper,
                      @Nonnull TCSObjectService objectService) {
    this.orderService = requireNonNull(orderService, "orderService");
    this.vehicleService = requireNonNull(vehicleService, "vehicleService");
    this.dispatcherService = requireNonNull(dispatcherService, "dispatcherService");
    this.kernelExecutor = requireNonNull(kernelExecutor, "kernelExecutor");
//    this.callWrapper = requireNonNull(callWrapper, "callWrapper");
    this.objectService = requireNonNull(objectService, "objectService");
  }

注入的原理是这些类的实例化都是通过专门的构造器来负责,它会读取类所需要的参数,把已经实例化的参数依次传递减去。可达到一个服务被不同的类所使用,但是不用负责创建(实例化),因为某些服务只能创建一次,比如opentcs的订单服务,但是需要用到的场合确是很多的。

openTCS-Kernel/src/guiceConfig/java/org/opentcs/kernel/RunKernel.java
public static void main(String[] args)
    throws Exception {
  System.setSecurityManager(new SecurityManager());
  Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(false));
  System.setProperty(org.opentcs.util.configuration.Configuration.PROPKEY_IMPL_CLASS,
                     org.opentcs.util.configuration.XMLConfiguration.class.getName());
  Environment.logSystemInfo();
  LOG.debug("Setting up openTCS kernel {}...", Environment.getBaselineVersion());
  Injector injector = Guice.createInjector(customConfigurationModule());
  injector.getInstance(KernelStarter.class).startKernel();
}

private static Module customConfigurationModule() {
  List<KernelInjectionModule> defaultModules
      = Arrays.asList(new DefaultKernelInjectionModule(),
                      new DefaultDispatcherModule(),
                      new DefaultRouterModule(),
                      new DefaultSchedulerModule(),
                      new DefaultRecoveryEvaluatorModule());
  ConfigurationBindingProvider bindingProvider = configurationBindingProvider();
  for (KernelInjectionModule defaultModule : defaultModules) {
    defaultModule.setConfigBindingProvider(bindingProvider);
  }
  return Modules.override(defaultModules)
      .with(findRegisteredModules(bindingProvider));
}

private static List<KernelInjectionModule> findRegisteredModules(
    ConfigurationBindingProvider bindingProvider) {
  List<KernelInjectionModule> registeredModules = new LinkedList<>();
  for (KernelInjectionModule module : ServiceLoader.load(KernelInjectionModule.class)) {
    LOG.info("Integrating injection module {}", module.getClass().getName());
    module.setConfigBindingProvider(bindingProvider);
    registeredModules.add(module);
  }
  return registeredModules;
}

private static ConfigurationBindingProvider configurationBindingProvider() {
  return new Cfg4jConfigurationBindingProvider(
      Paths.get(System.getProperty("opentcs.base", "."),
                "config",
                "opentcs-kernel-defaults-baseline.properties")
          .toAbsolutePath(),
      Paths.get(System.getProperty("opentcs.base", "."),
                "config",
                "opentcs-kernel-defaults-custom.properties")
          .toAbsolutePath(),
      Paths.get(System.getProperty("opentcs.home", "."),
                "config",
                "opentcs-kernel.properties")
          .toAbsolutePath()
  );
}



可以看到入口函数main在内核源码目录的guiceConfig下面,所有模块的源码目录下都有这样一个目录,它控制着将我们写的类跟接口做绑定。
上面比较关键的地方是创建一个injector,然后再用它实例化kernelstarter。

⚠️opentcs这种在guiceconfig目录下面创建文件记录接口绑定的做法实际上是java自带的一套标准(SPI),跟guice这个框架没有什么关系,取这个名字纯粹是因为用了guice,并不是guice规定要写这么个配置文件。

1.3 createInjector vs getInstance 有何区别

创建注入器是前提,使用注入器实例化目标类是最终目的。前一步创建的东西是基础组建,后一步是启动最终的目标类。

具体的启动过程

customConfigurationModule()

我们先看findRegisteredModules

for (KernelInjectionModule module : ServiceLoader.load(KernelInjectionModule.class))

这句话比较关键,很容易让人摸不着头脑。debug后仔细看了module的名字才知道是所有基于KernelInjectionModule扩展类的名字,那么这个ServiceLoader load做了什么事情才可以达到如此效果呢。查询了解到这是java自在的功能,我们暂且不管它是什么。只有知道可以通过它获取到哪些类扩展了这个内核注入模块,会想一下扩展车辆适配器熟悉需要扩展内核注入模块,然后还需要提供一个文本文件,这个文件文件的名字就是内核注入模块的全称内容自然就是扩展类的全称。可以发现所有扩展模块HTTP接口、TCP接口、RMI接口都有这样一个文件,而这个MERA-INF.services路径是标准规定,ServiceLoader会根据load方法提供的类名去各模块下的标准目录寻找此类名的文本文件,再把文件里的内容读取出来,这样我们就可以知道有哪些类扩展了目标类。

查看ServiceLoader的源代码,可以发现有一个路径前缀,正是上述的标准路径。

public final class ServiceLoader<S>
    implements Iterable<S>
{
    private static final String PREFIX = "META-INF/services/";

请添加图片描述

⚠️ServiceLoader是SPI中的关键函数,用于查找哪些jar包里面有META-INF/services/这个文件夹。

  module.setConfigBindingProvider(bindingProvider);
    registeredModules.add(module

接着设置模块的privider,这个又是什么东西呢,其实就是一些路径和环境变量

ConfigurableInjectionModule->KernelInjectionModule

这个setConfigBindingProvider方法在ConfiguragleInjectionModule中定义,KernelInjectionModule是扩展了此类。

public class Cfg4jConfigurationBindingProvider
    implements ConfigurationBindingProvider {

  /**
   * This class's logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(Cfg4jConfigurationBindingProvider.class);
  /**
   * The key of the (system) property containing the reload interval.
   */
  private static final String PROPKEY_RELOAD_INTERVAL = "opentcs.cfg4j.reload.interval";
  /**
   * Default configuration file name.
   */
  private final Path defaultsPath;
  /**
   * Supplementary configuration files.
   */
  private final Path[] supplementaryPaths;
  /**
   * The (cfg4j) configuration provider.
   */
  private final ConfigurationProvider provider;

下面是比较关键的一步,一开始创建的四个类其实是空的,现在需要把它们替换成继承了这些类的类,说起来比较绕口。我们注意到创建的不是四个基础类吗,但是实际上却远不止这些类。还记得KernelInjectionModule被扩展了很多次吗,每个车辆适配器加API接口就有四五个了,而后面的四个类跟调度相关只有一个唯一的扩展类,这个涉及到注入的另外一个概念了(singleton 单、多绑定)。

( new DefaultKernelInjectionModule(),
  new DefaultDispatcherModule(),
  new DefaultRouterModule(),
  new DefaultSchedulerModule(),
  new DefaultRecoveryEvaluatorModule())
return Modules.override(defaultModules)
.with(findRegisteredModules(bindingProvider))

经过这一步内核只是找到了注册的模块,从名字上我们可以看出,此时还没有开始实例化。

直到createInjector这一步模块开始实例化了,这里面发生了什么呢。

Injector injector = Guice.createInjector(customConfigurationModule());

public static Injector createInjector(Stage stage, Iterable<? extends Module> modules) {
return new InternalInjectorCreator().stage(stage).addModules(modules).build();
}

更加具体的过程需要了解juice注入的原理,我们目前先了解大概。这个createInjector帮我们把所有模块给实例化了,然后得到一个注入器injector,使用它再去启动KernelStarter。

现在还有一个疑惑,juice帮我们初始化的这些模块彼此之间又是有依赖关系的,如何保证没有依赖关系的模块先启动,有依赖关系的模块按照顺序启动呢。

⚠️分析每个类的初始化参数就能找出哪些类是没有依赖关系的。

[20210323-20:13:48-312] INFO main o.o.c.cfg4j.Cfg4jConfigurationBindingProvider.buildSource(): Using default configuration file /Users/touchmii/IntelliJProjects/OpenTCS-4.17/openTCS-Kernel/build/install/openTCS-Kernel/./config/opentcs-kernel-defaults-baseline.properties...
[20210323-20:13:48-344] WARNING main o.o.c.cfg4j.Cfg4jConfigurationBindingProvider.buildSource(): Supplementary configuration file /Users/touchmii/IntelliJProjects/OpenTCS-4.17/openTCS-Kernel/build/install/openTCS-Kernel/./config/opentcs-kernel-defaults-custom.properties not found, skipped.
[20210323-20:13:48-347] INFO main o.o.c.cfg4j.Cfg4jConfigurationBindingProvider.buildSource(): Using overrides from supplementary configuration file /Users/touchmii/IntelliJProjects/OpenTCS-4.17/openTCS-Kernel/build/install/openTCS-Kernel/./config/opentcs-kernel.properties...
[20210323-20:13:48-368] INFO main o.o.c.cfg4j.Cfg4jConfigurationBindingProvider.reloadInterval(): Using configuration reload interval of 10000 ms.
[20210323-20:13:48-371] INFO main o.c.provider.ConfigurationProviderBuilder.build() : Initializing ConfigurationProvider with org.opentcs.configuration.cfg4j.CachedConfigurationSource source, org.opentcs.configuration.cfg4j.PeriodicalReloadStrategy reload strategy and org.cfg4j.source.context.environment.DefaultEnvironment environment
[20210323-20:13:48-414] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.virtualvehicle.LoopbackCommAdapterModule
[20210323-20:13:48-419] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.adminwebapi.AdminWebApiModule
[20210323-20:13:48-424] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.servicewebapi.ServiceWebApiModule
[20210323-20:13:48-457] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.rmi.RmiServicesModule
[20210323-20:13:48-462] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.statistics.StatisticsModule
[20210323-20:13:48-471] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.xmlhost.TcpHostInterfaceModule
[20210323-20:13:48-474] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.sockethost.TcpMESInterfaceModule
[20210323-20:13:48-482] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.websockets.WebSocketsHostInterfaceModule
[20210323-20:13:48-496] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.zjw.vehicle.ExampleKernelInjectionModule
[20210323-20:13:48-500] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.vehicle.ModbusAdapterKernelInjectionModule
[20210323-20:13:48-507] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.http.HTTPAdapterKernelInjectionModule
[20210323-20:13:48-511] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.vehicleqian.QianAdapterKernelInjectionModule
[20210323-20:13:48-519] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.vehiclejbh.JbhAdapterKernelInjectionModule
[20210323-20:13:48-522] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.vehicletcp.TCPAdapterKernelInjectionModule
[20210323-20:13:48-535] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.vrep.ExampleKernelInjectionModule
[20210323-20:13:48-556] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.serial.SerialAdapterKernelInjectionModule
[20210323-20:13:48-560] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module com.lvsrobot.apollo.ApolloAdapterKernelInjectionModule
[20210323-20:13:48-570] INFO main o.o.kernel.RunKernel.findRegisteredModules() : Integrating injection module org.opentcs.kernel.extensions.controlcenter.ControlCenterModule
以上为模块查找阶段,以下为模块初始化
[20210323-20:13:50-166] WARNING main 
o.o.k.e.rmi.RmiServicesModule.configure() : SSL encryption disabled, connections will not be secured!
[20210323-20:13:55-527] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: org.opentcs.virtualvehicle.LoopbackCommunicationAdapterFactory
[20210323-20:13:55-537] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.zjw.vehicle.ExampleCommAdapterFactory
[20210323-20:13:55-538] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.vehicle.ExampleCommAdapterFactory
[20210323-20:13:55-572] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.http.ExampleCommAdapterFactory
[20210323-20:13:55-579] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.vehicleqian.ExampleCommAdapterFactory
[20210323-20:13:55-599] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.vehiclejbh.ExampleCommAdapterFactory
[20210323-20:13:55-607] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.vehicletcp.ExampleCommAdapterFactory
[20210323-20:13:55-613] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.vrep.ExampleCommAdapterFactory
[20210323-20:13:55-628] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.serial.ExampleCommAdapterFactory
[20210323-20:13:55-635] INFO main o.o.k.vehicles.VehicleCommAdapterRegistry.<init>() : Setting up communication adapter factory: com.lvsrobot.apollo.ExampleCommAdapterFactory
[20210323-20:14:00-184] INFO main o.o.k.e.rmi.XMLFileUserAccountPersister.loadUserAccounts(): Account data file does not exist, no user accounts available.
[UPTIME: 0s] Connected to MES : /127.0.0.1:8735
[20210323-20:14:01-143] INFO main o.o.kernel.StandardKernel.setState() : Switching kernel to state 'MODELLING'
[20210323-20:14:01-320] INFO Thread-2 o.e.j.u.log.Log.initialized() : Logging initialized @13814ms to org.eclipse.jetty.util.log.Slf4jLog
[20210323-20:14:01-647] INFO Thread-2 s.e.jetty.EmbeddedJettyServer.ignite() : == Spark has ignited ...
[20210323-20:14:01-648] INFO Thread-2 s.e.jetty.EmbeddedJettyServer.ignite() : >> Listening on 127.0.0.1:55100
[20210323-20:14:01-660] INFO Thread-2 o.e.j.server.Server.doStart() : jetty-9.4.18.v20190429; built: 2019-04-29T20:42:08.989Z; git: e1bc35120a6617ee3df052294e433f3a25ce7097; jvm 1.8.0_281-b09
[20210323-20:14:01-868] INFO Thread-2 o.e.j.s.session.DefaultSessionIdManager.doStart() : DefaultSessionIdManager workerName=node0
[20210323-20:14:01-869] INFO Thread-2 o.e.j.s.session.DefaultSessionIdManager.doStart() : No SessionScavenger set, using defaults
[20210323-20:14:01-876] INFO Thread-2 o.e.j.s.session.HouseKeeper.startScavenging() : node0 Scavenging every 600000ms
[20210323-20:14:01-926] INFO Thread-2 o.e.j.server.AbstractConnector.doStart() : Started ServerConnector@7d23cecc{HTTP/1.1,[http/1.1]}{127.0.0.1:55100}
[20210323-20:14:01-927] INFO Thread-2 o.e.j.server.Server.doStart()
public class DefaultKernelInjectionModule
    extends KernelInjectionModule {
  @Override
  protected void configure() {
    configureEventHub();
    configureKernelExecutor();
    // Ensure that the application's home directory can be used everywhere.
    File applicationHome = new File(System.getProperty("opentcs.home", "."));
    bind(File.class)
        .annotatedWith(ApplicationHome.class)
        .toInstance(applicationHome);
    // A single global synchronization object for the kernel.
    bind(Object.class)
        .annotatedWith(GlobalSyncObject.class)
        .to(Object.class)
        .in(Singleton.class);
    // The kernel's data pool structures.
    bind(TCSObjectPool.class).in(Singleton.class);
    bind(Model.class).in(Singleton.class);
    bind(TransportOrderPool.class).in(Singleton.class);
    bind(NotificationBuffer.class).in(Singleton.class);
    bind(ObjectNameProvider.class)
        .to(PrefixedUlidObjectNameProvider.class)
        .in(Singleton.class);
    configurePersistence();
    bind(VehicleCommAdapterRegistry.class)
        .in(Singleton.class);
    configureVehicleControllers();
    bind(AttachmentManager.class)
        .in(Singleton.class);
    bind(VehicleEntryPool.class)
        .in(Singleton.class);
    bind(StandardKernel.class)
        .in(Singleton.class);
    bind(LocalKernel.class)
        .to(StandardKernel.class);
    configureKernelStatesDependencies();
    configureKernelStarterDependencies();
    configureSslParameters();
    configureKernelServicesDependencies();
    // Ensure all of these binders are initialized.
    extensionsBinderAllModes();
    extensionsBinderModelling();
    extensionsBinderOperating();
    vehicleCommAdaptersBinder();
  }
  private void configureKernelServicesDependencies() {
    bind(StandardPlantModelService.class).in(Singleton.class);
    bind(PlantModelService.class).to(StandardPlantModelService.class);
    bind(InternalPlantModelService.class).to(StandardPlantModelService.class);
    bind(StandardTransportOrderService.class).in(Singleton.class);
    bind(TransportOrderService.class).to(StandardTransportOrderService.class);
    bind(InternalTransportOrderService.class).to(StandardTransportOrderService.class);
    bind(StandardVehicleService.class).in(Singleton.class);
    bind(VehicleService.class).to(StandardVehicleService.class);
    bind(InternalVehicleService.class).to(StandardVehicleService.class);
    bind(StandardTCSObjectService.class).in(Singleton.class);
    bind(TCSObjectService.class).to(StandardTCSObjectService.class);
    bind(StandardNotificationService.class).in(Singleton.class);
    bind(NotificationService.class).to(StandardNotificationService.class);
    bind(StandardRouterService.class).in(Singleton.class);
    bind(RouterService.class).to(StandardRouterService.class);
    bind(StandardDispatcherService.class).in(Singleton.class);
    bind(DispatcherService.class).to(StandardDispatcherService.class);
    bind(StandardSchedulerService.class).in(Singleton.class);
    bind(SchedulerService.class).to(StandardSchedulerService.class);
  }
  private void configureVehicleControllers() {
    install(new FactoryModuleBuilder().build(VehicleControllerFactory.class));
    bind(DefaultVehicleControllerPool.class)
        .in(Singleton.class);
    bind(VehicleControllerPool.class)
        .to(DefaultVehicleControllerPool.class);
    bind(LocalVehicleControllerPool.class)
        .to(DefaultVehicleControllerPool.class);
  }
  private void configurePersistence() {
    bind(ModelPersister.class).to(XMLFileModelPersister.class);
  }
  @SuppressWarnings("deprecation")
  private void configureEventHub() {
    EventBus newEventBus = new SimpleEventBus();
    bind(EventHandler.class)
        .annotatedWith(ApplicationEventBus.class)
        .toInstance(newEventBus);
    bind(org.opentcs.util.event.EventSource.class)
        .annotatedWith(ApplicationEventBus.class)
        .toInstance(newEventBus);
    bind(EventBus.class)
        .annotatedWith(ApplicationEventBus.class)
        .toInstance(newEventBus);
    // A binding for the kernel's one and only central event hub.
    BusBackedEventHub<org.opentcs.util.eventsystem.TCSEvent> busBackedHub
        = new BusBackedEventHub<>(newEventBus, org.opentcs.util.eventsystem.TCSEvent.class);
    busBackedHub.initialize();
    bind(new TypeLiteral<org.opentcs.util.eventsystem.EventListener<org.opentcs.util.eventsystem.TCSEvent>>() {
    })
        .annotatedWith(org.opentcs.customizations.kernel.CentralEventHub.class)
        .toInstance(busBackedHub);
    bind(new TypeLiteral<org.opentcs.util.eventsystem.EventSource<org.opentcs.util.eventsystem.TCSEvent>>() {
    })
        .annotatedWith(org.opentcs.customizations.kernel.CentralEventHub.class)
        .toInstance(busBackedHub);
    bind(new TypeLiteral<org.opentcs.util.eventsystem.EventHub<org.opentcs.util.eventsystem.TCSEvent>>() {
    })
        .annotatedWith(org.opentcs.customizations.kernel.CentralEventHub.class)
        .toInstance(busBackedHub);
  }
  private void configureKernelStatesDependencies() {
    // A map for KernelState instances to be provided at runtime.
    MapBinder<Kernel.State, KernelState> stateMapBinder
        = MapBinder.newMapBinder(binder(), Kernel.State.class, KernelState.class);
    stateMapBinder.addBinding(Kernel.State.SHUTDOWN).to(KernelStateShutdown.class);
    stateMapBinder.addBinding(Kernel.State.MODELLING).to(KernelStateModelling.class);
    stateMapBinder.addBinding(Kernel.State.OPERATING).to(KernelStateOperating.class);
    bind(OrderPoolConfiguration.class)
        .toInstance(getConfigBindingProvider().get(OrderPoolConfiguration.PREFIX,
                                                   OrderPoolConfiguration.class));
    transportOrderCleanupApprovalBinder();
    orderSequenceCleanupApprovalBinder();
  }
  private void configureKernelStarterDependencies() {
    bind(KernelApplicationConfiguration.class)
        .toInstance(getConfigBindingProvider().get(KernelApplicationConfiguration.PREFIX,
                                                   KernelApplicationConfiguration.class));
  }
  private void configureSslParameters() {
    SslConfiguration configuration
        = getConfigBindingProvider().get(SslConfiguration.PREFIX,
                                         SslConfiguration.class);
    SslParameterSet sslParamSet = new SslParameterSet(SslParameterSet.DEFAULT_KEYSTORE_TYPE,
                                                      new File(configuration.keystoreFile()),
                                                      configuration.keystorePassword(),
                                                      new File(configuration.truststoreFile()),
                                                      configuration.truststorePassword());
    bind(SslParameterSet.class).toInstance(sslParamSet);
  }
  private void configureKernelExecutor() {
    ScheduledExecutorService executor
        = new LoggingScheduledThreadPoolExecutor(
            1,
            (runnable) -> {
              Thread thread = new Thread(runnable, "kernelExecutor");
              thread.setUncaughtExceptionHandler(new UncaughtExceptionLogger(false));
              return thread;
            }
        );
    bind(ScheduledExecutorService.class)
        .annotatedWith(KernelExecutor.class)
        .toInstance(executor);
    bind(ExecutorService.class)
        .annotatedWith(KernelExecutor.class)
        .toInstance(executor);
    bind(Executor.class)
        .annotatedWith(KernelExecutor.class)
        .toInstance(executor);
  }
}

2.扩展HTTP接口

2.1通过HTTP接口发送指令给指定车辆

发送指令给适配器需要获取车辆名称的引用,再调用VehicleService的sendCommAdapter方法即可。在适配器中重写BasicVehicleCommAdapter中的

excute方法,接收VehicleCommAdapterEvent事件。

public String sendCommand(String name, Command command) {
    try {
      TCSObjectReference<Vehicle> vehicleReference = vehicleService.fetchObject(Vehicle.class, name).getReference();
      VehicleCommAdapterEvent event = new VehicleCommAdapterEvent(name, command.getCommand());
      try {
//      callWrapper.call(() -> vehicleService.sendCommAdapterCommand(vehicleReference, new PublishEventCommand(event)));
        vehicleService.sendCommAdapterCommand(vehicleReference, new PublishEventCommand(event));

      } catch (Exception e) {
        LOG.warn("Can't send command to vehicle");
        e.getMessage();
        throw new ObjectUnknownException(("Can't send command to vehicle"));
      }

    } catch (Exception e) {
      e.getMessage();
      LOG.warn("Can't found vechile name: {}", name);
      throw new ObjectUnknownException("Unknow Vehicle name: " + name);
    }
    return String.format("Send command: %s to Vehicle: %s success.", command.getCommand(),name);
  }

@Override
public void execute(AdapterCommand command) {
        PublishEventCommand publishCommand = (PublishEventCommand) command;

3.适配器任务

基础适配器有两个列表分别是移动指令列表和以发送指令列表,当判断适配器可以发送命令则从移动指令列表取出添加到已发送列表

BasicVehicleCommAdapter
/**
     * This adapter's command queue.
     */
    private final Queue<MovementCommand> commandQueue = new LinkedBlockingQueue<>();
    /**
     * Contains the orders which have been sent to the vehicle but which haven't
     * been executed by it, yet.
     */
    private final Queue<MovementCommand> sentQueue = new LinkedBlockingQueue<>();
    
    if (getSentQueue().size() < sentQueueCapacity) && !getCommandQueue().isEmpty()
    curCmd = getCommandQueue().poll();
                    if (curCmd != null) {
                        try {
                            sendCommand(curCmd);
                            //send driver order,adapter implement sendCommand,receive curCmd
                            getSentQueue().add(curCmd);
                            //add driving order to the queue of sent orders
                            getProcessModel().commandSent(curCmd);
                            //Notify the kernel that the drive order has been sent to the vehicle

跟车辆通行的适配器只操作已发送指令列表,使用peek获取要发送的命令,车辆到达预期地点则使用poll删除已发送指令头部,再使用ProcessModel通知内核,代表车辆执行此命令成功。然后获取下一个指令按照上面的步骤重复执行,直到将所有指令执行情况都上报给内核,此时才会判断路径执行成功。

ExampleCommAdapter
curCommand = getSentQueue().peek();
MovementCommand sentcmd = getSentQueue().poll();
getProcessModel().commandExecuted(curCommand);
3.2车辆执行完的移动命令如何通知到内核
/**
   * Notifies observers that the given command has been executed by the comm adapter/vehicle.
   *
   * @param executedCommand The command that has been executed.
   */
  public void commandExecuted(@Nonnull MovementCommand executedCommand) {
    getPropertyChangeSupport().firePropertyChange(Attribute.COMMAND_EXECUTED.name(),
                                                  null,
                                                  executedCommand);
  }

  /**
   * Notifies observers that the given command could not be executed by the comm adapter/vehicle.
   *
   * @param failedCommand The command that could not be executed.
   */
  public void commandFailed(@Nonnull MovementCommand failedCommand) {
    getPropertyChangeSupport().firePropertyChange(Attribute.COMMAND_FAILED.name(),
                                                  null,
                                                  failedCommand);
  }

DefaultVehicleController 重写PropertyChangeListener的方法实现监听适配器发送过来的事件,如果执行命令失败会取消此车辆的订单

DefaultVehicleController

//属性变更回调函数,使用getProcessModel发送车辆消息监听到车辆属性变更时调用
  @Override
  public void propertyChange(PropertyChangeEvent evt) {
    if (evt.getSource() != commAdapter.getProcessModel()) {
      return;
    }

    handleProcessModelEvent(evt);
  }
//处理驱动器消息类型,调用不同的处理函数,如指令发送成功或位置变更
  private void handleProcessModelEvent(PropertyChangeEvent evt) {
    eventBus.onEvent(new ProcessModelEvent(evt.getPropertyName(),
            commAdapter.createTransferableProcessModel()));

    if (Objects.equals(evt.getPropertyName(), VehicleProcessModel.Attribute.POSITION.name())) {
      updateVehiclePosition((String) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.PRECISE_POSITION.name())) {
      updateVehiclePrecisePosition((Triple) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.ORIENTATION_ANGLE.name())) {
      vehicleService.updateVehicleOrientationAngle(vehicle.getReference(),
              (Double) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.ENERGY_LEVEL.name())) {
      vehicleService.updateVehicleEnergyLevel(vehicle.getReference(), (Integer) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.LOAD_HANDLING_DEVICES.name())) {
      vehicleService.updateVehicleLoadHandlingDevices(vehicle.getReference(),
              (List<LoadHandlingDevice>) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(), VehicleProcessModel.Attribute.STATE.name())) {
      updateVehicleState((Vehicle.State) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.COMM_ADAPTER_STATE.name())) {
      updateCommAdapterState((VehicleCommAdapter.State) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.COMMAND_EXECUTED.name())) {
      commandExecuted((MovementCommand) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.COMMAND_FAILED.name())) {
      dispatcherService.withdrawByVehicle(vehicle.getReference(), true, false);
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.USER_NOTIFICATION.name())) {
      notificationService.publishUserNotification((UserNotification) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.COMM_ADAPTER_EVENT.name())) {
      eventBus.onEvent((VehicleCommAdapterEvent) evt.getNewValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.VEHICLE_PROPERTY.name())) {
      VehicleProcessModel.VehiclePropertyUpdate propUpdate
              = (VehicleProcessModel.VehiclePropertyUpdate) evt.getNewValue();
      vehicleService.updateObjectProperty(vehicle.getReference(),
              propUpdate.getKey(),
              propUpdate.getValue());
    }
    else if (Objects.equals(evt.getPropertyName(),
            VehicleProcessModel.Attribute.TRANSPORT_ORDER_PROPERTY.name())) {
      VehicleProcessModel.TransportOrderPropertyUpdate propUpdate
              = (VehicleProcessModel.TransportOrderPropertyUpdate) evt.getNewValue();
      if (currentDriveOrder != null) {
        vehicleService.updateObjectProperty(currentDriveOrder.getTransportOrder(),
                propUpdate.getKey(),
                propUpdate.getValue());
      }
    }
  }
3.4commandExcuted 判断移动指令是否真的执行成功
private void commandExecuted(MovementCommand executedCommand) {
    requireNonNull(executedCommand, "executedCommand");

    synchronized (commAdapter) {
      // Check if the executed command is the one we expect at this point.
      MovementCommand expectedCommand = commandsSent.peek();
      if (!Objects.equals(expectedCommand, executedCommand)) {
        LOG.warn("{}: Communication adapter executed unexpected command: {} != {}",
                vehicle.getName(),
                executedCommand,
                expectedCommand);
        // XXX The communication adapter executed an unexpected command. Do something!
      }
      // Remove the command from the queue, since it has been processed successfully.
      lastCommandExecuted = commandsSent.remove();
      // Free resources allocated for the command before the one now executed.
      Set<TCSResource<?>> oldResources = allocatedResources.poll();
      if (oldResources != null) {
        LOG.debug("{}: Freeing resources: {}", vehicle.getName(), oldResources);
        scheduler.free(this, oldResources);
      }
      else {
        LOG.debug("{}: Nothing to free.", vehicle.getName());
      }
      // Check if there are more commands to be processed for the current drive order.
      if (pendingCommand == null && futureCommands.isEmpty()) {
        LOG.debug("{}: No more commands in current drive order", vehicle.getName());
        // Check if there are still commands that have been sent to the communication adapter but
        // not yet executed. If not, the whole order has been executed completely - let the kernel
        // know about that so it can give us the next drive order.
        if (commandsSent.isEmpty() && !waitingForAllocation) {
          LOG.debug("{}: Current drive order processed", vehicle.getName());
          currentDriveOrder = null;
          // Let the kernel/dispatcher know that the drive order has been processed completely (by
          // setting its state to AWAITING_ORDER).
          vehicleService.updateVehicleRouteProgressIndex(vehicle.getReference(),
                  Vehicle.ROUTE_INDEX_DEFAULT);
          vehicleService.updateVehicleProcState(vehicle.getReference(),
                  Vehicle.ProcState.AWAITING_ORDER);
        }
      }
      // There are more commands to be processed.
      // Check if we can send another command to the comm adapter.
      else if (canSendNextCommand()) {
        allocateForNextCommand();
      }
    }
  }

/**
   * Sets the point which a vehicle is expected to occupy next.
   *
   * @param vehicleRef A reference to the vehicle to be modified.
   * @param pointRef A reference to the point which the vehicle is expected to
   * occupy next.
   * @throws ObjectUnknownException If the referenced vehicle does not exist.
   * @deprecated Use{@link InternalVehicleService#updateVehicleNextPosition(
   * org.opentcs.data.TCSObjectReference, org.opentcs.data.TCSObjectReference)} instead.
   */
  @Deprecated
  void setVehicleNextPosition(TCSObjectReference<Vehicle> vehicleRef,
                              TCSObjectReference<Point> pointRef)
      throws ObjectUnknownException;

/**
   * Sets a vehicle's index of the last route step travelled for the current
   * drive order of its current transport order.
   *
   * @param vehicleRef A reference to the vehicle to be modified.
   * @param index The new index.
   * @throws ObjectUnknownException If the referenced vehicle does not exist.
   * @deprecated Use{@link InternalVehicleService#updateVehicleRouteProgressIndex(
   * org.opentcs.data.TCSObjectReference, int)} instead.
   */
  @Deprecated
  void setVehicleRouteProgressIndex(TCSObjectReference<Vehicle> vehicleRef,
                                    int index)
      throws ObjectUnknownException;
      
 /**
   * Sets a transport order's state.
   * Note that transport order states are intended to be manipulated by the
   * dispatcher only. Calling this method from any other parts of the kernel may
   * result in undefined behaviour.
   *
   * @param ref A reference to the transport order to be modified.
   * @param newState The transport order's new state.
   * @throws ObjectUnknownException If the referenced transport order does not
   * exist.
   * @deprecated Use {@link InternalTransportOrderService#updateTransportOrderState(
   * org.opentcs.data.TCSObjectReference, org.opentcs.data.order.TransportOrder.State)} instead.
   */
  @Deprecated
  void setTransportOrderState(TCSObjectReference<TransportOrder> ref,
                              TransportOrder.State newState)
      throws ObjectUnknownException;     
@Override
public void run() {
  transportOrderService.fetchObjects(Vehicle.class).stream()
      .filter(vehicle -> vehicle.hasProcState(Vehicle.ProcState.AWAITING_ORDER))
      .forEach(vehicle -> checkForNextDriveOrder(vehicle));
}

private void checkForNextDriveOrder(Vehicle vehicle) {
  LOG.debug("Vehicle '{}' finished a drive order.", vehicle.getName());
  // The vehicle is processing a transport order and has finished a drive order.
  // See if there's another drive order to be processed.
  transportOrderService.updateTransportOrderNextDriveOrder(vehicle.getTransportOrder());
  TransportOrder vehicleOrder = transportOrderService.fetchObject(TransportOrder.class,
                                                                  vehicle.getTransportOrder());
  if (vehicleOrder.getCurrentDriveOrder() == null) {
    LOG.debug("Vehicle '{}' finished transport order '{}'",
              vehicle.getName(),
              vehicleOrder.getName());
    // The current transport order has been finished - update its state and that of the vehicle.
    transportOrderUtil.updateTransportOrderState(vehicle.getTransportOrder(),
                                                 TransportOrder.State.FINISHED);
    // Update the vehicle's procState, implicitly dispatching it again.
    vehicleService.updateVehicleProcState(vehicle.getReference(), Vehicle.ProcState.IDLE);
    vehicleService.updateVehicleTransportOrder(vehicle.getReference(), null);
    // Let the router know that the vehicle doesn't have a route any more.
    router.selectRoute(vehicle, null);
    // Update transport orders that are dispatchable now that this one has been finished.
    transportOrderUtil.markNewDispatchableOrders();
  }
  else {
    LOG.debug("Assigning next drive order to vehicle '{}'...", vehicle.getName());
    // Get the next drive order to be processed.
    DriveOrder currentDriveOrder = vehicleOrder.getCurrentDriveOrder();
    if (transportOrderUtil.mustAssign(currentDriveOrder, vehicle)) {
      if (configuration.rerouteTrigger() == DRIVE_ORDER_FINISHED) {
        LOG.debug("Trying to reroute vehicle '{}' before assigning the next drive order...",
                  vehicle.getName());
        rerouteUtil.reroute(vehicle);
      }
      
      // Get an up-to-date copy of the transport order in case the route changed
      vehicleOrder = transportOrderService.fetchObject(TransportOrder.class,
                                                       vehicle.getTransportOrder());
      currentDriveOrder = vehicleOrder.getCurrentDriveOrder();

      // Let the vehicle controller know about the new drive order.
      vehicleControllerPool.getVehicleController(vehicle.getName())
          .setDriveOrder(currentDriveOrder, vehicleOrder.getProperties());

      // The vehicle is still processing a transport order.
      vehicleService.updateVehicleProcState(vehicle.getReference(),
                                            Vehicle.ProcState.PROCESSING_ORDER);
    }
    // If the drive order need not be assigned, immediately check for another one.
    else {
      vehicleService.updateVehicleProcState(vehicle.getReference(),
                                            Vehicle.ProcState.AWAITING_ORDER);
      checkForNextDriveOrder(vehicle);
    }
  }
}

4.如何监听内核事件

@ApplicationEventBus EventSource eventSource

implicitDispatchTrigger = new ImplicitDispatchTrigger(this);
eventSource.subscribe(implicitDispatchTrigger);

/**
 * A handler for events emitted by an {@link EventSource}.
 *
 * @author Stefan Walter (Fraunhofer IML)
 */
public interface EventHandler {

  /**
   * Processes the event object.
   *
   * @param event The event object.
   */
  void onEvent(Object event);
}

实现EventHandler接口,重写onEvent方法,再注入EventSource,使用EventSource订阅EventHandler接口的实现类。

当产生事件时会调用onEvent方法,判断event事件的类型即可。

例子,此例子为tcp状态接口,端口号默认为44444,连接后不会自动断开,有新的事件都会发给客户端。

创建一个继承EventHandler接口的类ConnectionHandler,重写onEvent方法

/**
 * The task handling client connections.
 */
class ConnectionHandler
    implements Runnable,
               EventHandler {
      /**
       * The source of status events.
       */
      private final EventSource eventSource;

    /**
       * Creates a new ConnectionHandler.
       *
       * @param clientSocket The socket for communication with the client.
       * @param evtSource The source of the status events with which the handler
       * is supposed to register.
       */
      ConnectionHandler(Socket clientSocket,
                        EventSource evtSource,
                        String messageSeparator) {
        this.socket = requireNonNull(clientSocket, "clientSocket");
        this.eventSource = requireNonNull(evtSource, "evtSource");
        this.messageSeparator = requireNonNull(messageSeparator, "messageSeparator");
        checkArgument(clientSocket.isConnected(), "clientSocket is not connected");
      }

    /**
       * Adds an event to this handler's queue.
       *
       * @param event The event to be processed.
       */
      @Override
      public void onEvent(Object event) {
        requireNonNull(event, "event");
        if (event instanceof TCSObjectEvent) {
          commands.offer(new ConnectionCommand.ProcessObjectEvent((TCSObjectEvent) event));
        }
      }
    

}

使用,使用eventSource订阅ConnectionHandler实例,即可监听event事件

ConnectionHandler newHandler = new ConnectionHandler(clientSocket);

eventSource.subscribe(newHandler);

4.3题外话

那么这个方法时怎么找到的呢,首先我并不清楚tcp状态接口的机制,以为是轮询式的查询获取状态。所以我把目光锁定在了订单服务的类上面,

猜测,订单状态改变,或车辆被分配订单肯定会有事件之类的方法调用。否则不同的类要想知道订单和车辆状态发生了什么变化则很难实现,只能走通信的方式。

在tranportorder tool里面有个分配订单给车辆的方法中有调用emitObjectEvent方法,猜猜此方法的作用就是发送事情,必定有相应的接收事件方法。一路往上找,发现了

EventHandle接口,在查找此接口的实现发现在各处都有,基本可以判定就是通过它实现事件的监听,果不其然在tcp的状态接口中找到了简单的应用。

public TransportOrder setTransportOrderProcessingVehicle(
      TCSObjectReference<TransportOrder> orderRef,
      TCSObjectReference<Vehicle> vehicleRef)
      throws ObjectUnknownException {
    LOG.debug("method entry");
    TransportOrder order = objectPool.getObject(TransportOrder.class, orderRef);
    TransportOrder previousState = order.clone();
    if (vehicleRef == null) {
      order = objectPool.replaceObject(order.withProcessingVehicle(null));
    }
    else {
      Vehicle vehicle = objectPool.getObject(Vehicle.class, vehicleRef);
      order = objectPool.replaceObject(order.withProcessingVehicle(vehicle.getReference()));
    }
    objectPool.emitObjectEvent(order.clone(),
                               previousState,
                               TCSObjectEvent.Type.OBJECT_MODIFIED);
    return order;
  }

5.如何保存订单

当提交一个订单时,内核通过TransportOrderPool(TCSObjectPool)来储存订单,当订单被分配到具体车辆时则会添加到OrderReservationPool(strategies里)。我们应该要保存的订单是内核中未分配给车辆和执行完成的订单,

如需要实时保存可以写一个内核扩展,类似http服务扩展那样,监听订单的变化,将结果写入到数据库中。内核启动时读取数据库中的订单再写入到内核中。

如保存在文本文件中则可以使用json的格式在内核关闭时统一写入文件中,但是这样需要清空之前的纪录才可保证不会出现重复但不同状态的订单。

综上使用sqlite保存订单记录是不错的方式,后期如有需要跟换其它数据库也比较方便。

6.关于自动充电

@Inject
public ExampleCommAdapter(@Assisted Vehicle vehicle, ExampleAdapterComponentsFactory componentsFactory, @KernelExecutor ExecutorService kernelExecutor, TransportOrderService orderService, @Nonnull TCSObjectService objectService) {
    //父类BasicVehicleCommAdapter实例需要的参数
    super(new ExampleProcessModel(vehicle), 30, 30, "Charge");

新建适配器的时候需要提供充电的动作名称,在充电类型的点中添加此动作。如果对应不上的话会提示找不到合适的充电点,通过下面的源码可以看到其实就是判断车辆的充电动作和地图上所有位置允许的动作是否匹配。

那么这样做有什么用意呢,我们不妨这样想,如果调度系统连接了多种类型的车辆,而不同车辆的充电方式可能不一样,那么它们需要去到不同的点才能充电,这样调度系统就可以区分出来了,不得不说openTCS考虑的还是挺周到的。

openTCS-Strategies-Default/src/main/java/org/opentcs/strategies/basic/dispatching/phase/recharging/DefaultRechargePositionSupplier.java
@Override
public List<DriveOrder.Destination> findRechargeSequence(Vehicle vehicle) {
  requireNonNull(vehicle, "vehicle");

  if (vehicle.getCurrentPosition() == null) {
    return new ArrayList<>();
  }

  Map<Location, Set<Point>> rechargeLocations
      = findLocationsForOperation(vehicle.getRechargeOperation(),
                                  vehicle,
                                  router.getTargetedPoints());

  String assignedRechargeLocationName = vehicle.getProperty(PROPKEY_ASSIGNED_RECHARGE_LOCATION);
  if (assignedRechargeLocationName != null) {
    Location location = pickLocationWithName(assignedRechargeLocationName,
                                             rechargeLocations.keySet());
    if (location == null) {
      return new ArrayList<>();
    }
    // XXX Strictly, we should check whether there is a viable route to the location.
    return Arrays.asList(createDestination(location, vehicle.getRechargeOperation()));
  }

  String preferredRechargeLocationName = vehicle.getProperty(PROPKEY_PREFERRED_RECHARGE_LOCATION);
  if (assignedRechargeLocationName != null) {
    Location location = pickLocationWithName(preferredRechargeLocationName,
                                             rechargeLocations.keySet());
    if (location != null) {
      // XXX Strictly, we should check whether there is a viable route to the location.
      return Arrays.asList(createDestination(location, vehicle.getRechargeOperation()));
    }
  }

  Location bestLocation = findCheapestLocation(rechargeLocations, vehicle);
  if (bestLocation != null) {
    return Arrays.asList(createDestination(bestLocation, vehicle.getRechargeOperation()));
  }

  return new ArrayList<>();
}
private Map<Location, Set<Point>> findLocationsForOperation(String operation,
                                                            Vehicle vehicle,
                                                            Set<Point> targetedPoints) {
  Map<Location, Set<Point>> result = new HashMap<>();

  for (Location curLoc : plantModelService.fetchObjects(Location.class)) {
    LocationType lType = plantModelService.fetchObject(LocationType.class, curLoc.getType());
    if (lType.isAllowedOperation(operation)) {
      Set<Point> points = findUnoccupiedAccessPointsForOperation(curLoc,
                                                                 operation,
                                                                 vehicle,
                                                                 targetedPoints);
      if (!points.isEmpty()) {
        result.put(curLoc, points);
      }
    }
  }
6.2 相关配置

临界电量

7.控制控制中心界面扩展

8.上位机界面扩展

9.调度优化

关于默认策略

再api base包里面定义了内核的所有接口,如下图
在这里插入图片描述

在这里插入图片描述

默认策略实现的是Dispatcher, Scheduler, Router, Recovery这四个服务里面的方法, 实际的服务是再Kernel这个包里面运行的上面的四个服务具体就是调用了默认策略里面的类.

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
opentcs是一个开源的自动化物流系统,可以用于控制和优化物流车辆的运行。对于opentcs二次开发,可以参考官方的开发者文档和指南,这些文档提供了详细的指导和参考资料。 在opentcs二次开发中,注入是一个重要的概念。通过注入,可以方便地使用opentcs的各种服务和功能。比如,如果需要改写某个内核扩展或适配器的功能,可以使用注入来获取需要使用的服务,如DispatcherService和ObjectService。使用注入可以避免手动创建和传递所需的服务,使开发过程更加简洁和高效。 在opentcs二次开发中,通过createInjector方法可以创建一个注入器(Injector)对象。注入器是一个用于管理和提供依赖关系的容器,它可以根据配置和模块来初始化和注入相关的服务和组件。 总结来说,opentcs二次开发可以通过参考开发文档和指南,了解注入的使用以及如何创建和配置注入器来实现。这样就可以根据需求改写和扩展opentcs的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [openTCS:开发人员指南zh.pdf](https://download.csdn.net/download/weixin_42289548/12446481)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [openTCS二次开发简要事项](https://blog.csdn.net/touchmii/article/details/127429199)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [openTCS二次开发简要事项详细)](https://blog.csdn.net/touchmii/article/details/127430188)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值