2018 ICPA FMIS implications and example
R. Andres Ferreyra (Unlicensed), Stuart Rhea (Unlicensed)
Prompt from abstract:
This enables the FMIS to read/write to a wide variety of systems with little incremental effort, using ADAPT as a form of a digital agriculture Rosetta stone.
Write below:
Incoming data from a Machine and Implement Control System (MICS, i.e., the controller in the cab. Data flow shown in red): A proprietary-format data file coming from a controller in the field is converted by a manufacturer-specific plug-in into an instance of the object model; a farm management information system (“FMIS A”) consumes the data.
Communication between Farm Management Information Systems (FMIS, i.e., farm management software. Data flow shown in blue): FMIS A creates an instance of the object model, populates it with the data it wants to transmit, and uses the ADAPT plug-in to serialize it to a file. This ADAPT-formatted file is transmitted to another FMIS using the Internet or another means. (File transport is out of scope of ADAPT, accommodating different solutions available in the industry.) FMIS B uses the ADAPT plug-in to convert the ADAPT-formatted file to an instance of the object model, and then consumes the data.
Note how FMIS A and FMIS B are both supported by Reference Data, a distributed system of common unique identifiers for products to be shared across the industry by manufacturers and third-party data providers.
The ApplicationDataModel is the root object in ADAPT
ApplicationDataModel export = new ApplicationDataModel();
The Catalog object (inside the ApplicationDataModel) holds all the items you would expect to find in a "pick list". Alternatively, you could think of it as the place you put everything used "by reference" in any of the Documents you are trying to send.
export.Catalog = new Catalog();
The Documents object (inside the ApplicationDataModel) holds all the Plans, Recommendations, WorkOrders, and WorkRecords (and their component parts). We won't be using this in this example.
export.Documents = new Documents();
Create a "crop year" TimeScope to tag objects with.
TimeScope cropYear = new TimeScope();
cropYear.Description = "2018";
cropYear.DateContext = DateContextEnum.CropSeason;
export.Catalog.TimeScopes.Add(cropYear);
Create the Grower object. The constructor will automatically create the Id property and assign the next available ReferenceId integer.
Grower adaptGrower = new Grower();
Associate your internal, unique identifier to the Grower object by creating a UniqueId object and adding it to the Grower object's CompoundIdentifier.
UniqueId ourId = new UniqueId();
ourId.Id = "7d2253f0-fce6-4740-b3c3-f9c8ab92bfaa";
Notice the available IdTypeEnum choices. Not everybody uses the same way of identifying things in their system. As a result, we must support a number of identification schemes.
ourId.IdType = IdTypeEnum.UUID;
Almost as important as the identifier is knowing who created it (or where it came from).
ourId.Source = "www.some-company.com";
ourId.SourceType = IdSourceTypeEnum.URI;
Each CompoundIdentifier that is used in ADAPT can have multiple unique identifiers associated with it. Consider the possibilities here, not only can your identifier for something be persisted but also the identifiers that your trading partner assigns to the same object. PLEASE CONSIDER PERSISTING AND RETURNING IDENTIFIERS PASSED TO YOU IN THIS FASHION. This has the potential to result in a "frictionless" conversation once the initial mapping is done, but this benefit will only emerge if we all are "good neighbors".
adaptGrower.Id.UniqueIds.Add(ourId);
You may notice that many of the objects in ADAPT have a minimal number of properties. Don't panic if you can't find a place to put all your data. It may be in an associated object or intended to be expressed as a ContextItem.
adaptGrower.Name = "Some Random Grower";
Add the Grower object to the Catalog.
export.Catalog.Growers.Add(adaptGrower);
Create the Farm object. The constructor will automatically create the Id property and assign the next available ReferenceId integer.
Farm adaptFarm = new Farm();
ourId = new UniqueId();
ourId.Id = "c4618a14-4c60-4873-964f-52cc804f1856";
ourId.IdType = IdTypeEnum.UUID;
ourId.Source = "www.some-company.com";
ourId.SourceType = IdSourceTypeEnum.URI;
adaptFarm.Id.UniqueIds.Add(ourId);
adaptFarm.Description = "Some Random Farm";
Here we link this farm object to the grower. Note that this is the integer (ReferenceId) in the Grower's CompountIdentifier object.
adaptFarm.GrowerId = adaptGrower.Id.ReferenceId;
Add the Farm object to the Catalog.
export.Catalog.Farms.Add(adaptFarm);
The important take away here is that as part of integrating ADAPT, the FMIS must include logic that maps the data from its internal business objects (Grower, Farm, Field, Product, etc) into the corresponding business objects in ADAPT. In the same fashion, plugin creators must map the business objects contained in their targeted formats to ADAPT objects.
The PluginFactory looks at all the DLLs in the target directory to find any that implement the IPlugin interface.
var pluginFactory = new PluginFactory(pluginPath);
We're only interested in the ADMPlugin here, so I address it directly instead of looking through all the available plugins that the PluginFactory found.
var admPlugin = pluginFactory.GetPlugin("ADMPlugin");
The ADMPlugin doesn't require any initialization parameters.
admPlugin.Initialize();
Export to a local directory using the ADMPlugin
admPlugin.Export(export, outputPath);
The ADMPlugin creates an "adm" subdirectory in the indicated local directory that contains the following items:
An additional "documents" subdirectory that contains the protobuf-encoded document files.
An AdmVersion.info file that contains version information.
A ProprietaryValues.adm file
A Catalog.adm file that contains the zipped JSON serialization of the ApplicationDataModel.Catalog object.
This is logic to import the same data from the "adm" subdirectory we just created so you can compare it in the debugger if you want.
var pluginFactory2 = new PluginFactory(applicationPath);
var admPlugin2 = pluginFactory.GetPlugin("ADMPlugin");
admPlugin2.Initialize();
Note that when a plugin imports, the returned object is a list of ApplicationDataModel objects.
var imports = admPlugin2.Import(outputPath);