Permalink
Cannot retrieve contributors at this time
222 lines (187 sloc)
9.33 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python2 | |
| # File: md380tools/md380remote.pyw | |
| # Purpose: 'Remote screen viewer' and screenshot utility | |
| # for MD380, MD390, RT3, RT8, and the like . | |
| # | |
| # Latest modifications: | |
| # 2017-06: Initial version by DL4YHF, using wxPython V4.0.0, | |
| # and examples from zetcode.com/wxpython/layout/ . | |
| # Later modified for wxPython V3.something because | |
| # installing wxPython V4.0.0a ("Phoenix") on a certain | |
| # Linux distro was a painful experience . | |
| # So stick to wxPython V3 until the Phoenix bird can fly. | |
| # | |
| import wx | |
| import array | |
| from md380_tool import * | |
| dfu = None | |
| online = 0 | |
| # ---------------------------------------------------------------------- | |
| class RemoteScreenPanel(wx.Panel): | |
| def __init__(self, parent): | |
| wx.Panel.__init__(self, parent, size=(160, 128)) | |
| self.Bind(wx.EVT_PAINT, self.OnPaint) | |
| self.make_offline_bitmap(self.Size.width, self.Size.height) | |
| self.timer = wx.Timer(self) | |
| self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer ) | |
| # ex: self.timer.StartOnce(500) # interval in milliseconds, fire ONCE ! | |
| # -> "AttributeError: 'Timer' object has no attribute 'StartOnce'" . Sigh,, | |
| self.timer.Start(500, wx.TIMER_ONE_SHOT) # interval in milliseconds, fire ONCE ! | |
| self.n_errors = 0 | |
| def make_offline_bitmap(self, width, height): | |
| # Make a bitmap using an array of RGB bytes, similar as lcd_driver.c : LCD_ColorGradientTest() | |
| bpp = 3 # bytes per pixel | |
| rgb_bytes = array.array('B', [0] * width*height*bpp) | |
| h = self.Size.height | |
| w = self.Size.width | |
| for y in xrange(height): | |
| for x in xrange(width): | |
| offset = y*width*bpp + x*bpp | |
| r = 255 * x / w # increasing from left to right: RED | |
| g = 255 - (255*x/w) # increasing from right to left: GREEN | |
| b = 255 * y / h # increasing from top to bottom: BLUE | |
| rgb_bytes[offset + 0] = r | |
| rgb_bytes[offset + 1] = g | |
| rgb_bytes[offset + 2] = b | |
| # ex: self.rgbBmp = wx.BitmapFromBuffer(width, height, bytes) : "deprecated" in wxPython V4 | |
| # ex: self.rgbBmp = wx.Bitmap.FromBuffer(width, height, bytes) : type object 'Bitmap' has no ..'FromBuffer' . | |
| self.rgbBmp = wx.BitmapFromBuffer(width, height, rgb_bytes) # back to the 'deprecated' stuff. Grrrr. | |
| def DrawRemoteScreen(self, dc, bmp): | |
| dc.DrawBitmap(bmp, 0, 0, True) | |
| if not online: | |
| dc.DrawText( "offline",60,60 ) | |
| def OnPaint(self, evt): | |
| dc = wx.PaintDC(self) | |
| self.DrawRemoteScreen(dc, self.rgbBmp) | |
| def GrabRemoteScreen(self): | |
| # Grab the LCD framebuffer via USB and convert to a (wx-) bitmap | |
| # Read image data with 160 pixels per line, 3 bytes per pixel. | |
| width = 160 # width of the MD380 LCD in pixels | |
| height= 128 # height of the MD380 LCD in pixels | |
| bpp = 3 # bytes per pixel | |
| bytes = array.array('B', [0] * width*height*bpp) | |
| h = self.Size.height | |
| w = self.Size.width | |
| for y in xrange(height): | |
| buf = dfu.read_framebuf_line(y) | |
| if len(buf) == bpp*width: # successfully read from framebuffer | |
| for x in xrange(width): | |
| offset = y*width*bpp + x*bpp | |
| bytes[offset + 0] = buf[3*x+2] # red | |
| bytes[offset + 1] = buf[3*x+1] # green | |
| bytes[offset + 2] = buf[3*x+0] # blue | |
| else: # framebuffer access failed (e.g. LCD driver or controller was busy) | |
| self.n_errors +=1 # don't worry, the next periodic update will fix it | |
| self.rgbBmp = wx.BitmapFromBuffer(width, height, bytes) # "deprecated" in wxPython V4 | |
| # ex: self.rgbBmp = wx.Bitmap.FromBuffer(width, height, bytes) # incompatible with wxPython V3 | |
| def OnTimer(self,event): | |
| if online: | |
| self.GrabRemoteScreen() | |
| self.Refresh() # let wxPython call OnPaint() when it's time to.. | |
| # Allow some time to pass BETWEEN two timer-calls, | |
| # regardless of how long the above operations have taken. | |
| # Firing the timer event every 200 ms may be asking too much. | |
| # ex: self.timer.StartOnce(200) # ok for wxPython V4, incompatible with V3 | |
| self.timer.Start(200, wx.TIMER_ONE_SHOT) # interval in milliseconds, fire ONCE ! | |
| #---------------------------------------------------------------------- | |
| class RemoteKeyboardPanel(wx.Panel): | |
| def __init__(self, parent): | |
| wx.Panel.__init__(self, parent, size=(160, 128)) | |
| # for the keyboard panel, the "grid" sizer is the "main" sizer. | |
| gridSizer = wx.GridSizer(4, 4) # rows, cols | |
| for row in (("M", unichr(0x02C4), unichr(0x02C5), "B"), | |
| ("1", "2", "3", "*"), | |
| ("4", "5", "6", "0"), | |
| ("7", "8", "9", "#")): | |
| for label in row: | |
| b = wx.Button(self, -1, label, size=(32,28)) | |
| if label=="M": | |
| b.SetBackgroundColour('green') | |
| if label=="B": | |
| b.SetBackgroundColour('red') | |
| b.Bind(wx.EVT_LEFT_DOWN, self.OnLeftBtnDownForRemote ) | |
| b.Bind(wx.EVT_LEFT_UP, self.OnLeftBtnUpForRemote ) | |
| gridSizer.Add(b) | |
| self.SetSizer(gridSizer) | |
| gridSizer.Fit(self) | |
| def label_to_key(self, label ): | |
| # For most buttons, the label is the same as the keyboard code | |
| # used in md380tools/applet/src/app_menu.c, except these two: | |
| if label==unichr(0x02C4): | |
| key= 'U' # 'Up' (cursor key) | |
| elif label==unichr(0x02C5): | |
| key= 'D' # 'Down' (cursor key) | |
| else: | |
| key= label.encode('ascii','ignore') | |
| return key | |
| def OnLeftBtnDownForRemote(self, evt): | |
| # Get the clicked button's label, and convert to single-char 'key' | |
| key = self.label_to_key( evt.GetEventObject().GetLabel() ) | |
| if online: # send the key as 'remote control' key ? | |
| dfu.send_keyboard_event( key.encode('ascii','ignore'), 1) | |
| # from Robin Dunn: | |
| # > It may depend on the platform, but I've seen cases where | |
| # > if the LEFT_DOWN handler eats the event then the system | |
| # > doesn't see it and won't know that a dclick event should be sent | |
| # > if there is another left-down within the time out period. 7 | |
| # > Calling Skip should allow it to keep working if you need it. | |
| evt.Skip() # let others (the "system"?) process this event, too | |
| def OnLeftBtnUpForRemote(self, evt): | |
| key = self.label_to_key(evt.GetEventObject().GetLabel()) | |
| if online: # signal 'key RELEASED' to the remotely controlled rig: | |
| dfu.send_keyboard_event( key.encode('ascii', 'ignore'), 0) | |
| evt.Skip() # let others (the "system"?) process this event, too | |
| #---------------------------------------------------------------------- | |
| class MainFrame ( wx.Frame ): | |
| def __init__( self ): | |
| # loosely based on wiki.wxpython.org/BoxSizerTutorial .. | |
| wx.Frame.__init__ ( self, None, wx.ID_ANY, title = "MD380 Screenshot", | |
| size = wx.Size( 240,300 ), | |
| style= wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX ) | |
| # Add a panel so it looks correct on all platforms | |
| self.panel = wx.Panel(self, wx.ID_ANY) | |
| # Create the widgets first.. proceed from top to bottom: | |
| self.imgPanel = RemoteScreenPanel(self.panel) | |
| keyPanel = RemoteKeyboardPanel(self.panel) | |
| captureBtn = wx.Button(self.panel, wx.ID_ANY, 'Capture') | |
| exitBtn = wx.Button(self.panel, wx.ID_ANY, 'Exit') | |
| # Create 'BoxSizers', used here to align and position widgets on the form | |
| topSizer = wx.BoxSizer(wx.VERTICAL) | |
| imgSizer = wx.BoxSizer(wx.HORIZONTAL) | |
| btnSizer = wx.BoxSizer(wx.HORIZONTAL) | |
| # Let the 'BoxSizers' know what's inside, from top to bottom. | |
| # mySizer.Add(window, proportion, flag(s), border [, userData] ) | |
| # border : number of pixels around the widget that's been added | |
| imgSizer.Add(self.imgPanel, 0, wx.ALL, 5) | |
| btnSizer.Add(captureBtn, 0, wx.ALL, 5) | |
| btnSizer.Add(exitBtn, 0, wx.ALL, 5) | |
| topSizer.Add(imgSizer, 0, wx.CENTER) | |
| topSizer.Add(keyPanel, 1, wx.CENTER) | |
| topSizer.Add(wx.StaticLine(self.panel,), 0, wx.ALL|wx.EXPAND, 5) | |
| topSizer.Add(btnSizer, 0, wx.ALL|wx.CENTER, 5) | |
| self.panel.SetSizer(topSizer) | |
| topSizer.Fit(self) | |
| # Bind widgets to their event handlers | |
| self.Bind(wx.EVT_BUTTON, self.OnCapture, captureBtn) | |
| self.Bind(wx.EVT_BUTTON, self.OnExit, exitBtn) | |
| # Start capture-file sequence at index ONE | |
| self.CaptureIndex = 0 | |
| def __del__( self ): | |
| pass | |
| def OnCapture(self, event): | |
| # Try to save the currently visible bitmap (imgPanel.rgbBmp) | |
| # as a file, without asking dumb questions about where to save it. | |
| self.CaptureIndex = self.CaptureIndex+1 | |
| self.imgPanel.rgbBmp.SaveFile('screenshot_'+str(self.CaptureIndex)+'.png', wx.BITMAP_TYPE_PNG) | |
| def OnExit(self, event): | |
| self.Close(True) | |
| if __name__ == '__main__': | |
| try: | |
| dfu = init_dfu() # raises exception when radio not connected | |
| online = True | |
| except Exception: | |
| online = False | |
| app = wx.App() | |
| frame = MainFrame() | |
| frame.Show() | |
| app.MainLoop() |