File Coverage

blib/lib/Convert/EDS/XDD.pm
Criterion Covered Total %
statement 79 111 71.1
branch 9 34 26.4
condition 1 15 6.6
subroutine 15 17 88.2
pod 2 2 100.0
total 106 179 59.2


line stmt bran cond sub pod time code
1 4     4   212339 use strict;
  4         5  
  4         98  
2 4     4   13 use warnings;
  4         5  
  4         75  
3 4     4   38 use v5.10.0;
  4         8  
4             package Convert::EDS::XDD;
5 4     4   13 no warnings 'uninitialized';
  4         8  
  4         212  
6              
7             # ABSTRACT: Converts the CANopen EDS format to XDD
8             our $VERSION = '0.007'; # VERSION
9              
10 4     4   16 use Carp;
  4         3  
  4         198  
11 4     4   1681 use Config::Tiny;
  4         3073  
  4         105  
12 4     4   2818 use DateTime;
  4         1305532  
  4         186  
13 4     4   2714 use XML::Writer;
  4         41443  
  4         130  
14 4     4   2176 use List::MoreUtils qw(natatime);
  4         29003  
  4         28  
15              
16 4     4   1889 use Exporter 'import';
  4         4  
  4         4519  
17             our @EXPORT_OK = qw(eds2xdd eds2xdd_string); # symbols to export on request
18              
19             =pod
20              
21             =encoding utf8
22              
23             =head1 NAME
24              
25             Convert::EDS::XDD - Convert CANopen EDS to XDD
26              
27             =head1 SYNOPSIS
28              
29             perl -MConvert::EDS::XDD=eds2xdd -e "print eds2xdd 'profile.eds'" > profile.xdd
30              
31             =head1 DESCRIPTION
32              
33             EDS and XDD are device profile formats based on L<ini|Config::Tiny> and L<XML|XML::Writer> respectively and are specified by the CiA e.V.
34              
35             This module Takes in an EDS file or a string with its content and returns a XDD string. By default, The C<ISO15745Profile> section is B<Ethernet POWERLINK> specific, but can be adjusted by the user. A self-contained (fatpacked) C<epl2xdd> script is available L<at the Github releases page|https://github.com/epl-viz/Convert-EDS-XDD/releases/latest>.
36              
37             =head1 LIMITATIONS
38              
39             May not handle all details of the EDS. Pull requests and reports (issues on Github) welcome.
40              
41             =cut
42              
43             sub _hex_or_dec {
44 0     0   0 my $num = shift;
45 0 0       0 return $num =~ /^0x/ ? hex($num) : $num;
46             }
47             sub _hashref_filterout {
48 9     9   9 my $hash = shift;
49 9         29 delete @$hash{ grep { not defined $hash->{$_} } keys %$hash };
  153         187  
50              
51             }
52             sub _array_filterout {
53 9     9   77 my $it = natatime 2, @_;
54 9         11 my @out;
55 9         52 while (my @vals = $it->()) {
56 167 100       482 push @out, @vals if defined $vals[1];
57             }
58 9         65 return @out;
59             }
60              
61              
62             my @PDOmapping_str_of = qw( no yes );
63             sub _extract {
64 0     0   0 my $obj = shift;
65             _array_filterout (
66             name => $obj->{ParameterName},
67             objectType => _hex_or_dec($obj->{ObjectType}),
68             dataType => sprintf("%04X", _hex_or_dec($obj->{DataType})),
69             accessType => $obj->{AccessType},
70             PDOmapping => $PDOmapping_str_of[$obj->{PDOMapping}],
71             lowLimit => _hex_or_dec($obj->{LowLimit}),
72             highLimit => _hex_or_dec($obj->{HighLimit}),
73             defaultValue => $obj->{DefaultValue},
74             actualValue => $obj->{ActualValue},
75 0         0 );
76             }
77              
78              
79             =head1 METHODS AND ARGUMENTS
80              
81             =over 4
82              
83             =item eds2xdd($filename, [$encoding])
84              
85             Here, the [] indicate an optional parameter.
86              
87             Returns the EDS' content as XML string on success or undef on error in file contents.
88              
89             Function croaks if opening file fails.
90              
91             $encoding may be used to indicate the encoding of the file, e.g. 'utf8' or
92             'encoding(iso-8859-1)'.
93              
94             Do not add a prefix to $encoding, such as '<' or '<:'.
95              
96             =cut
97              
98             sub eds2xdd {
99 1     1 1 181 my($file, $encoding) = @_;
100 1 50 33     8 croak 'No file name provided' if !defined $file || $file eq '';
101              
102             # Slurp in the file.
103 1 50       4 $encoding = $encoding ? "<:$encoding" : '<';
104 1         3 local $/;
105              
106 1 50       86 open(my $eds, $encoding, $file) or croak "Failed to open file '$file' for reading: $!";
107 0         0 my $contents = <$eds>;
108 0         0 close($eds);
109              
110 0 0       0 croak "Reading from '$file' returned undef" unless defined $contents;
111 0 0       0 eds2xdd_string($contents) or return undef;
112             };
113              
114             my $template = do {
115             local $/;
116             <DATA>
117             };
118              
119              
120             =item eds2xdd_string($string)
121              
122             Returns the EDS string as XML string
123              
124             =cut
125              
126             sub eds2xdd_string {
127 9     9 1 520 my $str = shift;
128 9         14 $str =~ s/#.*//gm;
129 9         55 my $eds = Config::Tiny->read_string($str);
130              
131 9         190 my ($basename, $extension) = $eds->{FileInfo}->{FileName} =~ /^(.*)(\.[^.]*)/;
132 9         9 $basename = undef;
133              
134 9         8 my $comments = do {
135 9 50       22 if($eds->{Comments}) {
136 0         0 my $comments = "<!--\n" . ('*' x76) . "\n";
137 0         0 for my $i (1..$eds->{Comments}->{Lines}) {
138 0         0 $comments .= $eds->{Comments}->{"Line$i"} . "\n";
139             }
140 0         0 $comments .= ('*' x 76) . "\n-->";
141             }
142 9         15 delete $eds->{Comments};
143             };
144              
145             my %placeholder = _mktemplate(
146             fileCreator => $eds->{FileInfo}->{CreatedBy},
147             fileModifiedBy => $eds->{FileInfo}->{ModifiedBy},
148             ProfileName => $eds->{FileInfo}->{Description},
149             fileCreationTime => $eds->{FileInfo}->{CreationTime},
150             fileCreationDate => $eds->{FileInfo}->{CreationDate},
151             fileModificationTime => $eds->{FileInfo}->{ModificationTime},
152             fileModificationDate => $eds->{FileInfo}->{ModificationDate},
153             basename => $basename,
154             extension => $extension,
155             version => sprintf('%02u.%02u', $eds->{FileInfo}->{FileVersion},
156             $eds->{FileInfo}->{FileRevision}),
157              
158             vendorID => $eds->{DeviceInfo}->{VendorNumber},
159             vendorName => $eds->{DeviceInfo}->{VendorName},
160             productName => "$eds->{DeviceInfo}->{ProductName} ".
161             $eds->{DeviceInfo}->{ProductNumber},
162             product_version => $eds->{DeviceInfo}->{RevisionNumber},
163 9         134 comments => $comments,
164              
165             @_
166             );
167 9         37 delete $eds->{FileInfo};
168 9         12 delete $eds->{DeviceInfo};
169              
170 9         49 my $writer = XML::Writer->new(OUTPUT => 'self', DATA_MODE => 1, DATA_INDENT => 2);
171 9         974 $writer->startTag("ObjectList");
172              
173 9         390 my ($in_sublist, $in_6000, $in_2000, $in_1000) = (0) x 4;
174 9         12 my @sections = (sort keys %{$eds});
  9         24  
175 9         24 foreach my $section_index (0 .. @sections - 1) {
176 2         4 my $section = $sections[$section_index];
177 2 50       13 unless ($section =~ /([[:xdigit:]]{4})(?:sub([[:xdigit:]]))|([[:xdigit:]]{4})/) {
178 2         319 carp "Ignoring unknown section $section\n";
179 2         5 next;
180             }
181 0   0     0 my ($index, $subindex) = ($1 // $3, $2);
182              
183 0         0 my $obj = $eds->{$section};
184              
185 0         0 my @object = _extract($obj);
186 0 0       0 if (not defined $subindex) {
187 0 0       0 $writer->endTag("Object") if $in_sublist;
188 0 0 0     0 if (!$in_6000 && hex($index) >= 0x6000) {
    0 0        
    0 0        
189 0         0 $writer->comment('Standardised Device Profile Area (0x6000 - 0x9FFF): may be used according to a CiA device profile.'
190             .'The profile to be used is given by NMT_DeviceType_U32');
191 0         0 $in_6000 = 1;
192             } elsif (!$in_2000 && hex($index) >= 0x2000) {
193 0         0 $writer->comment('Manufacturer Specific Profile Area (0x2000 - 0x5FFF): may freely be used by the device manufacturer');
194 0         0 $in_2000 = 1;
195             } elsif (!$in_1000 && hex($index) >= 0x1000) {
196 0         0 $writer->comment('Communication Profile Area (0x1000 - 0x1FFF): defined by EPSG 301');
197 0         0 $in_1000 = 1;
198             }
199              
200 0         0 $in_sublist = 0;
201              
202 0         0 unshift @object, index => sprintf('%04X', hex($index));
203 0 0       0 if ($sections[$section_index+1] =~ /^${index}sub/) {
204 0         0 $writer->startTag("Object", @object);
205 0         0 $in_sublist = 1;
206             } else {
207 0         0 $writer->emptyTag("Object", @object);
208             }
209             } else {
210 0         0 unshift @object, subIndex => sprintf('%02X', hex($subindex));
211 0         0 $writer->emptyTag("SubObject", @object);
212             }
213             }
214              
215 9 50       27 $writer->endTag("Object") if $in_sublist;
216 9         26 $writer->endTag("ObjectList");
217              
218 9         151 my $ObjectList = $writer->end();
219 9         114 my $xdd = $template;
220 9 50       852 if ($xdd =~ s/^([ \t]+?)\$ObjectList/\$ObjectList/m) {
221 9         19 my $ObjectList_indent = $1;
222 9         26 $ObjectList =~ s/^/$ObjectList_indent/mg;
223             }
224 9         39 $xdd =~ s/(\$\w+(?:\{\w+\})?)/$1/gee;
  252         7899  
225 9         836 return $xdd;
226             }
227              
228             sub _mktemplate {
229 9     9   44 my $dt = DateTime->now();
230 9         2147 @_ = _array_filterout(@_);
231 9         37 my %placeholder = (
232             basename => 'unknown',
233             extension => '',
234             date => $dt->ymd,
235             time => $dt->hms,
236             version => '01.00',
237             product_version => '1.00',
238              
239             #@_
240             );
241             %placeholder = (
242             fileName => "$placeholder{basename}.xdd",
243             comment => "Generated from $placeholder{basename}$placeholder{extension} by " . __PACKAGE__,
244             ProfileName => "POWERLINK $placeholder{basename}",
245              
246             fileCreator => __PACKAGE__,
247             fileCreationDate => $placeholder{date},
248             fileCreationTime => $placeholder{time},
249              
250             fileModifiedBy => __PACKAGE__,
251             fileModificationDate => $placeholder{date},
252             fileModificationTime => $placeholder{time},
253              
254             vendorName => 'Unknown vendor',
255             vendorID => '0x00000000',
256             productName => $placeholder{basename},
257             versionHW => $placeholder{product_version},
258             versionFW => $placeholder{product_version},
259             versionSW => $placeholder{product_version},
260              
261 9         285 transferRate => '100 MBit/s',
262              
263             @_
264             );
265 9         28 _hashref_filterout(\%placeholder);
266 9         108 %placeholder
267             }
268              
269              
270             1;
271              
272             =back
273              
274             =head1 GIT REPOSITORY
275              
276             L<http://github.com/epl-viz/Convert-EDS-XDD>
277              
278             =head1 SEE ALSO
279              
280             L<EPL-Viz - Visualization for Ethernet POWERLINK|http://github.com/epl-viz>
281              
282             =head1 AUTHOR
283              
284             Ahmad Fatoum C<< <athreef@cpan.org> >>, L<http://a3f.at>
285              
286             =head1 COPYRIGHT AND LICENSE
287              
288             Copyright (C) 2017 Ahmad Fatoum
289              
290             This library is free software; you can redistribute it and/or modify
291             it under the same terms as Perl itself.
292              
293             =cut
294              
295             __DATA__
296             <?xml version="1.0" encoding="UTF-8"?>
297             <!-- $placeholder{comment} -->
298             $placeholder{comments}
299             <ISO15745ProfileContainer xmlns="http://www.ethernet-powerlink.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.ethernet-powerlink.org Powerlink_Main.xsd">
300             <ISO15745Profile>
301             <ProfileHeader>
302             <ProfileIdentification>Powerlink_Device_Profile</ProfileIdentification>
303             <ProfileRevision>1</ProfileRevision>
304             <ProfileName>$placeholder{ProfileName} device profile</ProfileName>
305             <ProfileSource/>
306             <ProfileClassID>Device</ProfileClassID>
307             <ISO15745Reference>
308             <ISO15745Part>4</ISO15745Part>
309             <ISO15745Edition>1</ISO15745Edition>
310             <ProfileTechnology>Powerlink</ProfileTechnology>
311             </ISO15745Reference>
312             </ProfileHeader>
313             <ProfileBody xsi:type="ProfileBody_Device_Powerlink" fileName="$placeholder{fileName}" fileCreator="$placeholder{fileCreator}" fileCreationDate="$placeholder{fileCreationDate}" fileCreationTime="$placeholder{fileCreationTime}" fileModificationDate="$placeholder{fileModificationDate}" fileModificationTime="$placeholder{fileModificationTime}" fileModifiedBy="$placeholder{fileModifiedBy}" fileVersion="$placeholder{version}" supportedLanguages="en">
314             <DeviceIdentity>
315             <vendorName>$placeholder{vendorName}</vendorName>
316             <vendorID>$placeholder{vendorID}</vendorID>
317             <productName>$placeholder{productName}</productName>
318             <version versionType="HW">$placeholder{versionHW}</version>
319             <version versionType="SW">$placeholder{versionSW}</version>
320             <version versionType="FW">$placeholder{versionFW}</version>
321             </DeviceIdentity>
322             <DeviceFunction>
323             <capabilities>
324             <characteristicsList>
325             <characteristic>
326             <characteristicName>
327             <label lang="en">Transfer rate</label>
328             </characteristicName>
329             <characteristicContent>
330             <label lang="en">$placeholder{transferRate}</label>
331             </characteristicContent>
332             </characteristic>
333             </characteristicsList>
334             </capabilities>
335             </DeviceFunction>
336             </ProfileBody>
337             </ISO15745Profile>
338             <ISO15745Profile>
339             <ProfileHeader>
340             <ProfileIdentification>Powerlink_Communication_Profile</ProfileIdentification>
341             <ProfileRevision>1</ProfileRevision>
342             <ProfileName></ProfileName>
343             <ProfileSource/>
344             <ProfileClassID>CommunicationNetwork</ProfileClassID>
345             <ISO15745Reference>
346             <ISO15745Part>4</ISO15745Part>
347             <ISO15745Edition>1</ISO15745Edition>
348             <ProfileTechnology>Powerlink</ProfileTechnology>
349             </ISO15745Reference>
350             </ProfileHeader>
351             <ProfileBody xsi:type="ProfileBody_CommunicationNetwork_Powerlink" fileName="$placeholder{fileName}"
352             fileCreator="$placeholder{fileCreator}" fileCreationDate="$placeholder{fileCreationDate}" fileCreationTime="$placeholder{fileCreationTime}" fileModificationDate="$placeholder{fileModificationDate}" fileModificationTime="$placeholder{fileModificationTime}" fileModifiedBy="$placeholder{fileModifiedBy}" fileVersion="$placeholder{version}" supportedLanguages="en">
353             <ApplicationLayers>
354             <identity>
355             <vendorID>$placeholder{vendorID}</vendorID>
356             </identity>
357             <DataTypeList>
358             <defType dataType="0001"> <Boolean/> </defType>
359             <defType dataType="0002"> <Integer8/> </defType>
360             <defType dataType="0003"> <Integer16/> </defType>
361             <defType dataType="0004"> <Integer32/> </defType>
362             <defType dataType="0005"> <Unsigned8/> </defType>
363             <defType dataType="0006"> <Unsigned16/> </defType>
364             <defType dataType="0007"> <Unsigned32/> </defType>
365             <defType dataType="0008"> <Real32/> </defType>
366             <defType dataType="0009"> <Visible_String/> </defType>
367             <defType dataType="0010"> <Integer24/> </defType>
368             <defType dataType="0011"> <Real64/> </defType>
369             <defType dataType="0012"> <Integer40/> </defType>
370             <defType dataType="0013"> <Integer48/> </defType>
371             <defType dataType="0014"> <Integer56/> </defType>
372             <defType dataType="0015"> <Integer64/> </defType>
373             <defType dataType="000A"> <Octet_String/> </defType>
374             <defType dataType="000B"> <Unicode_String/> </defType>
375             <defType dataType="000C"> <Time_of_Day/> </defType>
376             <defType dataType="000D"> <Time_Diff/> </defType>
377             <defType dataType="000F"> <Domain/> </defType>
378             <defType dataType="0016"> <Unsigned24/> </defType>
379             <defType dataType="0018"> <Unsigned40/> </defType>
380             <defType dataType="0019"> <Unsigned48/> </defType>
381             <defType dataType="001A"> <Unsigned56/> </defType>
382             <defType dataType="001B"> <Unsigned64/> </defType>
383             <defType dataType="0401"> <MAC_ADDRESS/> </defType>
384             <defType dataType="0402"> <IP_ADDRESS/> </defType>
385             <defType dataType="0403"> <NETTIME/> </defType>
386             </DataTypeList>
387              
388             $ObjectList
389             </ApplicationLayers>
390             <TransportLayers/>
391             <NetworkManagement>
392             <GeneralFeatures DLLFeatureMN="false" NMTBootTimeNotActive="9000000" NMTCycleTimeMin="400" NMTCycleTimeMax="4294967295" NMTErrorEntries="2" NWLIPSupport="false" PHYExtEPLPorts="2" PHYHubIntegrated="true" SDOServer="true" SDOMaxConnections="2" SDOMaxParallelConnections="2" SDOCmdWriteAllByIndex="false" SDOCmdReadAllByIndex="false" SDOCmdWriteByName="false" SDOCmdReadByName="false" SDOCmdWriteMultParam="false" NMTFlushArpEntry="false" NMTNetHostNameSet="false" PDORPDOChannels="3" PDORPDOChannelObjects="25" PDOSelfReceipt="false" PDOTPDOChannelObjects="25"/>
393             <CNFeatures DLLCNFeatureMultiplex="true" DLLCNPResChaining="true" NMTCNSoC2PReq="0"/>
394             <Diagnostic/>
395             </NetworkManagement>
396             </ProfileBody>
397             </ISO15745Profile>
398             </ISO15745ProfileContainer>