├── .gitignore ├── device.py ├── icons.py ├── icons ├── icon-16.png ├── icon-24.png ├── icon-256.png ├── icon-32.png └── icon.ico ├── main.py ├── modes.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | 3 | import itertools 4 | import platform 5 | 6 | from time import sleep 7 | from glob import glob 8 | from pyfirmata import Arduino, OUTPUT 9 | 10 | 11 | class CubeDevice(object): 12 | green_pins = [5, 9] 13 | red_pins = [6, 10] 14 | 15 | def __init__(self): 16 | self.board = None 17 | 18 | def discover(self): 19 | if platform.system() == 'Windows': 20 | return list(self._discover_windows()) 21 | elif platform.system() == 'Darwin': 22 | return list(self._discover_posix(['/dev/tty.usbmodem*', '/dev/tty.usbserial*'])) 23 | else: 24 | return list(self._discover_posix(['/dev/ttyACM*', '/dev/ttyUSB*'])) 25 | 26 | def _discover_windows(self): 27 | import _winreg as winreg 28 | path = r'HARDWARE\DEVICEMAP\SERIALCOMM' 29 | try: 30 | key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) 31 | except WindowsError: 32 | return 33 | 34 | for i in itertools.count(): 35 | try: 36 | val = winreg.EnumValue(key, i) 37 | yield str(val[1]) 38 | except EnvironmentError: 39 | break 40 | 41 | def _discover_posix(self, patterns): 42 | for p in patterns: 43 | for match in glob(p): 44 | yield match 45 | 46 | def _write_pins(self, pins, value): 47 | for p in pins: 48 | self.board.digital[p].write(value) 49 | 50 | def go_green(self): 51 | self._write_pins(self.red_pins, 0) 52 | self._write_pins(self.green_pins, 1) 53 | 54 | def go_red(self): 55 | self._write_pins(self.green_pins, 0) 56 | self._write_pins(self.red_pins, 1) 57 | 58 | def blink(self): 59 | for i in xrange(5): 60 | self.go_red() 61 | sleep(0.1) 62 | self.go_green() 63 | sleep(0.1) 64 | 65 | def connect(self, port): 66 | self.board = Arduino(port) 67 | for p in self.green_pins + self.red_pins: 68 | self.board.digital[p].mode = OUTPUT 69 | 70 | def disconnect(self): 71 | if not self.board: 72 | return 73 | self._write_pins(self.red_pins, 0) 74 | self._write_pins(self.green_pins, 0) 75 | -------------------------------------------------------------------------------- /icons.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | 3 | from wx.lib.embeddedimage import PyEmbeddedImage 4 | 5 | icon_32 = PyEmbeddedImage( 6 | "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9i" 7 | "ZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tl" 8 | "dCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1l" 9 | "dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUu" 10 | "MC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpS" 11 | "REYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgt" 12 | "bnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8v" 13 | "bnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNv" 14 | "bS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEu" 15 | "MC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9w" 16 | "IENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjY5MzI1RDQ3NkEwOTEx" 17 | "REY4NDMwQzg4MUZENDU1RkU0IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjY5MzI1RDQ4" 18 | "NkEwOTExREY4NDMwQzg4MUZENDU1RkU0Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmlu" 19 | "c3RhbmNlSUQ9InhtcC5paWQ6NjkzMjVENDU2QTA5MTFERjg0MzBDODgxRkQ0NTVGRTQiIHN0" 20 | "UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NjkzMjVENDY2QTA5MTFERjg0MzBDODgxRkQ0NTVG" 21 | "RTQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBh" 22 | "Y2tldCBlbmQ9InIiPz4/EgeCAAADB0lEQVR42sSXsU4bQRCGZ+7wwu7dKZIVyaHMA6CUiAeg" 23 | "TJGnoKZzY0GBXCLaSLxDilDzAHkG2jgFXbCt4NvzMbN3Zwy7s46EgEWL7/x5b2ZnZ/7dw7qu" 24 | "4T0bnpycACJ+pevdN7T7hyb+8/T0FLb4jm526eb7W1knW0fd9dZyuXSRaB15/ZAjAtnE5w44" 25 | "49xvro4gz3rewMIoyPbP4eaIeK/l/JjW50IRP2d+RTwLcEN83113Np840DWjUxj0tTgDkxLX" 26 | "Ma6J96NRCDrAnxyBylqw1EMtpd+8lLdLAGIOlLaCsrSP4VsLY8K8Ii4YaLhd47XHvRyo6IHr" 27 | "OWBt6XqoKeZl6brMbZS7KLc2g0lorTxDx6tN3AZ4E8rOxpMlaL3BDmiFtI6VmDyaQrg+A58r" 28 | "4lbk2IyXy3D38FLMXscvN/HDKBcjwF9yv54cQ5b7OmAKBXt4BpPJBPI893WiKNzsro8nkPVy" 29 | "TweMKmDvDP0IPE9CbVLoD8J1Xs9rMMbAYDAI8vl8Djo10NcDIQJzPwnXHeAIcBJJdcycfx/j" 30 | "nMQxniRJ0AFcVUHZ6AB6VdzqBJVYKZRZUwXEqYyl8W0EUI5ARAcaHo9QowMy95agfRiu64D0" 31 | "gDqpnXEpAhzeZgmlCCWtDStHQGl0YhOcAf1p2ogkHXBqh1oevwQxCVcROCguonXM5RbjBxfF" 32 | "Rh0I5kCnA6PfI+gFdEDlCobpEEa//kIv83VAmQKGn2cwGhEP6IAiHRgOZ6tKEnMgpfOA/ijo" 33 | "wIK5Ad2X6nwKKemA1jLfmAMbs9y+TAc2VkFMaJoDS7sdB84LzfgyUgW1H4HnDkSFJq3dVhvb" 34 | "jlnEytL+vwN3d3dPTkRVJISsA82RS55hVcU5N7LpOZB0VYA78f0elY6eFxB1dDx3spmsHJhO" 35 | "p86B7otxfxw90Y6/pPT/n8zHcc6NbD46cHt7C1mWYUrH7e3t7Vd/MWE7tG3jbDZbScUHpdQ3" 36 | "Oih8eqtXM8qFyWKx+EGfLhl2WKio8/R7r22b+n3bLTlwj+/9ev4gwAAojC2jjutN0QAAAABJ" 37 | "RU5ErkJggg==") 38 | 39 | icon_16 = PyEmbeddedImage( 40 | "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9i" 41 | "ZSBJbWFnZVJlYWR5ccllPAAAAbtJREFUeNqkk79Kw1AUxr9e88fEBCIYsI6dnaWDm119Bh0L" 42 | "rkJdSgvtJkEHB6E+hR0FH8BH0E2xxaEg0g7JTVLvOUlvW7diCr38cs/58n0nN5Vut3sKoIrN" 43 | "r3Gn0xkaaZoe9Hq9+0272+12k1YjyzLM53O8Pp7Bd03e3D8Z4O38HL5Z8oB4qNhlrj40QH0s" 44 | "IKWs5HkOxxIIA5tvMgvF9ipbigPN1McCKgILpFJCJpILtpQ6s/zLCbNJ9apv4YAV9xsDnY/s" 45 | "ke11bmguHRQRkiRhB08fTbh+kbnu32I0GsH3fWZany9GcM2Cj+92QX16BvQEyxEIwiJzNsvg" 46 | "OA7CMGSezWawhIPADktH8XIGCwcylUhKWySoMmqbxFKmep/qtYM4jlmg7t0A0zKz+nmeh+l0" 47 | "qnPXbzwsCvLcAvWtObh8v4S5U8ygH/TRevmB6RaZ+4cVtFqKyxlcXxvrDsii2Baw98oZxIot" 48 | "B3awyPwFoWZg6xl8Lx2oAVXoJMqV9168plSfC83lPtVTHwuMx2NDqFMXVaPlQbeA6Ej94Uff" 49 | "iKIlC2GA+lhgMpl81mq1q00/JvXQT1rZ/n+uXwEGAOa/G6mCJlcxAAAAAElFTkSuQmCC") 50 | -------------------------------------------------------------------------------- /icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amperka/cube/232ca11627af559c9e480db2b4da074089034afb/icons/icon-16.png -------------------------------------------------------------------------------- /icons/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amperka/cube/232ca11627af559c9e480db2b4da074089034afb/icons/icon-24.png -------------------------------------------------------------------------------- /icons/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amperka/cube/232ca11627af559c9e480db2b4da074089034afb/icons/icon-256.png -------------------------------------------------------------------------------- /icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amperka/cube/232ca11627af559c9e480db2b4da074089034afb/icons/icon-32.png -------------------------------------------------------------------------------- /icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amperka/cube/232ca11627af559c9e480db2b4da074089034afb/icons/icon.ico -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | 3 | import platform 4 | import threading 5 | import wx 6 | import modes 7 | import icons 8 | 9 | from wx.lib.newevent import NewEvent 10 | from device import CubeDevice 11 | 12 | 13 | #============================================================================== 14 | class CubeApp(wx.App): 15 | def __init__(self): 16 | super(CubeApp, self).__init__() 17 | self.device = CubeDevice() 18 | 19 | def Run(self): 20 | frame = MainFrame(None, self.device) 21 | frame.Show() 22 | self.SetTopWindow(frame) 23 | self.SetExitOnFrameDelete(True) 24 | TaskBarIcon() 25 | self.MainLoop() 26 | self.device.disconnect() 27 | 28 | #============================================================================== 29 | 30 | class TaskBarIcon(wx.TaskBarIcon): 31 | def __init__(self): 32 | super(TaskBarIcon, self).__init__() 33 | self.SetupIcon() 34 | self.Bind(wx.EVT_TASKBAR_LEFT_DOWN, self.OnLeftClick) 35 | 36 | def CreatePopupMenu(self): 37 | menu = wx.Menu() 38 | exit = wx.MenuItem(menu, -1, u'Закрыть') 39 | menu.Bind(wx.EVT_MENU, self.OnExit, id=exit.GetId()) 40 | menu.AppendItem(exit) 41 | return menu 42 | 43 | def SetupIcon(self): 44 | img = icons.icon_16 45 | self.SetIcon(icons.icon_16.GetIcon(), u"Куб") 46 | 47 | def OnLeftClick(self, event): 48 | frame = wx.GetApp().GetTopWindow() 49 | frame.Show() 50 | frame.Restore() 51 | frame.Raise() 52 | 53 | def OnExit(self, event): 54 | wx.GetApp().GetTopWindow().Destroy() 55 | 56 | #============================================================================== 57 | class ActionPanel(wx.Panel): 58 | def __init__(self, parent, device): 59 | super(ActionPanel, self).__init__(parent) 60 | self.device = device 61 | self.panels = {} 62 | self.active_panel = None 63 | self.InitUI() 64 | 65 | def InitUI(self): 66 | self.mode_combobox = wx.ComboBox(self, style=wx.CB_READONLY) 67 | self.mode_combobox.Bind(wx.EVT_COMBOBOX, self.OnComboboxChanged) 68 | 69 | self.sizer = wx.BoxSizer(wx.VERTICAL) 70 | self.sizer.Add(self.mode_combobox, 0, wx.EXPAND | wx.ALL, 10) 71 | 72 | self.AddMode(u'Управлять вручную', ManualControlPanel(self, self.device)) 73 | 74 | slack_mode = modes.SlackMode(self.device) 75 | self.AddMode(u'Проверять Slack', SlackPanel(self, slack_mode, False)) 76 | 77 | gmail_mode = modes.GMailMode(self.device) 78 | self.AddMode(u'Проверять GMail', MailPanel(self, gmail_mode, False)) 79 | 80 | mailru_mode = modes.MailruMode(self.device) 81 | self.AddMode(u'Проверять Mail.ru', MailPanel(self, mailru_mode, False)) 82 | 83 | generic_imap_mode = modes.ImapMode(self.device) 84 | self.AddMode(u'Проверять почту через IMAP', MailPanel(self, generic_imap_mode, True)) 85 | 86 | self.ShowPanel(u'Управлять вручную') 87 | self.SetSizer(self.sizer) 88 | 89 | def AddMode(self, name, panel): 90 | self.panels[name] = panel 91 | self.mode_combobox.Append(name) 92 | panel.Hide() 93 | self.sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 10) 94 | 95 | def ShowPanel(self, name): 96 | if self.active_panel: 97 | self.active_panel.DeactivateMode() 98 | self.active_panel.Hide() 99 | self.panels[name].Show() 100 | self.active_panel = self.panels[name] 101 | self.active_panel.ActivateMode() 102 | self.mode_combobox.SetSelection(self.mode_combobox.FindString(name)) 103 | self.Layout() 104 | 105 | def OnComboboxChanged(self, event): 106 | self.ShowPanel(self.mode_combobox.GetValue()) 107 | 108 | 109 | #============================================================================== 110 | class MailPanel(wx.Panel): 111 | def __init__(self, parent, mode, show_imap_host_port): 112 | super(MailPanel, self).__init__(parent) 113 | self.mode = mode 114 | self.show_imap_host_port = show_imap_host_port 115 | 116 | self.InitUI() 117 | self.Bind(wx.EVT_WINDOW_DESTROY, lambda e: self.mode.stop()) 118 | 119 | def InitUI(self): 120 | sizer = wx.BoxSizer(wx.VERTICAL) 121 | 122 | credentials_panel = self.CreateCredentialsUI() 123 | self.run_button = wx.Button(self, label=u'Поехали!') 124 | self.cancel_button = wx.Button(self, label=u'Остановить') 125 | self.status_label = wx.StaticText(self) 126 | 127 | self.run_button.Bind(wx.EVT_BUTTON, self.OnRunButton) 128 | self.cancel_button.Bind(wx.EVT_BUTTON, self.OnCancelButton) 129 | 130 | sizer.Add(credentials_panel, 1, wx.EXPAND) 131 | sizer.Add(self.status_label, 0, wx.EXPAND | wx.ALL, 10) 132 | sizer.Add(self.run_button, 0, wx.EXPAND | wx.ALL, 10) 133 | sizer.Add(self.cancel_button, 0, wx.EXPAND | wx.ALL, 10) 134 | self.cancel_button.Hide() 135 | 136 | self.SetSizer(sizer) 137 | 138 | def CreateCredentialsUI(self): 139 | cp = wx.Panel(self) 140 | 141 | sizer = wx.FlexGridSizer(rows=0, cols=2, vgap=10, hgap=10) 142 | sizer.AddGrowableCol(1, 1) 143 | 144 | if self.show_imap_host_port: 145 | hbox = wx.BoxSizer(wx.HORIZONTAL) 146 | hp_panel = wx.Panel(cp) 147 | 148 | self.host_input = wx.TextCtrl(hp_panel) 149 | self.port_input = wx.TextCtrl(hp_panel) 150 | hbox.AddMany([ 151 | (self.host_input, 3, wx.EXPAND), 152 | 153 | (wx.StaticText(hp_panel, label=u" Порт"), 0, wx.ALIGN_CENTRE_VERTICAL), 154 | (self.port_input, 1, wx.EXPAND), 155 | ]) 156 | 157 | hp_panel.SetSizer(hbox) 158 | 159 | sizer.AddMany([ 160 | (wx.StaticText(cp, label=u"Хост"), 0, wx.ALIGN_CENTRE_VERTICAL), 161 | (hp_panel, 1, wx.EXPAND), 162 | ]) 163 | 164 | 165 | self.login_input = wx.TextCtrl(cp) 166 | self.login_label = wx.StaticText(cp, label=u"Логин") 167 | self.password_input = wx.TextCtrl(cp, style=wx.TE_PASSWORD) 168 | self.password_label = wx.StaticText(cp, label=u"Пароль") 169 | sizer.AddMany([ 170 | (self.login_label, 0, wx.ALIGN_CENTRE_VERTICAL), 171 | (self.login_input, 1, wx.EXPAND), 172 | 173 | (self.password_label, 0, wx.ALIGN_CENTRE_VERTICAL), 174 | (self.password_input, 1, wx.EXPAND), 175 | ]) 176 | 177 | cp.SetSizer(sizer) 178 | 179 | return cp 180 | 181 | def ActivateMode(self): 182 | if self.show_imap_host_port: 183 | self.host_input.Enable() 184 | self.port_input.Enable() 185 | 186 | self.login_input.Enable() 187 | self.password_input.Enable() 188 | self.run_button.Show() 189 | self.cancel_button.Hide() 190 | self.status_label.SetLabel(u'') 191 | 192 | def DeactivateMode(self): 193 | self.mode.stop() 194 | 195 | def OnRunButton(self, event): 196 | if self.show_imap_host_port: 197 | self.mode.set_host_port(self.host_input.GetValue(), 198 | self.port_input.GetValue()) 199 | 200 | self.mode.set_credentials(self.login_input.GetValue(), 201 | self.password_input.GetValue()) 202 | 203 | if self.show_imap_host_port: 204 | self.host_input.Disable() 205 | self.port_input.Disable() 206 | 207 | self.login_input.Disable() 208 | self.password_input.Disable() 209 | self.run_button.Hide() 210 | self.cancel_button.Show() 211 | self.Layout() 212 | 213 | self.mode.bind(modes.EVT_STATUS_CHANGED, self.OnStatusChanged) 214 | threading.Thread(target=self.mode.loop).start() 215 | 216 | def OnCancelButton(self, event): 217 | self.DeactivateMode() 218 | self.ActivateMode() 219 | self.mode.unbind(modes.EVT_STATUS_CHANGED, self.OnStatusChanged) 220 | self.Layout() 221 | 222 | def OnStatusChanged(self, event): 223 | self.status_label.SetLabel(event.status) 224 | 225 | 226 | #============================================================================== 227 | class SlackPanel(MailPanel): 228 | 229 | def CreateCredentialsUI(self): 230 | res = super(SlackPanel, self).CreateCredentialsUI() 231 | self.password_label.Hide() 232 | self.password_input.Hide() 233 | self.login_label.LabelText = u'Slack токен' 234 | return res 235 | 236 | 237 | #============================================================================== 238 | class ManualControlPanel(wx.Panel): 239 | def __init__(self, parent, device): 240 | super(ManualControlPanel, self).__init__(parent) 241 | self.device = device 242 | self.InitUI() 243 | 244 | def InitUI(self): 245 | self.red_button = wx.Button(self, label=u'Красный') 246 | self.green_button = wx.Button(self, label=u'Зелёный') 247 | 248 | self.red_button.Bind(wx.EVT_BUTTON, self.OnRedButton) 249 | self.green_button.Bind(wx.EVT_BUTTON, self.OnGreenButton) 250 | 251 | box = wx.BoxSizer(wx.HORIZONTAL) 252 | box.Add(self.red_button, 1, wx.EXPAND | wx.BOTTOM, 10) 253 | box.Add(self.green_button, 1, wx.EXPAND | wx.BOTTOM, 10) 254 | self.SetSizer(box) 255 | 256 | def OnRedButton(self, event): 257 | self.device.go_red() 258 | 259 | def OnGreenButton(self, event): 260 | self.device.go_green() 261 | 262 | def ActivateMode(self): 263 | pass 264 | 265 | def DeactivateMode(self): 266 | pass 267 | 268 | 269 | #============================================================================== 270 | class LongTaskPanel(wx.Panel): 271 | def __init__(self, parent, text): 272 | super(LongTaskPanel, self).__init__(parent) 273 | self.InitUI(text) 274 | 275 | self.timer = wx.Timer(self) 276 | self.timer.Start(50) 277 | self.Bind(wx.EVT_TIMER, lambda e: self.bar.Pulse()) 278 | 279 | def InitUI(self, text): 280 | panel = wx.Panel(self) 281 | label = wx.StaticText( 282 | panel, label=text, 283 | style=wx.ALIGN_CENTRE_HORIZONTAL) 284 | self.bar = wx.Gauge(self) 285 | self.bar.Pulse() 286 | 287 | vbox = wx.BoxSizer(wx.VERTICAL) 288 | hbox = wx.BoxSizer(wx.HORIZONTAL) 289 | 290 | hbox.Add(label, 1, wx.ALIGN_BOTTOM, 10) 291 | 292 | vbox.Add(panel, 1, wx.ALIGN_CENTRE_HORIZONTAL | wx.LEFT | wx.RIGHT, 10) 293 | vbox.Add(self.bar, 0, wx.EXPAND | wx.ALIGN_TOP | wx.ALL, 10) 294 | vbox.Add(wx.Panel(self), 1, wx.EXPAND, 10) 295 | 296 | panel.SetSizer(hbox) 297 | self.SetSizer(vbox) 298 | 299 | #============================================================================== 300 | PortSelectedEvent, EVT_PORT_SELECTED = NewEvent() 301 | 302 | class PortSelectionPanel(wx.Panel): 303 | def __init__(self, parent, device): 304 | super(PortSelectionPanel, self).__init__(parent) 305 | self.device = device 306 | self.InitUI() 307 | 308 | def InitUI(self): 309 | box = wx.BoxSizer(wx.VERTICAL) 310 | 311 | self.listbox = wx.ListBox(self) 312 | self.listbox.Bind(wx.EVT_LISTBOX_DCLICK, self.OnOK) 313 | self.listbox.Bind(wx.EVT_LISTBOX, self.OnSelectionChange) 314 | 315 | self.ok_button = wx.Button(self, label=u'OK') 316 | self.ok_button.Bind(wx.EVT_BUTTON, self.OnOK) 317 | self.ok_button.Disable() 318 | 319 | box.Add(wx.StaticText(self, label=u'К какому порту подключен куб?'), 0, wx.EXPAND | wx.ALL, 10) 320 | box.Add(self.listbox, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) 321 | box.Add(self.ok_button, 0, wx.EXPAND | wx.ALL, 10) 322 | 323 | self.SetSizer(box) 324 | 325 | def SetPorts(self, ports): 326 | self.listbox.InsertItems(ports, 0) 327 | 328 | def OnOK(self, event): 329 | port = self.listbox.GetString(self.listbox.GetSelection()) 330 | wx.PostEvent(self, PortSelectedEvent(port=port)) 331 | 332 | def OnSelectionChange(self, event): 333 | self.ok_button.Enable(self.listbox.GetSelection() != wx.NOT_FOUND) 334 | 335 | 336 | #============================================================================== 337 | SearchAgainEvent, EVT_SEARCH_AGAIN = NewEvent() 338 | 339 | class PortNotFoundPanel(wx.Panel): 340 | def __init__(self, parent): 341 | super(PortNotFoundPanel, self).__init__(parent) 342 | self.InitUI() 343 | 344 | def InitUI(self): 345 | vbox = wx.BoxSizer(wx.VERTICAL) 346 | 347 | search_again_button = wx.Button(self, label=u'Искать снова') 348 | search_again_button.Bind(wx.EVT_BUTTON, lambda e: wx.PostEvent(self, SearchAgainEvent())) 349 | 350 | txt_box = wx.BoxSizer(wx.HORIZONTAL) 351 | txt_panel = wx.Panel(self) 352 | txt = wx.StaticText( 353 | txt_panel, label=u'Куб не найден.\nУбедитесь, что он подключён к компьютеру.', 354 | style=wx.ALIGN_CENTRE_HORIZONTAL | wx.TE_MULTILINE) 355 | txt_box.Add(txt, 1, wx.ALIGN_CENTRE_VERTICAL, 10) 356 | txt_panel.SetSizer(txt_box) 357 | 358 | vbox.Add(txt_panel, 1, wx.ALIGN_CENTRE_HORIZONTAL, 10) 359 | vbox.Add(search_again_button, 0, wx.EXPAND | wx.ALL, 10) 360 | 361 | self.SetSizer(vbox) 362 | 363 | 364 | #============================================================================== 365 | DiscoveredEvent, EVT_DISCOVERED = NewEvent() 366 | ConnectedEvent, EVT_CONNECTED = NewEvent() 367 | 368 | class MainFrame(wx.Frame): 369 | def __init__(self, parent, device): 370 | super(MainFrame, self).__init__( 371 | parent, title=u'Технокуб', size=(320, 360), 372 | style=(wx.RESIZE_BORDER | wx.CAPTION | wx.CLOSE_BOX | wx.FRAME_NO_TASKBAR)) 373 | 374 | self.device = device 375 | self.active_panel = None 376 | 377 | self.InitUI() 378 | self.Centre() 379 | self.Show() 380 | self.Discover() 381 | 382 | def InitUI(self): 383 | self.sizer = wx.BoxSizer(wx.VERTICAL) 384 | 385 | self.discovery_panel = self.AddPanel(LongTaskPanel(self, u'Поиск куба…')) 386 | self.connection_panel = self.AddPanel(LongTaskPanel(self, u'Подключение к кубу…')) 387 | self.port_selection_panel = self.AddPanel(PortSelectionPanel(self, self.device)) 388 | self.port_not_found_panel = self.AddPanel(PortNotFoundPanel(self)) 389 | self.action_panel = self.AddPanel(ActionPanel(self, self.device)) 390 | 391 | self.port_selection_panel.Bind(EVT_PORT_SELECTED, self.OnPortSelected) 392 | self.port_not_found_panel.Bind(EVT_SEARCH_AGAIN, self.OnSearchAgain) 393 | self.Bind(EVT_DISCOVERED, self.OnDiscovered) 394 | self.Bind(EVT_CONNECTED, self.OnConnected) 395 | self.Bind(wx.EVT_CLOSE, self.OnClose) 396 | 397 | self.SetSizer(self.sizer) 398 | 399 | self.SetIcon(icons.icon_32.GetIcon()) 400 | 401 | def AddPanel(self, panel): 402 | panel.Hide() 403 | self.sizer.Add(panel, 1, wx.EXPAND) 404 | return panel 405 | 406 | def ShowPanel(self, panel): 407 | if self.active_panel: 408 | self.active_panel.Hide() 409 | panel.Show() 410 | self.active_panel = panel 411 | self.Layout() 412 | 413 | def Discover(self): 414 | self.ShowPanel(self.discovery_panel) 415 | threading.Thread(target=self._DoDiscover).start() 416 | 417 | def _DoDiscover(self): 418 | ports = self.device.discover() 419 | wx.PostEvent(self, DiscoveredEvent(ports=ports)) 420 | 421 | def OnDiscovered(self, event): 422 | ports = event.ports 423 | if len(ports) > 1: 424 | self.port_selection_panel.SetPorts(ports) 425 | self.ShowPanel(self.port_selection_panel) 426 | elif len(ports) == 1: 427 | self.ConnectDevice(ports[0]) 428 | else: 429 | self.ShowPanel(self.port_not_found_panel) 430 | 431 | def OnPortSelected(self, event): 432 | self.ConnectDevice(event.port) 433 | 434 | def OnSearchAgain(self, event): 435 | self.ShowPanel(self.discovery_panel) 436 | self.Discover() 437 | 438 | def ConnectDevice(self, port): 439 | self.ShowPanel(self.connection_panel) 440 | threading.Thread(target=self._DoConnectDevice, args=(port, )).start() 441 | 442 | def _DoConnectDevice(self, port): 443 | self.device.connect(port) 444 | wx.PostEvent(self, ConnectedEvent()) 445 | 446 | def OnConnected(self, event): 447 | self.ShowPanel(self.action_panel) 448 | 449 | def OnClose(self, event): 450 | if platform.system() == 'Windows': 451 | self.Hide() 452 | else: 453 | self.Destroy() 454 | 455 | 456 | 457 | if __name__ == '__main__': 458 | CubeApp().Run() 459 | -------------------------------------------------------------------------------- /modes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | 3 | import wx 4 | import imaplib 5 | import socket 6 | import urllib2 7 | import json 8 | import threading 9 | import logging 10 | 11 | from time import sleep 12 | from wx.lib.newevent import NewEvent 13 | 14 | StatusChangedEvent, EVT_STATUS_CHANGED = NewEvent() 15 | 16 | 17 | class Mode(object): 18 | def __init__(self): 19 | self._evt_transport = wx.Frame(wx.GetApp().GetTopWindow()) 20 | 21 | def bind(self, evt, handler): 22 | self._evt_transport.Bind(evt, handler) 23 | 24 | def unbind(self, evt, handler): 25 | self._evt_transport.Unbind(evt, handler=handler) 26 | 27 | def _post_event(self, event): 28 | if isinstance(self._evt_transport, wx.EvtHandler): 29 | wx.PostEvent(self._evt_transport, event) 30 | 31 | 32 | class ImapMode(Mode): 33 | def __init__(self, device, interval=20): 34 | super(ImapMode, self).__init__() 35 | self.device = device 36 | self.interval = interval 37 | self.status = u'' 38 | self._prev_count = 0 39 | self._host = None 40 | self._port = None 41 | self._login = None 42 | self._password = None 43 | 44 | def set_host_port(self, host, port): 45 | self._host = host 46 | self._port = port 47 | 48 | def set_credentials(self, login, password): 49 | self._login = login 50 | self._password = password 51 | 52 | def loop(self): 53 | self._stopped = False 54 | while not self._stopped: 55 | self.set_status(u"Проверка почты…") 56 | count = 0 57 | 58 | try: 59 | count = self._fetch_unread_count() 60 | message = u"Писем: {}".format(count) 61 | except imaplib.IMAP4.error as e: 62 | message = u"Неверные логин/пароль" 63 | except socket.error: 64 | message = u"Нет соединения с сервером" 65 | 66 | self.set_status(message) 67 | 68 | if self._stopped: 69 | break 70 | 71 | if count > self._prev_count: 72 | self.device.blink() 73 | 74 | if count: 75 | self.device.go_green() 76 | else: 77 | self.device.go_red() 78 | 79 | self._prev_count = count 80 | 81 | countdown = self.interval 82 | while countdown > 0 and not self._stopped: 83 | sleep(0.1) 84 | countdown -= 0.1 85 | self.set_status(u"{} ~ {:.0f}".format(message, countdown)) 86 | 87 | def stop(self): 88 | self._stopped = True 89 | 90 | def set_status(self, status): 91 | self.status = status 92 | self._post_event(StatusChangedEvent(status=status)) 93 | 94 | def _fetch_unread_count(self): 95 | connection = imaplib.IMAP4_SSL(self._host, self._port) 96 | connection.login(self._login, self._password) 97 | connection.select() 98 | resp = connection.search(None, 'UnSeen') 99 | return len(resp[1][0].split()) 100 | 101 | 102 | class GMailMode(ImapMode): 103 | def __init__(self, device, interval=20): 104 | super(GMailMode, self).__init__(device, interval) 105 | self.set_host_port('imap.gmail.com', '993') 106 | 107 | 108 | class MailruMode(ImapMode): 109 | def __init__(self, device, interval=20): 110 | super(MailruMode, self).__init__(device, interval) 111 | self.set_host_port('imap.mail.ru', '993') 112 | 113 | 114 | class SlackMode(ImapMode): 115 | def __init__(self, device, interval=10): 116 | self.UNREADS = [] 117 | self._init_logging() 118 | return super(SlackMode, self).__init__(device, interval) 119 | 120 | def _init_logging(self): 121 | logger = logging.getLogger(SlackMode.__name__) 122 | logger.setLevel(logging.INFO) 123 | console_handler = logging.StreamHandler() 124 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - ' 125 | '%(message)s') 126 | console_handler.setFormatter(formatter) 127 | logger.addHandler(console_handler) 128 | self._logger = logger 129 | 130 | def decorate(func): 131 | base_url = 'https://slack.com/api/' 132 | 133 | def wrapper(self, uri_part): 134 | uri_part = func(self, uri_part) 135 | token = self._login 136 | return '%s%s?token=%s' % (base_url, uri_part, token) 137 | return wrapper 138 | 139 | @decorate 140 | def get_url(self, uri_part): 141 | return uri_part 142 | 143 | def get_unreads_count(self, item_id, uri_part): 144 | history_url = self.get_url('%s.history' % uri_part) 145 | url = '%s&channel=%s&unreads=True' % (history_url, item_id) 146 | try: 147 | history = json.loads(urllib2.urlopen(url).read()) 148 | self.UNREADS.append(history['unread_count_display']) 149 | except urllib2.URLError, e: 150 | self.error(e) 151 | 152 | def get_unreads(self, uri, key): 153 | url = self.get_url(uri) 154 | resp = json.loads(urllib2.urlopen(url).read()) 155 | if 'error' in resp: 156 | self.error(str(resp['error'])) 157 | 158 | items = resp[key] 159 | ids = map(lambda x: x['id'], items) 160 | threads = [] 161 | for item_id in ids: 162 | uri_part = uri.split('.')[0] 163 | t = threading.Thread(target=self.get_unreads_count, 164 | args=(item_id, uri_part)) 165 | threads.append(t) 166 | map(lambda x: x.start(), threads) 167 | map(lambda x: x.join(), threads) 168 | 169 | def _fetch_threads(self): 170 | self.get_unreads('channels.list', 'channels') 171 | self.get_unreads('im.list', 'ims') 172 | 173 | def error(self, message): 174 | self._logger.error('Ошибка: %s', message) 175 | self.set_status(message) 176 | self.stop() 177 | raise LoginError 178 | 179 | def _fetch_unread_count(self): 180 | self.UNREADS = [] 181 | if not self._login: 182 | message = 'Отсутствует токен.' 183 | self.error(message) 184 | 185 | self._fetch_threads() 186 | res = sum(self.UNREADS) 187 | if res: 188 | self._logger.info('Нових писем: %s', res) 189 | return res 190 | 191 | 192 | class LoginError(Exception): 193 | pass 194 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; -*- 2 | 3 | from distutils.core import setup 4 | import py2exe 5 | 6 | includes = [] 7 | excludes = ['_gtkagg', '_tkagg', 'bsddb', 'curses', 'email', 'pywin.debugger', 8 | 'pywin.debugger.dbgcon', 'pywin.dialogs', 'tcl', 9 | 'Tkconstants', 'Tkinter'] 10 | packages = [] 11 | dll_excludes = ['libgdk-win32-2.0-0.dll', 'libgobject-2.0-0.dll', 'tcl84.dll', 12 | 'tk84.dll'] 13 | 14 | setup( 15 | options = {"py2exe": {"compressed": 2, 16 | "optimize": 2, 17 | "includes": includes, 18 | "excludes": excludes, 19 | "packages": packages, 20 | "dll_excludes": dll_excludes, 21 | "bundle_files": 3, 22 | "dist_dir": "dist", 23 | "xref": False, 24 | "skip_archive": False, 25 | "ascii": False, 26 | "custom_boot_script": '', 27 | } 28 | }, 29 | windows=[{ 30 | 'script': 'main.py', 31 | 'icon_resources': [(0, "icons/icon.ico")], 32 | 'dest_base': 'cube', 33 | }] 34 | ) 35 | --------------------------------------------------------------------------------