Changeset 67 for remote_control
- Timestamp:
- 06/27/10 05:26:28 (12 years ago)
- Location:
- remote_control
- Files:
-
- 6 edited
Legend:
- Unmodified
- Added
- Removed
-
remote_control/interface/qt4_form.py
r66 r67 3 3 # Form implementation generated from reading ui file 'qt4_form.ui' 4 4 # 5 # Created: Thu Jun 24 23:40:0720105 # Created: Sat Jun 26 00:39:52 2010 6 6 # by: PyQt4 UI code generator 4.7.3 7 7 # … … 24 24 self.textLabelTitle.setObjectName("textLabelTitle") 25 25 self.textLabelBluetooth = QtGui.QLabel(Form) 26 self.textLabelBluetooth.setGeometry(QtCore.QRect(310, 2 20, 161, 30))26 self.textLabelBluetooth.setGeometry(QtCore.QRect(310, 250, 161, 21)) 27 27 self.textLabelBluetooth.setWordWrap(False) 28 28 self.textLabelBluetooth.setObjectName("textLabelBluetooth") … … 33 33 self.line8.setObjectName("line8") 34 34 self.textLabelBluetoothStatus = QtGui.QLabel(Form) 35 self.textLabelBluetoothStatus.setGeometry(QtCore.QRect(310, 2 60, 161, 30))35 self.textLabelBluetoothStatus.setGeometry(QtCore.QRect(310, 270, 161, 30)) 36 36 self.textLabelBluetoothStatus.setWordWrap(False) 37 37 self.textLabelBluetoothStatus.setObjectName("textLabelBluetoothStatus") … … 1033 1033 self.labelPuzzlebox.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) 1034 1034 self.labelPuzzlebox.setObjectName("labelPuzzlebox") 1035 self.label = QtGui.QLabel(Form) 1036 self.label.setGeometry(QtCore.QRect(310, 220, 161, 21)) 1037 self.label.setObjectName("label") 1035 1038 1036 1039 self.retranslateUi(Form) … … 1064 1067 self.lineEditBluetoothPort.setText(QtGui.QApplication.translate("Form", "N/A", None, QtGui.QApplication.UnicodeUTF8)) 1065 1068 self.pushButtonTurnLeft.setShortcut(QtGui.QApplication.translate("Form", "A, Left", None, QtGui.QApplication.UnicodeUTF8)) 1069 self.label.setText(QtGui.QApplication.translate("Form", "LEGO Mindstorms NXT", None, QtGui.QApplication.UnicodeUTF8)) 1066 1070 -
remote_control/interface/qt4_form.ui
r66 r67 52 52 <rect> 53 53 <x>310</x> 54 <y>2 20</y>54 <y>250</y> 55 55 <width>161</width> 56 <height> 30</height>56 <height>21</height> 57 57 </rect> 58 58 </property> … … 84 84 <rect> 85 85 <x>310</x> 86 <y>2 60</y>86 <y>270</y> 87 87 <width>161</width> 88 88 <height>30</height> … … 3168 3168 </property> 3169 3169 </widget> 3170 <widget class="QLabel" name="label"> 3171 <property name="geometry"> 3172 <rect> 3173 <x>310</x> 3174 <y>220</y> 3175 <width>161</width> 3176 <height>21</height> 3177 </rect> 3178 </property> 3179 <property name="text"> 3180 <string>LEGO Mindstorms NXT</string> 3181 </property> 3182 </widget> 3170 3183 </widget> 3171 3184 <layoutdefault spacing="5" margin="10"/> -
remote_control/puzzlebox_brainstorms_client.py
r48 r67 41 41 42 42 def __init__(self, log, \ 43 server_host , \44 server_port , \43 server_host=SERVER_HOST, \ 44 server_port=SERVER_PORT, \ 45 45 max_connection_attempts = MAX_CONNECTION_ATTEMPTS): 46 46 -
remote_control/puzzlebox_brainstorms_client_interface_qt.py
r65 r67 2 2 # -*- coding: utf-8 -*- 3 3 # 4 # Puzzlebox - Brainstorms - Client Interface 4 # Puzzlebox - Brainstorms - Client Interface - Qt 5 5 # 6 6 # Copyright Puzzlebox Productions, LLC (2010) … … 9 9 # For more information please refer to http://www.gnu.org/copyleft/gpl.html 10 10 # 11 # Last Update: 2010.06. 1011 # Last Update: 2010.06.26 12 12 # 13 13 ##################################################################### … … 15 15 import os, sys 16 16 17 from PyQt4.QtCore import Qt, SIGNAL 18 from PyQt4.QtGui import * 17 try: 18 import PySide as PyQT4 19 except: 20 print "Using PyQt4 module" 21 else: 22 print "Using PySide module" 23 24 from PyQt4 import QtCore, QtGui, QtNetwork 19 25 20 26 sys.path.append(os.path.join(os.getcwd(), "interface")) 21 27 from qt4_form import Ui_Form 22 28 23 import pygame 24 #import pygame.font 25 import pygame.image 26 import pygame.locals 27 28 from twisted.internet import reactor, protocol, defer 29 import simplejson as json 29 30 30 31 import puzzlebox_brainstorms_configuration as configuration … … 40 41 DEBUG = 1 41 42 42 FLIP = 143 44 DISCRETE_CONTROL_COMMANDS = configuration.DISCRETE_CONTROL_COMMANDS43 #FLIP = 1 44 45 #DISCRETE_CONTROL_COMMANDS = configuration.DISCRETE_CONTROL_COMMANDS 45 46 46 47 SERVER_HOST = configuration.SERVER_HOST 47 48 SERVER_PORT = configuration.SERVER_PORT 48 49 49 DISPLAY_WINDOW_X_COORDINATE = configuration.DISPLAY_WINDOW_X_COORDINATE50 DISPLAY_WINDOW_Y_COORDINATE = configuration.DISPLAY_WINDOW_Y_COORDINATE51 DISPLAY_WINDOW_X_DIMENSION = configuration.DISPLAY_WINDOW_X_DIMENSION52 DISPLAY_WINDOW_Y_DIMENSION = configuration.DISPLAY_WINDOW_Y_DIMENSION53 54 WINDOW_BACKGROUND_COLOR = configuration.WINDOW_BACKGROUND_COLOR55 56 IMAGE_DIRECTORY = configuration.IMAGE_DIRECTORY57 58 BUTTON_LAYOUT = configuration.BUTTON_LAYOUT50 #DISPLAY_WINDOW_X_COORDINATE = configuration.DISPLAY_WINDOW_X_COORDINATE 51 #DISPLAY_WINDOW_Y_COORDINATE = configuration.DISPLAY_WINDOW_Y_COORDINATE 52 #DISPLAY_WINDOW_X_DIMENSION = configuration.DISPLAY_WINDOW_X_DIMENSION 53 #DISPLAY_WINDOW_Y_DIMENSION = configuration.DISPLAY_WINDOW_Y_DIMENSION 54 55 #WINDOW_BACKGROUND_COLOR = configuration.WINDOW_BACKGROUND_COLOR 56 57 #IMAGE_DIRECTORY = configuration.IMAGE_DIRECTORY 58 59 #BUTTON_LAYOUT = configuration.BUTTON_LAYOUT 59 60 60 61 ##################################################################### … … 62 63 ##################################################################### 63 64 64 class puzzlebox_brainstorms_client_interface( \ 65 component.puzzlebox_brainstorms_client_interface_component, \ 66 client.puzzlebox_brainstorms_client): 67 68 def __init__(self, log, \ 69 display_window_x_coordinate, \ 70 display_window_y_coordinate, \ 71 display_window_x_dimension, \ 72 display_window_y_dimension, \ 73 window_background_color, \ 74 DEBUG=DEBUG): 75 65 class puzzlebox_brainstorms_client_interface_qt(QtGui.QWidget, Ui_Form, \ 66 client.puzzlebox_brainstorms_client): 67 68 def __init__(self, log, DEBUG=DEBUG, parent = None): 69 76 70 self.log = log 77 71 self.DEBUG = DEBUG 78 72 79 self.display_window_x_coordinate = display_window_x_coordinate 80 self.display_window_y_coordinate = display_window_y_coordinate 81 self.display_window_x_dimension = display_window_x_dimension 82 self.display_window_y_dimension = display_window_y_dimension 83 self.window_background_color = window_background_color 84 85 self.server_host = configuration.SERVER_HOST 86 self.server_port = configuration.SERVER_PORT 87 self.max_connection_attempts = configuration.MAX_CONNECTION_ATTEMPTS 88 89 if (os.name == "nt") or (os.name == "dos"): 90 self.operating_system = "windows" 73 QtGui.QWidget.__init__(self, parent) 74 self.setupUi(self) 75 76 self.configureSettings() 77 self.connectWidgets() 78 self.configureNetwork() 79 80 81 ################################################################## 82 83 def configureSettings(self): 84 85 # Bluetooth readout current not available 86 self.textLabelBluetoothStatus.setText("Status: N/A") 87 88 # Display communication port for LEGO Mindstorms NXT device 89 self.lineEditBluetoothPort.setText(configuration.BLUETOOTH_DEVICE) 90 91 # Bluetooth connection control not available 92 self.pushButtonBluetoothConnect.setEnabled(False) 93 94 # Set concentration threshold value 95 concentration_threshold = \ 96 self.getMinimumThreshold(configuration.POWER_THRESHOLDS['concentration']) 97 self.spinBoxConcentrationThreshold.setValue(concentration_threshold) 98 self.spinBoxConcentrationThreshold.update() 99 100 # Concentration threshold value not modifiable 101 self.spinBoxConcentrationThreshold.setEnabled(False) 102 103 # Set relaxation threshold value 104 relaxation_threshold = \ 105 self.getMinimumThreshold(configuration.POWER_THRESHOLDS['relaxation']) 106 self.spinBoxRelaxationThreshold.setValue(relaxation_threshold) 107 self.spinBoxRelaxationThreshold.update() 108 109 # Relaxation threshold value not modifiable 110 self.spinBoxRelaxationThreshold.setEnabled(False) 111 112 113 ################################################################## 114 115 def getMinimumThreshold(self, threshold): 116 117 '''Return the minimum detection level which results 118 in a non-zero power setting''' 119 120 minimum = 100 121 122 threshold_keys = threshold.keys() 123 threshold_keys.sort() 124 threshold_keys.reverse() 125 126 for key in threshold_keys: 127 128 if ((threshold[key] < minimum) and \ 129 (threshold[key] > 0)): 130 minimum = key 131 132 133 return(minimum) 134 135 136 ################################################################## 137 138 def connectWidgets(self): 139 140 self.connect(self.pushButtonTurnLeft, QtCore.SIGNAL("pressed()"), \ 141 self.turnLeft) 142 self.connect(self.pushButtonTurnLeft, QtCore.SIGNAL("released()"), \ 143 self.stopMotors) 144 145 self.connect(self.pushButtonForward, QtCore.SIGNAL("pressed()"), \ 146 self.driveForward) 147 self.connect(self.pushButtonForward, QtCore.SIGNAL("released()"), \ 148 self.stopMotors) 149 150 self.connect(self.pushButtonTurnRight, QtCore.SIGNAL("pressed()"), \ 151 self.turnRight) 152 self.connect(self.pushButtonTurnRight, QtCore.SIGNAL("released()"), \ 153 self.stopMotors) 154 155 self.connect(self.pushButtonTurnLeftReverse, QtCore.SIGNAL("pressed()"), \ 156 self.turnLeftInReverse) 157 self.connect(self.pushButtonTurnLeftReverse, QtCore.SIGNAL("released()"), \ 158 self.stopMotors) 159 160 self.connect(self.pushButtonReverse, QtCore.SIGNAL("pressed()"), \ 161 self.driveReverse) 162 self.connect(self.pushButtonReverse, QtCore.SIGNAL("released()"), \ 163 self.stopMotors) 164 165 self.connect(self.pushButtonTurnRightReverse, QtCore.SIGNAL("pressed()"), \ 166 self.turnRightInReverse) 167 self.connect(self.pushButtonTurnRightReverse, QtCore.SIGNAL("released()"), \ 168 self.stopMotors) 169 170 171 self.connect(self.pushButtonBluetoothConnect, \ 172 QtCore.SIGNAL("clicked()"), \ 173 self.test) 174 175 176 ################################################################## 177 178 def configureNetwork(self): 179 180 self.blockSize = 0 181 self.tcpSocket = QtNetwork.QTcpSocket(self) 182 183 self.tcpSocket.readyRead.connect(self.printReply) 184 self.tcpSocket.error.connect(self.displayError) 185 186 187 ################################################################## 188 189 def printReply(self, reply=None): 190 191 if (reply != None): 192 193 print "reply:", 194 print reply 195 196 197 ################################################################## 198 199 def displayError(self, socketError): 200 201 if socketError == QtNetwork.QAbstractSocket.RemoteHostClosedError: 202 pass 203 204 elif socketError == QtNetwork.QAbstractSocket.HostNotFoundError: 205 QtGui.QMessageBox.information(self, "Puzzlebox Brainstorms Client", 206 "The server host was not found. Please check the host name and " 207 "port settings.") 208 209 elif socketError == QtNetwork.QAbstractSocket.ConnectionRefusedError: 210 QtGui.QMessageBox.information(self, "Puzzlebox Brainstorms Client", 211 "The server connection was refused by the peer. Make sure the " 212 "server is running, and check that the host name " 213 "and port settings are correct.") 214 91 215 else: 92 self.operating_system = os.name 93 94 self.pygame = pygame 95 self.screen = None 96 self.FLIP = FLIP 97 self.DISCRETE_CONTROL_COMMANDS = DISCRETE_CONTROL_COMMANDS 98 self.image_directory = IMAGE_DIRECTORY 99 100 self.initialize_pygame() 101 102 self.pygame.FLIP = self.FLIP 103 104 self.buttons = {} 105 self.match_key_index = {} 106 107 self.initialize_buttons(BUTTON_LAYOUT) 108 109 self.update_display() 110 111 112 ##################################################################### 113 114 def initialize_pygame(self): 115 116 if (self.operating_system != "windows"): 117 os.environ['SDL_VIDEO_WINDOW_POS'] = \ 118 "%i,%i" % (self.display_window_x_coordinate, \ 119 self.display_window_y_coordinate) 120 121 self.pygame.init() 122 123 if (self.FLIP): 124 125 self.screen = self.pygame.display.set_mode((self.display_window_x_dimension, \ 126 self.display_window_y_dimension), \ 127 pygame.HWSURFACE|pygame.DOUBLEBUF) 128 129 else: 130 131 self.screen = self.pygame.display.set_mode((self.display_window_x_dimension, \ 132 self.display_window_y_dimension), \ 133 pygame.HWSURFACE) 134 135 136 if (self.DEBUG >= 2): 137 print "Display Driver:", 138 print pygame.display.get_driver() 139 print 140 print "Display Info:" 141 print pygame.display.Info() 142 #print 143 #print "Windows System Info:" 144 #print pygame.display.get_wm_info() 145 #print 146 #print "Get Flags:", 147 #print pygame.display.screen.get_flags() 148 149 150 #self.pygame.mouse.set_visible(0) 151 self.pygame.mouse.set_visible(1) 152 153 self.pygame.display.set_caption('Puzzlebox Brainstorms - Client Interface') 154 155 # Set Background 156 self.background = pygame.Surface(self.screen.get_size()) 157 #self.background.set_alpha(0) 158 self.draw_background() 159 160 161 ################################################################## 162 163 def draw_background(self): 164 165 self.background.fill(self.window_background_color) 166 167 self.screen.blit(self.background, (0,0)) 168 169 170 ##################################################################### 171 172 def initialize_buttons(self, button_layout): 173 174 self.buttons = {} 175 self.match_key_index = {} 176 177 for each in button_layout.keys(): 178 179 button = button_layout[each] 180 181 if ((button['button_image'] != None) and \ 182 (button['button_image'] != '.') and \ 183 (button['button_image'] != '..') and \ 184 (os.path.exists(os.path.join(self.image_directory, button['button_image'])))): 185 186 button_image_path = \ 187 os.path.join(self.image_directory, button['button_image']) 188 189 else: 190 if self.DEBUG: 191 print "Error: Button image does not exist:" 192 print " %s" % os.path.join(self.image_directory, button['button_image']) 193 continue 194 195 196 if ((button['activated_image'] != None) and \ 197 (button['activated_image'] != '.') and \ 198 (button['activated_image'] != '..') and \ 199 (os.path.exists(os.path.join(self.image_directory, button['activated_image'])))): 200 201 activated_image_path = \ 202 os.path.join(self.image_directory, button['activated_image']) 203 204 else: 205 if self.DEBUG: 206 print "Error: Activated button image does not exist:" 207 print " %s" % os.path.join(self.image_directory, button['activated_image']) 208 continue 209 210 211 for match in button['match_keys']: 212 self.match_key_index[match] = button['command'] 213 214 215 new_button = \ 216 interface_button.puzzlebox_brainstorms_client_interface_button( \ 217 self.pygame, \ 218 self.screen, \ 219 button_image_path, \ 220 activated_image_path, \ 221 button['image_x'], \ 222 button['image_y'], \ 223 button['command'], \ 224 button['match_keys'], \ 225 self.DEBUG) 226 227 228 self.buttons[ button['command'] ] = new_button 229 230 231 ################################################################## 232 233 def update_button(self, command, activated): 234 235 self.buttons[command].activated=activated 236 self.buttons[command].display() 237 self.update_display() 238 239 240 ################################################################## 241 242 def process_mouse_click(self, position, mouse_button): 243 244 if (mouse_button == 1): 245 # first mouse button clicked 246 247 postion_rect = self.pygame.Rect(position, (1,1) ) 248 249 for each in self.buttons.keys(): 250 251 if self.buttons[each].check_collision(postion_rect): 252 253 command = self.buttons[each].command 254 self.update_button(command, activated=True) 255 self.send_command(command) 256 257 258 ################################################################## 259 260 def process_mouse_release(self, position, mouse_button): 261 262 if (mouse_button == 1): 263 # first mouse button released 264 265 postion_rect = self.pygame.Rect(position, (1,1) ) 266 267 for each in self.buttons.keys(): 268 269 if self.buttons[each].check_collision(postion_rect): 270 271 command = self.buttons[each].command 272 self.update_button(command, activated=False) 273 274 275 ################################################################## 276 277 def release_all_buttons(self): 278 279 for each in self.buttons.keys(): 280 281 command = self.buttons[each].command 282 self.update_button(command, activated=False) 283 284 285 ################################################################## 286 287 def check_events(self): 288 289 for event in self.pygame.event.get(): 290 291 if (event.type is self.pygame.locals.QUIT): 292 reactor.stop() 293 294 295 elif (event.type == self.pygame.locals.KEYDOWN): 296 297 if 'key' in event.dict.keys(): 298 299 key_pressed = event.dict['key'] 300 301 if (key_pressed == self.pygame.K_ESCAPE): 302 reactor.stop() 303 304 305 elif (key_pressed in self.match_key_index.keys()): 306 307 # keypress matches recognized key 308 command = self.match_key_index[key_pressed] 309 self.update_button(command, activated=True) 310 self.send_command(command) 311 312 313 elif (event.type == self.pygame.locals.KEYUP): 314 315 if 'key' in event.dict.keys(): 316 317 key_pressed = event.dict['key'] 318 319 if (key_pressed in self.match_key_index.keys()): 320 321 # keypress matches recognized key 322 command = self.match_key_index[key_pressed] 323 self.update_button(command, activated=False) 324 325 326 elif (event.type == self.pygame.locals.MOUSEBUTTONDOWN): 327 328 position = event.pos 329 mouse_button = event.button 330 331 self.process_mouse_click(position, mouse_button) 332 333 334 elif (event.type == self.pygame.locals.MOUSEBUTTONUP): 335 336 # We release all activated buttons when a mouse button is 337 # released because its possible the user may have pressed 338 # the mouse button down while hovering over one button on 339 # the screen, then dragged the pointer while still holding 340 # down the mouse button, finally releasing it over another 341 # button on the screen. 342 343 mouse_button = event.button 344 345 if (mouse_button == 1): 346 # First mouse button released 347 self.release_all_buttons() 348 349 350 else: 351 352 if self.DEBUG > 2: 353 print "Unrecognized event:", 354 print event 355 356 357 # Sleep timer provides approximately 33.3 fps 358 reactor.callLater(0.03, self.check_events) 359 360 361 ##################################################################### 362 363 class puzzlebox_brainstorms_client_interface_qt(QWidget, Ui_Form): 364 365 def __init__(self, parent = None): 366 367 QWidget.__init__(self, parent) 368 self.setupUi(self) 369 370 self.connect(self.pushButtonBluetoothConnect, \ 371 SIGNAL("clicked()"), \ 372 self.test) 373 374 216 QtGui.QMessageBox.information(self, "Puzzlebox Brainstorms Client", 217 "The following error occurred: %s." % self.tcpSocket.errorString()) 218 219 self.getFortuneButton.setEnabled(True) 220 221 222 ################################################################## 223 224 def turnLeft(self): 225 self.sendCommand('turn_left') 226 227 def driveForward(self): 228 self.sendCommand('drive_forward') 229 230 def turnRight(self): 231 self.sendCommand('turn_right') 232 233 def turnLeftInReverse(self): 234 self.sendCommand('turn_left_in_reverse') 235 236 def driveReverse(self): 237 self.sendCommand('drive_reverse') 238 239 def turnRightInReverse(self): 240 self.sendCommand('turn_right_in_reverse') 241 242 def stopMotors(self): 243 self.sendCommand('stop_motors') 244 245 246 ################################################################## 247 248 def sendCommand(self, command): 249 250 if self.DEBUG: 251 print "Sending:", 252 print command 253 254 self.blockSize = 0 255 self.tcpSocket.abort() 256 self.tcpSocket.connectToHost(SERVER_HOST, SERVER_PORT) 257 258 data = json.dumps(command) 259 self.tcpSocket.write(data) 260 261 262 ################################################################## 263 375 264 def test(self): 376 265 377 266 print "nice." 378 267 379 self.spinBoxConcentrationThreshold.setValue(50)380 self.spinBoxConcentrationThreshold.update()381 382 print dir(self.progressBarConcentration)383 384 268 self.progressBarConcentration.setValue(50) 385 269 … … 395 279 if __name__ == '__main__': 396 280 397 app = QApplication(sys.argv)398 399 f = puzzlebox_brainstorms_client_interface_qt()400 f.show()401 402 #print dir(app)403 #app.setMainWidget(f)281 log = None 282 283 app = QtGui.QApplication(sys.argv) 284 285 window = puzzlebox_brainstorms_client_interface_qt(log, DEBUG) 286 window.show() 287 404 288 app.exec_() 405 406 #window.show() 407 408 #sys.exit(app_.exec_()) 409 410 #f = Ui_Form() 411 412 #print dir(f) 413 414 #f.setupUi(self) 415 416 #f.show() 417 #app.setMainWidget(f) 418 #app.exec_loop() 419 420 289 290 291 -
remote_control/puzzlebox_brainstorms_configuration.ini
r62 r67 39 39 40 40 ##################################################################### 41 # Client Interface configuration [Qt] 42 ##################################################################### 43 44 POWER_THRESHOLDS = {} 45 46 POWER_THRESHOLDS['concentration'] = { 0: 0, 10: 0, 20: 0, 30: 0, 40: 0, 50: 0, 60: 60, 70: 70, 75: 75, 80: 80, 90: 85, 100: 90 } 47 48 POWER_THRESHOLDS['relaxation'] = { 0: 0, 10: 0, 20: 0, 30: 0, 40: 0, 50: 0, 60: 0, 70: 0, 80: 10, 90: 15, 100: 20 } 49 50 51 ##################################################################### 41 52 # Logging 42 53 ##################################################################### -
remote_control/puzzlebox_brainstorms_configuration.py
r62 r67 109 109 110 110 ##################################################################### 111 # Client Interface configuration 111 # Client Interface configuration [Qt] 112 ##################################################################### 113 114 POWER_THRESHOLDS = { \ 115 116 'concentration': { \ 117 0: 0, \ 118 10: 0, \ 119 20: 0, \ 120 30: 0, \ 121 40: 0, \ 122 50: 0, \ 123 60: 60, \ 124 70: 70, \ 125 75: 75, \ 126 80: 80, \ 127 90: 85, \ 128 100: 90, \ 129 }, \ 130 131 'relaxation': { \ 132 0: 0, \ 133 10: 0, \ 134 20: 0, \ 135 30: 0, \ 136 40: 0, \ 137 50: 0, \ 138 60: 0, \ 139 70: 0, \ 140 80: 10, \ 141 90: 15, \ 142 100: 20, \ 143 }, \ 144 145 } # POWER_THRESHOLDS 146 147 148 ##################################################################### 149 # Client Interface configuration [Pygame] 112 150 ##################################################################### 113 151
Note: See TracChangeset
for help on using the changeset viewer.