@@ -136,10 +136,12 @@ class Logix( Message_Router ):
136136
137137 """
138138
139- # TODO: MAX_BYTES is arbitrary. We're supposed to be able to return data sufficient to fill the
139+ # MAX_BYTES is arbitrary. We're supposed to be able to return data sufficient to fill the
140140 # remaining reply package size, but how can we do that? We'd have to be informed of the
141- # remaining packet size available, as an argument to the produce method...
142- MAX_BYTES = 500
141+ # remaining packet size available, as an argument to the produce method... The user may alter
142+ # this limit (eg. according to the capacity of Forward Open channel). However, it is still
143+ # quite fragile, especially when producing Multiple Service Packet requests/responses.
144+ MAX_BYTES = 488 # Wild guess; leave room for ENIP headers, etc. in reply
143145
144146 RD_TAG_NAM = "Read Tag"
145147 RD_TAG_CTX = "read_tag"
@@ -192,8 +194,7 @@ def reply_elements( self, attribute, data, context ):
192194 off = 0
193195 if data .service in (self .RD_FRG_RPY , self .WR_FRG_RPY ):
194196 off = data [context ].get ( 'offset' ) or 0 # nonexistent/None/0 --> 0
195- assert siz and off % siz == 0 , \
196- "Requested byte offset %d is not on a %d-byte data element boundary" % ( off , siz )
197+ max_size = data [context ].get ( 'max_size' ) or self .MAX_BYTES
197198
198199 # Compute the extents of the full reply, given no byte offset, unlimited reply size and
199200 # complete data. If no 'elements' has been provided (only possible when hand-forming a
@@ -203,8 +204,6 @@ def reply_elements( self, attribute, data, context ):
203204 cnt = len ( attribute )
204205 elm = data [context ].get ( 'elements' , cnt - beg ) # Read/Write Tag defaults to all
205206 endactual = beg + elm
206- assert 0 < endactual <= cnt , \
207- "Attribute %s ending element invalid: %r" % ( attribute , (beg , endactual ) )
208207
209208 # Maximum elements for read is the capacity of the reply message, for write is the number
210209 # actually provided in request. Compute this from the beginning element deduced from the
@@ -213,31 +212,42 @@ def reply_elements( self, attribute, data, context ):
213212 # incomplete number of data elements provided. The 'end' can only get smaller than the
214213 # (known valid) 'endactual'.
215214 #
216- # TODO: This is not strictly correct; we must be able to return a response w/ an offset that
217- # starts mid-element! Also, for STRUCT types, the size of each UDT element (eg. 600 bytes)
218- # can be greater than the maximum connection size, preventing even one complete element from
219- # being returned. For now, always round up to return a complete element; this may prevent
220- # some clients from accepting the data (eg. if the elements are too big, and a Small Forward
221- # Open is used.
222- begadvance = off // siz
223- log .detail ( "index: {index!r} beg: {beg}, cnt: {cnt}, elm: {elm}; endactual: {endactual}, begadvance: {begadvance}" .format (
215+ # This is not strictly correct; we must be able to return a response w/ an offset that
216+ # starts/ends arbitrarily mid-element! Also, for STRUCT types, the size of each UDT element
217+ # (eg. 600 bytes) can be greater than the maximum connection size, preventing even one
218+ # complete element from being returned. For this function, always round outwards to return
219+ # complete element(s); this will prevent some clients from accepting the data (eg. if the
220+ # elements are too big, and a Small Forward Open is used; it is assumed that the response
221+ # data will be trimmed elsewhere, according to the actual byte offset requested in the Read
222+ # Tag Fragmented request.
223+ begadvance = off // siz # Rounds down to the start of the element at offset
224+ offremains = off - begadvance * siz # off is how many bytes into beg element?
225+ log .info ( "index: {index!r} beg: {beg}, cnt: {cnt}, elm: {elm}; endactual: {endactual}, begadvance: {begadvance}" .format (
224226 index = index , beg = beg , cnt = cnt , elm = elm , endactual = endactual , begadvance = begadvance ))
225- assert begadvance * siz == off , \
226- "Fragment offset {off} is not on an element {siz}-byte boundary" .format ( off = off , siz = siz )
227227 beg += begadvance
228228 if data .service in (self .RD_TAG_RPY , self .RD_FRG_RPY ):
229- endmax = beg + max ( self .MAX_BYTES // siz , 1 )
229+ # Return at least enough elements to satisfy max_size, beginning at offset 'off'. We
230+ # have a 'beg' Element that contains the first byte at offset 'off'; compute the endmax
231+ # that contains the last byte at offset off+max_siz-1. The data may specify the
232+ # (remaining) .max_size payload available.
233+ endadv = max (( offremains + max_size + siz - 1 ) // siz , 1 ) # rounds up
234+ endmax = beg + endadv
230235 else :
231- endmax = beg + len ( data [context ].data )
236+ endadv = len ( data [context ].data )
237+ endmax = beg + endadv
232238 assert endmax <= endactual , \
233239 "Attribute %s capacity exceeded; writing %d elements beginning at index %d" % (
234240 attribute , len ( data [context ].data ), beg )
235241 end = min ( endactual , endmax )
242+ log .info ( "offset: {off:6d} siz: {siz:3d}, beg: {beg:3d}, endadv: {endadv:3d}, end: {end:3d}, endmax: {endmax:3d}, offremains: {offremains}" .format (
243+ off = off , siz = siz , beg = beg , end = end , endadv = endadv , endmax = endmax , offremains = offremains ))
236244 assert 0 <= beg < cnt , \
237245 "Attribute %r initial element invalid: %r" % ( attribute , (beg , end ) )
246+ assert elm <= cnt , \
247+ "Attribute %r elements requested invalid: %r" % ( attribute , elm )
238248 assert beg < end , \
239249 "Attribute %r ending element before beginning: %r" % ( attribute , (beg , end ) )
240- return (beg ,end ,endactual )
250+ return (beg ,end ,endactual , offremains , max_size )
241251
242252 def request ( self , data , addr = None ):
243253 """Any exception should result in a reply being generated with a non-zero status."""
@@ -308,7 +318,7 @@ def request( self, data, addr=None ):
308318 # We need to find the attribute for all requests, and it better be ours!
309319 data .status = 0x05 # On Failure: Request Path destination unknown
310320 data .status_ext = {'size' : 1 , 'data' :[0x0000 ]}
311- clid , inid , atid = resolve ( data .path , attribute = True )
321+ clid , inid , atid = resolve ( data .path , attribute = 1 ) # eg. @<cls>/<ins>[<elm>] defaults to Attribute 1!
312322 attribute = lookup ( clid , inid , atid )
313323 assert clid == self .class_id and inid == self .instance_id , \
314324 "Path %r processed by wrong Object %r" % ( data .path ['segment' ], self )
@@ -384,24 +394,45 @@ def request( self, data, addr=None ):
384394 data .status_ext = {'size' : 1 , 'data' : [ 0x2105 ]} # Number of elements beyond end of tag
385395
386396 # Compute (beg,end] for this reply, given data.path...element, data.elements/offset.
387- # The end element of the full request (not the size/data-limited end) is in endactual
388- beg ,end ,endactual = self .reply_elements ( attribute , data , context )
397+ # The end element of the original request (not the size/data-limited end) is in
398+ # endactual. If a .offset (and optionally .max_size) is provided, these must be used to
399+ # constrain the actual payload bytes returned; the [beg,end) should index elements
400+ # containing the first byte to return (at .offset), up tothe last byte
401+ # (.offset+.max_size-1). Since we might have advances 'beg', we get back the adjusted
402+ # 'offremains', as well the target 'max_size'.
403+ beg ,end ,endactual ,offremains ,max_size \
404+ = self .reply_elements ( attribute , data , context )
389405 log .debug ( "Replying w/ elements [%3d-%-3d/%3d] for %r" , beg , end , endactual , data )
390406 if data .service in (self .RD_TAG_RPY , self .RD_FRG_RPY ):
391407 # Read Tag [Fragmented]
392408 recs = attribute [beg :end ]
393409 if attribute .parser .tag_type == STRUCT .tag_type :
394410 # Render the STRUCT UDTs to binary. Assume that each record has its data.input
395- # representation
411+ # representation.
396412 input = b''
397413 for r in recs :
398414 input += octets_encode ( r .data .input )
399- recs = dict ( input = input )
415+ # For STRUCTs *only*, we support arbitrary .offset and max_size; trim it
416+ # here. (For other basic data types, we'll simply return the designated
417+ # elements, which may be less than, or slightly more than the .max_size /
418+ # self.MAX_BYTES by some portion of one element size) Trim it here. If we've
419+ # returned the end element of the request, and all of its bytes, we're complete.
420+ trimmed = input [offremains :offremains + max_size ]
421+ recs = dict ( input = trimmed )
422+ completed = end == endactual and offremains + max_size >= len ( input )
423+ else :
424+ # We don't presently support a non-zero .offset for indeterminately sized types
425+ # (eg. STRING/SSTRING, etc.), or a sub-element offset for basic data types.
426+ assert offremains == 0 or (
427+ attribute .parser .tag_type < STRING .tag_type
428+ and offremains % attribute .parser .struct_calcsize == 0 )
429+ completed = end == endactual
400430 data [context ].data = recs
401- log .detail ( "%s Reading %3d elements %3d-%3d from %s: %r" ,
402- self , end - beg , beg , end - 1 , attribute , data [context ].data )
431+ log .detail ( "%s Reading %3d elements %3d-%3d %s from %s: %r" ,
432+ self , end - beg , beg , end - 1 , "(done)" if completed else "(more)" ,
433+ attribute , data [context ].data )
403434 # Final .status is 0x00 if all requested elements were shipped; 0x06 if not
404- data .status = 0x00 if end == endactual else 0x06
435+ data .status = 0x00 if completed else 0x06
405436 data .pop ( 'status_ext' ) # non-empty dotdict level; use pop instead of del
406437 else :
407438 # Write Tag [Fragmented]. We know the type is right.
@@ -668,7 +699,7 @@ def setup_tag( key, val ):
668699 # doesn't exist. Then, find the Attribute, ensuring it is consistent if it exists.
669700 cls ,ins ,att = 0x02 ,1 ,None # The (Logix?) Message Router, by default
670701 if 'path' in val and val ['path' ]:
671- cls ,ins ,att = resolve ( val ['path' ], attribute = True )
702+ cls ,ins ,att = resolve ( val ['path' ], attribute = True ) # No default Attribute for new Tags
672703 # See if the tag's Instance exists. If not, we'll need to create it. If the Class'
673704 # "meta" Instance exists, we'll use it to create the Instance (its always at
674705 # Instance 0). Otherwise, we'll create an Object class with the appropriate
0 commit comments