source: src/mlx/logger.py@ 633:940d5bc945a7

Last change on this file since 633:940d5bc945a7 was 604:0ec6a6f58f08, checked in by István Váradi <ivaradi@…>, 9 years ago

Added a new widget to list the faults and provide space for the user to enter an explanation (re #248).

File size: 14.1 KB
Line 
1
2from fs import sendMessage
3import const
4import util
5
6import sys
7import time
8import bisect
9
10#--------------------------------------------------------------------------------------
11
12## @package mlx.logger
13#
14# The module for the logger.
15#
16# While the program itself is "logger", it contains an internal logger, which
17# maintains the textual log containing information on the various events and is
18# the reason why the program is called "logger".
19#
20# The log is made up of lines containing an optional timestamp and the text of
21# the message. A line can be updated after having been put into the log by
22# referring to its index.
23#
24# The logger object also maintains a separate set of faults and ensures that
25# one fault type has only one score, even if that fault has been reported
26# multiple times.
27
28#--------------------------------------------------------------------------------------
29
30class Logger(object):
31 """The class with the interface to log the various events.
32
33 It contains a list of entries ordered by their timestamps and their ever
34 increasing IDs."""
35
36 class Entry(object):
37 """An entry in the log."""
38
39 # The ID of the next entry to be created
40 _nextID = 1
41
42 def __init__(self, timestamp, text, showTimestamp = True,
43 faultID = None, faultScore = 0, id = None):
44 """Construct the entry."""
45 if id is None:
46 self._id = self._nextID
47 Logger.Entry._nextID += 1
48 else:
49 self._id = id
50
51 self._timestamp = timestamp
52 self._text = text
53 self._showTimestamp = showTimestamp
54
55 self._faultID = faultID
56 self._faultScore = faultScore
57
58 @property
59 def id(self):
60 """Get the ID of the entry."""
61 return self._id
62
63 @property
64 def timestamp(self):
65 """Get the timestamp of this entry."""
66 return self._timestamp
67
68 @property
69 def timestampString(self):
70 """Get the timestamp string of this entry.
71
72 It returns None, if the timestamp of the entry is not visible."""
73 return util.getTimestampString(self._timestamp) \
74 if self._showTimestamp else None
75
76 @property
77 def text(self):
78 """Get the text of this entry."""
79 return self._text
80
81 @property
82 def isFault(self):
83 """Determine if this is a log entry about a fault."""
84 return self._faultID is not None
85
86 @property
87 def faultID(self):
88 """Get the fault ID of the entry.
89
90 It may be None, if the entry is not a fault entry."""
91 return self._faultID
92
93 @property
94 def faultScore(self):
95 """Get the fault score of the entry, if it is a fault."""
96 return self._faultScore
97
98 def copy(self, timestamp = None, clearTimestamp = False, text = None,
99 faultID = None, faultScore = None, clearFault = False):
100 """Create a copy of this entry with the given values changed."""
101 return Logger.Entry(None if clearTimestamp
102 else self._timestamp if timestamp is None
103 else timestamp,
104
105 self._text if text is None else text,
106
107 showTimestamp = self._showTimestamp,
108
109 faultID =
110 None if clearFault
111 else self._faultID if faultID is None
112 else faultID,
113
114 faultScore =
115 None if clearFault
116 else self._faultScore if faultScore is None
117 else faultScore,
118
119 id = self._id)
120
121 def __cmp__(self, other):
122 """Compare two entries
123
124 First their timestamps are compared, and if those are equal, then
125 their IDs."""
126 result = cmp(self._timestamp, other.timestamp)
127 if result==0:
128 result = cmp(self._id, other._id)
129 return result
130
131 class Fault(object):
132 """Information about a fault.
133
134 It contains the list of log entries that belong to this fault. The list
135 is ordered so that the first element contains the entry with the
136 highest score, so that it should be easy to find the actual score."""
137 def __init__(self, entry):
138 """Construct the fault info with the given log entry as its only
139 one."""
140 self._entries = [entry]
141
142 @property
143 def score(self):
144 """Get the score of this fault, i.e. the score of the entry with
145 the highest score."""
146 return self._entries[0].faultScore if self._entries else 0
147
148 def addEntry(self, entry):
149 """Add an entry to this fault.
150
151 The entries will be sorted."""
152 entries = self._entries
153 entries.append(entry)
154 entries.sort(key = lambda entry: entry.faultScore, reverse = True)
155
156 def removeEntry(self, entry):
157 """Remove the given entry.
158
159 Returns True if at least one entry remains, False otherwise."""
160 entries = self._entries
161 for index in range(0, len(entries)):
162 if entry is entries[index]:
163 del entries[index]
164 break
165
166 return len(entries)>0
167
168 def getLatestEntry(self):
169 """Get the entry with the highest score."""
170 return self._entries[0]
171
172 # FIXME: shall we use const.stage2string() instead?
173 _stages = { const.STAGE_BOARDING : "Boarding",
174 const.STAGE_PUSHANDTAXI : "Pushback and Taxi",
175 const.STAGE_TAKEOFF : "Takeoff",
176 const.STAGE_RTO : "RTO",
177 const.STAGE_CLIMB : "Climb",
178 const.STAGE_CRUISE : "Cruise",
179 const.STAGE_DESCENT : "Descent",
180 const.STAGE_LANDING : "Landing",
181 const.STAGE_TAXIAFTERLAND : "Taxi",
182 const.STAGE_PARKING : "Parking",
183 const.STAGE_GOAROUND : "Go-Around",
184 const.STAGE_END : "End" }
185
186 NO_GO_SCORE = 10000
187
188 def __init__(self, output):
189 """Construct the logger."""
190 self._entries = {}
191 self._lines = []
192
193 self._faults = {}
194
195 self._output = output
196
197 @property
198 def lines(self):
199 """Get the lines of the log."""
200 return [(entry.timestampString, entry.text) for entry in self._lines]
201
202 @property
203 def faultLineIndexes(self):
204 """Get the sorted array of the indexes of those log lines that contain
205 a fault."""
206 faultLineIndexes = []
207 lines = self._lines
208 for index in range(0, len(lines)):
209 if lines[index].isFault:
210 faultLineIndexes.append(index)
211 return faultLineIndexes
212
213 def reset(self):
214 """Reset the logger.
215
216 The faults logged so far will be cleared."""
217 self._entries = {}
218 self._lines = []
219 self._faults = {}
220
221 def message(self, timestamp, msg):
222 """Put a simple textual message into the log with the given timestamp.
223
224 Returns an ID of the message so that it could be referred to later."""
225 return self._addEntry(Logger.Entry(timestamp, msg))
226
227 def untimedMessage(self, msg):
228 """Put an untimed message into the log."""
229 timestamp = self._lines[-1].timestamp if self._lines else 0
230 return self._addEntry(Logger.Entry(timestamp, msg,
231 showTimestamp = False))
232
233 def debug(self, msg):
234 """Log a debug message."""
235 print "[DEBUG]", msg
236
237 def stage(self, timestamp, stage):
238 """Report a change in the flight stage."""
239 s = Logger._stages[stage] if stage in Logger._stages else "<Unknown>"
240 self.message(timestamp, "--- %s ---" % (s,))
241 if stage==const.STAGE_END:
242 self.untimedMessage("Rating: %.0f" % (self.getRating(),))
243 else:
244 messageType = \
245 const.MESSAGETYPE_INFLIGHT if stage in \
246 [const.STAGE_CLIMB, const.STAGE_CRUISE, \
247 const.STAGE_DESCENT, const.STAGE_LANDING] \
248 else const.MESSAGETYPE_INFORMATION
249 sendMessage(messageType, "Flight stage: " + s, 3)
250
251 def fault(self, faultID, timestamp, what, score,
252 updatePrevious = False, updateID = None):
253 """Report a fault.
254
255 faultID as a unique ID for the given kind of fault. If another fault of
256 this ID has been reported earlier, it will be reported again only if
257 the score is greater than last time. This ID can be, e.g. the checker
258 the report comes from.
259
260 If updatePrevious is True, and an instance of the given fault is
261 already in the log, only that instance will be updated with the new
262 timestamp and score. If there are several instances, the latest one
263 (with the highest score) will be updated. If updatePrevious is True,
264 and the new score is not greater than the latest one, the ID of the
265 latest one is returned.
266
267 If updateID is given, the log entry with the given ID will be
268 'upgraded' to be a fault with the given data.
269
270 Returns an ID of the fault, or -1 if it was not logged."""
271 fault = self._faults[faultID] if faultID in self._faults else None
272
273 if fault is not None and score<=fault.score:
274 return fault.getLatestEntry().id if updatePrevious else -1
275
276 text = "%s (NO GO)" % (what) if score==Logger.NO_GO_SCORE \
277 else "%s (%.1f)" % (what, score)
278
279 if updatePrevious and fault is not None:
280 latestEntry = fault.getLatestEntry()
281 id = latestEntry.id
282 newEntry = latestEntry.copy(timestamp = timestamp,
283 text = text,
284 faultScore = score)
285 self._updateEntry(id, newEntry)
286 if latestEntry.isFault:
287 self._output.updateFault(id, newEntry.timestampString, text)
288 else:
289 self._output.addFault(id, newEntry.timestampString, text)
290 elif updateID is not None:
291 id = updateID
292 oldEntry = self._entries[id]
293 newEntry = oldEntry.copy(timestamp = timestamp,
294 text = text, faultID = faultID,
295 faultScore = score)
296 self._updateEntry(id, newEntry)
297 if oldEntry.isFault:
298 self._output.updateFault(id, newEntry.timestampString, text)
299 else:
300 self._output.addFault(id, newEntry.timestampString, text)
301 else:
302 entry = Logger.Entry(timestamp, text, faultID = faultID,
303 faultScore = score)
304 id = self._addEntry(entry)
305 self._output.addFault(id, entry.timestampString, text)
306
307 if updateID is None:
308 (messageType, duration) = (const.MESSAGETYPE_NOGO, 10) \
309 if score==Logger.NO_GO_SCORE \
310 else (const.MESSAGETYPE_FAULT, 5)
311 sendMessage(messageType, text, duration)
312
313 return id
314
315 def noGo(self, faultID, timestamp, what):
316 """Report a No-Go fault."""
317 return self.fault(faultID, timestamp, what, Logger.NO_GO_SCORE)
318
319 def getRating(self):
320 """Get the rating of the flight so far."""
321 totalScore = 100
322 for fault in self._faults.itervalues():
323 score = fault.score
324 if score==Logger.NO_GO_SCORE:
325 return -score
326 else:
327 totalScore -= score
328 return totalScore
329
330 def updateLine(self, id, line):
331 """Update the line with the given ID with the given string.
332
333 Note, that it does not change the status of the line as a fault!"""
334 self._updateEntry(id, self._entries[id].copy(text = line))
335
336 def clearFault(self, id, text):
337 """Update the line with the given ID to contain the given string,
338 and clear its fault state."""
339 newEntry = self._entries[id].copy(text = text, clearFault = True)
340 self._updateEntry(id, newEntry)
341 self._output.clearFault(id)
342
343 def _addEntry(self, entry):
344 """Add the given entry to the log.
345
346 @return the ID of the new entry."""
347 assert entry.id not in self._entries
348
349 self._entries[entry.id] = entry
350
351 if not self._lines or entry>self._lines[-1]:
352 index = len(self._lines)
353 self._lines.append(entry)
354 else:
355 index = bisect.bisect_left(self._lines, entry)
356 self._lines.insert(index, entry)
357
358 if entry.isFault:
359 self._addFault(entry)
360
361 self._output.insertFlightLogLine(index, entry.timestampString,
362 entry.text, entry.isFault)
363
364 return entry.id
365
366 def _updateEntry(self, id, newEntry):
367 """Update the entry with the given ID from the given new entry."""
368 self._removeEntry(id)
369 self._addEntry(newEntry)
370
371 def _removeEntry(self, id):
372 """Remove the entry with the given ID."""
373 assert id in self._entries
374
375 entry = self._entries[id]
376 del self._entries[id]
377
378 for index in range(len(self._lines)-1, -1, -1):
379 if self._lines[index] is entry:
380 break
381 del self._lines[index]
382
383 if entry.isFault:
384 faultID = entry.faultID
385 fault = self._faults[faultID]
386 if not fault.removeEntry(entry):
387 del self._faults[faultID]
388
389 self._output.removeFlightLogLine(index)
390
391 def _addFault(self, entry):
392 """Add the given fault entry to the fault with the given ID."""
393 faultID = entry.faultID
394 if faultID in self._faults:
395 self._faults[faultID].addEntry(entry)
396 else:
397 self._faults[faultID] = Logger.Fault(entry)
398
399#--------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.